LangGraph State Flow — Reducer vs. Overwrite
A beginner lab for building intuition about how state moves through a LangGraph StateGraph.
1What problem this demonstrates
In LangGraph, every field in a state schema becomes a channel when the graph compiles. Nodes don't set state directly — they return a partial update, and the channel decides what to do with it. By default a channel just replaces its old value with the new one (last write wins). If you annotate a field with a reducer function, the channel instead merges the new value into the existing one.
This one design decision — reducer or no reducer — is the single most common source of "why did my state disappear?" surprises when learning LangGraph. This lab makes it visible by running two channels through the exact same three nodes and comparing them side by side.
2Architecture & flow
A single linear graph, three nodes, no branching, no LLM calls:
Each node returns the same shape of update:
{"last_note": "node_X ran", "results": ["x"]}
But the two fields are declared differently in the state schema:
| Channel | Declaration | Update rule |
|---|---|---|
last_note |
str (plain field) |
Overwrite — new value replaces old |
results |
Annotated[list[str], accumulate_results] |
Merge — reducer combines old + new |
The graph is streamed with stream_mode="values", which prints the full state
dict after every node runs — so both channels' behavior is visible on every line of output.
3What the learner owns
Everything is scaffolded except one function: accumulate_results(existing, new).
It is left as TODO: YOUR CORE and raises NotImplementedError until
filled in — running the script before implementing it is expected to fail, and that failure
is the starting point, not a bug.
The task: make results accumulate every node's contribution, in order, while
leaving last_note, the nodes, and the graph wiring untouched.
4Steps & observable signals
- Read the scaffold (
graph_lab.py) before running anything. - Fill in your predictions for each checkpoint before running the graph.
- Implement the reducer where marked
TODO: YOUR CORE. - Run
pip install -r requirements.txt && python graph_lab.pyand read the printed state after each node. - Confirm: does
last_noteshow only the latest node whileresultsgrows by one entry per node?
5The failure / comparison case
This is the core of the lab, side by side in the same run:
- Clobbered —
last_notehas no reducer, so each printed state shows only the most recent node's note. Earlier notes are gone the moment the next node runs. - Built up —
resultshas a reducer, so each printed state shows the list growing: one entry after node_a, two after node_b, three after node_c.
Same graph. Same three nodes. Same kind of return value. The only difference between
"clobbered" and "built up" is one Annotated[...] type hint in the state schema.
6Prerequisites, cost, mock fallback
- Prerequisites: Python 3.10+ and
pip install langgraph. No API keys, no accounts, no network calls at runtime. - Cost: none — the graph never calls a model or external service.
- Mock fallback: not applicable; this lab is already fully local and
self-contained by design (
local-mocksetup mode).
7Optional deepening
Not required, and intentionally not scaffolded here to keep this lab minimal:
- Checkpoint / time travel — add a checkpointer and replay state as of
node_b. - Human-in-the-loop interrupts — pause before
node_cand inspect the accumulatedresultsmid-run.