Human in the loop
A graph can pause before a step, hand the state to a person, and continue with their decision. The pause is a checkpoint; the decision is just another update.
Pause before the action, not after
The useful place to interrupt is immediately before the step that cannot be undone: sending the email, issuing the refund, deploying the change. Interrupting after it means the decision was already made.
In LangGraph that is interrupt_before=["send"]: the graph runs up to that node, saves the state, and stops with send as the next node. Nothing has happened yet, and everything needed to do it is in the checkpoint.
A worked pause: the refund workflow prepares an amount of 42.00 and a message. The interrupt fires before send, so the thread stops with both values in the state and send still pending. A reviewer opens the thread, reads the amount and the message, and approves. Only then does the send node run. If the process crashes while waiting, the checkpoint is still there and the approval is not lost.
The human answers with an update
Resuming is an ordinary invocation on the same thread. The person’s decision goes into the state — approved: true, or an edited draft — and the graph continues from where it stopped, with the new value available to the node that was waiting.
That is why the interrupt is cheap to add. It does not need a new mechanism, a callback or a second graph; it needs a checkpoint and a key in the state.
A worked resume: update_state(config, {"approved": True}) writes the decision into the checkpoint, and invoke(None, config) continues. The send node reads approved and runs. The None is not a missing input; it means “use the state you have”. Passing a new input would start a new turn and can overwrite the paused state.
Pausing for approval
graph = builder.compile(checkpointer=MemorySaver(), interrupt_before=["send"])
config = {"configurable": {"thread_id": "refund-1024"}}
graph.invoke({"order": "A-1024"}, config)
state = graph.get_state(config)
print(state.next) # ('send',) — waiting
graph.update_state(config, {"approved": True})
graph.invoke(None, config) # continue from the checkpointApprove or reject
Run until the interrupt, approve, and resume. Then start a second thread and reject it, sending the graph back to the draft node.
Rejection is a route, not an error
A rejected approval should send the graph somewhere useful: back to the draft node with the reviewer’s note in the state, or to a terminal node that records the refusal. Treating it as an exception leaves a thread that cannot be resumed.
A revision loop is often the right answer. The reviewer says what is wrong, the graph revises, and the interrupt fires again before the same action.
A worked rejection: the reviewer writes approved: false and a note, and a router after the interrupt sends the thread to a revise node. Revise edits the message, the graph returns to the interrupt, and the reviewer sees the new draft. A revision cap keeps a stubborn reviewer and a stubborn writer from looping forever. The cap is the same counter as a retry loop.
A paused thread is a piece of state someone must eventually answer. Give approvals an expiry, or abandoned threads accumulate and a queue nobody drains becomes an outage.
Who is allowed to approve?
The graph records a decision; it does not decide who may make one. Authorisation belongs in the application around the graph: the approver’s identity, the amount they are allowed to approve, and the tenant they belong to. Write the approver id into the state so the audit trail names a person.
The interrupt is not an access control either. Anyone who can call update_state on the thread can approve, so the API that exposes it has to check permissions before it forwards the call.
Editing before resuming
The same mechanism lets a person fix the content rather than approve or reject it. Update the draft in the state, resume, and the send node uses the edited text. No special “edit mode” is needed.
That is a useful pattern for the first weeks of a new workflow: let a human correct the output while you collect the corrections. The diffs become your evaluation set.
Keep the diff. When a reviewer edits the draft, store the original and the edited text together, not just the final message. The diffs are the most honest evaluation data you will get: they show what the system gets wrong in the words of the person who fixed it.
The reviewer edits the draft
Pause before sending, replace the draft in the state, and resume. The send node should use the edited text with no extra machinery.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Where should an approval interrupt fire?
