## TL;DR

Every node in a LangGraph graph must return a state update that touches at least one channel in the graph's state schema. This error means some node returned `None`, an empty dict, or only keys the graph does not know about. Find the node whose return value misses the state schema and make every branch return at least one valid state key.

## The error

```text
must write to at least one of [] langgraph
```

The full form names the channels the engine expected:

```text
langgraph.errors.InvalidUpdateError: Must write to at least one of ['messages']
```

The list in brackets is your checklist - the node's return value has to include at least one of those keys.

## Steps

### 1. Read the channel list in the error message

Copy the list from inside the brackets. Those are the state channels the failing node was required to write to.

Expected: you have a concrete list of keys, like `['messages']` or `['input', 'plan', 'response']`.

### 2. Find which node produced the bad write

Run the graph with a small input and print what each node returns. The culprit is usually a routing or conditional node:

```python
def debug_node(state):
    result = real_node(state)
    print("node returned:", result)
    return result
```

Expected: you spot the node returning `None`, `{}`, or a dict whose keys are not in your state schema.

### 3. Make every branch return a state key

The most common cause is a conditional node that forgets to return a payload on one branch. Even a no-op branch must return something valid:

```python
def router(state):
    if should_skip(state):
        return {"messages": []}
    return {"messages": [do_work(state)]}
```

Expected: every branch returns a dict containing at least one key from the state schema.

### 4. Check subgraph and Send-based nodes against the parent schema

If a node runs inside a subgraph or is triggered by a `Send`, its return value is validated against the graph it writes to, not its own private state. Returning only private keys the parent graph does not declare raises this exact error. Either return keys the parent state declares, or declare the private shape with the `output` argument:

```python
graph = StateGraph(State, output=PrivateState)
```

Expected: the subgraph node writes keys the parent graph accepts, and the error stops.

### 5. Re-run and confirm the graph completes

Invoke the graph with the same input that failed.

Expected: no `InvalidUpdateError`, and the final state contains the updates the nodes produced.

## Use this when

- A LangGraph run raises `InvalidUpdateError: Must write to at least one of [...]`
- A conditional routing node returns `None` or an empty dict on some branch
- A subgraph node returns private-state keys the parent graph schema does not declare
- A `Send`-triggered node writes keys outside the graph's state channels

## Not for this skill when

- The error is `At key 'X': Can receive only one value per step` - that is concurrent writes to one channel without a reducer, a different problem
- The graph runs but produces wrong results - that is a logic bug, not a write-validation failure
- The failure is a tool-calling or model error inside the node, before any state write happens

## Variant phrasings

### InvalidUpdateError: Must write to at least one of ['input', 'plan', 'past_steps', 'response']
Same error with a multi-channel state. The fix is identical: the node's return must overlap with at least one listed key.

### Node returned None in langgraph
The most common concrete trigger. A node that implicitly returns `None` (no return statement, or a branch that falls through) fails the write check.

### PrivateState not working with Send in langgraph
The subgraph variant: a node activated by `Send` returns its private keys, but the graph validates against the parent state schema. Return parent-schema keys or set the graph's `output` shape.

## Why it happens

LangGraph's Pregel runtime validates every node's output before merging it into the state channels. A node's return dict is intersected with the declared state channels; if the intersection is empty, the engine refuses to proceed rather than silently dropping the write, because an empty write usually means the node forgot to produce its result. The strictness is deliberate - it turns a silent no-op into a loud, fixable error.

## Edge cases

- **Older langgraph versions reject an explicit empty patch `{}`:** if every component legitimately returns `{}` by design, upgrade langgraph to the pinned version instead of restructuring your nodes.
- **Reducers and None:** a channel with a reducer may tolerate `None` in some configurations, but an explicit `None` return still fails validation in others. Return the key explicitly rather than relying on reducer leniency.
- **Conditional edges pointing at removed nodes:** after refactoring the state schema or renaming nodes, a route name may no longer exist. The error then appears at the routing node - re-check the edge map.
- **Skip flags:** prefer returning an explicit empty-but-valid update (like `{"messages": []}`) over returning nothing when a node intentionally does no work.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_9nHdeTw9Yw6GpI7awjr3Cw
