Recall Module 25’s own recurring question, hiding underneath every failure mode it covered: which node actually ran, what state went in, what came out, and why did the graph take this specific edge instead of another. This module gives you the real, concrete tools to answer that question — for a live, running graph, and for one you’re debugging after the fact.
Example 1: stream_mode="values" — full state after every step
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
count: int
def step_one(state: State) -> dict:
return {"count": state["count"] + 1}
def step_two(state: State) -> dict:
return {"count": state["count"] + 10}
builder = StateGraph(State)
builder.add_node("step_one", step_one)
builder.add_node("step_two", step_two)
builder.add_edge(START, "step_one")
builder.add_edge("step_one", "step_two")
builder.add_edge("step_two", END)
graph = builder.compile()
for chunk in graph.stream({"count": 0}, stream_mode="values"):
print(chunk)
"values" gives you the entire state after each node finishes — genuinely the clearest way to watch a graph’s real, complete state evolve, one full snapshot at a time.
Example 2: stream_mode="updates" — just what changed
for chunk in graph.stream({"count": 0}, stream_mode="updates"):
print(chunk)
"updates" shows only what each node actually returned — {"step_one": {"count": 1}}, then {"step_two": {"count": 11}} — genuinely useful when you specifically want to isolate which node changed which field, rather than re-reading the full state every time.
Example 3: stream_mode="messages" — token-level streaming
Recall streaming from your LangChain course — this is the same real idea, now at the graph level.
from langgraph.graph import StateGraph, START, END, MessagesState
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4o-mini")
def call_model(state: MessagesState) -> dict:
return {"messages": [model.invoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", END)
graph = builder.compile()
for chunk, metadata in graph.stream({"messages": [{"role": "user", "content": "Explain RAG in one sentence."}]}, stream_mode="messages"):
print(chunk.content, end="", flush=True)
Genuinely the same token-by-token responsiveness from your LangChain course’s own streaming module, now flowing through a graph’s own node structure rather than a single, isolated model call.
Example 4: astream_events — the richest real, per-step feed
Recall this exact API from your LangChain course, applied there to create_agent. It works identically here, because — recall Module 15 — that was always this same underlying engine.
import asyncio
async def main():
async for event in graph.astream_events({"count": 0}, version="v2"):
if event["event"] == "on_chain_start" and event["name"] in ("step_one", "step_two"):
print(f"→ Starting: {event['name']}")
if event["event"] == "on_chain_end" and event["name"] in ("step_one", "step_two"):
print(f"→ Finished: {event['name']}, output: {event['data'].get('output')}")
asyncio.run(main())
This is the most granular real view available — every node’s start, every node’s end, every intermediate tool call inside it, streamed live as the graph actually runs.
Example 5: streaming into subgraphs
Recall Module 22’s subgraphs — by default, a subgraph’s internal steps stay hidden from the parent’s stream. subgraphs=True opens that visibility directly.
for chunk in main_graph.stream({"query": "test"}, stream_mode="updates", subgraphs=True):
print(chunk)
Without subgraphs=True, you’d only ever see the subgraph’s node ("billing", for instance) as one opaque step. With it, you see every real, individual step happening inside that subgraph too — genuinely important once your graph has grown modular enough that debugging requires looking inside a specific piece, not just at the top level.
Example 6: post-hoc debugging with get_state_history
Recall this directly from Module 17 — real, historical debugging, after a run has already finished.
history = list(graph.get_state_history({"configurable": {"thread_id": "debug-session"}}))
for snapshot in reversed(history):
print(f"After {snapshot.metadata.get('step')}: {snapshot.values}")
This is the real, complete answer to “why did this specific execution take the path it did” — not a live stream, but a genuine, persisted record you can inspect at any point afterward, exactly like reviewing a flight recorder after the fact.
The real debugging discipline, stated plainly
flowchart TD
A["A graph misbehaved.\nWhat actually happened?"] --> B[Which node ran?]
B --> C[What state went into it?]
C --> D[What did it actually return?]
D --> E[Which edge got taken next, and why?]
E --> F["Use astream_events live,\nor get_state_history after the fact,\nto answer all four"]
Recall Module 2’s own opening promise — graph.invoke() was never magic, and this diagram is the concrete, practical proof: every one of these four questions has a real, mechanical, inspectable answer, never a guess.
Why this matters at genuinely production scale
It’s worth being direct about this — Module 25’s real Portkey data showed that failures across 650+ real organizations were routine, not exceptional. Observability is precisely what turns “something went wrong” into “node X received field Y as null, because node Z’s reducer silently dropped it” — the real, specific, fixable diagnosis a production team actually needs, rather than a vague report that “the agent behaved oddly” with no way to reproduce or genuinely understand why.
Common mistakes worth avoiding
Debugging exclusively with print() statements scattered through node functions. Recall this module’s own real tools — stream_mode="updates" and astream_events give you this same visibility, structured and consistent, without needing to modify and redeploy your actual node code every time you want to inspect something new.
Forgetting subgraphs=True and assuming a subgraph’s own internal failure is invisible. Recall Example 5 — the failure is genuinely there, in the subgraph’s own execution; you simply weren’t watching for it without this flag.
Treating a live stream as a substitute for get_state_history. A live stream only shows you what’s happening right now; genuine post-hoc debugging of a run that already finished — or crashed — needs the real, persisted history, not a live feed you’d have needed to be watching at the exact right moment.
What you should take away from this module
stream_mode="values","updates", and"messages"each answer a genuinely different real question — full state, just the diff, or token-level text.astream_eventsgives the richest, most granular real view of a graph’s live execution, node by node.subgraphs=Trueopens visibility into a subgraph’s own internal steps, not just its outer node.get_state_historyis the real, post-hoc equivalent — reconstructing exactly what happened, after the fact, from persisted, genuine data.
Where this goes next
The next module covers Testing — writing real, layered tests for nodes, routers, reducers, loops, interrupts, subgraphs, and entire graphs, so you catch these same real problems before a real user ever encounters them.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed