Human-in-the-loop
Pause a run for a person to approve, edit, or answer something — and resume it later, possibly hours later, from a different client. An interrupted run holds no connection and no timer, only a checkpoint, so your process can restart while it waits.
Pause the graph
Call LangGraph's interrupt() from a node. Whatever you pass becomes the payload your UI renders:
import { interrupt } from "@langchain/langgraph";
function askForApproval(state: State) {
const answer = interrupt({
question: "Send this email?",
draft: state.draft,
});
return { sent: answer === true };
}The run settles, the thread's status becomes interrupted, and the interrupt surfaces in the stream. Nothing is holding anything open.
Resume it
await client.runs.create(threadId, "agent", {
input: null, // not a new turn — pick up where the interrupt parked
command: { resume: true },
});input: null plus a command is the whole idiom. The run resumes from the checkpoint the interrupt parked on rather than starting over, and the value you pass in resume is what interrupt() returns inside the node.
Three commands are available:
| Command | Does | Use for |
|---|---|---|
resume | Returns a value from the interrupt() call | Approve / reject / supply an edited answer |
update | Writes state before continuing | Correcting something the graph got wrong |
goto | Continues at a named node | Sending the turn down a different path |
If you're building on POST /threads/{id}/commands directly, it infers the assistant from the thread's last run, so you don't have to track it — and it 409s (thread_not_interrupted) on a thread that isn't paused, rather than quietly starting a new turn.
In a React UI, this is free
useStream renders the interrupt and resumes it for you — no extra endpoints, no polling. The interrupt arrives on the stream, you render your approval card, and submitting a command continues the same conversation.
Find what's waiting
An approvals queue is a thread search:
const waiting = await client.threads.search({ status: "interrupted" });The console ships this view already — its Overview lists what's waiting for you, and the Interrupts view approves, rejects, or resumes with any JSON, without you building a UI first.
What you must get right
A checkpointer is required. Without one there is no checkpoint to park on, and resume silently no-ops — the single most common way this fails.
skein devgives you one; in code, useembedInMemoryGraphs/embedPostgresGraphsor supply your own. See embedding.md.Use durable storage for anything real. With the in-memory driver an interrupted thread is gone on restart, which defeats the point of pausing for a human. Postgres keeps it.
The pause has no timeout. A thread waits indefinitely. If your product needs "auto-reject after 48 hours", that's a cron sweeping
status: "interrupted"threads — skein won't do it for you.Resuming starts a new run. The interrupted run is terminal; the resume is a fresh run on the same thread. So a webhook fires per run, and the run id you resumed with is not the id you get back.
A plain start on a waiting thread is not refused for you. Because the interrupted run is terminal, the thread holds no inflight run — so
multitask_strategy, which arbitratespending/running, never sees it, and a fresh start succeeds and discards the pending question. Passif_thread_statuswhen the caller might not know the thread is waiting, which is every asynchronous channel:ts// 409 `thread_status_mismatch` instead of silently trampling the interrupt. { assistant_id: "support", input, if_thread_status: ["idle", "error"] }The check runs inside the driver's atomic create, so it holds across replicas — an in-process lock cannot give you this.
Not available on
POST /invoke/:graph_id, which has no thread to park on. See serving-a-single-graph.md.
Interrupting from outside the graph
You can also pause without an interrupt() call, by naming nodes on run creation:
await client.runs.create(threadId, "agent", { input, interruptBefore: ["send_email"] });interruptBefore / interruptAfter take a list of node names, or "*" for every node. This is a debugging and stepping tool more than a product feature — interrupt() is what you want when the pause is part of the design, because it carries a payload the UI can render.
Working examples
triage-agent— reached by a conditional edge, so only items that need a human park. Runs with no API key and no network; approve from the console.chat-app— an approval card in a real Next.js UI.
See also
- Threads — the
interruptedstatus, and editing state before you resume - Runs — what a resume run is, and multitask strategies
- Frontend SDKs &
useStream— rendering and resuming in React - The console — approving without building a UI
- Building a runner — implementing interrupts for a non-LangGraph runtime