A graph is not a chain of calls
A request/response call has one boundary and one failure; a compiled state graph is a durable, resumable execution whose every superstep is a checkpoint — which is why its UI cannot be a spinner and a result.
Both agent products you have shipped are the same shape underneath. HouseWarm takes a document, runs extraction, and returns fields with confidence scores. The flight recommender takes a question and returns an answer. Different domains, identical execution model: one request, one wait, one response. The UI has three states — idle, pending, settled — and the pending state is a spinner because there is genuinely nothing else to say. Nothing is observable between the call going out and the answer coming back.
Now somebody hands you a compiled state graph and asks what the interface should look like. The instinct that has served you on every product so far — find the endpoint, model the loading state, model the error state, render the result — produces a design that is wrong in a way that is hard to see from a mock. It is not that the spinner is ugly. It is that the graph is emitting a running commentary on its own execution and your design threw all of it away.
The unit of execution is the superstep
A graph does not run your nodes the way a promise chain runs its then handlers. It runs a message-passing loop — the docs describe the model as inspired by Google’s Pregel system — and the tick of that loop has a name. LangGraph’s Graph API documentation puts it this way:
“A super-step can be considered a single iteration over the graph nodes. Nodes that run in parallel are part of the same super-step, while nodes that run sequentially belong to separate super-steps.” (LangChain, Graph API)
Nodes start inactive, become active when a message arrives on an incoming edge, and vote to halt when they have nothing left to do. The run ends when every node is inactive and no messages are in flight. That is a different termination condition from “the last function in the chain returned,” and it is why a graph can fan out, run three nodes concurrently in one superstep, and converge — while your request/response mental model has no vocabulary for what the user should be looking at during that.
Worth naming the source honestly: LangChain writes and publishes this documentation, and sells the hosted platform it leads to. Vendor documenting its own product. That does not make the execution model wrong — by their own account the model is borrowed rather than invented — but it does mean the framing throughout their docs is “here is why you want this,” and you should read it for the mechanism rather than the argument.
Every superstep boundary is a save point
This is the part that actually changes your job. A superstep is not just a scheduling unit; it is the granularity at which the runtime persists. With a checkpointer configured, LangGraph writes a checkpoint at each boundary — the docs define a checkpoint as “a snapshot of the graph state saved at each super-step”, grouped under a thread, which is “a unique ID or thread identifier assigned to each checkpoint… the accumulated state of a sequence of runs.”
Read that snapshot’s fields as a UI spec, because that is effectively what it is. The documented shape of a StateSnapshot:
values // state channel values at this checkpoint
next // node names to execute next; [] means complete
config // thread_id, checkpoint_ns, checkpoint_id
metadata // source, writes, step
createdAt // ISO 8601
parentConfig // config of the previous checkpoint, null for the first
tasks // PregelTask[] — each with id, name, error, interruptsEvery one of those has an interface consequence. values is what you render. next is “what is still owed” — the difference between a progress bar you invented and a progress indicator the runtime can actually justify, and the thing that tells you an empty array means done. parentConfig is a backwards link, which is to say the run has a history you can walk. tasks carries interrupts, which is how a UI that did not start the run finds out the run is sitting there waiting for a human.
Compare that to what your HouseWarm parser exposes between request and response, which is nothing, because there is no between. The extraction either completed or it did not.
What the spinner was hiding
Here is the honest version of the HouseWarm parser as it stands. A broker uploads a document. Your server runs extraction. It returns fields, confidences, crops and raw OCR text. Your review gate renders them beautifully — genuinely, that gate is the strongest agentic UX on your résumé — and holds the downstream step until a human approves or corrects.
Now list what that architecture structurally cannot do, not because you built it badly but because request/response has no room for it:
- Show which extraction step is running, because the steps are not individually observable from outside the call.
- Survive the broker closing the tab mid-review, because nothing durable is holding a paused run — the run already finished; what is waiting is your application state, not the agent’s.
- Resume after a failure at step four without redoing steps one through three, because there is no checkpoint at step three.
- Let a second person pick up the review, because there is no thread identifier that means “this run,” only a session that means “this browser.”
- Let anyone see what the agent was doing when it went wrong, because the only record is the final response.
None of those are missing features. They are all the same missing primitive, showing up five times. And notice that the fix is not a UI fix — you cannot design your way to a resumable review gate on top of an execution model that does not persist. That is the sentence worth carrying into an interview.
Three states versus a stream of them
Concretely, the state machine your two shipped products taught you:
type CallState =
| { status: 'idle' }
| { status: 'pending' }
| { status: 'done'; result: Extraction }
| { status: 'error'; error: Error }And the shape a graph hands you instead — not a finished type to copy, a sketch of the vocabulary you now have to model:
type RunState = {
threadId: string // which run this is, across tabs and people
values: GraphState // everything the agent has accumulated so far
next: string[] // what it will do next; [] means finished
tasks: PregelTask[] // including any pending interrupts
history: Checkpoint[] // every superstep boundary, walkable backwards
}The second one is not a more detailed version of the first. It is a different kind of object: CallState describes a request you made, RunState describes a process that exists whether or not you are currently looking at it. That is the distinction the rest of this module is built on.
Retrieval check
Your review gate already pauses work and waits for a human. Name precisely what a checkpointed graph adds that it does not have — without overclaiming.
Check your answer
It does not add the pause. You already have a pause, and it is a good one. What it adds is that the pause is durable and belongs to the run rather than to the request. In HouseWarm, the agent finished, and your application is holding an approval decision. In a graph, the agent is suspended mid-execution with its state persisted, and a resume continues from that checkpoint rather than re-running from the top.
Everything else follows from that one difference: a thread ID other people and other tabs can address, a history you can walk backwards, anext array telling you what is still owed, and failure recovery that does not start over. If you find yourself saying “a graph lets you have a human in the loop” — you already had that. Say “a graph makes the pause part of the agent’s execution instead of part of my app” and you are describing something you actually gained.
Hands on
Draw the parser as what it is, then as what it would be
Done when: ARTIFACT.md’s “Why not a chain of calls” section names at least four things the current parser structurally cannot do, each traced to the missing primitive rather than to a missing feature — and the node/edge table has a row per node with nothing invented.
- Open
learning/agent-graphs/ARTIFACT.md. It is seeded and empty on purpose. You are filling in module one. - Write out HouseWarm’s current parse-and-review flow as the request/response it is. One line per step, and mark the single point where the boundary between client and server sits. There will be exactly one, which is the finding.
- Under Why not a chain of calls, list at least four things that flow structurally cannot do. For each, name the primitive that is missing — durable state, a superstep boundary, a thread identifier, an observable intermediate — rather than the feature you would build. “No progress indicator” is a symptom; “no observable intermediate state” is the cause.
- Now decompose the same flow into nodes, one row per node in the node/edge sketch table. Keep it honest — the real parser probably has three or four real steps, not ten. For each node write what a user should see while that node is running, and leave the column blank if the honest answer is “nothing worth showing.” A blank there is a real finding too.
- Mark which node is the review step. Do not design the interrupt yet — just put a flag on the row. That node is the one the whole course converges on.
- Bring the two lists into the chat. I will push back hardest on any “cannot do” that is really a feature you never built, and on any node in the table that is a UI screen wearing a node’s name.
What this does not cover
This lesson deliberately treats the graph as a black box that emits checkpoints. It has said nothing about how control actually moves between nodes, which is where the branches the user experiences come from — a graph with a conditional edge in it has screens that a linear one does not, and you can read exactly which ones off the routing function. That is the nodes-and-edges lesson.
It has also said nothing about what is inside values. The snapshot fields above are the envelope; the state schema is the contract, and reading it is what tells you whether a given channel wants a transcript component or a form field. That is the state-as-contract lesson. Interrupts get named here only as a field on tasks — the whole interrupt module comes later, and deliberately so, because it is much easier to reason about a pause once you already know what a superstep is.
Read this next — primary source
Thinking in LangGraphLangChain — docs.langchain.com, JavaScript docs, fetched 2026-09-02. Vendor documenting its own product
This lesson takes the execution model from it and stops. The page itself walks the decomposition end to end — how you go from a process you can describe in a sentence to a set of nodes, what belongs in state versus what belongs in a node’s locals, and where the boundaries land when a step can fail. That decomposition exercise is the part you will actually repeat on somebody else’s product, and it is worth doing once with the docs in front of you rather than inferring it from a finished graph.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.