Agent Orchestration Explained
← /learn · 02

State, channels and reducers

Two parallel branches write the same key: no reducer is an error, an add reducer keeps both in a fixed order, a custom reducer picks one; and what a router can see.

Loading the animation…

Concept

The state is a set of channels, one per key, and each channel has a reducer: the rule that turns the current value and a write into the new value. A key declared without one keeps the last value written, and accepts one write per super-step. A key declared Annotated[list, operator.add] appends; any two-argument function will do, such as max.

Parallel nodes are where this matters. The animation runs a planner and then two branches, fast (700 ms) and careful (1,600 ms), that both write the key answer in the same super-step. Neither sees the other's write; both writes meet at the barrier.

  • No reducer: two writes to one key in one step is an error, LangGraph's InvalidUpdateError ("Can receive only one value per step"). The step saves no checkpoint; the last good one stays the thread's latest.
  • Add: both writes are kept, answer = [42, 41]. Note the order: careful finishes last but its write comes first, because LangGraph applies a step's writes in task order (for plain nodes, sorted by node name), not in the order they finish. The result does not depend on timing, so a run replays the same way.
  • Max: a custom reducer keeps the larger answer, 42.

The fourth graph is about what a node can see. Node p has a conditional edge whose router goes to r only if x = 7, and its sibling q sets x = 7 in the same step. The router runs with the snapshot plus its own node's writes, not its sibling's, so it sees no x and the run ends without r, although the state after the step does hold x = 7.

Maths

Let Ws(k)=(w(1),…,w(m))W_s(k) = (w_{(1)}, \ldots, w_{(m)}) be the writes to key kk in super-step ss, sorted by the writing node's name. The new value is the left fold in the equation above. With rk(a,b)=br_k(a, b) = b and m>1m > 1 the step fails. With list concatenation the result keeps every write in that order; with an associative and commutative reducer such as max⁡\max or integer ++ the order does not matter at all, which is the property to aim for when parallel branches write one key.

A router on node uu evaluates on vs⊕wuv_s \oplus w_u, the snapshot with only uu's own writes applied through the reducers. So a routing decision can depend on what uu just wrote, but never on a sibling in the same step: if it must, put the router after a join.

Code

The reducers at the barrier, and the router's view, cut from the vendored engine (graph.ts):

if (chan.reducer === "overwrite" && seen[k]! > 1)
  throw new InvalidUpdateError(`At key '${k}': Can receive only one value per step.`);
out[k] = reduceValue(chan, out[k], k in out, v);
const own = applyWrites(this.spec, values, [[name, upd]]);
for (const t of route(e.router, own)) {

How the conformance check declares the same channels in real LangGraph (conformance/langgraph_graphs.py):

fields[s["key"]] = Annotated[t, operator.add]