LangGraph Agents

Course Content

LangGraph Agents

7 sections · 49 lessons

What is a reducer in LangGraph, and why is it needed for state updates?


The same kind of update under four merge rules[a, b][c][c][a, b][c][a, b, c][m1, m2][m2 edited][m1, m2edited]0.40.70.7oldupdateresultno reduceroperator.addadd_messagesmax
Without a reducer a partial update silently replaces history; add_messages appends but replaces a message that reuses an id.

What you need to know

Python
import operatorfrom typing import Annotated, TypedDictfrom langgraph.graph.message import add_messagesdef keep_last_5(old: list, new: list) -> list:    return (old + new)[-5:]class State(TypedDict):    messages: Annotated[list, add_messages]       # append, replace by id    sources: Annotated[list, operator.add]        # concatenate    best_score: Annotated[float, max]             # keep the largest    recent_errors: Annotated[list, keep_last_5]   # custom, bounded    step: int                                     # overwrite

add_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_messages removes 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.