LangGraphLangGraph 1.2 · Python 3.10+
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
37 small wins to finish your pathNext lesson

When two nodes write the same key

Both checks want to add a note to the same list. Run it and the program does not quietly get it wrong. It refuses.

Two nodes, one list

python
from typing_extensions import TypedDict

class State(TypedDict):
    notes: list

Two checks, each adding one note. Exactly the fan-out from the last lesson, except that this time they write to the same key.

python
def check_account(state):
    return {"notes": ["account is active"]}

def check_payments(state):
    return {"notes": ["two charges on 3 March"]}
python
from langgraph.graph import StateGraph, START

builder = StateGraph(State)
builder.add_node("check_account", check_account)
builder.add_node("check_payments", check_payments)
builder.add_edge(START, "check_account")
builder.add_edge(START, "check_payments")

Now run it

Example
from langgraph.errors import InvalidUpdateError

try:
    print(builder.compile().invoke({"notes": []}))
except InvalidUpdateError as e:
    print("refused:", e)

By default, a key a node returns replaces whatever was there. For a category or a reply that is right. Two nodes finishing in the same step both want to replace the same key, and there is no sensible answer to which one wins, so LangGraph raises InvalidUpdateError rather than picking for you.

The message tells you the fix. It wants an annotated key, which means telling LangGraph how to combine two values instead of replacing.

The fix is one line

Nothing about the nodes or the edges changes. Only the state.

python
import operator
from typing import Annotated
from typing_extensions import TypedDict

class State(TypedDict):
    notes: Annotated[list, operator.add]

Annotated is plain Python, like TypedDict. It pins an extra piece of information to a type without changing the type. Here that extra piece is operator.add, the function LangGraph should use to put the old value and the new one together. On two lists, adding them joins them.

That second thing has a name. A reducer is the function that combines what is already in the state with what a node returned. No reducer means replace. A reducer means combine, however you say.

python
from langgraph.graph import StateGraph, START

def check_account(state):
    return {"notes": ["account is active"]}

def check_payments(state):
    return {"notes": ["two charges on 3 March"]}
python
builder = StateGraph(State)
builder.add_node("check_account", check_account)
builder.add_node("check_payments", check_payments)
builder.add_edge(START, "check_account")
builder.add_edge(START, "check_payments")
Example
print(builder.compile().invoke({"notes": []})["notes"])

Both notes, in the order the nodes were added. The refusal is gone because there is now an answer to what happens when two writers meet.

You can write your own

A reducer is an ordinary function of two arguments: what is there, and what came back.

python
def keep_longest(current, new):
    return new if len(new) > len(current) else current

class State(TypedDict):
    best_answer: Annotated[str, keep_longest]
Do not put reducers everywhere
Every key without a reducer still replaces, and that is usually what you want. Add one only where two writers can meet, or where you are collecting rather than setting.
Try it yourself
  • Take Annotated back off and confirm the refusal returns.
  • Add a third check and see all three notes arrive.
  • Swap operator.add for a reducer that keeps only the newest note.

Every expert started right here.