Why do LangGraph state fields need reducers?
Any LangGraph state field that should accumulate or that parallel nodes might write in one superstep needs a reducer; without one, last write wins overwrite silently drops history.
Picture a shared whiteboard during a brainstorming meeting. By default, when someone walks up and writes on a section, they wipe whatever was there and write their own line. That works for a status field that just shows the latest temperature. It fails badly for the section where everyone is supposed to add an idea. You would end up with only the last person's idea on the board. The fix is a rule per section that says 'add to this section, do not replace it.' That rule is the reducer.
Detailed answer & concept explanation~5 min readEverything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example.
Everything important, quickly.
5 minutes: the default overwrite rule, the Annotated reducer syntax, what add_messages actually does, and why parallel branches break without reducers.
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
import operator
class State(TypedDict):
# Chat history: needs append-semantics, de-duplication by ID
messages: Annotated[list[BaseMessage], add_messages]
# Retrieved docs accumulated across parallel retriever nodes
docs: Annotated[list[dict], operator.add]
# Latest value wins: no reducer needed
current_step: str
# Custom reducer for merging partial worker outputs
worker_results: Annotated[dict, lambda a, b: {**a, **b}]| Field shape | Reducer | Why |
|---|---|---|
| Chat history | `add_messages` | Append + de-dup by message ID |
| Accumulating list | `operator.add` | Concatenate lists across nodes |
| Merging dicts | Custom lambda or function | Spread-merge keys from parallel writers |
| Latest status / flag | No reducer (overwrite) | Last write reflects current truth |
| Counter | Custom `lambda a, b: a + b` | Sum increments from parallel branches |
Real products, models, and research that use this idea.
- LangGraph's official chat tutorial uses `Annotated[list, add_messages]` as the canonical pattern; every LangGraph chat example you read uses this shape.
- LangChain's `create_react_agent` builds its state on top of `MessagesState`, which is a pre-built TypedDict with the `add_messages` reducer baked in.
- Production LangGraph deployments commonly write custom reducers to merge structured outputs from parallel worker branches. E.g. accumulating retrieval results from three retriever nodes that all run in the same superstep.
- The Send API for dynamic fan-out (`Send('worker', payload)`) is one of the most common sources of parallel writes and depends on reducers to merge worker results into shared state.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you write a custom reducer to merge ranked retrieval results from three parallel retriever nodes?
QWhat does `add_messages` do that `operator.add` does not?
Don't say thisRed flags and common mistakes that signal junior thinking. Click to expand.
Red flags and common mistakes that signal junior thinking. Click to expand.
Declaring `messages: list[BaseMessage]` without `Annotated[..., add_messages]` and then debugging why chat history disappears after every node return.
The night-before-the-interview bullets. Scan these on the way to the call.
Primary sources. Skim if you want the original framing.
Same topic, related formats. Practice these next.