AI Agent Approval Gate: Human-in-the-Loop for Sensitive Tool Execution

TL;DR — AI agent approval gate: human-in-the-loop for sensitive tools. OpenAI Agents SDK: "Pause agent execution until person approves or rejects sensitive tool calls. Tools declare approval needs. RunState serializes and resumes runs after decisions." OpenAI Guardrails: "Result returns interruptions plus resumable state. Approve or reject pending items. Resume same run from state. Serialize, store, resume later." LangChain Deep Agents: "interrupt_on parameter configures which tools require approval. HumanInTheLoopMiddleware intercepts. LangGraph interrupt capabilities." Elastic: "Coding assistant asks permission before executing commands. Claude Code asks confirmation before Bash." DigitalApplied: "LangGraph v1.0 interrupt primitive as primary HITL mechanism. Google Vertex AI ADK supports pausing for human input." Learn more with what is AI agent, tool loop, LangChain agent, and no framework.

OpenAI Agents SDK defines the pattern: "Use the human-in-the-loop (HITL) flow to pause agent execution until a person approves or rejects sensitive tool calls. Tools declare when they need approval, run results surface pending approvals as interruptions, and RunState lets you serialize and resume runs after decisions are made."

LangChain Deep Agents describes the framework integration: "Some tool operations may be sensitive and require human approval before execution. Deep Agents support human-in-the-loop workflows through LangGraph's interrupt capabilities. You can configure which tools require approval using the interrupt_on parameter."

Approval Gate Architecture

flowchart TD subgraph Agent["Agent Loop"] Call["1. LLM Returns Tool Call
name, arguments"] Check{"2. Approval Required?"} Execute["6. Execute Tool
run function with args"] Skip["7. Skip Tool
append rejection message"] Continue["8. Continue Loop
LLM processes result"] end subgraph Gate["Approval Gate"] Intercept["3. Intercept Tool Call
pause execution"] Display["4. Display to Human
tool name, arguments
context, risk level"] Decision{"5. Human Decision"} Approve["Approve
proceed with execution"] Reject["Reject
skip tool, inform agent"] Timeout["Timeout
default reject after N seconds"] end subgraph Sensitive["Sensitive Tools (Require Approval)"] Delete["delete_file
rm, drop"] Write["write_file
modify production"] Email["send_email
notify external"] Exec["run_code
execute on server"] DB["db_write
INSERT/UPDATE/DELETE"] API["api_post
external POST"] Bash["bash_command
shell, terminal"] Money["transfer_money
financial"] end subgraph Safe["Safe Tools (No Approval)"] Search["search
web search"] Read["file_read
read only"] Calc["calculate
math"] Query["db_select
SELECT only"] Get["api_get
GET requests"] end Call --> Check Check -->|In APPROVAL_REQUIRED set| Intercept Check -->|Not in set| Execute Intercept --> Display Display --> Decision Decision -->|Approved| Approve Decision -->|Rejected| Reject Decision -->|No response| Timeout Approve --> Execute Reject --> Skip Timeout --> Skip Execute --> Continue Skip --> Continue Continue --> Call Delete --> Check Write --> Check Email --> Check Exec --> Check DB --> Check API --> Check Bash --> Check Money --> Check Search --> Check Read --> Check Calc --> Check Query --> Check Get --> Check style Gate fill:#4169E1,color:#fff style Sensitive fill:#FF6B6B,color:#fff style Safe fill:#39FF14,color:#000

Tool Classification

Category Examples Approval Risk
Destructive delete_file, drop_table, rm Required Irreversible
Write write_file, update_record Required Modifies state
External send_email, api_post, payment Required External impact
Execution run_code, bash_command Required System access
Read-only search, file_read, db_select Not required No side effects
Compute calculate, process_data Not required Local only

Implementation

from dataclasses import dataclass
from typing import Optional
from enum import Enum

class ApprovalResult(Enum):
    APPROVED = "approved"
    REJECTED = "rejected"
    TIMEOUT = "timeout"

@dataclass
class ApprovalGateGuide:
    """AI agent approval gate implementation guide."""

    def get_no_framework_approval(self) -> str:
        """Approval gate without framework."""
        return (
            "import json\n"
            "from openai import OpenAI\n"
            "\n"
            "client = OpenAI()\n"
            "\n"
            "# Tools that require\n"
            "# human approval\n"
            "APPROVAL_REQUIRED = {\n"
            "    'delete_file',\n"
            "    'write_file',\n"
            "    'send_email',\n"
            "    'run_code',\n"
            "    'bash_command',\n"
            "    'db_write',\n"
            "    'api_post',\n"
            "    'transfer_money',\n"
            "}\n"
            "\n"
            "def get_human_approval(\n"
            "    tool_name: str,\n"
            "    args: dict\n"
            ") -> bool:\n"
            "    '''Prompt human for\n"
            "    approval.'''\n"
            "    print(f'\\n=== APPROVAL'\n"
            "          f' REQUIRED ===')\n"
            "    print(f'Tool: {tool_name}')\n"
            "    print(f'Args: {json.dumps('\n"
            "          f'args, indent=2)}')\n"
            "    print(f'Risk: '\n"
            "          f'{classify_risk('\n"
            "          f'tool_name)}')\n"
            "    resp = input(\n"
            "        'Approve? (y/n): ')\n"
            "    return resp.lower() == 'y'\n"
            "\n"
            "def classify_risk(\n"
            "    tool_name: str\n"
            ") -> str:\n"
            "    '''Classify risk level.'''\n"
            "    destructive = {\n"
            "        'delete_file', 'drop_table'}\n"
            "    external = {\n"
            "        'send_email',\n"
            "        'api_post',\n"
            "        'transfer_money'}\n"
            "    if tool_name in \\\n"
            "        destructive:\n"
            "        return 'HIGH -'\n"
            "            ' Irreversible'\n"
            "    if tool_name in external:\n"
            "        return 'HIGH -'\n"
            "            ' External impact'\n"
            "    return 'MEDIUM -'\n"
            "        ' Modifies state'\n"
            "\n"
            "def run_agent_with_approval(\n"
            "    user_msg: str,\n"
            "    max_steps: int = 10\n"
            ") -> str:\n"
            "    '''Agent with approval\n"
            "    gate.'''\n"
            "    messages = [\n"
            "        {'role': 'system',\n"
            "         'content': SYSTEM},\n"
            "        {'role': 'user',\n"
            "         'content': user_msg}\n"
            "    ]\n"
            "    \n"
            "    for step in range(max_steps):\n"
            "        resp = client.chat\\\n"
            "            .completions.create(\n"
            "            model='gpt-4o',\n"
            "            messages=messages,\n"
            "            tools=TOOLS)\n"
            "        msg = resp\\\n"
            "            .choices[0].message\n"
            "        messages.append(msg)\n"
            "        \n"
            "        if not msg.tool_calls:\n"
            "            return msg.content\n"
            "        \n"
            "        for call in \\\n"
            "            msg.tool_calls:\n"
            "            name = call.function\\\n"
            "                .name\n"
            "            args = json.loads(\n"
            "                call.function\n"
            "                .arguments)\n"
            "            \n"
            "            # APPROVAL GATE\n"
            "            if name in \\\n"
            "                APPROVAL_REQUIRED:\n"
            "                approved = \\\n"
            "                    get_human_approval(\n"
            "                        name, args)\n"
            "                if not approved:\n"
            "                    messages.append({\n"
            "                        'role': 'tool',\n"
            "                        'tool_call_id':\n"
            "                            call.id,\n"
            "                        'content':\n"
            "                            'REJECTED by'\n"
            "                            ' human. Do not'\n"
            "                            ' retry.'\n"
            "                    })\n"
            "                    continue\n"
            "            \n"
            "            # Execute tool\n"
            "            func = TOOL_MAP.get(\n"
            "                name)\n"
            "            if func:\n"
            "                try:\n"
            "                    result = func(\n"
            "                        **args)\n"
            "                except Exception as e:\n"
            "                    result = f'Err: {e}'\n"
            "            else:\n"
            "                result = (\n"
            "                    f'Unknown: {name}')\n"
            "            \n"
            "            messages.append({\n"
            "                'role': 'tool',\n"
            "                'tool_call_id':\n"
            "                    call.id,\n"
            "                'content': str(\n"
            "                    result)\n"
            "            })\n"
            "    \n"
            "    return 'Max steps'"
        )

    def get_openai_sdk_approval(self) -> str:
        """OpenAI Agents SDK HITL."""
        return (
            "from agents import (\n"
            "    Agent, Runner, RunState\n"
            ")\n"
            "\n"
            "# OpenAI Agents SDK HITL\n"
            "# Tools declare approval\n"
            "# needs, runs surface as\n"
            "# interruptions, RunState\n"
            "# serializes and resumes\n"
            "\n"
            "agent = Agent(\n"
            "    name='SafeAgent',\n"
            "    model='gpt-4o',\n"
            "    tools=[\n"
            "        search_tool,\n"
            "        # delete_tool has\n"
            "        # needs_approval=True\n"
            "        delete_tool,\n"
            "        email_tool,\n"
            "    ]\n"
            ")\n"
            "\n"
            "# Run agent\n"
            "state = Runner.run_streamed(\n"
            "    agent, 'Delete old files')\n"
            "\n"
            "# Check for interruptions\n"
            "if state.interruptions:\n"
            "    for interrupt in \\\n"
            "        state.interruptions:\n"
            "        print(f'Approval needed:')\n"
            "        print(f'  Tool: {interrupt.tool_name}')\n"
            "        print(f'  Args: {interrupt.args}')\n"
            "        \n"
            "        # Get human decision\n"
            "        decision = \\\n"
            "            get_human_approval(\n"
            "                interrupt.tool_name,\n"
            "                interrupt.args)\n"
            "        \n"
            "        if decision:\n"
            "            state.approve(\n"
            "                interrupt.id)\n"
            "        else:\n"
            "            state.reject(\n"
            "                interrupt.id)\n"
            "    \n"
            "    # Resume from state\n"
            "    # (same run, not new)\n"
            "    result = Runner.resume(\n"
            "        state)\n"
            "else:\n"
            "    result = state.result\n"
            "\n"
            "# If review takes time:\n"
            "# serialize state, store,\n"
            "# resume later — same run\n"
            "serialized = state.serialize()\n"
            "# Store serialized...\n"
            "# Later: resume\n"
            "# state = RunState.deserialize(\n"
            "#     serialized)\n"
            "# result = Runner.resume(\n"
            "#     state)"
        )

    def get_langgraph_approval(self) -> str:
        """LangGraph interrupt for approval."""
        return (
            "from langchain.agents import (\n"
            "    create_agent\n"
            ")\n"
            "from langchain.agents.middleware \\\n"
            "    .human_in_the_loop import (\n"
            "        InterruptOnConfig,\n"
            "        HITLRequest\n"
            "    )\n"
            "\n"
            "# LangGraph interrupt\n"
            "# primitive as primary\n"
            "# HITL mechanism\n"
            "# Configure which tools\n"
            "# require approval\n"
            "\n"
            "agent = create_agent(\n"
            "    model=model,\n"
            "    tools=tools,\n"
            "    system_prompt='...',\n"
            "    interrupt_on=[\n"
            "        'delete_file',\n"
            "        'write_file',\n"
            "        'send_email',\n"
            "        'run_code'\n"
            "    ],\n"
            "    checkpointer=\n"
            "        InMemorySaver()\n"
            ")\n"
            "\n"
            "# First invocation —\n"
            "# pauses at sensitive\n"
            "# tool\n"
            "result = agent.invoke(\n"
            "    {'messages': [\n"
            "        {'role': 'user',\n"
            "         'content': 'Delete'\n"
            "          ' old log files'}\n"
            "    ]},\n"
            "    config={\n"
            "        'configurable': {\n"
            "            'thread_id': 's1'\n"
            "        }\n"
            "    }\n"
            ")\n"
            "\n"
            "# Check for interrupt\n"
            "# Agent paused before\n"
            "# delete_file execution\n"
            "\n"
            "# Human reviews and\n"
            "# approves\n"
            "result = agent.invoke(\n"
            "    None,  # No new message\n"
            "    config={\n"
            "        'configurable': {\n"
            "            'thread_id': 's1'\n"
            "        }\n"
            "    }\n"
            ")\n"
            "# Agent resumes from\n"
            "# checkpoint and executes\n"
            "# approved tool"
        )

    def get_approval_patterns(self) -> dict:
        """Approval patterns."""
        return {
            "pre_execution": (
                "Pause before tool "
                "execution. Human reviews "
                "tool name and arguments. "
                "Most common pattern."
            ),
            "post_execution_review": (
                "Execute tool, then human "
                "reviews result. Used for "
                "quality control, not "
                "safety. Less common."
            ),
            "batch_approval": (
                "Agent proposes multiple "
                "actions. Human approves "
                "all at once. Efficient "
                "for bulk operations."
            ),
            "tiered_approval": (
                "Low-risk: auto-approve. "
                "Medium-risk: team lead. "
                "High-risk: manager. "
                "Risk-based escalation."
            ),
            "timeout_reject": (
                "If no human response in "
                "N seconds, default to "
                "reject. Prevents agent "
                "from hanging indefinitely."
            ),
        }

    def get_audit_trail(self) -> dict:
        """Audit trail for compliance."""
        return {
            "log_decisions": (
                "Log every approval "
                "decision: timestamp, "
                "tool, args, approver, "
                "decision, reason."
            ),
            "store_state": (
                "Serialize RunState or "
                "LangGraph checkpoint. "
                "Enables resume across "
                "sessions and audit."
            ),
            "risk_classification": (
                "Classify each tool by "
                "risk level: HIGH "
                "(irreversible), MEDIUM "
                "(modifies state), LOW "
                "(read-only)."
            ),
            "escalation_log": (
                "Log escalation chain: "
                "who approved, when, "
                "what context was shown. "
                "Required for regulated "
                "industries."
            ),
        }

AI Agent Approval Gate Checklist

  • [ ] Approval gate: pause agent execution before sensitive tool calls, require human approval
  • [ ] OpenAI Agents SDK: tools declare approval needs, runs surface as interruptions, RunState serializes and resumes
  • [ ] OpenAI: result returns interruptions plus resumable state — approve or reject, resume same run
  • [ ] OpenAI: if review takes time, serialize state, store, resume later — still same run
  • [ ] LangGraph: interrupt primitive as primary HITL mechanism (v1.0)
  • [ ] LangGraph: interrupt_on parameter configures which tools require approval
  • [ ] LangGraph: HumanInTheLoopMiddleware intercepts when those tools are called
  • [ ] LangGraph: execution pauses at interrupt point, resume from checkpoint with decision
  • [ ] LangGraph: state persisted via checkpointer — can resume across sessions
  • [ ] Deep Agents: support HITL through LangGraph interrupt capabilities
  • [ ] Google Vertex AI ADK: supports pausing for human input anywhere in agent flow
  • [ ] Claude Code: uses HITL to ask confirmation before executing Bash commands
  • [ ] Sensitive tools requiring approval: delete_file, write_file, send_email, run_code, bash_command, db_write, api_post, transfer_money
  • [ ] Safe tools NOT requiring approval: search, file_read, calculate, db_select, api_get
  • [ ] Risk classification: HIGH (irreversible — delete, drop), MEDIUM (modifies state — write, update), LOW (read-only — search, read)
  • [ ] Approval flow: (1) LLM returns tool call, (2) check if approval required, (3) intercept and pause, (4) display to human, (5) human decides, (6) execute or skip, (7) continue loop
  • [ ] If rejected: append rejection message as tool result, inform agent not to retry
  • [ ] If approved: execute tool, append result, continue agent loop
  • [ ] No-framework implementation: APPROVAL_REQUIRED set, get_human_approval function, check before execute
  • [ ] No-framework: ~20 lines added to agent loop for approval gate
  • [ ] OpenAI SDK: RunState.serialize() and RunState.deserialize() for async approval
  • [ ] OpenAI SDK: Runner.resume(state) to continue from approval point
  • [ ] LangGraph: interrupt_on parameter in create_agent, invoke(None) to resume after approval
  • [ ] LangGraph: checkpointer persists state across sessions for async approval
  • [ ] Approval patterns: pre_execution (most common), post_execution_review, batch_approval, tiered_approval, timeout_reject
  • [ ] Tiered approval: low-risk auto-approve, medium-risk team lead, high-risk manager
  • [ ] Timeout: if no human response in N seconds, default to reject — prevents hanging
  • [ ] Audit trail: log every decision — timestamp, tool, args, approver, decision, reason
  • [ ] Audit trail: store RunState or checkpoint for resume and audit
  • [ ] Audit trail: risk classification per tool, escalation chain log
  • [ ] Required for regulated industries: full audit trail of all approval decisions
  • [ ] Read what is AI agent for architecture
  • [ ] Read tool loop for agent loop
  • [ ] Read LangChain agent for LangGraph interrupt
  • [ ] Read no framework for vanilla implementation
  • [ ] Test: approval gate pauses before sensitive tool execution
  • [ ] Test: safe tools execute without approval
  • [ ] Test: rejection appends message and agent adapts
  • [ ] Test: approval executes tool and continues
  • [ ] Test: timeout defaults to reject after N seconds
  • [ ] Test: RunState serialize/deserialize works for async approval
  • [ ] Test: LangGraph checkpoint resume works across sessions
  • [ ] Test: audit trail captures all decisions with metadata
  • [ ] Document APPROVAL_REQUIRED set, risk classification, approval pattern, timeout, audit trail

FAQ

What is an AI agent approval gate?

An approval gate pauses agent execution before sensitive tool calls, requiring human approval to proceed. OpenAI Agents SDK: "Use the human-in-the-loop (HITL) flow to pause agent execution until a person approves or rejects sensitive tool calls. Tools declare when they need approval, run results surface pending approvals as interruptions, and RunState lets you serialize and resume runs after decisions are made." LangChain Deep Agents: "Some tool operations may be sensitive and require human approval before execution. Deep Agents support human-in-the-loop workflows through LangGraph interrupt capabilities. Configure which tools require approval using the interrupt_on parameter. When interrupt_on is set, HumanInTheLoopMiddleware intercepts." Elastic: "A common example is when your coding assistant asks for permission to execute a command on the terminal or shows step-by-step thinking for you to approve before starting. Claude Code uses human in the loop to ask for confirmation before executing a Bash command." Approval gate: (1) Tool declares it needs approval. (2) Agent pauses before executing. (3) Human reviews tool name, arguments, context. (4) Human approves or rejects. (5) If approved: execute tool, resume agent. (6) If rejected: skip tool, inform agent, continue.

How does the OpenAI Agents SDK handle human-in-the-loop?

Tools declare approval needs, runs surface pending approvals as interruptions, RunState serializes and resumes. OpenAI Agents SDK: "Tools declare when they need approval, run results surface pending approvals as interruptions, and RunState lets you serialize and resume runs after decisions are made. That approval flow is built into the SDK." OpenAI Guardrails: "The result returns interruptions plus a resumable state. Your application approves or rejects the pending items. You resume the same run from state instead of starting a new user turn. If the review might take time, serialize state, store it, and resume later. That is still the same run." OpenAI HITL: (1) Tool defines needs_approval=True. (2) Agent run pauses at tool call. (3) RunState captures current state. (4) Application shows pending approval to human. (5) Human approves or rejects. (6) Resume run from RunState with decision. (7) If review takes time: serialize state, store, resume later — same run.

How does LangGraph handle approval interrupts?

LangGraph uses the interrupt primitive to pause execution and wait for human input. LangChain Deep Agents: "Deep Agents support HITL through LangGraph interrupt capabilities. Configure which tools require approval using interrupt_on parameter. HumanInTheLoopMiddleware intercepts when those tools are called." DigitalApplied: "LangGraph v1.0 positioned its interrupt primitive as the primary HITL mechanism. Google Vertex AI Agent Development Kit similarly supports pausing for human input anywhere in the agent flow." Elastic: "LangGraph interrupt capabilities for human-in-the-loop. Coding assistant asks permission before executing commands." LangGraph HITL: (1) interrupt_on parameter lists tools requiring approval. (2) HumanInTheLoopMiddleware intercepts tool calls. (3) Execution pauses at interrupt point. (4) Human reviews and decides. (5) Resume from checkpoint with decision. (6) State persisted via checkpointer — can resume across sessions.

Which tools should require human approval?

Sensitive tools that have irreversible or high-impact side effects should require approval. OpenAI Agents SDK: "Pause agent execution until a person approves or rejects sensitive tool calls." Elastic: "Coding assistant asks for permission to execute a command on the terminal. Claude Code asks for confirmation before executing a Bash command." DigitalApplied: "Escalation design for AI agents — when to involve humans." Tools requiring approval: (1) File deletion: rm, delete, drop. (2) File writing: modify production files. (3) Email sending: send_email, notify. (4) Code execution: run arbitrary code on server. (5) Database writes: INSERT, UPDATE, DELETE, DROP. (6) API calls: external POST requests, payments. (7) System commands: bash, shell, terminal. (8) Money transfers: financial transactions. Tools NOT requiring approval: (1) Read-only: search, file_read, GET requests. (2) Calculations: math, data processing. (3) Internal queries: database SELECT, API GET.

How do you implement an approval gate without a framework?

Check tool name before execution, prompt for approval, proceed or skip based on response. OpenAI Agents SDK: "Tools declare when they need approval. RunState lets you serialize and resume." OpenAI Guardrails: "Serialize state, store it, and resume later. That is still the same run." Implementation: (1) Define APPROVAL_REQUIRED set with sensitive tool names. (2) Before executing each tool: if tool name in APPROVAL_REQUIRED: prompt human. (3) Show tool name, arguments, and context to human. (4) If approved: execute tool, append result. (5) If rejected: append rejection message, let agent adapt. (6) For async: serialize state, wait for approval, resume. (7) Log all approval decisions for audit trail. (8) Timeout: if no response in N seconds, default to reject. Total: ~20 lines added to agent loop.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →