Unit 08.00: Showing progress instead of a spinner
A workflow taking forty seconds behind a spinner is indistinguishable from one that has hung. Streaming node completions costs almost nothing and fixes that.
One event per finished node
stream yields an event as each node completes, rather than returning once at the end. The graph is unchanged - only how you consume it differs.
The code streams a three-node workflow.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
stage: str
g = StateGraph(State)
for name in ("research", "draft", "review"):
g.add_node(name, (lambda n: (lambda s: {"stage": n}))(name))
g.add_edge(START, "research")
g.add_edge("research", "draft")
g.add_edge("draft", "review")
g.add_edge("review", END)
app = g.compile()
print("what the user sees, one line per completed node:")
for chunk in app.stream({"stage": ""}):
for node, update in chunk.items():
print(f" finished {node:10} -> {update}")
# A spinner says the system is alive. This says which of three stages is done,
# which is the difference between a user waiting 40 seconds and a user
# reloading the page at 15.
Three events, one per node, each naming what finished. From the user's side that is a progress line rather than an unmoving spinner.
The difference is behavioural, not cosmetic. A user watching a spinner reloads the page at around fifteen seconds; a user watching stages complete will wait considerably longer, because each event is evidence the system is working rather than stuck.
The mistake this prevents
The mistake is streaming the raw node names to the user. research and sum_items are function names, chosen for the codebase, and they change when someone refactors. Map them to labels - which is the next unit but one - so a rename does not change what a user reads.
Takeaway
Stream node completions rather than returning once. It is the cheapest available improvement to how a long workflow feels, and it turns a hang into something visible.
