Course Content
LangGraph Agents
7 sections · 49 lessons
What is a reducer in LangGraph, and why is it needed for state updates?
What you need to know
1import operator2from typing import Annotated, TypedDict3from langgraph.graph.message import add_messages45def keep_last_5(old: list, new: list) -> list:6 return (old + new)[-5:]78class State(TypedDict):9 messages: Annotated[list, add_messages] # append, replace by id10 sources: Annotated[list, operator.add] # concatenate11 best_score: Annotated[float, max] # keep the largest12 recent_errors: Annotated[list, keep_last_5] # custom, bounded13 step: int # overwriteadd_messages in detail
- Appends new messages to the list.
- Converts dicts like
{"role": "user", "content": "hi"}into message objects and gives them ids. - If a new message has the same id as an existing one, it replaces it. This is how you edit a message.
- A
RemoveMessage(id=...)deletes that message;RemoveMessage(id=REMOVE_ALL_MESSAGES)clears the list.
Bypassing a reducer
Sometimes you want to reset an appending key. Return Overwrite(value) from langgraph.types: {"sources": Overwrite([])} replaces the list instead of adding to it.
Writing your own
Parallel branches finish in any order, so a reducer should give the same result regardless of order. operator.add on lists changes order but keeps all items; max and set union are fully order-independent. If order matters, sort in the node that reads the list.
A real-life example
A research agent has three parallel search nodes — news, filings and analyst notes — all writing sources. The first version declared sources: list. The run crashed with InvalidUpdateError: At key 'sources': Can receive only one value per step. A developer "fixed" it by running the searches one after another, tripling latency from 4 seconds to 12. The real fix was one line: sources: Annotated[list, operator.add]. The searches run in parallel again and all 27 sources arrive in state.
Follow-up questions to expect
- "What is the default reducer?" — None: the new value replaces the old one, and only one write per super-step is allowed.
- "How do you delete old messages?" — Return
RemoveMessage(id=...)objects for those messages;add_messagesremoves them. - "Can a reducer call an LLM?" — It should not. Reducers must be fast, pure and deterministic; they run on every update and during replay.