langgraph "At key 'messages': Can receive only one value per step"
Fixes LangGraph's can receive only one value per step error on the messages channel. Use when parallel branches both write to messages in the same step, when fan-out nodes append messages concurrently, or when an agent's parallel tool calls collide. Not for invalid-update errors, for recursion limits, or for reducer misconfigurations.
TL;DR
Two parallel nodes wrote to the messages channel in the same super-step, and without a reducer that merges lists, LangGraph rejects the second write. Fix it by declaring messages with an append reducer (Annotated[list, add_messages]), or by routing the parallel branches through a single writer node.
langgraph "At key 'messages': Can receive only one value per step"Use this when
- Parallel branches in a LangGraph run fail with this message
- Fan-out nodes each append to messages
- An agent fans out tool calls that all write messages
Not for this skill when
- Updates arent dicts at all (thats the invalid-update error)
- The graph loops (thats the recursion limit)
- A single node writes twice (restructure the node)
Steps
- Confirm the collision: the error names the key and the step. Check which nodes ran in parallel:
for step in app.stream(input, stream_mode="updates"):
print(list(step.keys()))Expected output: two node names in the same step dict. Those are the writers colliding on messages.
- Declare the messages channel with a merging reducer:
from typing import Annotated
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]Expected output: parallel writes now merge instead of colliding. add_messages appends; without the annotation the channel accepts exactly one write per step.
- If only one branch should own messages, serialize the writes instead:
# route branch_a, then collector, then branch_b, with collector merging resultsExpected output: the graph runs the writers in sequence and the error disappears. Use this when merge order matters and parallel appends would interleave unpredictably.
- For fan-out tool patterns, collect tool results in a separate channel and write messages once:
class State(TypedDict):
messages: Annotated[list, add_messages]
tool_results: Annotated[list, operator.add]Expected output: parallel nodes write tool_results, one downstream node formats them into messages. No collision, deterministic ordering.
Variant phrasings
same error on a custom key, not messages
That key also needs a reducer. Any channel written by parallel nodes needs one; operator.add works for lists.
error appears only with certain inputs
Only some inputs trigger the parallel path. The fix is the same; the input just decides whether the collision happens.
Why it happens
LangGraph executes parallel branches in the same super-step, and each channel accepts one write per step unless a reducer defines how to combine multiple. messages without add_messages is single-write, so the second parallel write is rejected. The default tutorials use the annotated form, but hand-built or agent-built schemas often declare a bare list and only fail when the graph actually fans out.
Edge cases
add_messagesdedupes by message id; parallel branches emitting identical ids will silently merge, which can look like lost messages.- Reducers run in branch completion order, not definition order; dont rely on ordering across parallel writes.
- Conditional edges that skip one branch avoid the collision, which is why the error is input-dependent.
Provenance
Resolved from the public thread: https://vectle.com/posts/pstuyyu5Ygw0iFdms4ZkCoXQ