Skip to content

StateGraph.compile: compiling the blueprint into a Pregel runtime

源码版本1.2.9

Responsibilities

StateGraph.compile is the "translator" between the builder and the runtime. It takes a StateGraph (node specs, edges, branch conditions, channel dict) and produces a CompiledStateGraph (CompiledStateGraph:1391) — the latter inherits from Pregel and carries all runtime methods: invoke / stream / astream / get_state. Before compile your graph is just configuration data; after compile it becomes an executable.

Its core job is four things: 1) validate the graph structure (validate), 2) decide output/stream channels, 3) inject runtime dependencies (checkpointer / store / cache / interrupt configuration), 4) translate every StateNodeSpec / edge / BranchSpec into Pregel PregelNode + channel subscriptions (construct CompiledStateGraph:1333-1357). The translation step is done by looping compiled.attach_node / compiled.attach_edge / compiled.attach_branch — these methods convert the declarative data structures in the builder into runtime fields like PregelNode.writers / PregelNode.triggers.

compile also handles two secondary responsibilities: 1) apply defaults set by set_node_defaults (retry / cache / error_handler / timeout) to all nodes that did not specify them explicitly, and 2) when strict msgpack serialization is enabled (_serde.STRICT_MSGPACK_ENABLED), build a serde allowlist so the checkpointer only serializes fields that appear in the schema (serde allowlist:1220-1241).

Design motivation

Why not make StateGraph itself a Pregel, instead of splitting into two steps?

  • Build-time vs runtime separation of concerns: the builder stage only cares about "what the graph looks like"; the runtime stage cares about "how to run it". Splitting them lets a builder be compiled multiple times into different runtimes — the same StateGraph can produce multiple Pregel instances with different checkpointers / stores / interrupt nodes.
  • Immutable runtime: after compile, the CompiledStateGraph fields are essentially fixed in Pregel.__init__ (Pregel.__init__:758-836). auto_validate=False lets compile itself control when to validate, avoiding premature validation by the constructor.
  • Deferred validation: when the user calls add_node, it only appends to containers and does not immediately check "does the edge's start exist" — this allows registering nodes and edges in any order; real validation happens once, at compile time (validate call:1247-1254).
  • Interrupt configuration is parameterized: interrupt_before / interrupt_after are compile parameters, not properties of the graph itself — the same graph can be compiled into both an "interrupt" version and a "no-interrupt" version, common in human-in-the-loop scenarios (interrupt parameters:1170-1171).
  • Checkpointer type normalization: compile(checkpointer=...) accepts None / True / False / BaseCheckpointSaver; ensure_valid_checkpointer (ensure_valid_checkpointer:107-117) validates uniformly to avoid runtime explosions.
  • Defaults applied at compile time (defaults application:1299-1325) — this lets the user call set_node_defaults after add_node; the new defaults override any node that did not specify them explicitly.

Key files

  • compile signature:1164-1217 — accepts checkpointer / store / cache / interrupt_before / interrupt_after / debug / name / transformers.
  • ensure_valid_checkpointer:107-117 — validates that the checkpointer is one of None / True / False / BaseCheckpointSaver, else TypeError.
  • ensure_valid_checkpointer call:1218 — normalizing the checkpointer is the first thing compile does.
  • serde allowlist:1220-1241 — under strict msgpack, builds an allowlist and applies it to the checkpointer so the checkpoint only stores schema fields.
  • interrupt merge + validate:1243-1254"*" means All (all nodes); merges interrupt_before / interrupt_after into one list passed to validate.
  • output / stream channels:1256-1273 — when single-field and __root__, uses the string directly; otherwise uses a list, filtering out managed values.
  • default error handler node:1278-1297 — the global error_handler set by set_node_defaults is injected as a special node named __default_error_handler__.
  • defaults application:1299-1325 — applies set_node_defaults retry / cache / error_handler / timeout to every spec that did not specify them explicitly; cache and error_handler are not applied to error-handler nodes themselves.
  • node_error_handler_map:1327-1331 — produces the node_name -> handler_node_name map; at runtime failed-node execution is routed to the corresponding handler based on this map.
  • construct CompiledStateGraph:1333-1357 — merges the builder's channels / managed, adds START: EphemeralValue(input_schema) as the entry channel, sets stream_mode="updates", input_channels=START.
  • attach trio:1360-1388 — loops compiled.attach_node(START, None) + each node, attach_edge for each edge, attach_branch for each branch; finally compiled.validate().
  • Pregel.__init__:758-836CompiledStateGraph.__init__ delegates to Pregel via super().__init__(**kwargs); all runtime fields are expanded here, and self.validate() is called when auto_validate=True.

Data flow

Here is the key section of compile actually constructing the runtime — stuffing all the builder's fields into CompiledStateGraph and adding a START entry channel:

python
compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
    builder=self,
    schema_to_mapper={},
    context_schema=self.context_schema,
    nodes={},
    channels={
        **self.channels,
        **self.managed,
        START: EphemeralValue(self.input_schema),
    },
    input_channels=START,
    stream_mode="updates",
    output_channels=output_channels,
    stream_channels=stream_channels,
    checkpointer=checkpointer,
    interrupt_before_nodes=interrupt_before,
    interrupt_after_nodes=interrupt_after,
    auto_validate=False,
    debug=debug,
    store=store,
    cache=cache,
    node_error_handler_map=node_error_handler_map,
    name=name or "LangGraph",
    stream_transformers=transformers,
)
compiled._serde_allowlist = serde_allowlist

compiled.attach_node(START, None)
for key, node in self.nodes.items():
    compiled.attach_node(key, node)

(construct + attach_node:1333-1362)

Finally edges and branches are translated into Pregel channel subscriptions, and a validate is triggered:

python
for start, end in self.edges:
    compiled.attach_edge(start, end)

for starts, end in self.waiting_edges:
    compiled.attach_edge(starts, end)

for start, branches in self.branches.items():
    for name, branch in branches.items():
        compiled.attach_branch(start, name, branch)

return compiled.validate()

(attach edge/branch:1378-1388)

After Pregel.__init__ receives these fields, it translates NodeBuilders in nodes into PregelNodes, installs a Topic(Send, accumulate=False) on the TASKS channel, and then calls self.validate() when auto_validate=True (Pregel init:800-836):

python
self.nodes = {
    k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items()
}
self.channels = channels or {}
if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic):
    raise ValueError(
        f"Channel '{TASKS}' is reserved and cannot be used in the graph."
    )
else:
    self.channels[TASKS] = Topic(Send, accumulate=False)

(Pregel init nodes:800-809)

Boundaries and failure modes

  • checkpointer=True cannot be used on the root graph (True raises:2583-2584) — True means "inherit from the parent graph", only subgraphs can use it; the root graph must explicitly provide a saver or False.
  • interrupt_before="*" and interrupt_after="*" mutual-exclusion handling (* handling:1249-1253) — only when interrupt_after != "*" is the interrupt_before list merged into interrupt; this is the precedence convention when using * to mean "all nodes".
  • The TASKS channel name is reserved (TASKS reserved:804-807) — a user-defined field named __pregel_tasks in the schema raises directly; this is the internal Topic channel the engine uses to fan out Sends.
  • Re-attaching a node with the same name: the builder stage already blocks duplicates (duplicate name:792-793) — but auto-generated node names like __default_error_handler__ also need to avoid colliding with user nodes (default handler conflict:1280-1284).
  • cache and error_handler defaults are not applied to handler nodes themselves (cache not for handler:1313-1321) — because "caching a handler's result" is unsafe (the failed node's state may differ each time), and "a handler catching itself" would be an infinite loop.
  • Compiling the same builder multiple times is allowed: each compile creates a new CompiledStateGraph. The builder's compiled field is set to True during validate() (compiled=True:1161), but it is only a flag — it does not block further compiles or continued add_node calls (though those will warn).

Summary

compile is the boundary between the builder and the runtime — it translates all the specs accumulated by add_node / add_edge / add_conditional_edges in StateGraph into Pregel runtime structures, attaches the checkpointer / store / interrupt configuration, and finally returns a Pregel subclass that can be invoked directly. Understanding it means understanding the whole "graph declaration → executable" chain.

Next you can look at how Pregel.__init__ is followed by invoke / stream to run this compiled artifact in the Pregel engine, or at how StateSnapshot retrieves state from the compiled artifact once a checkpointer is configured.

See official docs: LangGraph · README.