Click any words you think contain an error. Click again to unmark.
The hits field needs Annotated[list[str], operator.add] so parallel workers' contributions concatenate instead of last writer wins clobbering each other.
Picture a group project where four students each write a paragraph and submit it to the same shared document. If the document only keeps the most recent paste, only the last student's paragraph survives and the other three are gone. To save everyone's work you need a rule that says append instead of replace. LangGraph's state has the same problem when several agents run in parallel. The fix is to tell the state, in advance, 'for this field, combine the updates by adding them together.' Without that rule, the last worker wins and everyone else's contribution silently vanishes.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
This is the most reported bug in LangGraph multi-agent code. It shows up almost identically every time: a developer builds a fan-out of N research workers, each worker returns its own contribution to a shared list, and the downstream reducer somehow sees only one worker's data. The fan-out worked. Every worker logged its output. The data simply vanished between the worker's return and the next node's input.
The cause is a one-line omission in the state schema. The cure is a one-line fix. The diagnostic is worth knowing cold because the framework gives no warning and the symptom is silent.
LangGraph's per-key merge contract
Every node in a LangGraph workflow returns a partial state dict. The runtime takes those partial dicts and merges them into the existing state on a per-key basis. The merge rule is the reducer.
Reducers are declared on the state schema, not on the node. For each field in the TypedDict, you can attach a reducer using the Annotated type hint:
hits: Annotated[list[str], operator.add]
The first slot is the type (used for type checking). The second slot is the reducer (used at merge time). LangGraph reads the annotation at graph-compile time and wires the reducer into the merge pipeline for that key.
The default is overwrite
If a field has no reducer annotation (plain list[str] instead of Annotated[list[str], operator.add]), LangGraph uses overwrite. The new value replaces the existing value. For most scalar fields (status flags, counters, current-step pointers) this is the right default and is what you want.
For accumulating fields (lists of hits, message logs, score arrays) overwrite is exactly wrong. When two parallel workers each return their own list, overwrite keeps only the last one. The other worker's data is silently discarded.
The framework cannot guess which fields are which from the type alone. Both hits (which should accumulate) and status (which should overwrite) might have type list[str] or str. The annotation is how you tell the framework which semantic you want.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
from typing import Annotated, TypedDict
import operator
from langgraph.graph import StateGraph
class State(TypedDict):
question: str
hits: Annotated[list[str], operator.add] # reducer: concatenate
summary: str # reducer: overwrite (default)
def research_worker(state: State) -> State:
new_hits = retrieve(state['question'])
return {'hits': new_hits} # now safely merged across parallel workers
# Custom reducer example for dedup on append:
# def dedup_extend(existing: list[str], update: list[str]) -> list[str]:
# seen = set(existing)
# return existing + [x for x in update if not (x in seen or seen.add(x))]
# hits: Annotated[list[str], dedup_extend]Real products, models, and research that use this idea.
- LangGraph's `create_react_agent` prebuilt uses `add_messages` (a custom reducer for chat-message merging) on its messages field, which is the framework-blessed example of the reducer pattern.
- Production research-agent workflows at companies like Perplexity-style search products rely on the operator.add reducer for parallel-worker hit collection; the bug shows up in QA when the answer cites fewer sources than expected.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you write a custom reducer that dedups by URL while preserving the order of first appearance?
Take the existing list as a base, iterate the update preserving order, and add each item only if its URL is not already in a seen set built from the existing list. Return the extended list.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Trusting that LangGraph will merge list-typed fields sensibly by default. The default is overwrite; merging requires an explicit reducer annotation per accumulating field.
60 second bullets to scan on the way to the call.
LangGraph's per-key merge contract and the default overwrite semantic
Why a plain list of str type alone is insufficient for accumulating fields
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.