Annotated types for state fields
Why this matters
State in LangGraph is mutable and shared across nodes: without reducers, concurrent updates or multiple writes to the same field will overwrite each other instead of merging intelligently. Annotated types let you define merge behavior declaratively without custom merge logic.
Explanation
What it is: Annotated[Type, reducer_function] is a way to attach a reducer function to a state field. The reducer defines how to combine the current field value with a new update, enabling intelligent merging instead of replacement. How it works: When a node updates a state field annotated with a reducer, LangGraph calls the reducer function with (current_value, update_value) and stores the result. For example, Annotated[list, operator.add] means "append new items to the list instead of replacing it." Without the reducer, a list update would overwrite the old list entirely. When to use it: Use Annotated whenever a state field might be updated by multiple nodes or in multiple steps: lists that accumulate messages, dicts that merge metadata, or counters that increment.
Analogy
Think of state fields like a whiteboard that multiple team members write on. Without a reducer, each person who writes erases what came before. With a reducer, you're saying "I want you to append my notes to the existing list" or "merge my edits with what's already there" instead of erasing.
Code
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
import operator
class State(TypedDict):
messages: Annotated[list, operator.add]
counter: Annotated[int, operator.add]
def node_a(state: State) -> dict:
return {
"messages": ["from node_a"],
"counter": 1
}
def node_b(state: State) -> dict:
return {
"messages": ["from node_b"],
"counter": 2
}
graph = StateGraph(State)
graph.add_node("a", node_a)
graph.add_node("b", node_b)
graph.add_edge(START, "a")
graph.add_edge("a", "b")
graph.add_edge("b", END)
compiled_graph = graph.compile()
result = compiled_graph.invoke({})
print("Messages:", result["messages"])
print("Counter:", result["counter"]) Messages: ['from node_a', 'from node_b'] Counter: 3
What just happened?
Node A returned a list with one message and counter=1. LangGraph initialized state with those values. Node B then returned a list with one message and counter=2. Because both fields have <code>operator.add</code> as the reducer, LangGraph called <code>operator.add(["from node_a"], ["from node_b"])</code> for messages (resulting in a combined list) and <code>operator.add(1, 2)</code> for counter (resulting in 3). Without the Annotated reducers, node B's updates would have completely replaced node A's: messages would only contain ["from node_b"] and counter would be 2.
Common gotcha
Developers often forget that without a reducer, state updates are replacement, not merge. They write code expecting a list to accumulate items, but instead each node's update wipes out the previous one. The fix: always use Annotated[list, operator.add] for accumulating lists, Annotated[dict, operator.or_] for merging dicts, and Annotated[int, operator.add] for counting.
Error recovery
TypeError: unsupported operand type(s)State field keeps getting overwrittenImportError: cannot import AnnotatedExperienced dev note
The mental model: state reducers are exactly like Redux reducers or stream fold operations. If you've written event-sourced systems or worked with accumulator functions, Annotated is just making that pattern explicit and built-in. The win: you never write custom merge logic or debug "why did my data vanish" in parallel branches. Let the framework handle it declaratively. Also: operator.add, operator.or_, and operator.iadd (in-place add) are your friends: they're fast and clear about intent.
Check your understanding
If two nodes both update the same Annotated[list, operator.add] field in parallel branches that reconverge, what list do you get in the final state? What would happen if the field was plain list with no Annotated reducer?
Show answer hint
A correct answer explains that with the reducer, both nodes' list contributions get combined (operator.add concatenates them), whereas without it, one update would overwrite the other depending on execution order. The key insight is understanding that Annotated controls merge semantics, not just type hints.