StateGraph is a Pregel-style superstep engine. You declare a typed state, register nodes and edges, then compile() to an executable graph that runs sync or async with identical semantics. It is LangGraph-compatible in shape, in about 360 lines.The mental model
A graph is a set of nodes (functions) connected by edges. Execution runs in supersteps. In each superstep, every node in the active frontier runs against one frozen snapshot of the state. Their writes are batched, then committed through channels (so reducers actually reduce). The next frontier is the union of every active node’s successors. That union is what makes real fan-out and diamond joins work.Typed state
State is aTypedDict. You pass the class to StateGraph. Each field becomes a channel:
- A plain field becomes a
LastValuechannel. It accepts exactly one write per superstep. - An
Annotated[type, reducer]field becomes aBinOpchannel. It folds every write of the superstep through the reducer, so multiple nodes can contribute.
f(current, new). operator.add concatenates lists and sums numbers. You can pass your own function for custom merge logic.
How channels are inferred
StateGraph reads the schema’s type hints with include_extras=True. If a field’s annotation carries metadata whose first element is callable, that callable is the reducer and the field becomes BinOp. Every other field becomes LastValue. If you pass no schema (or dict), channels are created on demand as LastValue when a node first writes a key.
The three channel types live in infy.graph.channels:
Nodes
A node is a function that takes the current state dict and returns one of:- a partial state dict to merge through the channels,
- a
Command(to update state and/or jump to a node), - or
None(no writes).
add_node. Reserved names START and END cannot be used. Extra keyword arguments (retry, timeout) are stored on the node spec.
Edges
add_edge(source, target) is an unconditional edge. A node may have several outgoing edges; all of their targets join the next frontier.
add_conditional_edges(source, condition, path_map=None, default=None) routes at runtime. The condition receives the current state and returns:
- a node name (looked up in
path_mapif provided, else used directly), - a list of node names (fan-out), or
- a
Sendobject or list ofSendobjects (dynamic fan-out with per-task input).
Dynamic fan-out with Send
Return Send(node, arg) objects from a conditional edge to spawn one task per item. Each task runs node with arg merged into its input. Plain edge traversals are de-duplicated so diamond joins run the shared node once, but Send tasks are always kept distinct, since fan-out tasks are meant to be separate units of work.
ainvoke/astream, the tasks in a frontier run concurrently. Writes are still folded in frontier order, so order-sensitive reducers behave the same as the sync path.
Compile and run
compile() validates the graph, builds the channels, and returns a CompiledGraph.
compile accepts:
Input handling
invoke normalizes its input before the first superstep:
- a dict is applied as channel updates,
- a bare list is applied as
{"messages": [...]}, - anything else is applied as
{"input": ...}.
Sync and async
The compiled graph runs three ways with identical channel, reducer, checkpoint, and interrupt semantics:Under
astream and ainvoke the frontier runs concurrently, so a node that raises does not cancel its already-running siblings. The first error in frontier order is surfaced. Keep concurrent nodes side-effect-free or idempotent.The canonical agent loop
The tool-calling agent is a two-node graph: a model node, a tools node, and a conditional edge that loops until the model stops asking for tools.model runs, route decides, and either tools runs and feeds back into model, or the loop ends. The messages reducer (operator.add) accumulates the full transcript across supersteps.
Where to go next
Checkpointing and interrupts
Persist state per
thread_id, pause for human input, and resume a run later.Prebuilt graph agent
Use
create_graph_agent for the agent loop with state persistence built in.Governance
Add deny-by-default policy, approval, and a tamper-evident audit to any run.
Runnables
Compose models, parsers, and functions with the
| operator.