interrupt() and the Command that answers it
A graph-level pause is a thrown control-flow signal plus a checkpoint, and resuming is a second invocation carrying a Command — which means the payload you surface and the payload you send back are two halves of one designed contract.
Your review gate already knows how to wait for a person. A broker opens the extraction, sees per-field confidence, a crop of the source image and the raw OCR text, and either approves or corrects before anything downstream fires. The waiting is not the hard part and you solved it years ago.
What you have not done is hand the waiting to the runtime. In HouseWarm the extraction call finished, and what is holding the decision is your application: a row, a session, a component in a pending state. In a graph, the run itself stops mid-node with its state written to disk, and the thing that restarts it is a second call into the same graph. This lesson is the mechanics of that stop and that restart, and the argument that they are one designed payload rather than two API calls.
The pause is a throw
Inside a node you call one function. The generated API reference gives the signature as interrupt<I = unknown, R = any>(value: I): R, and describes both of its behaviours: it throws a GraphInterrupt carrying the value you passed when no resume value is available yet, and it returns the resume value once the graph is re-invoked with a Command.
That is one function with two lives. On the first pass it is a control-flow signal that leaves the node by throwing. On the pass after a human answers, the same call is an expression that evaluates to whatever the human sent. The node body is written once and reads straight down.
A caveat about that reference page: it lives on langchain-ai.github.io, an older generated site that publishes stale unions elsewhere in this course. It is cited here only for the signature, because the current reference returns a not-found for this symbol. When that site and docs.langchain.com disagree, the current docs win, and when the docs and the installed package disagree, the package wins.
Two prerequisites, and neither is optional
The interrupts page states what has to be in place before the call does anything useful: “A checkpointer to persist the graph state” and a thread ID in the config “so the runtime knows which state to resume from.”
Read those as the two halves of durability. The checkpointer is where the paused run lives. The thread ID is the handle anything else uses to find it — another tab, another person, a job that sweeps for stalled runs, a link in an email. Without the first there is no state to come back to. Without the second there is nothing to address.
This is also the honest reason a graph-level pause is not a drop-in upgrade to an app-level one. You are adding a store and a durable identifier, and both have consequences. The gate lesson in this module takes that up properly.
What the caller sees
When a node interrupts, the run does not error and it does not return a result. Pending interrupts surface to the caller under __interrupt__, as an array of objects with two fields:
{
__interrupt__: [
{ id: "…", value: { /* whatever you passed to interrupt() */ } }
]
}Two fields, and only two. Older material describes a richer Interrupt object; the current docs print id and value, so that is what this course teaches. It is an array because a single superstep can contain more than one interrupt, which matters the moment a graph fans out over several documents at once.
value is the part you control completely. It is a payload you wrote, and everything the pause screen renders has to come from inside it or from a fetch keyed off something inside it. The runtime adds nothing to it and validates nothing about it — it only requires that it be JSON-serializable, because it is going into a checkpoint.
Resuming is a second invocation
There is no resume() method on a paused run. You call the graph again, on the same thread, with a Command as the input:
import { Command, interrupt } from "@langchain/langgraph";
// inside a node:
const answer = interrupt({ action: "send_email", to, subject, body,
message: "Approve sending this email?" });
// resuming from the caller:
await graph.invoke(new Command({ resume: true }), config);Both symbols come from @langchain/langgraph. The docs describe new Command({ resume }) as the only Command pattern intended as input to invoke or stream, which is worth knowing because Command also appears inside node bodies for routing, and the two uses look alike in a search result.
config is where the thread ID lives — { configurable: { thread_id: "user-42-run-7" } } — and the same value that started the run is the value that resumes it. Reusing it continues the run. A new value starts an empty one. The checkpointers lesson later in the course builds a URL scheme on that fact; here it is enough to notice that your resume call needs a piece of state your pause screen has to have carried.
Notice what resume: true is doing in that sample. It is not a framework keyword for approval. It is the value that interrupt() returns inside the node, and the sample chose a boolean. It could as easily be a string, an object with a verdict and a corrected value, or an array. The decision-surface lesson in this module is entirely about that choice.
Two halves of one contract
Here is this course’s own framing of what you have just read, rather than a claim from the docs. The docs describe two mechanisms. The useful way to hold them is as one contract with two directions, and it is written on a single line of code:
const decision = interrupt(payloadYouSurface);
// ^^^^^^^^ ^^^^^^^^^^^^^^^^^
// what comes back what goes out
// from the human to the screenThe argument is an outbound message to a person who is not in the room. The return value is their answer, arriving as a plain local variable in a function that has already run once. Design them together or you will get the mismatch that shows up in every first attempt: a payload rich enough to render a good review screen, and a resume value so thin the node cannot act on what the person actually decided.
A rule of thumb that falls out of it. Ask what the node needs to do next under each possible human answer, and put exactly that in the resume value. Then ask what a person needs on screen to produce those answers responsibly, and put exactly that in the payload. Two questions, two objects, one call site.
Where people get burned
Do not wrap an interrupt() call in a bare try/catch. It pauses by throwing a GraphInterrupt, so a catch-all swallows the pause and the run continues as though nothing happened. This is a plausible thing to write — the call is doing I/O-shaped work, and defensive error handling around I/O is a habit — and the failure is silent.
What the old tutorials will show you instead
Search for human-in-the-loop LangGraph and you will find interruptBefore and interruptAfter, static options set when the graph is compiled. They still exist. The current interrupts page repositions them as a debugging tool and says they are not the recommended approach for human-in-the-loop, and they resume with null rather than a Command.
That last detail is the giveaway when you are reading inherited code. A resume that passes null is a static breakpoint and carries no payload from the human at all. A resume that passes a Command is a designed answer. You can tell which kind of review flow a codebase has from the resume call alone, before you find the node.
Retrieval check
A colleague says “we already have human-in-the-loop, we just await the approval before calling the next step.” Using only what is on this page, name the two things they do not have.
Check your answer
A checkpoint, and a thread ID. Their agent ran to completion and their application is holding a decision. Nothing is suspended, so there is no persisted mid-node state to come back to, and no identifier that means “this run” as opposed to “this browser session.”
The consequences follow from those two absences rather than from any missing feature. A second person cannot pick the review up, because there is nothing addressable to pick up. A closed tab loses the pending work rather than parking it. And a resume, if they built one, restarts from the top instead of continuing from where the agent stopped, because the only durable record is the finished response.
What they do have is a real pause and, in your case, a genuinely good review screen. Do not concede that part.
Hands on
Write the interrupt payload contract for the review node
Done when: ARTIFACT.md’s module 2 section holds a payload object and a resume value written as two TypeScript types, where every field in the payload is justified by something a reviewer has to see, and every field in the resume value is justified by something the node has to do next.
- Open
learning/agent-graphs/ARTIFACT.mdand go to Module 2. Module one’s node table already marks which node is the review step. That node is the one you are opening up. - Write the payload type first — the object you would pass to
interrupt(). Take it from the review screen you already built: field name, extracted value, confidence, the crop reference, the raw OCR span. For each field, write the reviewer decision it supports. Any field with no decision beside it comes out. - Check every field for JSON-serializability, and mark the ones that are references rather than contents. An image crop is almost certainly a key or a URL, not bytes, and noticing that here saves you a checkpoint full of base64.
- Now write the resume value type, without looking at the payload. Answer only this: what does the node need in order to proceed correctly under each thing a broker might decide? Do not design three verdicts yet, even if you can see them coming.
- Put the two types side by side and find the mismatch. There is almost always one field the screen shows that the resume value cannot act on, or one thing the node needs that the screen gave the reviewer no way to express. Write the mismatch down in the surprises list — it is the interview anecdote.
- Sketch the resume call, including where the thread ID comes from on the client. If you cannot say where the page got that string, you have found the next piece of design work rather than a missing API.
- Bring both types into the chat. I will push hardest on payload fields that exist because your current screen happens to render them, and on a resume value that is a boolean where the node clearly needs data.
What this does not cover
This lesson took the checkpointer as a given and described what it enables. It has not made the case for it, which is the argument you actually need in a room: what a durable pause adds over the review gate you already ship, stated precisely enough that nobody can call it overclaiming. That is the app-level-gate lesson in this module.
It also stopped at a boolean. Real review surfaces have at least three verdicts, and the interesting one is correction, where the resume value carries data the node uses instead of its original inputs. The decision-surface lesson takes that on, and the mid-tool-call lesson covers what happens to code that ran before the interrupt() when the node restarts. That rule changes where a side effect is allowed to live, and it is the one that turns a working review node into a dangerous one.
Threads, checkpoint history, and building a URL scheme around thread_id belong to the state-and-time module. Here the thread ID was only a prerequisite you had to have.
Read this next — primary source
InterruptsLangChain — docs.langchain.com, JavaScript docs, fetched 2026-09-05. Vendor documenting its own product
This lesson takes the call-and-answer half of the page: the two prerequisites, what a paused run surfaces, and what comes back on resume. The page carries two more things worth your time. It repositions the older static interruptBefore / interruptAfter options as a debugging tool and says plainly they are not the recommended way to do human-in-the-loop — which is what most 2025-era tutorials teach. And it ends with a list of common pitfalls that the mid-tool-call lesson in this module is built on. Read it once end to end; it is short.
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.