## 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.

```text
langgraph 'invalid update: expected a dict' checkpoint error
```

## Use 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

1. Find the offending update. The traceback names the node; read what it returns:

```python
# 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.

2. Wrap it in the channel key:

```python
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.

3. Check custom reducers too; they must return the merged value, and the update fed into them must still be a dict:

```python
def my_reducer(left, right):
    return left + right
```
Expected output: the reducer merges correctly. If a reducer returns None, the checkpoint layer sees an invalid update downstream.

4. If the error appears on restore rather than write, inspect the stored checkpoint for a corrupt entry:

```python
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
- `Command` objects 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 `await` can 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
