LangGraph interrupt() for Approval: What's Still Missing

April 22, 2026  ·  8 min read

LangGraph’s interrupt() function works exactly as advertised. Call it inside a node, execution pauses, state is saved, and the graph waits. The LangGraph interrupt approval problem shows up in production, not in development, because in development, the developer is the approver.

You’re watching the terminal. You see the interrupt fire. You type True and the graph resumes.

Then you deploy. The agent runs in production. It calls interrupt() and waits. Your approver — the finance lead, the compliance officer, the customer success manager — has no idea anything is waiting. The thread sits in the checkpointer indefinitely. The agent does nothing. Eventually someone notices the workflow has stalled, digs into the logs, and manually resumes or terminates it.

This is the gap between a working demo and a working approval system. This article maps it precisely.

Key takeaways

  • interrupt() pauses execution and persists state — it does not notify, time out, escalate, or produce an audit record.
  • Building the surrounding infrastructure (notifications, timeouts, escalation, audit log) involves seven distinct components and represents a significant engineering project.
  • Replacing the approval node with a single SDK call adds all four missing capabilities without changing the graph structure.
  • The two approaches combine well: interrupt() for in-session approvals, a dedicated approval API for out-of-band decisions.
  • dev_mode=True lets you test the full approval flow without spamming your approver during development.

What LangGraph interrupt() actually gives you

The mechanics are covered in depth in our LangGraph human-in-the-loop tutorial and the official interrupt() documentation. The short version:

interrupt(value) pauses graph execution inside a node. The value you pass is surfaced to the caller — the question, context, or action summary the approver needs to make a decision. LangGraph persists graph state to the checkpointer and marks the thread as interrupted.

To resume: call graph.invoke(Command(resume=response), config) with the same thread_id. The node restarts from the beginning (not from the interrupt() call), and the value in Command(resume=...) becomes the return value of interrupt().

Static breakpoints work similarly. Set interrupt_before or interrupt_after at compile time to always pause at a named node, regardless of state.

This is the right tool for:

  • Chatbot approvals where the user is active in the same conversation
  • Developer-facing tools where you are the reviewer
  • Streamlit prototypes where the approval UI is part of the application

The limitation shows up when the approver is somewhere else.

What a production approval workflow actually requires

When the approver is someone who is not watching your terminal, the LangGraph interrupt approval mechanism has three significant gaps.

No notification. LangGraph interrupt notifications don’t exist. When a thread interrupts, the graph state is persisted and the thread is marked. No email is sent. No Slack message fires. If your approver is Ibrahim in legal and he needs to sign off before the agent sends a contract, you need to build that entire notification layer yourself.

No timeout or escalation. Interrupted threads wait indefinitely. LangGraph has no built-in mechanism to say: “If nobody responds within 30 minutes, escalate to the backup approver. If nobody responds in two hours, auto-deny and log the expiry.” Getting this requires a cron job polling for stale interrupted threads, retry logic, fallback routing, and a way to programmatically resume with a timeout response.

No structured audit trail. LangGraph’s checkpointer records graph state — useful for debugging and resumability, but not a decision record. It does not produce a structured log of who was asked, when they responded, what they chose, and any notes they left. For most production deployments, and specifically for EU AI Act Article 14 compliance, that record is required.

These are not edge cases. They are the core requirements of any LangGraph HITL production deployment. For a framework to classify which actions need gating first, see how to prevent AI agents from taking irreversible actions.

What it costs to build the missing layer yourself

The open-source production template for LangGraph interrupt workflows on GitHub illustrates what rolling your own approval infrastructure actually involves: a FastAPI backend handling LangGraph state management, a Next.js frontend with real-time UI updates, and Docker Compose for deployment. The language breakdown is roughly 70% TypeScript, 26% Python. This is a real engineering project, not a few lines of glue code.

Consider what Meena’s team at a fintech startup discovered in 2025. They had a working LangGraph interrupt flow in development, where Meena herself clicked approve or deny during testing. After deploying to production, the agent would interrupt waiting for their risk officer, David, to respond. David had no way of knowing. The agent would sit for hours. Meena’s team spent three weeks building a Slack notification system, a cron-based timeout handler, and a basic audit log schema before the workflow was production-ready. Those three weeks came directly out of the feature backlog.

The seven components needed:

  1. Interception — detect the interrupt event and extract the action context
  2. Notification — send the approver an email, Slack message, or webhook with enough detail to decide
  3. Context delivery — format the action summary so the decision is informed, not blind
  4. Decision capture — receive a structured approved, denied, or modified response
  5. Timeout handling — define what happens if nobody responds in 30 minutes or two hours
  6. Audit log — record who was asked, when, what they chose, and any notes they provided
  7. Resume or abort — translate the decision back into a Command(resume=...) call or a graceful failure

Components 1 and 7 are agent code. Components 2 through 6 are infrastructure. Most teams build component 1 during development and discover the rest during their first production incident.

How The Handover replaces the approval node

The graph structure stays identical. The approval node changes.

Here is the interrupt() version:

from langgraph.types import interrupt, Command
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from typing import TypedDict

class State(TypedDict):
    action: str
    approved: bool

def approval_node(state: State):
    decision = interrupt(
        f"Agent wants to: {state['action']}. Approve? (True/False)"
    )
    return {"approved": decision}

Here is the same node using the Handover SDK:

from handover import HandoverClient

client = HandoverClient(api_key="ho_your_key_here")

def approval_node(state: State):
    decision = client.decisions.create(
        action=state["action"],
        context="Agent requesting sign-off before proceeding.",
        approver="approver@yourcompany.com",
        urgency="high",
        timeout_minutes=30,
    )
    return {"approved": decision.approved}

The approver receives an email with the action description, full context, and one-click Approve, Deny, or Modify buttons. No login required. The decision — including any notes the approver added — comes back as a structured response. Every decision is logged automatically.

This is what a human approval API for LangGraph agents provides that interrupt() does not: the full out-of-band approval lifecycle in a single SDK call.

This version does not require a checkpointer. The agent stays in the polling loop until the approver responds. If you would rather fire the decision and move on, pass a callback_url — the POST /decisions endpoint returns immediately and POSTs to your server the moment the approver decides.

For testing during development, initialise with dev_mode=True. Decisions are auto-approved without sending any notifications, so you can run the full workflow in CI or locally without reaching your approver.

client = HandoverClient(api_key="ho_your_key_here", dev_mode=True)

If you want the agent to raise on denial rather than returning approved=False and branching manually, set enforce=True. The SDK raises ActionDenied and the agent stops cleanly.

decision = client.decisions.create(
    action=state["action"],
    approver="approver@yourcompany.com",
    enforce=True,
)
# Only reached if approved

Beyond a simple approve or deny, The Handover supports rich response types: choose for picking from options, number for threshold inputs, text for free-form notes, file_upload for returning documents, and schedule for datetime responses. The LangChain integration guide has copy-paste examples for both the StateGraph pattern used here and the create_react_agent pattern. For a step-by-step walkthrough of the LangChain agent pattern, see the human approval LangChain tutorial.

Install with pip install handover-sdk or npm install @handover/sdk. See the full API reference to get started.

When to use LangGraph interrupt(), when to use The Handover

Need LangGraph interrupt() The Handover
Approver is in the same sessionSufficientOverkill
External approver (email)Build yourselfBuilt in
Slack DM notificationsBuild yourselfScale+
Timeout + auto-escalationBuild yourselfBuilt in
Structured audit trailBuild yourselfBuilt in
EU AI Act decision recordBuild yourselfBuilt in
No checkpointer neededNoYes
Dev mode for testingNodev_mode=True
Raise on denialBuild yourselfenforce=True
Rich structured responseRaw resume valuechoose, number, text, file_upload

The two approaches are not alternatives — they are layers. Use interrupt() when the approver is interactive and in the same session. Use The Handover when the approver is someone else who needs to be notified, whose decision needs to be tracked, and whose response needs to be logged.

Both work in the same graph. Checkpoints that validate the agent’s plan before execution are natural candidates for interrupt(). Out-of-band approvals — the kind where LangGraph interrupt timeout escalation and audit logging are required — are candidates for a dedicated decision.

Frequently asked questions

Does LangGraph interrupt() send email or Slack notifications?

No. interrupt() persists graph state and marks the thread as interrupted. No notification of any kind is sent. If your approver needs to know the agent is waiting, you must build that notification layer yourself or use a dedicated approval API.

What happens if nobody responds to a LangGraph interrupt?

The thread waits indefinitely. LangGraph has no built-in timeout or escalation. For production systems, you need external scheduling logic that polls for stale interrupted threads and either escalates or auto-denies after a defined window.

Can I use LangGraph interrupt() and The Handover in the same graph?

Yes. Use interrupt() for in-session approvals where the user is actively engaged. Use The Handover for out-of-band approvals where the approver receives a notification and responds asynchronously. A single graph can use both patterns in different nodes.

Does The Handover require a LangGraph checkpointer?

No. When the approval node calls client.decisions.create() and polls for a response, the agent stays in the loop without needing to persist state across a process restart. This simplifies the production setup considerably.

How do I test the approval flow locally without sending emails?

Pass dev_mode=True when initialising HandoverClient. Decisions auto-approve without sending notifications, so you can run the full agent workflow in CI or local development without reaching your approver.

The interrupt() function is the right primitive for pausing a LangGraph graph. A dedicated human approval API is the right wrapper for turning that pause into a complete LangGraph interrupt approval workflow. They solve different problems at different layers.

Ready to add human oversight to your agent?

Free to start. No credit card required. Takes five minutes.

Get Started Free