CloudCodeTree LogoCloudCodeTree
AI NewsTutorialsAbout
CloudCodeTree Logo
CloudCodeTree
  • AI News
  • Tutorials
  • About
← Back to AI News
Your Agent Shouldn't Act Without Asking: Human-in-the-Loop Approval With LangGraph interrupt()

Your Agent Shouldn't Act Without Asking: Human-in-the-Loop Approval With LangGraph interrupt()

Chris Harper

3 min read

Jul 10, 2026 · 04:03 UTC

AI
Tutorial
Agents
Best Practices

TL;DR: Call interrupt() inside any LangGraph node to freeze execution, snapshot state to a checkpointer, and wait for a human to approve or reject — then resume from exactly that line with Command(resume=...).

What you'll be able to do after this:

  • Pause any agent before a risky action (file delete, email send, external API call) and gate it on human approval
  • Persist the paused state so you can resume from the exact checkpoint — even across server restarts — using a database-backed checkpointer
  • Accept, reject, or redirect an agent mid-run by passing any value through Command(resume=...)

Human-in-the-loop (HITL) is a critical pattern for production agents: let the model plan and propose, but require a human sign-off before anything irreversible happens. LangGraph's interrupt() function is the cleanest way to wire this in.

How it works

interrupt() is a function you call inside a node. When the graph hits it, LangGraph raises a special exception that the runtime catches — it snapshots the full graph state to the checkpointer and returns control to the caller with the interrupt payload. No polling, no background thread: the graph simply pauses.

from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, MessagesState
import os

def risky_action_node(state: MessagesState):
    # Pass a payload describing what we want to do
    approved = interrupt({
        "action": "delete_file",
        "path": state["target"]
    })
    if not approved:
        return {"messages": [{"role": "assistant", "content": "Action cancelled."}]}
    os.remove(state["target"])
    return {"messages": [{"role": "assistant", "content": f"Deleted {state['target']}."}]}

builder = StateGraph(MessagesState)
builder.add_node("risky", risky_action_node)
builder.set_entry_point("risky")

# A checkpointer is REQUIRED — it stores the paused state
graph = builder.compile(checkpointer=InMemorySaver())

InMemorySaver is fine for development. Use PostgresSaver or SqliteSaver for production — the state survives process restarts.

Running and resuming

config = {"configurable": {"thread_id": "session-1"}}

# First call — runs until interrupt()
result = graph.invoke({"target": "/tmp/important.log"}, config)
# result contains the interrupt payload: {'action': 'delete_file', 'path': '/tmp/important.log'}

# Human reviews and decides — resume with True (approve) or False (cancel)
final = graph.invoke(Command(resume=True), config)

The value you pass to Command(resume=...) becomes the return value of interrupt() inside the node. So approved = interrupt(...) will be True after the first example above.

Compile-time breakpoints (for debugging)

If you want the graph to pause before a node runs (without changing the node code), use interrupt_before at compile time:

graph = builder.compile(
    interrupt_before=["risky"],
    checkpointer=InMemorySaver()
)
# Resume by passing None
graph.invoke(None, config)

The one gotcha

When a node resumes after interrupt(), LangGraph re-runs the entire node from the top. Any code before the interrupt() call executes again — so keep pre-interrupt side effects idempotent (reads are fine; writes need guards).

Sources: LangGraph interrupt docs · LangChain blog: Making it easier to build human-in-the-loop agents with interrupt · YouTube walkthrough: LangGraph interrupt