How to Fix 'state not updating' in LangGraph (Python)
If your LangGraph state is “not updating,” the graph is usually running fine, but your node is returning something LangGraph cannot merge back into the state. In practice, this shows up when you mutate state in place, return the wrong shape, or forget to declare the field as an updatable channel.
The symptom is usually one of these:
- •The next node still sees the old value
- •
graph.invoke()returns unchanged state - •You get errors like
InvalidUpdateError: Expected dict, got ...orValueError: No reducer found for key ...
The Most Common Cause
The #1 cause is returning a mutated object or the wrong payload shape instead of returning a partial state update.
LangGraph does not track in-place mutation the way a plain Python object does. Your node must return a dictionary containing only the fields you want to update.
| Broken pattern | Fixed pattern |
|---|---|
| Mutates state and returns nothing useful | Returns a partial dict update |
| Returns a full object with wrong shape | Returns only updated keys |
| Assumes mutation will persist automatically | Uses explicit state updates |
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
count: int
def bad_node(state: State):
state["count"] += 1
# Wrong: returning the same mutated object is not the update contract
return state
def good_node(state: State):
return {"count": state["count"] + 1}
builder = StateGraph(State)
builder.add_node("bad", bad_node)
builder.add_node("good", good_node)
builder.add_edge(START, "good")
builder.add_edge("good", END)
graph = builder.compile()
The broken version often looks harmless because state["count"] += 1 works in memory. But LangGraph expects node outputs to be merged through its reducer pipeline, not inferred from side effects.
A more realistic failure looks like this:
from langgraph.errors import InvalidUpdateError
# Typical runtime symptom:
# InvalidUpdateError: Expected dict, got 2
If your node returns an int, string, list, or custom object directly, LangGraph raises InvalidUpdateError. The fix is to always return a dict keyed by the state fields.
Other Possible Causes
1) The field has no reducer / channel behavior defined
If multiple nodes can write to the same key, LangGraph needs a merge strategy. Without it, you can get:
- •
ValueError: No reducer found for key 'messages' - •Or updates that appear to be ignored in multi-branch graphs
from typing import Annotated, TypedDict
from operator import add
class State(TypedDict):
# Good for list accumulation
messages: Annotated[list[str], add]
Without Annotated[..., add], concurrent writes to messages may fail or behave unexpectedly.
2) You are returning nested data when LangGraph expects flat keys
LangGraph merges top-level keys from your returned dict. If you wrap updates inside another object, nothing gets applied where you expect.
def bad_node(state):
return {"state": {"count": state["count"] + 1}}
def good_node(state):
return {"count": state["count"] + 1}
If your schema says count lives at the top level, returning {"state": {...}} does not update it.
3) Your node signature is wrong
Nodes should accept one argument: the current state. If you add extra positional args or wire dependencies incorrectly, you may end up debugging what looks like a state bug when it is actually a signature mismatch.
# Wrong
def node(state, config):
return {"count": state["count"] + 1}
# Right
def node(state):
return {"count": state["count"] + 1}
If you need runtime config, use the supported config pattern instead of inventing extra parameters.
4) You are using mutable defaults or shared objects across runs
This one bites hard in tests and long-lived services. If your state contains shared lists or dicts created at module scope, one run can contaminate another.
# Bad: shared mutable default
shared_messages = []
class State(TypedDict):
messages: list[str]
def node(state):
shared_messages.append("hello")
return {"messages": shared_messages}
Create new objects per run and treat graph state as request-scoped data.
How to Debug It
- •Print exactly what each node returns
- •Add temporary logging inside every node.
- •Verify the output is a plain dict with top-level keys matching your schema.
def node(state):
out = {"count": state["count"] + 1}
print("NODE OUT:", out)
return out
- •
Check for LangGraph exceptions first
- •Search for
InvalidUpdateError,ValueError: No reducer found for key ..., or schema validation errors. - •These usually point directly at the bad update shape.
- •Search for
- •
Reduce the graph to one node
- •Remove branches and loops.
- •If one-node updates work but multi-node updates fail, you likely need a reducer on a shared field.
- •
Inspect your state schema
- •Confirm every writable field exists in the schema.
- •For accumulating fields like lists, use
Annotatedwith a reducer.
from typing import Annotated, TypedDict
from operator import add
class State(TypedDict):
count: int
logs: Annotated[list[str], add]
Prevention
- •Return partial dict updates only. Do not rely on in-place mutation as your persistence mechanism.
- •Define reducers explicitly for any field that can receive multiple writes across branches.
- •Keep your state schema small and strict so bad updates fail early instead of silently disappearing.
If you’re still stuck, compare what your node returns against what your State type declares. In LangGraph, “state not updating” almost always means “the update never matched the merge contract.”
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit