DS DevShelfHub Projects · AI tools
Tutorials / LangChain / Human-in-the-Loop
LangGraph Advanced · 10 min read Page 15 of 20

Human-in-the-Loop in LangGraph: Approvals and Interrupts

By DevShelfHub

Pause a running LangGraph agent at any node, present the current state to a human for review, then resume, edit, or abort — giving you full control over autonomous actions.

Series progress15 / 20
Human-in-the-loop in LangGraph — pause agents to approve tool calls, edit state, and resume

Why Human-in-the-Loop?

Fully autonomous agents are powerful but risky: they can call APIs, write files, send emails, or make purchases. HITL lets you insert human judgment at critical decision points — approve before executing, review before replying, or correct a wrong assumption mid-flight.

Approve actions

Review and approve destructive tool calls before execution.

Edit state

Correct the agent's plan or assumptions mid-execution.

Provide input

Supply missing information the agent can't retrieve on its own.

Requirement: HITL requires a checkpointer. The graph must persist state so it can be resumed after the human responds. Use MemorySaver for development and SqliteSaver / PostgresSaver in production.

interrupt_before

Pass interrupt_before=["tools"] to .compile() to pause the graph before any tool is executed. The graph saves its state at the interrupt point and waits.

python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent

memory = MemorySaver()
agent = create_react_agent(
    model=llm,
    tools=tools,
    checkpointer=memory,
    interrupt_before=["tools"],   # pause BEFORE tools node runs
)

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

# The graph runs until it's about to call a tool, then pauses
result = agent.invoke(
    {"messages": [("user", "Delete all logs older than 30 days")]},
    config=config,
)

# result may be None or partial — the graph is interrupted
snapshot = agent.get_state(config)
print(snapshot.next)              # ('tools',) — waiting here
print(snapshot.values["messages"][-1].tool_calls)  # what it wants to call

Approving and Resuming

After inspecting the pending tool call, resume the graph by calling .invoke(None, config). Passing None as input tells LangGraph to continue from where it stopped.

python
# Inspect pending tool calls
snapshot = agent.get_state(config)
pending = snapshot.values["messages"][-1].tool_calls
for call in pending:
    print(f"Tool: {call['name']}, Args: {call['args']}")

# Human reviews and approves
human_decision = input("Approve? (y/n): ")

if human_decision.lower() == "y":
    # Resume: pass None to continue from the interrupt point
    final = agent.invoke(None, config=config)
    print(final["messages"][-1].content)
else:
    print("Action cancelled by user.")

Editing State Before Resuming

Use graph.update_state() to modify the graph's state at the interrupt point — change arguments, remove tool calls, or add messages — before resuming.

python
from langchain_core.messages import AIMessage

snapshot = agent.get_state(config)
last_ai_message = snapshot.values["messages"][-1]

# Modify the tool call arguments
modified_tool_calls = [
    {
        **call,
        "args": {**call["args"], "days": 90},  # change 30 → 90 days
    }
    for call in last_ai_message.tool_calls
]

# Create a new AIMessage with modified tool calls
corrected_message = AIMessage(
    content=last_ai_message.content,
    tool_calls=modified_tool_calls,
    id=last_ai_message.id,  # same ID replaces the original message
)

# Update the graph state
agent.update_state(
    config,
    {"messages": [corrected_message]},
)

# Resume with the edited state
final = agent.invoke(None, config=config)
print(final["messages"][-1].content)

interrupt_after

Pause after a node completes — useful for reviewing the agent's response before delivering it to the user, or checking intermediate reasoning.

python
agent = create_react_agent(
    model=llm,
    tools=tools,
    checkpointer=memory,
    interrupt_after=["agent"],   # pause AFTER agent node — review the response
)

result = agent.invoke(
    {"messages": [("user", "Draft a refund email for order #1234")]},
    config=config,
)

snapshot = agent.get_state(config)
draft = snapshot.values["messages"][-1].content
print("Draft response:", draft)

# Human edits the draft, then resume
# (or just call invoke(None, config) to approve as-is)
final = agent.invoke(None, config=config)

interrupt() Inside a Node

For fine-grained control, call interrupt() directly inside a node function. This lets you pause conditionally — only when certain criteria are met — rather than always pausing at a fixed node.

python
from langgraph.types import interrupt

def sensitive_action_node(state: MessagesState):
    action = determine_action(state)

    if action.is_destructive:
        # Pause and surface info to the human
        human_response = interrupt({
            "question": "This action will delete data. Proceed?",
            "action": action.description,
        })
        if human_response != "yes":
            return {"messages": [("assistant", "Action cancelled.")]}

    # Proceed if approved
    result = execute_action(action)
    return {"messages": [("tool", result)]}

The value passed to interrupt() is surfaced to your application layer via the __interrupt__ key in the graph output. Resume by calling Command(resume="yes").

HITL Pattern Summary

Pattern How Best For
interrupt_beforecompile(interrupt_before=[node])Approve every tool call
interrupt_aftercompile(interrupt_after=[node])Review agent output before delivery
interrupt()Call inside node functionConditional approval logic
update_state()Call between invocationsEdit agent plan or tool args
invoke(None)Resume after interruptContinue from paused state

LangGraph Human-in-the-Loop FAQ

What is human-in-the-loop in LangGraph?

Human-in-the-loop (HITL) is a pattern where a LangGraph agent pauses mid-execution so a person can review, approve, edit, or reject what the agent is about to do. Instead of running fully autonomously, the graph saves its state at a defined point and waits for human input before continuing, which is essential when agents can call APIs, write files, or take destructive actions.

How does interrupt_before work in LangGraph?

Pass interrupt_before with a list of node names to compile() or create_react_agent(), for example interrupt_before=['tools']. The graph runs until it is about to enter that node, then pauses and persists its state through the checkpointer. You inspect the pending action with get_state(config), and the snapshot.next field shows which node is waiting to run.

How do I approve a tool call before it runs?

When the graph is interrupted before the tools node, read the pending tool calls from snapshot.values['messages'][-1].tool_calls and present them to a human. If the human approves, resume the graph; if they reject, do not resume and surface a cancellation message instead. This gives you an explicit approval gate in front of any tool the agent wants to execute.

How do I resume a LangGraph agent after an interrupt?

Resume by calling invoke(None, config) with the same thread_id config used for the original run. Passing None as the input tells LangGraph to continue from the saved interrupt point rather than starting a new run. If you used interrupt() inside a node, resume by passing Command(resume=value) so the returned value flows back into the node.

Can I edit the agent state before resuming?

Yes. Call update_state(config, {...}) while the graph is paused to change tool-call arguments, remove tool calls, or add messages. Reusing the same message id replaces the original message instead of appending a new one. After updating the state you call invoke(None, config) to resume execution with the corrected state.

Why does human-in-the-loop require a checkpointer?

A checkpointer persists the graph state so it can be paused and resumed across separate invocations and even across processes. Without one, the agent has nowhere to store the interrupted state while it waits for a human. Use MemorySaver for development and SqliteSaver or PostgresSaver in production so interrupted sessions survive restarts.

Quick jump: API Reference