StateGraph.compile: compiling the blueprint into a Pregel runtime
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
CompiledStateGraphfields are essentially fixed inPregel.__init__(Pregel.__init__:758-836).auto_validate=Falselets 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_afterare 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=...)acceptsNone/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 callset_node_defaultsafteradd_node; the new defaults override any node that did not specify them explicitly.
Key files
compile signature:1164-1217— acceptscheckpointer/store/cache/interrupt_before/interrupt_after/debug/name/transformers.ensure_valid_checkpointer:107-117— validates that the checkpointer is one ofNone/True/False/BaseCheckpointSaver, elseTypeError.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); mergesinterrupt_before/interrupt_afterinto one list passed tovalidate.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 byset_node_defaultsis injected as a special node named__default_error_handler__.defaults application:1299-1325— appliesset_node_defaultsretry / 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 thenode_name -> handler_node_namemap; at runtime failed-node execution is routed to the corresponding handler based on this map.construct CompiledStateGraph:1333-1357— merges the builder'schannels/managed, addsSTART: EphemeralValue(input_schema)as the entry channel, setsstream_mode="updates",input_channels=START.attach trio:1360-1388— loopscompiled.attach_node(START, None)+ each node,attach_edgefor each edge,attach_branchfor each branch; finallycompiled.validate().Pregel.__init__:758-836—CompiledStateGraph.__init__delegates to Pregel viasuper().__init__(**kwargs); all runtime fields are expanded here, andself.validate()is called whenauto_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:
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:
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):
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)Boundaries and failure modes
checkpointer=Truecannot be used on the root graph (True raises:2583-2584) —Truemeans "inherit from the parent graph", only subgraphs can use it; the root graph must explicitly provide a saver orFalse.interrupt_before="*"andinterrupt_after="*"mutual-exclusion handling (* handling:1249-1253) — only wheninterrupt_after != "*"is theinterrupt_beforelist merged into interrupt; this is the precedence convention when using*to mean "all nodes".- The
TASKSchannel name is reserved (TASKS reserved:804-807) — a user-defined field named__pregel_tasksin the schema raises directly; this is the internal Topic channel the engine uses to fan outSends. - 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'scompiledfield is set toTrueduringvalidate()(compiled=True:1161), but it is only a flag — it does not block further compiles or continuedadd_nodecalls (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.