Skip to content

StateGraph: a declarative state-machine blueprint

源码版本1.2.9

Responsibilities

StateGraph is the class LangGraph users interact with most directly — it is a builder, not a runtime. You register state schemas, node functions, edges, and conditional edges on it, then call .compile() to compile it into a CompiledStateGraph (which inherits from Pregel) runtime. It has no invoke / stream itself; running a graph requires compiling first (doc warning:139-144).

In the architecture it sits between the user API and the Pregel engine: the user writes StateGraph(State).add_node(...).add_edge(...).compile(), and StateGraph translates each state field into a channel instance, wraps each node as a StateNodeSpec, and stores each edge in one of the three collections edges / waiting_edges / branches, to be handed off at compile time to CompiledStateGraph.attach_node / attach_edge / attach_branch and translated into PregelNode subscription relationships (StateGraph fields:201-213).

StateGraph's core contract is: a node's signature is State -> Partial<State>, and each state field may optionally be annotated with Annotated[type, reducer] to specify a merge function — when multiple nodes write the same field in the same step, the reducer merges them instead of overwriting (class docstring:131-138). This reducer mechanism is the source of the two channel types LastValue (default overwrite) and BinaryOperatorAggregate (e.g. operator.add).

Design motivation

Why not just write function call chains — why a "state machine + channels" model?

  • Decoupled nodes: a node only knows state, not other nodes; the edges decide who runs before whom, so changing the flow does not require touching node code. This is the core benefit of a state machine.
  • Reducers give parallel writes well-defined semantics: when multiple nodes write the messages field in the same superstep, if the field is annotated Annotated[list, operator.add], the engine knows to + instead of "last write overwrites" (reducer docs:135-137). Fields without a reducer go through the LastValue channel, whose behavior is "multiple writes in the same superstep raise" or "overwrite", depending on the channel type.
  • Build-time validation: validate() at compile time (validate:1116-1162) checks "every edge's start and end is in nodes", "START must be the start of some edge", "interrupt nodes exist", and so on — catching errors before the graph runs.
  • Schema separation of input/output/state: the same graph can have input_schema != state_schema != output_schema (schema params:217-221) — a narrow external interface with a wide internal state, a common pattern for agents where "the input schema for the LLM differs from the internally accumulated state".
  • Method chaining: add_node / add_edge / add_conditional_edges all return Self (Returns Self:749) — supporting chained calls so that building a graph reads like a config file rather than an imperative sequence.

Key files

  • StateGraph class definition:130-144Generic[StateT, ContextT, InputT, OutputT]; the docs state explicitly "builder, cannot be invoked directly".
  • class fields:201-213 — seven core containers: edges / nodes / branches / channels / managed / schemas / waiting_edges.
  • __init__:215-269 — accepts state_schema / context_schema / input_schema / output_schema; converts the legacy field names config_schema / input / output to the new names and warns; calls _add_schema to register the state/input/output schemas into channels / managed.
  • _add_schema:342-372 — uses _get_channels to derive channels + managed values from the schema; on conflict, anything other than LastValue raises.
  • validate:1116-1162 — pre-compile validation: iterates all edge sources/targets, checks START exists, checks interrupt nodes exist; finally self.compiled = True.
  • set_node_defaults:271-334 — sets default retry / cache / error_handler / timeout policies for the whole graph; lower priority than explicit parameters passed to add_node.
  • compile signature:1164-1217 — accepts checkpointer / store / cache / interrupt_before / interrupt_after / name etc.; returns a CompiledStateGraph.
  • CompiledStateGraph:1391-1409 — inherits from Pregel; only adds builder / schema_to_mapper fields and input/output JSON schema methods.
  • attach_node:1431-1470 — at compile time, translates StateNodeSpec into PregelNode and defines _get_updates to decide which channels to write from the node's return value.
  • BranchSpec.from_path:83-120 — translates the path (Runnable) + path_map from add_conditional_edges into a "condition -> target node" dict; when path_map is not given, attempts to infer from a Literal[...] return type.

Data flow

Here is the core section where StateGraph.__init__ registers schemas as channels — this is where "state field -> channel instance" translation happens:

python
self.nodes = {}
self.edges = set()
self.branches = defaultdict(dict)
self.schemas = {}
self.channels = {}
self.managed = {}
self.compiled = False
self.waiting_edges = set()

self.state_schema = state_schema
self.input_schema = cast(type[InputT], input_schema or state_schema)
self.output_schema = cast(type[OutputT], output_schema or state_schema)
self.context_schema = context_schema

self._node_defaults: _NodeDefaults = _NodeDefaults()

self._add_schema(self.state_schema)
self._add_schema(self.input_schema, allow_managed=False)
self._add_schema(self.output_schema, allow_managed=False)

(field initialization:251-269)

Each schema is split by _add_schema into a (channels, managed, type_hints) triple — channels go into the self.channels dict, managed goes into self.managed. Re-registering the same key is only allowed for LastValue:

python
self.schemas[schema] = {**channels, **managed}
for key, channel in channels.items():
    if key in self.channels:
        if self.channels[key] != channel:
            if isinstance(channel, LastValue):
                pass
            else:
                raise ValueError(
                    f"Channel '{key}' already exists with a different type"
                )
        else:
            self.channels[key] = channel

(_add_schema registration logic:353-364)

Boundaries and failure modes

  • StateGraph cannot be invoked directly (warning:139-144) — .compile() is required; using it as a Runnable directly would be missing runtime fields like nodes.
  • State field name conflicts raise (channel conflict:360-362) unless both are LastValue — this prevents implicit behavioral inconsistency from "the same key has different reducers in two schemas".
  • input_schema / output_schema cannot have managed channels (managed check:346-352) — managed is state-exclusive, because they are not simple value-storing channels but objects with a lifecycle, like ContextManager.
  • The config_schema legacy name is deprecated (config_schema deprecated:224-231) — since v1.0 it warns to use context_schema; it will be removed in 2.0.
  • compile() on an already-compiled graph only warns (compiled warning:932-936) — both add_node and add_edge print Adding ... to a graph that has already been compiled, but changes are not reflected in the already-compiled instance — a common pitfall is getting the chained-call order wrong.
  • validate requires at least one edge from START (entrypoint check:1129-1132) — Graph must have an entrypoint, otherwise there is no place to start running.

Summary

StateGraph is a declarative state-machine blueprint: you give it a schema plus nodes and edges, and it translates them into the PregelNode + channel subscriptions that Pregel can run. Its existence lets users avoid confronting Pregel's actor model directly, focusing only on three things: state fields, node signatures, and edges.

Continue with add_node / add_edge / add_conditional_edges on how to fill this blueprint with content, and compile on how to compile it into a Pregel. For the overall runtime model see Pregel engine.

See official docs: LangGraph · README.