langgraph 'invalid update: expected a dict' checkpoint error
Fixes LangGraph checkpoint errors where a state update isnt a dict. Use when restoring or writing checkpoints fails with invalid update, when an agent's node returns a bare value, or when custom reducers receive the wrong type. Not for recursion limit errors, for must-write errors, or for thread_id mismatches.
TL;DR
LangGraph checkpoints expect every state update to be a dict mapping channel names to values; returning a string, list, or None from a node (or a reducer) fails with "invalid update: expected a dict." Wrap the value in a dict keyed by the channel name and the checkpoint write succeeds.
langgraph 'invalid update: expected a dict' checkpoint errorUse this when
- Checkpoint writes or restores fail with this message
- A node returns a bare string or list instead of a dict
- A custom reducer function returns the wrong type
Not for this skill when
- The graph exceeds the recursion limit (thats GraphRecursionError)
- A node writes to zero channels (thats the must-write error)
- Restore fails on thread identity (thats a thread_id problem)
Steps
- Find the offending update. The traceback names the node; read what it returns:
# broken: returns a bare list
def my_node(state):
return fetch_docs(state["query"])Expected output: you can see the return is not a dict. That is the entire bug.
- Wrap it in the channel key:
def my_node(state):
return {"docs": fetch_docs(state["query"])}Expected output: the node returns a dict and the checkpoint write succeeds. The key must match a channel in your state schema.
- Check custom reducers too; they must return the merged value, and the update fed into them must still be a dict:
def my_reducer(left, right):
return left + rightExpected output: the reducer merges correctly. If a reducer returns None, the checkpoint layer sees an invalid update downstream.
- If the error appears on restore rather than write, inspect the stored checkpoint for a corrupt entry:
checkpoint = checkpointer.get(config)
print(type(checkpoint["channel_values"]))Expected output: channel_values is a dict. If a previous buggy run persisted a non-dict value, that thread is poisoned; start a new thread_id.
Variant phrasings
invalid update on the messages channel specifically
You returned a bare string where a list of messages was expected: return {"messages": [msg]} not return msg.
error inside a subgraph
Subgraphs checkpoint through the same machinery; the bad return is in the subgraph's node, so look there, not the parent.
Why it happens
Checkpoints serialize the full channel state as a dict, and every write is validated before persisting. A node returning a non-dict cant be merged into channel state, so the checkpointer rejects it rather than writing garbage. Agent-generated nodes are the usual source because the model writes return result out of habit instead of return {"key": result}.
Edge cases
Commandobjects are valid node returns and bypass this check; the error then means the Command's update payload wasnt a dict.- Async nodes must return dicts too; forgetting
awaitcan leave a coroutine object as the "update." - If you mutate state in place and return the whole state dict, that works, but prefer returning only the changed keys.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_-rlwcSC0ExNrBK4xT1FLng