Tutorials
Ren Okabe8 min read85 views

LangGraph InvalidUpdateError: Fix "Can Receive Only One Value Per Step" (2026)

InvalidUpdateError: Can receive only one value per step means two parallel LangGraph nodes wrote the same state key with no reducer. Reproduce it in 15 lines and fix it the right way in Python and TypeScript (2026).

Parallel railway tracks converging into a single line, evoking how parallel LangGraph nodes must merge their updates into one state key through a reducer.
Parallel railway tracks converging into a single line, evoking how parallel LangGraph nodes must merge their updates into one state key through a reducer.
On this page

Quick Answer (2026)

InvalidUpdateError: Can receive only one value per step. Use an Annotated key to handle multiple values means two or more nodes wrote to the same state key inside a single LangGraph super-step, and that key has no reducer to merge the writes. It almost always happens when nodes run in parallel (a fan-out, a Send map-reduce, a subgraph that writes a parent key, or a retry edge that re-runs a sibling).

The fix is one line: give the contested key a reducer so LangGraph knows how to combine concurrent updates instead of rejecting them.

python
import operator
from typing import Annotated
from typing_extensions import TypedDict

class State(TypedDict):
    # Was: results: list  ->  raised InvalidUpdateError under parallel writes
    results: Annotated[list, operator.add]   # concatenate every parallel write

If you only ever want the graph to keep one value for that key, the real fix is to stop two nodes from writing it in the same step. This guide reproduces the error in about 15 lines, explains the super-step model that causes it, and gives you the right reducer for every state shape, in both Python and TypeScript (verified against LangGraph, August 2026).

Before you start

You need Python 3.10+ and a current LangGraph install. Everything below runs with no API key, because the error has nothing to do with the LLM: it is pure graph state mechanics.

bash
pip install -U langgraph

LangChain LangGraph is LangChain's graph-orchestration library. If you are new to its state model, the sibling walkthrough on building a graph agent in Python sets up the same StateGraph scaffolding this article debugs.

Reproduce InvalidUpdateError in 15 lines

The smallest way to trigger it: fan out from START to two nodes that both return the same scalar key.

python
import operator
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    answer: str                      # scalar, no reducer

def node_a(state: State):
    return {"answer": "from A"}

def node_b(state: State):
    return {"answer": "from B"}

builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")    # both edges leave START,
builder.add_edge(START, "node_b")    # so A and B run in the SAME step
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)

graph = builder.compile()
graph.invoke({"answer": ""})

Run it and you get:

text
langgraph.errors.InvalidUpdateError: At key 'answer': Can receive only one value
per step. Use an Annotated key to handle multiple values.
For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE

Two nodes, one step, one key, no reducer. That is the entire bug.

Why LangGraph raises it (the super-step model)

LangGraph executes in super-steps (borrowed from Google's Pregel model). Every node that is eligible to run at the same time runs in one super-step, and each super-step produces exactly one committed value per state key.

When a single super-step hands LangGraph two writes for answer, it has no rule for which one wins or how to merge them, so instead of silently dropping data it fails loudly. A reducer is that missing merge rule: a function (current_value, new_value) -> merged_value attached to the key. With a reducer, two parallel writes become two reducer calls and the key is well-defined again.

Keys without a reducer use the default "overwrite" behavior, which is safe only when at most one node writes the key per step. Keys with a reducer can absorb any number of concurrent writes.

The fix in Python

Attach the reducer with typing.Annotated. The second argument is the merge function.

python
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    answers: Annotated[list, operator.add]   # list concat merges both writes

def node_a(state: State):
    return {"answers": ["from A"]}           # return a LIST, not a scalar

def node_b(state: State):
    return {"answers": ["from B"]}

builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)

graph = builder.compile()
print(graph.invoke({"answers": []}))
# {'answers': ['from A', 'from B']}   <- order between parallel branches is not guaranteed

Two changes matter: the key is now Annotated[list, operator.add], and each node returns a list so operator.add (list concatenation) has lists to add. This is exactly what the official LangChain docs recommend for the INVALID_CONCURRENT_GRAPH_UPDATE error: "If your graph executes nodes in parallel, make sure you have defined relevant state keys with a reducer."

For conversation history, use the purpose-built message reducer instead of operator.add, because it merges by message id and handles updates and deletions:

python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import AnyMessage

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

Which reducer should you use?

Pick the reducer from the shape of the data and how you want parallel writes combined (2026):

Scroll to see more

State key holdsReducerWhat parallel writes do
A list you want to accumulateoperator.addConcatenate every branch's list
Chat / message historyadd_messagesMerge by message id, apply updates and deletions
A set of unique itemscustom lambda a, b: list({*a, *b})Union, deduplicated
A dict you want mergedcustom lambda a, b: {**a, **b}Shallow-merge keys (last branch wins on conflicts)
A running counteroperator.add (on int)Sum the increments
A single value, only one writer per stepnone (default overwrite)Not applicable, keep it single-writer

The last row is the important one: if the value is genuinely a single scalar and you never want two writers, adding a reducer is the wrong fix. Restructure the graph so only one node writes it per step (see Step 5).

The four ways this error actually shows up

The reproduce above is the textbook case, but in real projects the error surfaces from four distinct patterns. The fix (a reducer, or single-writer restructuring) is the same, so learn to recognize the shape:

  1. Explicit fan-out. Two add_edge(START, ...) calls, or one node with two outgoing edges, put two nodes in the same super-step. Both write the key. Most common.
  2. Send / map-reduce. You dispatch Send("worker", item) for a list of items and every worker writes the same key. All workers land in one super-step. This is the canonical map-reduce pattern and requires a reducer on the collected key.
  3. A subgraph writing a parent key. A compiled subgraph runs as one node but writes a key that a sibling node also writes in the same step. LangGraph raises the same error even though the writers are in different graphs (see langgraph issue #6446).
  4. A retry / loop-back edge. A conditional edge re-runs a node while its sibling is still eligible, so both execute together and collide. This is exactly the report in the canonical issue #2336: "when one of the parallel nodes runs incorrectly, I gave it a conditional edge to retry it," and the retried node collided with its parallel sibling.

For pattern 4, a reducer will silence the error but may hide a real control-flow bug. If a node should not be running in parallel with its retrying sibling, fix the edges, do not paper over it with a reducer.

The same fix in TypeScript

LangGraph.js expresses reducers through Annotation. A channel declared with Annotation({ reducer, default }) behaves exactly like an Annotated key in Python; a bare Annotation() is a single-value channel that will throw InvalidUpdateError under parallel writes.

typescript
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";

const StateAnnotation = Annotation.Root({
  // Was: Annotation()  ->  throws under parallel writes
  answers: Annotation({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
});

const nodeA = async () => ({ answers: ["from A"] });
const nodeB = async () => ({ answers: ["from B"] });

const graph = new StateGraph(StateAnnotation)
  .addNode("nodeA", nodeA)
  .addNode("nodeB", nodeB)
  .addEdge(START, "nodeA")
  .addEdge(START, "nodeB")
  .addEdge("nodeA", END)
  .addEdge("nodeB", END)
  .compile();

await graph.invoke({ answers: [] });
// { answers: ["from A", "from B"] }

For messages, import the prebuilt MessagesAnnotation (or messagesStateReducer if you compose your own root) rather than writing the merge by hand:

typescript
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";

const graph = new StateGraph(MessagesAnnotation)
  // ...addNode / addEdge...
  .compile();

Custom reducers for dicts and "keep latest"

operator.add covers lists and counters. For anything else, write a two-argument function. A shallow dict merge, useful when parallel workers each contribute different keys:

python
from typing import Annotated
from typing_extensions import TypedDict

def merge_dicts(current: dict, new: dict) -> dict:
    return {**current, **new}   # later write wins on key conflicts

class State(TypedDict):
    scratch: Annotated[dict, merge_dicts]

A deliberate "keep the latest non-empty value" reducer, when you truly want one value but two branches may write it:

python
def keep_latest(current, new):
    return new if new is not None else current

class State(TypedDict):
    status: Annotated[str, keep_latest]

Use keep_latest with care: because parallel branch order is not deterministic, "latest" means "whichever branch the scheduler committed last," which can vary run to run. If determinism matters, restructure to a single writer instead. When your graph also persists state, confirm the reducer plays well with your checkpointer, as covered in the LangGraph checkpointer guide.

Common mistakes

  • Adding the reducer but still returning a scalar. Annotated[list, operator.add] with a node that returns {"answers": "A"} will try to add a string to a list and raise TypeError. Return ["A"].
  • Using operator.add on messages. It concatenates raw lists and creates duplicate messages on updates. Use add_messages.
  • Reaching for a reducer to fix a loop bug. If a retry edge is the real cause (pattern 4), the reducer hides a control-flow problem. Fix the edges first. If your loop never terminates, that is a different error, the GraphRecursionError / recursion limit, not InvalidUpdateError.
  • Forgetting default in TypeScript. A reducer channel with no default starts undefined, and undefined.concat(...) throws on the first write. Always give reducer channels a default.
  • Expecting ordered results from parallel branches. Reducers merge in commit order, which is not the order you declared the edges. Sort downstream if you need determinism.

FAQ

What does "Can receive only one value per step" mean in LangGraph?
It means two or more nodes wrote to the same state key during one super-step and that key has no reducer. LangGraph refuses to guess how to merge the writes, so it raises InvalidUpdateError. Add a reducer to the key, or ensure only one node writes it per step.

How do I add a reducer to a LangGraph state key?
In Python, annotate the key: Annotated[list, operator.add] for lists, or Annotated[list[AnyMessage], add_messages] for messages. In TypeScript, declare the channel with Annotation({ reducer, default }). The reducer is a function (current, update) that returns the merged value.

Why does the error appear only when I run nodes in parallel?
Sequential nodes each write in their own super-step, so a key gets one value per step and the default overwrite behavior is safe. Parallel nodes (fan-out, Send, subgraphs, or retry edges) share a super-step, so the same key can receive multiple writes at once, which needs a reducer.

Can I just use operator.add for everything?
No. operator.add is correct for lists and numeric counters. For chat history use add_messages (it dedupes by id). For dict merges or set unions, write a small custom reducer. Using operator.add on messages creates duplicates.

Is a reducer always the right fix?
No. If the key is genuinely a single value and two nodes should not both write it, a reducer masks a graph-design bug. The cleaner fix is to route the graph so only one node writes that key per super-step, then keep the key reducer-free.

R

Written by

Ren Okabe

Ren Okabe builds and debugs LLM agent systems, with a focus on LangGraph orchestration, evals, and production reliability.

Frequently asked questions

What does "Can receive only one value per step" mean in LangGraph?

It means two or more nodes wrote to the same state key during one super-step and that key has no reducer. LangGraph refuses to guess how to merge the writes, so it raises InvalidUpdateError. Add a reducer to the key, or ensure only one node writes it per step.

How do I add a reducer to a LangGraph state key?

In Python, annotate the key: Annotated[list, operator.add] for lists, or Annotated[list[AnyMessage], add_messages] for messages. In TypeScript, declare the channel with Annotation({ reducer, default }). The reducer is a function (current, update) that returns the merged value.

Why does the error appear only when I run nodes in parallel?

Sequential nodes each write in their own super-step, so a key gets one value per step and the default overwrite behavior is safe. Parallel nodes (fan-out, Send, subgraphs, or retry edges) share a super-step, so the same key can receive multiple writes at once, which needs a reducer.

Can I just use operator.add for everything?

No. operator.add is correct for lists and numeric counters. For chat history use add_messages, which dedupes by id. For dict merges or set unions, write a small custom reducer. Using operator.add on messages creates duplicate messages.

Is a reducer always the right fix?

No. If the key is genuinely a single value and two nodes should not both write it, a reducer masks a graph-design bug. The cleaner fix is to route the graph so only one node writes that key per super-step, then keep the key reducer-free.