Code Intermediate medium · 6 min

Passing config through nested chains

What you will learn
Use RunnableConfig to propagate settings like API keys, timeouts, and callbacks through deeply nested chain operations without modifying intermediate function signatures.

Why this matters

When you build complex chains with multiple levels of nesting, you need to pass runtime configuration (timeouts, callbacks, tags) to all layers without threading parameters through every intermediate function. RunnableConfig makes this clean and avoids leaky abstractions.

Skip if: If your chain is flat (one or two levels) or only needs one piece of configuration, explicit parameter passing is clearer. Don't use RunnableConfig as a general-purpose dependency injection system for business logic: it's specifically for runtime execution control.

Explanation

RunnableConfig is a built-in langchain mechanism for passing execution metadata through nested chains without polluting function signatures. When you invoke a chain with chain.invoke(input, config={'tags': [...], 'callbacks': [...], ...}), that config automatically flows to all child Runnable objects in the chain, even across multiple levels of nesting. Mechanically, each Runnable in the chain receives the config in its invoke method: you access it via run_manager in custom Runnables, or the chain automatically forwards it to all child Runnables without you doing anything. This works because LCEL (Langchain Expression Language) chains are composable Runnable objects that understand config propagation natively. Use RunnableConfig when you have deeply nested chains, need to add callbacks at runtime, set tags for observability, or apply timeouts globally without modifying nested function signatures.

Analogy

Like passing a request context through middleware in a web framework: the config flows through your entire chain stack automatically, available at every layer without being explicitly passed as a function argument.

Code

Illustrative only - not runnable without a valid API key
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableConfig, RunnableLambda
from langchain_core.callbacks import BaseCallbackHandler

class LoggingCallback(BaseCallbackHandler):
    def on_llm_start(self, serialized, prompts, **kwargs):
        print(f"[CALLBACK] LLM call starting with prompt: {prompts[0][:50]}...")

def nested_processor(text: str, config: RunnableConfig) -> str:
    run_manager = config.get("run_manager")
    tags = config.get("tags", [])
    print(f"[NESTED] Processing '{text}' with tags: {tags}")
    return f"Processed: {text}"

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

prompt = ChatPromptTemplate.from_template(
    "Summarize this in one sentence: {input}"
)

parser = StrOutputParser()

process_step = RunnableLambda(nested_processor)

chain = prompt | llm | parser | process_step

callback = LoggingCallback()

result = chain.invoke(
    {"input": "The quick brown fox jumps over the lazy dog."},
    config={
        "tags": ["production", "summarization"],
        "callbacks": [callback],
        "run_name": "nested_chain_example"
    }
)

print(f"\n[FINAL] {result}")
Output
[CALLBACK] LLM call starting with prompt: Summarize this in one sentence: The quick brown fox jumps over the lazy dog...\n[NESTED] Processing 'A quick brown fox jumps over a lazy dog.' with tags: ['production', 'summarization']\n\n[FINAL] Processed: A quick brown fox jumps over a lazy dog.

What just happened?

The config dict (containing tags and callbacks) was passed into chain.invoke(). The ChatOpenAI model received the callbacks automatically and triggered the callback handler when the LLM started. The RunnableLambda at the end received that same config via the nested_processor function, which extracted the tags. At no point did we manually thread the config through intermediate steps: LCEL handled it.

Common gotcha

Developers often try to access config as a positional argument in RunnableLambda: you must accept it as the second parameter and it only appears if you explicitly add it. If you define `RunnableLambda(lambda text: ...)` with only one argument, the config is ignored silently. The RunnableLambda must be defined as `RunnableLambda(lambda text, config: ...)` or use a regular function with both parameters.

Error recovery

TypeError: nested_processor() missing 1 required positional argument: 'config'
Your lambda or function only declared one parameter. Change `RunnableLambda(lambda x: ...)` to `RunnableLambda(lambda x, config: ...)` if you need config, or keep one parameter if you don't.
KeyError when accessing config['tags']
Config may not have that key. Use `config.get('tags', [])` with a default value instead of direct dict access, or check `'tags' in config` first.
Callbacks not firing in nested chain
Verify callbacks are passed in the config dict, not as a parameter to invoke(). The signature is `chain.invoke(input, config={'callbacks': [...]})`, not `chain.invoke(input, callbacks=[...])`. Also ensure your callback handler implements the right method (on_llm_start, on_chain_start, etc.).

Experienced dev note

In older LangChain patterns (< 0.2.0), people would pass configuration through function closures or class state, making tests hard to write. RunnableConfig makes config explicit and testable: you can mock it in unit tests via the config parameter. Also: config propagation only works with LCEL chains (the pipe operator). If you're using legacy AgentExecutor or custom Runnable subclasses without implementing config forwarding, you'll break the chain. Always call `super().invoke(input, run_manager=run_manager.get_child())` in custom Runnable.invoke() to pass config downstream.

Check your understanding

You have a three-layer chain: (LLM prompt) → (custom parser) → (RunnableLambda). You want callbacks to fire at the LLM layer and custom parser tags to be visible in the Lambda. How would you pass both without modifying the parser's function signature?

Show answer hint

A correct answer shows understanding that (1) config is passed once at invoke() time, (2) it flows automatically to LLM and through LCEL composition, (3) the RunnableLambda must accept config as a second parameter to access tags, and (4) you don't modify the parser function itself.

VERSION RunnableConfig pattern is stable in langchain-core >= 0.2.0. In earlier versions (< 0.2.0), config propagation was partial and callbacks required manual threading. Use the pipe operator (|) to ensure config flows automatically: avoid mixing LCEL chains with legacy LLMChain or AgentExecutor, which don't propagate config correctly.
NEXT

Once you master config propagation, learn how to build custom Runnables that properly implement invoke() to forward both input and config to child chains: this is how production-grade composable chains handle complexity.

Community Notes

No notes yetBe the first to share a version-specific fix or tip.