AI Agents LangGraph
Reducers
Intermediate
This post covers everything you need to know about Reducers in LangGraph — how they manage and merge state updates across nodes, especially during parallel execution. We explore built-in and custom reducers, conflict resolution strategies, reducers for messages and cyclic graphs, common mistakes developers make, and best practices for building reliable and predictable LangGraph workflows.
Reducers in LangGraph
Reducers are one of the
most important concepts
in LangGraph. They define
how state updates from different nodes are merged
into the shared state.
What Are Reducers?
A Reducer is a function that tells LangGraph how to combine a new update with the existing value of a state field.
When multiple nodes try to update the same field (especially in parallel execution or cycles), LangGraph uses the reducer to decide the final value.
Without reducers, LangGraph wouldn’t know how to safely merge updates, especially for lists like messages.
Why Reducers Are Needed
Imagine two nodes running in parallel both trying to update the
messages
list:
-
Node A returns:
{"messages": [msg1]} -
Node B returns:
{"messages": [msg2]}
- Replace the list?
- Append both?
- Take the last one?
- Safe concurrent updates
- Predictable behavior in cycles and parallel flows
- Clean accumulation of messages, documents, logs, etc.
How Reducers Work
You define reducers using
Annotated
when declaring your state:
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
from operator import add
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # ← Reducer
documents: Annotated[list[dict], add] # ← Reducer
iterations: int # No reducer = last write wins
metadata: Annotated[dict, lambda a, b: {**a, **b}] # Custom reducer
Execution Flow:
1.
Use Pydantic
- Node returns partial update → {"messages": [new_msg]}
- LangGraph calls the reducer: add_messages(old_messages, [new_msg])
- Result becomes the new value in state
Built-in Reducer Patterns
1.
add_messages
(Most Important)
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
Used for conversation history. Automatically handles
AIMessage
,
HumanMessage
,
ToolMessage
, etc.
2. operator.add (for lists)
from operator import add
class AgentState(TypedDict):
documents: Annotated[list[dict], add] # Concatenates lists
tool_calls: Annotated[list, add]
3. Last Write Wins (Default)
If no reducer is specified, the last update wins:
class AgentState(TypedDict):
current_status: str # Last node to write wins
final_answer: str | None
Custom Reducers
You can create your own reducers for complex merging logic.
def merge_dicts(left: dict, right: dict) -> dict:
"""Merge two dictionaries, right takes precedence"""
return {**left, **right}
def deduplicate_list(left: list, right: list) -> list:
"""Append only new items"""
seen = {id(x) for x in left}
return left + [x for x in right if id(x) not in seen]
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
metadata: Annotated[dict, merge_dicts]
unique_docs: Annotated[list[dict], deduplicate_list]
Advanced Custom Reducer with Pydantic:
from pydantic import BaseModel
def merge_tool_results(left: list, right: list):
return left + [r for r in right if r not in left]
class AgentState(BaseModel):
tool_results: Annotated[list[dict], merge_tool_results] = Field(default_factory=list)
Reducers in Parallel Execution
Reducers become critical when nodes run in parallel.
from langgraph.types import Send
def parallel_router(state):
return [
Send("web_search", state),
Send("vector_search", state),
Send("news_search", state)
]
# All three nodes can update the same field safely
class AgentState(TypedDict):
documents: Annotated[list[dict], add] # Reducer handles parallel updates
sources: Annotated[list[str], add]
graph.add_conditional_edges(START, parallel_router)
graph.add_edge("web_search", "aggregate")
graph.add_edge("vector_search", "aggregate")
graph.add_edge("news_search", "aggregate")
Without a proper reducer like
add
, parallel updates would conflict or overwrite each other.
Key Takeaways:
- Reducers define how state fields are merged
- add_messages is the most commonly used reducer
- Always use reducers for lists and complex data types
- Custom reducers give you full control over merging logic
- Reducers are essential for safe parallel and cyclic execution
Conflict Resolution in Parallel Updates
One of the most powerful features of reducers is how they handle
parallel execution
. When multiple nodes run at the same time and update the same state field, reducers resolve conflicts automatically.
from langgraph.types import Send
from operator import add
class AgentState(TypedDict):
documents: Annotated[list[dict], add] # Safe for parallel updates
sources: Annotated[list[str], add]
messages: Annotated[list, add_messages]
def parallel_router(state):
"""Launch multiple searches in parallel"""
return [
Send("web_search", state),
Send("vector_search", state),
Send("news_search", state)
]
graph.add_conditional_edges(START, parallel_router)
# All three nodes can safely return updates
def web_search(state):
results = ...
return {"documents": results, "sources": ["web"]}
def vector_search(state):
results = ...
return {"documents": results, "sources": ["vector"]}
How Conflict Resolution Works:
- add → Concatenates lists from all parallel branches
- add_messages → Intelligently merges messages (preserves order and metadata)
- Custom reducer → You define the exact merging logic
Reducers with Messages (
add_messages
)
This is the most important reducer in LangGraph.
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
How add_messages works:
- Appends new messages to the conversation history
- Handles different message types (HumanMessage, AIMessage, ToolMessage)
- Preserves message metadata and tool call IDs
- Prevents duplicate messages in some cases
def agent_node(state: AgentState):
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]} # add_messages will append
def tools_node(state: AgentState):
tool_results = execute_tools(...)
return {"messages": [ToolMessage(...)]} # Automatically appended
Advanced Usage:
# You can also pass multiple messages
return {
"messages": [
AIMessage(content="Let me check that..."),
ToolMessage(content="Tool result here", tool_call_id="123")
]
}
Reducers in Cyclic Graphs
Reducers are essential in loops because the same fields (especially messages) get updated repeatedly.
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
iterations: int = 0
def agent_node(state: AgentState):
response = llm.invoke(state["messages"])
return {
"messages": [response],
"iterations": state.get("iterations", 0) + 1
}
def router(state: AgentState):
if state.get("iterations", 0) >= 12:
return "END"
if state["messages"][-1].tool_calls:
return "tools"
return "END"
Why reducers shine in cycles:
-
add_messagessafely accumulates conversation history across many iterations - You can track progress with counters
- State remains consistent even after many loops
Common Reducer Mistakes
1. Forgetting Reducers on Lists
# Wrong
documents: list[dict] # Last write wins → loses data in parallel
2. Using Mutable Defaults
# Dangerous
documents: list = [] # Shared mutable default!
Correct:
documents: list[dict] = Field(default_factory=list) # Pydantic
# or
documents: Annotated[list, add] # TypedDict
3. Overcomplicating Custom Reducers
Creating overly complex reducers instead of using simple ones like
add
.
4. Returning Full State Instead of Partial
# Bad
return state.copy() # Risky and inefficient
Best Practices for Reducers
Always use reducers for lists and complex types
messages: Annotated[list, add_messages]
documents: Annotated[list[dict], add]
Prefer built-in reducers ( add_messages , add ) when possible
Keep custom reducers simple and pure
def merge_metadata(left: dict, right: dict):
return {**left, **right}
Use Pydantic
Field(default_factory=...)
for better defaults
Document your reducers
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # Conversation history
tool_outputs: Annotated[list, add] # All tool results
metadata: Annotated[dict, merge_metadata] # Control data
Test reducers independently during development
Reducers are the glue that makes stateful, parallel, and cyclic execution safe and predictable in LangGraph.
Mastering reducers is essential for building robust, scalable AI agents.
AI agent LangChain LangGraph Python