LangGraph State Flow — Reducer vs. Overwrite

A beginner lab for building intuition about how state moves through a LangGraph StateGraph.

Setup mode: local-mock Time box: 20–30 min No external services Concepts: state schema · channels & reducers

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:

START node_a node_b node_c END

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:

ChannelDeclarationUpdate 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

  1. Read the scaffold (graph_lab.py) before running anything.
  2. Fill in your predictions for each checkpoint before running the graph.
  3. Implement the reducer where marked TODO: YOUR CORE.
  4. Run pip install -r requirements.txt && python graph_lab.py and read the printed state after each node.
  5. Confirm: does last_note show only the latest node while results grows by one entry per node?

5The failure / comparison case

This is the core of the lab, side by side in the same run:

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

7Optional deepening

Not required, and intentionally not scaffolded here to keep this lab minimal: