Interrupt and Human-in-the-Loop Workflows

Pause agent execution for human approval using interrupt(), resume with the Command API, and protect users with a regex PII guardrail node.

Jun 15, 202619 min readFollow

Topics You Will Master

Using interrupt() to pause graph execution inside a tool when a condition is met (e.g. large money transfer)
Resuming paused execution with Command(resume={"decision": "approve"}) for approval workflows
Building a PII guardrail node that blocks SSN, credit card, mobile number, email, and URL patterns using regex
Wiring a three-node graph: guardrail → agent → tools with conditional routing

Human-in-the-loop workflows let agents pause mid-execution to request human approval before taking irreversible actions. LangGraph provides two primitives for this: interrupt() pauses the graph and returns control to the caller, and Command(resume=...) sends the human's decision back to the paused tool.

This lesson also adds a PII guardrail — a preprocessing node that blocks requests containing Social Security Numbers, credit card numbers, phone numbers, email addresses, or URLs before they reach the agent.

Prerequisites: langgraph, langchain-ollama, langgraph-checkpoint-sqlite, python-dotenv installed. Ollama running with qwen3.

BASH
pip install -U langgraph langchain-ollama langgraph-checkpoint-sqlite python-dotenv
ollama pull qwen3

LangGraph & Ollama — AI Agent Development

Build production-ready AI agents with persistent memory, tool calling, and multi-model workflows using LangGraph and local LLMs.

Enroll on Udemy →

Setup

PYTHON
from typing_extensions import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver

from langgraph.types import Command, interrupt

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode
import os
import re

# Configuration
BASE_URL = "http://localhost:11434"
MODEL_NAME = "qwen3"
llm = ChatOllama(model=MODEL_NAME, base_url=BASE_URL)
PYTHON
from dotenv import load_dotenv
load_dotenv()
OUTPUT
True

State Definition

PYTHON
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]

Transfer Money Tool with Interrupt

The transfer_money tool checks the transfer amount. If it exceeds $1,000, the tool calls interrupt() to pause execution and return an approval request to the caller. The caller must resume with a Command containing the decision:

PYTHON
@tool
def transfer_money(amount:int, recipient:str):
    """
    Transfer money. Large transfers require approval.

    Args:
        amount: Amount in dollars
        recipient: Recipient name
    """

    # let's ask user approval if money is more than 1000
    if amount > 1000:
        approval = interrupt(
            {
                "type": "approval_required",
                "amount": amount,
                "recipient": recipient
            }
        )

        if approval.get("decision") != "approve":
            return "Transfer is cancelled!"

    return f"Transferred {amount} to {recipient}"

Test with a small transfer (no interrupt):

PYTHON
transfer_money.invoke({'amount': 100, 'recipient': 'John'})
OUTPUT
'Transferred 100 to John'

Note

When called directly outside the graph, interrupt() is not active — the tool completes normally. The interrupt mechanism only works when the tool runs inside a compiled LangGraph graph with a checkpointer.


PII Guardrail Node

The guardrail node runs before the agent. It scans the user's message for five categories of personally identifiable information using regex patterns:

Diagram showing the regex guardrail blocking SSN, card, phone, email, or URL before the agent

PYTHON
patterns = {
        "SSN": r'\b\d{3}-\d{2}-\d{4}\b',  # SSN: 123-45-6789
        "Credit Card": r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b',  # Credit Card: 1234-5678-9012-3456
        "Mobile Number": r'\b(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b',  # Mobile: +1-234-567-8900
        "Email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',  # Email: user@example.com
        "URL/Link": r'https?://[^\s]+|www\.[^\s]+'  # URL: http://example.com or www.example.com
    }


def guardrail_node(state:AgentState):
    last_message = state['messages'][-1].content

    for pii_type, pattern in patterns.items():
        if re.search(pattern, last_message):
            return {
                "messages": [SystemMessage(f"Request Blocked: Contains {pii_type}. Please don't share sensitive personal information")]
            }
        
    return {"messages": []}

If PII is detected, the guardrail returns a SystemMessage blocking the request. If no PII is found, it returns an empty message list and the graph continues to the agent node.


Agent Node

PYTHON
def agent_node(state:AgentState):

    tools = [transfer_money]

    llm_with_tools = llm.bind_tools(tools)

    system_message = SystemMessage("You are a financial assistant. use transfer_money tool to transfer the money.")

    messages = [system_message] + state['messages']

    response = llm_with_tools.invoke(messages)

    if hasattr(response, 'tool_calls') and response.tool_calls:
        for tc in response.tool_calls:
            print(f"[AGENT] called Tool {tc.get('name', '?')} with args {tc.get('args', '?')}")
    else:
        print(f"[AGENT] Responding...")

    
    return {"messages": [response]}

Conditional Routing

Two routers control the graph flow:

  1. guardrail_router — if the guardrail produced a SystemMessage (PII detected), route to END. Otherwise, route to the agent.
  2. should_continue — standard tool-call routing: if the agent's response has tool_calls, route to tools; otherwise route to END.
PYTHON
def should_continue(state: AgentState):
    last = state['messages'][-1]
    
    if hasattr(last, 'tool_calls') and last.tool_calls:
        return "tools"
    else:
        return END
    
def guardrail_router(state:AgentState):
    last = state['messages'][-1]

    if isinstance(last, SystemMessage):
        last.pretty_print()
        return  END
    
    return 'agent'

Graph Construction

PYTHON
import sqlite3

db_name = "db/checkpoints.db"

os.makedirs('db', exist_ok=True)
PYTHON
def create_agent():

    builder = StateGraph(AgentState)

    # add all nodes
    builder.add_node("guardrail", guardrail_node)
    builder.add_node("agent", agent_node)
    builder.add_node("tools", ToolNode([transfer_money]))

    # add edges
    builder.add_edge(START, "guardrail")
    builder.add_conditional_edges("guardrail", guardrail_router, ['agent', END])
    builder.add_conditional_edges("agent", should_continue, ['tools', END])

    builder.add_edge("tools", "agent")

    # always get a fresh connection
    conn = sqlite3.connect(db_name, check_same_thread=False)
    checkpointer = SqliteSaver(conn)

    graph = builder.compile(checkpointer=checkpointer)

    return graph


agent = create_agent()
agent
OUTPUT
<langgraph.graph.state.CompiledStateGraph object at 0x0000027B23DFBDA0>

The graph has three nodes: guardrail → agent → tools, with conditional edges controlling the flow at each step.

Diagram showing the three-node graph where input passes the guardrail before the agent loops through tools


Demo 1: Small Transfer (No Interrupt)

Transfers under $1,000 complete automatically without human approval:

PYTHON
config = {"configurable": {'thread_id': 'demo-2'}}

query = "Transfer 500 to John."
result = agent.invoke({
    "messages": [HumanMessage(query)]
}, config)
OUTPUT
[AGENT] called Tool transfer_money with args {'amount': 500, 'recipient': 'John'}
[AGENT] Responding...

The agent called the tool, the transfer completed (1,000 threshold), and the agent responded with a confirmation. No interrupt was triggered.


Demo 2: Large Transfer (Approval Required)

Transfers over $1,000 trigger interrupt() and pause the graph:

Diagram showing SqliteSaver persisting the paused state so approval can happen later

PYTHON
query = "Transfer 5000 to John."
result = agent.invoke({
    "messages": [HumanMessage(query)]
}, config)
OUTPUT
[AGENT] called Tool transfer_money with args {'amount': 5000, 'recipient': 'John'}

The graph paused at the interrupt() call inside transfer_money. The result contains an __interrupt__ key with the approval request data.

Handling the Interrupt

Check for the interrupt and resume with the human's decision using Command(resume=...):

PYTHON
if '__interrupt__' in result:

    interrupt_data = result['__interrupt__'][0]

    result = agent.invoke(
        Command(resume={'decision': input("do you approve or reject?")}),
        config
    )

    result['messages'][-1].pretty_print()
OUTPUT
[AGENT] Responding...
================================== Ai Message ==================================

The transfer to John was cancelled. Please check if there are any limits or additional requirements for large transfers. Let me know if you need further assistance!

The user rejected the transfer, so the tool returned "Transfer is cancelled!" and the agent communicated the cancellation.

Reusable Chat Helper

Wrap the interrupt handling into a reusable function:

PYTHON
def chat(query, thread_id):
    config = {"configurable": {'thread_id': thread_id}}
    result = agent.invoke({
        "messages": [HumanMessage(query)]
    }, config)
    

    if '__interrupt__' in result:
        # use this data in front end design for approval workflow
        interrupt_data = result['__interrupt__'][0]

        result = agent.invoke(
            Command(resume={'decision': input("do you approve or reject?")}),
            config
        )

        
    result['messages'][-1].pretty_print()

Test with approval:

PYTHON
query = "Transfer 5000 to John."
thread_id = "demo-3"

chat(query, thread_id)
OUTPUT
[AGENT] called Tool transfer_money with args {'amount': 5000, 'recipient': 'John'}
[AGENT] Responding...
================================== Ai Message ==================================

The transfer of $5000 to **John** has been successfully processed. Let me know if you need further assistance!

Demo 3: PII Guardrail Blocking

The guardrail node blocks requests before they reach the agent. Each PII type triggers a specific block message:

Email Blocked

PYTHON
thread_id = "demo-3"
query = "Transfer 5000 to user@example.com."

chat(query, thread_id)
OUTPUT
================================= System Message =================================

Request Blocked: Contains Email. Please don't share sensitive personal information

Credit Card Blocked

PYTHON
thread_id = "demo-3"
query = "Transfer 5000 to card 1234-5678-9012-3456."

chat(query, thread_id)
OUTPUT
================================= System Message =================================

Request Blocked: Contains Credit Card. Please don't share sensitive personal information

URL Blocked

PYTHON
thread_id = "demo-3"
query = "Transfer 5000 to John and send receipt to https://example.com/receipt."

chat(query, thread_id)
OUTPUT
================================= System Message =================================

Request Blocked: Contains URL/Link. Please don't share sensitive personal information

Mobile Number Blocked

PYTHON
thread_id = "demo-3"
query = "Transfer 5000 to John and send receipt to +1-234-567-8900."

chat(query, thread_id)
OUTPUT
================================= System Message =================================

Request Blocked: Contains Mobile Number. Please don't share sensitive personal information

All five PII types (SSN, Credit Card, Mobile, Email, URL) are caught by the guardrail before the agent processes any sensitive information.


The Interrupt and Guardrail Workflow

Diagram showing transfer_money triggering interrupt() to pause for human approval, then Command(resume) approving or cancelling the transfer


What You Built

In this lesson you built a financial assistant with two safety layers:

  • interrupt() — pauses execution inside a tool when a condition is met (transfer > $1,000)
  • Command(resume=...) — sends the human's approval/rejection decision back to the paused graph
  • PII guardrail — regex-based preprocessing node that blocks SSN, credit card, mobile, email, and URL patterns before the agent sees them
  • Three-node graphguardrail → agent → tools with conditional routing at each stage
  • Persistent stateSqliteSaver ensures the interrupt state survives across calls, enabling async approval workflows

The guardrail protects user privacy at the graph level — no PII ever reaches the LLM. The interrupt mechanism enables production approval workflows where sensitive actions require explicit human authorization.

Found this useful? Keep building with me.

New tutorials every week on YouTube — or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments