Reducers: combining values in the state
A reducer decides how a node's returned value for a key is combined with the value already in the state. By default a returned key overwrites the old value; a reducer can append instead.
Last updated: 27 Sep, 2026 · LangGraph 1.2
Overwriting is fine for a category or a reply. It is wrong for a running list, such as notes gathered by several steps, where each step should add to the list, not replace it.
How you declare a reducer
from typing_extensions import TypedDict, Annotated
import operator
class State(TypedDict):
plain: int # no reducer: overwrite
acc: Annotated[list[int], operator.add] # reducer: append (list + list)The state
Start with the state. plain is a normal key. acc is wrapped in Annotated with operator.add, the reducer that appends to a list instead of overwriting it.
from typing_extensions import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
plain: int # no reducer: each write overwrites
acc: Annotated[list[int], operator.add] # reducer: each write is appendedThe two writing nodes
Add two nodes that run one after the other. Each writes to both keys, so both keys are written twice during the run.
builder = StateGraph(State)
builder.add_node("n1", lambda s: {"plain": 1, "acc": [1]}) # writes 1 to both keys
builder.add_node("n2", lambda s: {"plain": 2, "acc": [2]}) # writes 2 to both keys
builder.add_edge(START, "n1")
builder.add_edge("n1", "n2")
builder.add_edge("n2", END)Running the graph
Compile and run once, starting from an empty list, and print the final state.
print(builder.compile().invoke({"plain": 0, "acc": []}))The reducer in a full run
The same pieces in one file, ready to run.
from typing_extensions import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
plain: int # no reducer: overwrite
acc: Annotated[list[int], operator.add] # reducer: append (list + list)
builder = StateGraph(State)
builder.add_node("n1", lambda s: {"plain": 1, "acc": [1]})
builder.add_node("n2", lambda s: {"plain": 2, "acc": [2]})
builder.add_edge(START, "n1")
builder.add_edge("n1", "n2")
builder.add_edge("n2", END)
print(builder.compile().invoke({"plain": 0, "acc": []}))What each key ended as
plainhas no reducer, so each write overwrites: it ends at 2, the last value written.accis annotated withoperator.add, so the two writes combine:[1] + [2]becomes[1, 2].- A reducer is a small function that takes the old value and the new one and returns the combined value.
Default vs reducer
| Key | Behavior | Ends as |
|---|---|---|
plain (no reducer) | Overwrite with the latest write | 2 |
acc (operator.add) | Append each write | [1, 2] |
When you reach for a reducer
- Collecting results from steps that run in parallel (each adds to a list).
- A conversation's messages, which must append, not replace, one message at a time.
- Any running total or log the graph builds up across steps.
Related
- Previous: Conditional edges: choosing a path
- Next: Parallel nodes: fan-out with edges
- Reference: Graph API: reducers
- Remove the
Annotated[...]onaccand run again. What isaccnow? - Change the reducer to keep only the largest value instead of appending.
- Add a third node that writes
acc: [3]and predict the final list.
Slow is fine. Stopping is the only problem.