Skip to content

RunnableConfig: how configuration flows through the graph

源码版本1.2.9

Responsibilities

Running a LangGraph graph requires a runtime context that goes well beyond the input data itself. recursion_limit, thread_id, checkpoint_id, callbacks, tags, metadata, plus the store / stream_writer used by nodes — all of them must be passed in, merged, validated, and finally embedded into the configurable dict when graph.invoke(input, config) is called, so they can be threaded through to each node's RunnableConfig. This unified configuration object is RunnableConfig, which directly reuses the LangChain Core type of the same name (from langchain_core.runnables import RunnableConfig) rather than being a LangGraph reinvention.

It sits at the very entry of the architecture: as soon as Pregel.invoke / Pregel.stream is entered, ensure_config (ensure_config:322) is called to fill in defaults for scattered user-supplied fields and merge the inherited parent-graph configurable with what the caller passed in, producing a complete config that is then handed to PregelLoop. Throughout the superstep loop, this config is both the locating coordinate for checkpoints (thread_id + checkpoint_ns + checkpoint_id) and the entry point for nodes to obtain runtime tools (store / stream_writer / runtime).

Nodes do not reach into the config dict directly to read these fields — LangGraph exposes three shortcut functions: get_config (get_config:17), get_store, and get_stream_writer. They internally read the RunnableConfig from the current contextvar and pull the Runtime instance out of the configurable dict. So end to end, the config passes through five stages: "user passes it in → ensure_config fills defaults → loop threads it through → contextvar injection → node consumes it".

Design motivation

Why pack everything into RunnableConfig instead of defining a brand-new GraphConfig class?

  • Reuse the LangChain ecosystem: The Runnable protocol, tracing, and callbacks all understand RunnableConfig, so graph nodes are natively compatible with the LangChain toolchain. ensure_config (empty = RunnableConfig(...):322-337) even follows LangChain's merge rules for tags / metadata / callbacks.
  • Inheritable and overridable: When a subgraph runs inside a parent-graph node, the parent's configurable is automatically threaded through. But as soon as the subgraph explicitly supplies its own thread_id, that is treated as "resetting the checkpoint lineage" — see the "explicit config" logic in ensure_config (explicit checkpoint coordinate:355-367). Otherwise the subgraph would write checkpoints under the parent namespace and never find them again.
  • Clear field boundaries: The standard fields (tags / metadata / callbacks / recursion_limit / configurable) are CONFIG_KEYS, while LangGraph's own runtime fields are uniformly given a __pregel_ prefix and stashed in configurable to avoid colliding with user-defined keys — see CONFIG_KEY_* constants:33-77.
  • Defaults are env-tunable: recursion_limit defaults to the LANGGRAPH_DEFAULT_RECURSION_LIMIT environment variable (DEFAULT_RECURSION_LIMIT:32), so the loop ceiling can be raised without a code change.

Key files

  • get_config:17-29 — Reads the current RunnableConfig from the var_child_runnable_config contextvar; raises RuntimeError if not inside a runnable context.
  • get_store:32-123 — Reads store from config[CONF][CONFIG_KEY_RUNTIME].store; the docs show both StateGraph and entrypoint usage.
  • get_stream_writer:126-196 — Returns runtime.stream_writer; nodes use it to emit custom stream events (stream_mode="custom").
  • ensure_config:322-420 — Merges multiple configs, fills the default recursion_limit=DEFAULT_RECURSION_LIMIT, and handles checkpoint-coordinate resets.
  • merge_configs:147-180 — The underlying deep-merge implementation for multiple configs; the configurable dict is shallow-merged.
  • patch_configurable:52-62 — Patches only the configurable key, preserving the rest — the loop uses it to dynamically inject __pregel_checkpointer and friends.
  • CONFIG_KEY_* constants:33-77 — The full set of reserved keys: __pregel_send / __pregel_read / __pregel_checkpointer / __pregel_runtime / __pregel_resuming and so on.
  • CONF constant:91-92CONF = "configurable"; all reserved keys live under config[CONF].
  • recursion_limit validation:2563-2564 — In _setup_stream, reads config["recursion_limit"] directly and raises if it is less than 1.
  • loop.stop:1701self.stop = self.step + self.config["recursion_limit"] + 1, translating the recursion ceiling into "the maximum step index to run to".
  • GraphRecursionError:3005-3011 — Raised when execution reaches out_of_steps, telling the user to raise recursion_limit.

Data flow

The pivotal piece is ensure_config: it merges the contextvar-inherited config with the caller-supplied config, and when it sees an explicit checkpoint coordinate it resets the ambient configurable, ensuring recursion_limit / configurable / tags / metadata / callbacks are all in place.

python
empty = RunnableConfig(
    tags=[],
    metadata=ChainMap(),
    callbacks=None,
    recursion_limit=DEFAULT_RECURSION_LIMIT,
    configurable={},
)
if var_config := var_child_runnable_config.get():
    empty.update(
        {k: v.copy() if k in COPIABLE_KEYS else v
         for k, v in var_config.items() if _is_not_empty(v)},
    )

(ensure_config default values:331-345)

After merging, Pregel._setup_stream (_setup_stream:2563) reads CONFIG_KEY_CHECKPOINTER / CONFIG_KEY_RUNTIME / CONFIG_KEY_CACHE from config[CONF] to decide which checkpointer / store / cache to use for this run — the precedence is "config-injected > constructor-supplied".

python
if self.checkpointer is False:
    checkpointer: BaseCheckpointSaver | None = None
elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}):
    checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER]
elif self.checkpointer is True:
    raise RuntimeError("checkpointer=True cannot be used for root graphs.")
else:
    checkpointer = self.checkpointer

(checkpointer resolution:2579-2586)

Boundaries and failure modes

  • get_config raises RuntimeError outside a runnable context (get_config error:29), so calling get_store() directly in a unit test blows up — you must go through graph.invoke or a function decorated with entrypoint to obtain it.
  • get_store / get_stream_writer are unavailable under async on Python < 3.11 (async warning:53-57), because contextvar propagation depends on the asyncio.create_task behavior added in 3.11.
  • recursion_limit < 1 raises immediately (validation:2563-2564), not a silent fallback to the default.
  • checkpointer=True cannot be used on a root graph (True error:2583-2584). True means "inherit from parent", and the root graph has no parent, so you must supply an explicit BaseCheckpointSaver or False.
  • Enabling a checkpointer without supplying thread_id or other coordinates in configurable raises (checkpointer requires coordinates:2589-2593), asking for one of thread_id / checkpoint_ns / checkpoint_id.
  • A subgraph that explicitly passes thread_id resets the ambient configurable (explicit coordinate reset:362-367). This is what prevents the subgraph from writing checkpoints under the parent namespace where they cannot be found — but conversely, if you expect the subgraph to inherit a custom key from the parent's configurable, this reset means you will not get it.

Summary

RunnableConfig is the contract between LangGraph and the LangChain ecosystem, and the single carrier for checkpoint addressing, runtime-tool injection, callbacks, and tracing. Once you understand the merge rules in ensure_config and the division of labor among the __pregel_* reserved keys, you have essentially grasped the path "how configuration flows from the user into a node".

Continue with StateSnapshot to see how this config is bound to a checkpoint, and with the Pregel engine to see how loop.stop uses recursion_limit to cap the loop.

See official docs: LangGraph docs · README.