Most beginner graphs work because every node updates a different field. Real workflows stop being that polite. Multiple steps may append to messages, collect findings, add search results, or build a running log. That is where reducers matter.
A reducer tells LangGraph how to combine a new update with existing state for a specific field. Without that merge rule, an append-like field can be accidentally replaced, which turns debugging into guesswork.
This page is about disciplined mutation: knowing exactly which fields replace, which append, and which should be summarized before they grow forever.
A reducer is not a general business-rule engine. It is a merge rule for one field. When two updates touch that field across steps, the reducer defines whether values replace, append, concatenate, or otherwise combine.
This is especially common for message histories, accumulated documents, logs, and lists of extracted facts.
If you do nothing, the intuitive assumption is usually replacement. That is fine for many current-value fields. It is dangerous for conversation memory or audit trails because each node can erase what came before.
Explicit reducers communicate intent. A teammate reading the state definition should know immediately whether a field is a timeline or a single current value.
Appending forever is not a strategy. Message-heavy graphs become expensive, slow, and noisy if you never trim or summarize state. Reducers solve merge correctness, not memory hygiene.
As soon as a workflow can run across many turns or many tool calls, decide what stays verbatim, what gets summarized, and what can be dropped after checkpointed persistence.
Production systems often need more than plain append. You may want a reducer that deduplicates documents by ID, keeps only the top N findings, or merges tool results keyed by tool name.
The main rule is predictability. If a reducer hides surprising logic, the graph becomes harder to reason about than if you had used explicit nodes to maintain those structures.
Imagine two nodes both write to `messages`. If the field replaces, the second node wipes out the first node output. If the field appends, the state preserves the conversation trail. Same nodes, different merge rule, very different graph behavior.
That is why reducers are part of architecture, not syntactic trivia.
Reducers decide how multiple updates to the same state field combine. That makes them collaboration rules for nodes. A message history may append. A counter may add. A status field may overwrite. A risk list may merge unique items. Choosing the wrong reducer can silently corrupt workflow meaning.
Default overwriting is often safest for fields with one owner. If only the classifier should set `category`, overwriting is clear. If many nodes can append observations, a reducer may be appropriate. Do not use append reducers because they feel convenient; use them because multiple writers are intentional.
Reducers should be tested with small examples. Give them old state, two updates, and expected merged state. This catches duplicates, order sensitivity, lost fields, and accidental mutation.
State updates should be minimal. A node should return only the fields it owns or intentionally changes. Returning the whole state from every node makes it easy to overwrite unrelated fields and hard to trace responsibility.
LangGraph state should be treated as an immutable contract between nodes. Nodes receive state and return updates. Mutating nested objects in place can create confusing behavior, especially when checkpoints, retries, or parallel branches are involved.
Design state fields by lifecycle. Some fields are input facts, some are working decisions, some are accumulated evidence, some are operational metadata, and some are final output. Mixing all of these into one dictionary field removes the benefits of typed state.
Parallelism makes update discipline even more important. If multiple branches write to the same field, define a reducer and decide how conflicts resolve. If branches should not conflict, give them separate fields and merge later in a dedicated node.
Finally, state design is product design. The fields you choose determine what can be explained, resumed, tested, and audited. A graph with clear state is much easier to make reliable than a graph that hides meaning inside message history.
Print the state schema and mark each field with its owner, lifecycle, reducer, and privacy level. If no node clearly owns a field, the field is likely to become confusing. If many nodes own it, it probably needs a reducer or a merge node.
Then review one complete run and identify every state update. Each update should be small, intentional, and traceable. Large updates that rewrite unrelated fields make debugging harder and increase the chance of accidental data loss.
Finally, write reducer tests for every merged field. Reducer bugs are subtle because the graph may still run while producing incorrect accumulated state.
This is the canonical reducer pattern: keep every message instead of replacing the list on each node update.
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class ChatState(TypedDict):
messages: Annotated[list[str], operator.add]
def user_turn(state: ChatState) -> dict:
return {"messages": ["User: Where is my order?"]}
def assistant_turn(state: ChatState) -> dict:
return {"messages": ["Assistant: I will look it up."]}
Many graphs need mixed behavior: one field should always show the newest status while another preserves the history.
import operator
from typing import Annotated
from typing_extensions import TypedDict
class ReviewState(TypedDict):
status: str
audit_log: Annotated[list[str], operator.add]
def validate_invoice(state: ReviewState) -> dict:
return {
"status": "validated",
"audit_log": ["validator: invoice fields passed"],
}
def send_for_approval(state: ReviewState) -> dict:
return {
"status": "waiting_for_approval",
"audit_log": ["workflow: sent to approver queue"],
}
A custom reducer can deduplicate retrieved items when multiple search nodes may surface the same source.
from typing import Annotated
from typing_extensions import TypedDict
def merge_unique_docs(existing: list[dict], incoming: list[dict]) -> list[dict]:
by_id = {doc["id"]: doc for doc in existing}
for doc in incoming:
by_id[doc["id"]] = doc
return list(by_id.values())
class RetrievalState(TypedDict):
docs: Annotated[list[dict], merge_unique_docs]
No. Messages are the common example, but reducers are useful for logs, sources, findings, and any field that should merge instead of replace.
You can, but it becomes brittle and easy to overwrite accidentally as the graph grows.
Silent state corruption. The graph may run successfully while carrying incomplete or duplicated context.
Explore 500+ free tutorials across 20+ languages and frameworks.