#8672·langgraph

stream_mode="values" skips a step when a node returns None or {}, while "updates" emits it

Author: siddharthgaur1Created Aug 21, 2026Updated Sep 16, 2026
Labelsexternal

The docstrings for Pregel.stream / .astream document the two modes as:

  • "values": Emit all values in the state after each step, including interrupts.
  • "updates": Emit only the node or task names and updates returned by the nodes or tasks after each step.

Both say "after each step". In practice they disagree about how many steps happened when a node returns None or {}: updates emits a chunk for that node, values emits nothing.

Repro

No LLM, no network:

python
from typing import Annotated, TypedDict
from operator import add
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver

class S(TypedDict):
    steps: Annotated[list, add]
    n: int

def a(s): return {"steps": ["a"], "n": s["n"] + 1}
def b(s): return {"steps": ["b"], "n": s["n"] + 1}
def mid(s): return None          # same result with `return {}`

g = StateGraph(S)
g.add_node("a", a); g.add_node("mid", mid); g.add_node("b", b)
g.add_edge(START, "a"); g.add_edge("a", "mid"); g.add_edge("mid", "b"); g.add_edge("b", END)
app = g.compile(checkpointer=InMemorySaver())

print(list(app.stream({"steps": [], "n": 0}, {"configurable": {"thread_id": "u"}}, stream_mode="updates")))
print(list(app.stream({"steps": [], "n": 0}, {"configurable": {"thread_id": "v"}}, stream_mode="values")))

Observed

updates: [{'a': {'steps': ['a'], 'n': 1}}, {'mid': None}, {'b': {'steps': ['b'], 'n': 2}}]
values : [{'steps': [], 'n': 0}, {'steps': ['a'], 'n': 1}, {'steps': ['a', 'b'], 'n': 2}]

updates has three entries (a, mid, b). values has three (initial, after a, after b) — the mid step is absent. So the usual invariant len(values) == len(updates) + 1 does not hold, and it fails silently: nothing signals that a step is missing from one view.

With stream_mode=["updates", "values"] the same asymmetry shows up as an uneven interleave rather than strict alternation:

values, updates, values, updates, updates, values

which is what led me here — I was pairing the two streams positionally to drive a UI, and they drifted out of alignment on exactly the no-op nodes.

Versions

Reproduces identically on:

  • langgraph 1.2.9 / langgraph-checkpoint 4.1.1
  • langgraph 1.2.11 / langgraph-checkpoint 4.2.0 (latest at time of filing)

Python 3.11, Windows.

Question rather than a bug assertion

Suppressing a no-op state emission looks deliberate, and if it is, the fix is in the docstring rather than the code — "values" would be documented as "after each step that changes state". But if "after each step" is meant literally for both modes, a step that produced no writes should still emit the unchanged state.

I didn't want to guess which one is intended. Happy to send a PR for whichever you prefer.