What reducers do: merging state updates
Why this matters
In agentic workflows, multiple nodes might update the same field (like a list of messages or a running total). Without reducers, each update overwrites the previous one: you lose data. Reducers let you append, concatenate, or combine updates intelligently.
Explanation
What it is: A reducer is a function that takes the current value of a state field and an incoming update, then returns the merged result. It runs automatically whenever that field changes.
How it works mechanically: When you define a state field with a reducer in langgraph, you're saying "whenever this field gets updated, don't just replace it: call this function with (current_value, new_value) and use the return value as the final state." The reducer function runs on the langgraph side, before the state is stored. This is different from a node function; reducers only care about merging, not logic.
When to use: Use reducers for list fields (append messages), dictionary fields (merge dicts), or numeric fields (sum updates). If a field is ever written to by multiple nodes or you want to accumulate updates, add a reducer.
Analogy
A reducer is like a mail sorting system. Each node drops off an update in a mailbox. Instead of the mailbox contents being replaced each time, the reducer says 'combine this new envelope with what's already in the mailbox.' Without a reducer, each new letter just replaces the old ones. With a reducer, they stack up.
Code
from typing import Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.types import StateSnapshot
import operator
from typing_extensions import TypedDict
class State(TypedDict):
messages: Annotated[list, operator.add]
counter: Annotated[int, operator.add]
state_graph = StateGraph(State)
def node_a(state: State) -> dict:
return {"messages": ["Node A says hello"]}
def node_b(state: State) -> dict:
return {"messages": ["Node B says goodbye"]}
def increment_node(state: State) -> dict:
return {"counter": 5}
state_graph.add_node("node_a", node_a)
state_graph.add_node("node_b", node_b)
state_graph.add_node("increment", increment_node)
state_graph.add_edge(START, "node_a")
state_graph.add_edge("node_a", "node_b")
state_graph.add_edge("node_b", "increment")
state_graph.add_edge("increment", END)
graph = state_graph.compile()
result = graph.invoke({"messages": [], "counter": 0})
print("Final messages:", result["messages"])
print("Final counter:", result["counter"]) Final messages: ['Node A says hello', 'Node B says goodbye'] Final counter: 5
What just happened?
The state was defined with two fields: <code>messages</code> with <code>operator.add</code> as its reducer, and <code>counter</code> also with <code>operator.add</code>. When node_a ran, it returned a dict with messages as a single-item list. The reducer merged it with the empty initial list using addition (list concatenation), resulting in ['Node A says hello']. When node_b ran next, its return value ['Node B says goodbye'] was added to the existing list via the same reducer, producing ['Node A says hello', 'Node B says goodbye']. For counter, increment_node returned 5, which was added to the initial 0 via operator.add, giving 5. The final state contained both accumulated results.
Common gotcha
Developers often forget that the reducer function signature is reducer(current_value, new_value) and returns the merged result: it's NOT a node function that receives the full state. If you use operator.add, it literally does current + new. For lists, that's concatenation. For ints, that's arithmetic addition. For strings, that's string concatenation. If you write a custom reducer, it must accept exactly two arguments and return the merged value, or you'll get a signature error at runtime.
Error recovery
TypeError: unsupported operand type(s)TypeError: reducer() missing 1 required positional argumentExperienced dev note
Reducers are not where you put business logic. That's a trap. A reducer should be a pure merge function: fast, side-effect-free, deterministic. Put validation, filtering, and decision-making in node functions, then return the raw data for the reducer to merge. If you're tempted to call an API or run a model inside a reducer, that's a sign your logic belongs in a node instead. Also: reducers run synchronously on every state update, so they must be fast. A slow reducer blocks the graph.
Check your understanding
You have a field attempts: Annotated[list, operator.add] in your state. Node A returns {"attempts": ["try_1"]}, then Node B returns {"attempts": ["try_2"]}. Without running the code, what will the final state's attempts field be, and why?
Show answer hint
The answer requires understanding that <code>operator.add</code> concatenates lists, not replaces them. The correct answer is that <code>attempts</code> will be <code>["try_1", "try_2"]</code>, because each return value is merged into the existing list by addition, preserving both. This only works because the reducer was declared; without it, the second update would have overwritten the first.
Annotated[type, reducer_function] syntax. In early 0.1.x versions, the reducer pattern was less standardized. If you're on < 0.2.0, consult your version's StateGraph documentation: the pattern may differ.