StateGraph: a declarative state-machine blueprint
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
messagesfield in the same superstep, if the field is annotatedAnnotated[list, operator.add], the engine knows to+instead of "last write overwrites" (reducer docs:135-137). Fields without a reducer go through theLastValuechannel, 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_edgesall returnSelf(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-144—Generic[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— acceptsstate_schema/context_schema/input_schema/output_schema; converts the legacy field namesconfig_schema/input/outputto the new names and warns; calls_add_schemato register the state/input/output schemas intochannels/managed._add_schema:342-372— uses_get_channelsto derive channels + managed values from the schema; on conflict, anything other thanLastValueraises.validate:1116-1162— pre-compile validation: iterates all edge sources/targets, checks START exists, checks interrupt nodes exist; finallyself.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 toadd_node.compile signature:1164-1217— acceptscheckpointer/store/cache/interrupt_before/interrupt_after/nameetc.; returns aCompiledStateGraph.CompiledStateGraph:1391-1409— inherits fromPregel; only addsbuilder/schema_to_mapperfields and input/output JSON schema methods.attach_node:1431-1470— at compile time, translatesStateNodeSpecintoPregelNodeand defines_get_updatesto decide which channels to write from the node's return value.BranchSpec.from_path:83-120— translates thepath(Runnable) +path_mapfromadd_conditional_edgesinto a "condition -> target node" dict; whenpath_mapis not given, attempts to infer from aLiteral[...]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:
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:
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
StateGraphcannot beinvoked directly (warning:139-144) —.compile()is required; using it as a Runnable directly would be missing runtime fields likenodes.- State field name conflicts raise (
channel conflict:360-362) unless both areLastValue— this prevents implicit behavioral inconsistency from "the same key has different reducers in two schemas". input_schema/output_schemacannot have managed channels (managed check:346-352) — managed is state-exclusive, because they are not simple value-storing channels but objects with a lifecycle, likeContextManager.- The
config_schemalegacy name is deprecated (config_schema deprecated:224-231) — since v1.0 it warns to usecontext_schema; it will be removed in 2.0. compile()on an already-compiled graph only warns (compiled warning:932-936) — bothadd_nodeandadd_edgeprintAdding ... 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.