Skip to content

Suspending a graph

What LangGraph gives you, and what it does not

interrupt() stops a graph. The state is written to a checkpointer and the process exits. Later, from a different machine if you like, Command(resume=…) picks up exactly where it stopped.

What LangGraph deliberately has no opinion about is the human: how they are contacted, what they see, how their answer comes back. create_kiroku_interrupt_node fills that in.

Two things are required and easy to miss:

  • Compile with a checkpointer. Without one there is nothing to suspend into.
  • Invoke with a thread_id. It is the key the state is saved under, and the handle you pass back to resume.

The re-execution rule

This catches everyone once.

When LangGraph resumes a graph, the interrupted node runs again from its first line. interrupt() returns the answer this time instead of stopping, but everything above it already ran once and is about to run again.

So a node that creates a review case above its interrupt() call creates a second case on resume: second form, second token, second email to a reviewer who is now looking at two identical requests.

This library handles it by deriving a task id from the graph's thread and sending it as an Idempotency-Key. The server replays an idempotent create, so the second execution finds the first case instead of minting one. You can watch it in tests/test_graph.py::test_resuming_does_not_create_a_second_case: two calls, one case.

One node interrupting twice in a thread

The default id is derived from the node's name and the thread, so a node that interrupts twice in one run, in a loop, would reuse a single case. Pass task_id_for=lambda state: … to make the id depend on whatever separates the iterations.

Three ways to say what the reviewer sees

When to use it
fields=[…] A fixed form. Mints one form per case, which is exempt from your form quota but still accumulates rows
data_key="…" Generates one field per entry in state[data_key], so the reviewer sees this run's values. Use this whenever the form has to show something that varies
template_id="…" Reuses a form you already own. Best for a review step that runs often: same page, same field names, no debris

fields and title are fixed when the node is built, so they cannot read state. That is what data_key is for.

Resuming

final = graph.invoke(resume_with_answers(client, task_id), config)

resume_with_answers reads what the reviewer actually submitted and wraps it in a Command. The alternative is resuming with whatever the operator types, which makes the form decorative.

Pass wait=True to block until they answer. The default reads once, which is the right shape for a webhook-driven resume: you already know they answered, because that is why you were called.

The older, blocking way

create_kiroku_interrupt_handler is deprecated since 0.3.0. It creates the task and then polls in a loop inside the node, by default for up to an hour, holding a worker open the whole time. The graph is never suspended: nothing is checkpointed and nothing can resume in another process.

It still works, because published code imports it, and it emits a DeprecationWarning. Migration is usually four lines, and both write to state["human_verification"], so nothing downstream changes.