The first version of our adjustment chat interrogated the user endlessly, one question after another. Unbearable. The right posture is act-first: act by default with explicit assumptions, and ask only on a genuine blocker.
an interrogation.
a good colleague.
LangGraph provides interrupt(): called inside a node, it suspends the run and surfaces a question to your app. The graph doesn’t “block” a server thread — it stops and hands control back.
On the app side, the first call stops at the interrupt and returns the question. You show it, the user answers, and you re-run the same run with their answer via Command({ resume }).
For the agent to resume exactly where it left off — even after a redeploy, or an answer two hours later — the state must be persisted. That’s the checkpointer’s job: it saves the graph state at each step, here in Postgres.
With a checkpointer, a run becomes a resumable conversation, not a throwaway call. That’s what makes human-in-the-loop usable in real production.
Reserve the pause for real decision points (destructive actions, costly ambiguity). An interrupt at every step and you recreate the interrogation you meant to escape.
Act-first has one clean exception: irreversible actions. Deleting data, emailing a customer, confirming a payment, pushing to prod — there, the agent doesn't guess, it asks for approval. Anthropic puts it well in Building effective agents: you place checkpoints before high-stakes actions, not at every step. It's the same interrupt() as for a clarification, but what you surface is no longer an open question — it's a plan to approve.
A good approval gate isn't just "yes / no." You also want to let the human edit the action before it fires. The interrupt payload surfaces the proposed action, and the answer via Command({ resume }) carries the decision — which the node then executes.
Classic trap: on resume, LangGraph re-runs the node from the top, not from the interrupt line — see the Interrupts docs. So any side effect placed before interrupt() (log, write, API call) replays. Keep the node "pure" before the gate, and put the irreversible action only after the decision.
An approval gate with no time limit becomes a zombie run: suspended forever because nobody answered. Since state is persisted by the checkpointer (thread_id + Postgres), the run costs nothing while it waits — but you still need a policy. In practice I store a deadline alongside the checkpoint and run a job that, past the delay, resumes the thread with a default decision (usually reject) or escalates to another approver.
A checkpointer gives you resumption: state is written at each step, you can re-run a thread by its thread_id, even after a crash — well documented on the Persistence side and the durable execution side. But resuming isn't automatically running to completion. The checkpoint saves your place; it's still on you to detect the failure and re-run with the right identifier.
"I saved your state."
"it will run to completion."
Diagrid lays out the nuance well in Checkpoints are not durable execution. For plain human-in-the-loop, the checkpointer is more than enough. If your workflows run for hours with automatic retries and multiple workers, look at a real durable-execution engine (Temporal, Restate) that caches finished steps instead of replaying them.
The architecture that survives a traffic spike: EC2 for the site, RDS for the database, S3 + Lambda for WebP-optimized product photos.
Get the agent out of the black box: instrument every step, measure cost, and score it with an LLM-as-a-judge.