StateGraph does not keep state as a single mutable map. It lowers each schema key into a channel object with its own update rule, persistence shape, and trigger behavior. That bridge matters more than the surface syntax on the page, because the channel decides what happens when multiple nodes write, how values survive checkpoints, and when downstream nodes wake up. For the official references on reducer semantics, channel types, and update errors, see graph-api
, pregel
, INVALID_CONCURRENT_GRAPH_UPDATE
, and INVALID_GRAPH_NODE_RETURN_VALUE
.
The useful mental model is operational: a node returns a batch of writes, the engine routes each write to a channel, and the channel decides how to absorb that batch. A plain key becomes LastValue, a reducer key becomes BinaryOperatorAggregate, and an explicit channel instance stays the channel that the graph uses. ManagedValue comes from the runtime scratchpad, so the schema can name a runtime-supplied value without storing it in checkpointed state.
class State(TypedDict):
messages: Annotated[list, add_messages]
That line compiles the messages key to BinaryOperatorAggregate. add_messages is an ordinary two-argument reducer in libs/langgraph/langgraph/graph/message.py.
How compilation resolves a field
StateGraph reads the schema with get_type_hints(..., include_extras=True) in libs/langgraph/langgraph/graph/state.py and walks each field through _get_channels. _get_channel resolves a field in a fixed order: managed value first, explicit channel instance or channel class in Annotated metadata next, then a callable reducer when the last Annotated item accepts two positional arguments, and LastValue as the fallback. A callable with the wrong arity raises Invalid reducer signature. Expected (a, b) -> c. When a node returns something that is not a valid update shape, the runtime points to errors/INVALID_GRAPH_NODE_RETURN_VALUE. The order matters because the same annotation can mean a runtime channel, a reducer key, or a plain value depending on the metadata.
Compilation also materializes the input side as START: EphemeralValue(self.input_schema), then uses ephemeral and barrier channels to wire control-flow for branches and joins. The schema line only describes intent; compilation turns that intent into channel objects with concrete runtime behavior.
Channel behavior is the point
LastValue
Plain annotations use LastValue by default. It stores one value and rejects a second write in the same step. When parallel branches write the same plain key, the graph raises INVALID_CONCURRENT_GRAPH_UPDATE, which is the signal that the schema asked for single-writer behavior.
BinaryOperatorAggregate
BinaryOperatorAggregate folds each step’s writes with the reducer. apply_writes sorts tasks by task_path_str(t.path[:3]) before grouping writes, so the fold order stays deterministic even when tasks arrive from different paths. That determinism does not remove order sensitivity, so fan-in reducers should stay associative and commutative when graph behavior should not depend on write order.
Topic
Topic behaves like a pub-sub list channel. Multiple writers can append to it, and the accumulate flag decides whether it keeps prior values across steps or resets the list each step. That makes it useful when the graph needs a stream of events rather than a single latest value.
EphemeralValue
EphemeralValue carries a one-step signal and clears after use. The engine also uses ephemeral channels for control-flow signals, including the START input channel and branch triggers. See ./04-control-flow-is-channels-too.md for how that wiring shapes branch execution.
NamedBarrierValue
NamedBarrierValue acts as the fan-in primitive for joins. It stays unreadable until all named writers report in, which lets a join wait for a specific set of upstream branches before it releases the next node. See ./04-control-flow-is-channels-too.md for the join edges built on this channel.
DeltaChannel
As of mid 2026, DeltaChannel remains beta, uses a batch-shaped reducer signature, and its on-disk format is not stable yet.
Channels and persistence
Each channel snapshots itself with checkpoint() and rebuilds itself with from_checkpoint(). Persisted graph state therefore consists of channel snapshots plus the version bookkeeping that tracks which channel writes each node has already consumed. The next page, ./05-why-checkpoints-look-like-that.md, covers the checkpoint format. This page only needs one rule: channel state survives because the channel object can round trip through checkpoint() and from_checkpoint(), not because LangGraph serializes a raw state dict.
Channels and triggering
Channel versions drive scheduling. When apply_writes updates a channel, it bumps that channel’s version. Version comparison then wakes subscribed nodes, so channel choice changes run order as well as stored value. See ./02-what-runs-next.md for the scheduling side of that mechanism.
Decision table
| Schema pattern | Channel | Semantics | Typical use |
|---|---|---|---|
| Bare annotation | LastValue | Stores one value and rejects concurrent writes in the same step | Single-writer state |
Annotated[T, reducer] | BinaryOperatorAggregate | Folds all writes with the reducer | Accumulation, fan-in |
| Explicit channel instance | The instance itself | Uses the channel object exactly as declared | Custom runtime behavior |
Annotated[T, ChannelClass] | ChannelClass(T) | Uses the declared channel class with the field type | Direct channel selection |
| Managed value marker | ManagedValue | Reads runtime data from the scratchpad, not the checkpoint | Ephemeral runtime context |
Where to look in the code
libs/langgraph/langgraph/graph/state.py— schema parsing, channel lowering, and compile-time channel wiring.libs/langgraph/langgraph/pregel/_algo.py— write grouping, channel version updates, and downstream triggering.libs/langgraph/langgraph/channels/base.py— the checkpoint and restore contract shared by every channel.libs/langgraph/langgraph/channels/last_value.py— plain key behavior and the concurrent update error.libs/langgraph/langgraph/channels/binop.py— reducer backed aggregation forAnnotated[...]fields.libs/langgraph/langgraph/graph/message.py— theadd_messagesreducer example used by message state.