AI Agent Action Approval: Human-in-the-Loop for High-Risk Operations

TL;DR — AI agent action approval is the human-in-the-loop governance pattern for high-risk agent operations. Three-tier policy: allow (safe, proceed), deny (dangerous, blocked), escalate (ambiguous, needs human). Agent pauses execution, notifies approver with full context (action, reasoning, data sources, consequences), waits for decision. Fail-safe: auto-deny on timeout (default 5 min) — unanswered escalation never becomes approval. Multi-level escalation: primary reviewer → secondary → management → auto-deny. Microsoft Agent Governance Toolkit provides require_approval policy action with Slack webhook integration. AWS recommends timeout policies with safe fallbacks and escalation paths. Use for: financial transactions > threshold, data deletion, external communications, code execution, production changes. Don't use for: read-only operations, low-risk SOP actions. Integrates with agentic AI governance, OWASP ASI09, EU AI Act Art. 14, MCP security, and SOP automation.

Microsoft's Agent Governance Toolkit defines the core principle: "In regulated industries — FSI, healthcare, legal — certain agent actions require explicit human authorization. AGT's require_approval policy action pauses the agent, notifies a human, and resumes or denies based on their response."

Strata notes that organizations need "an identity-aware orchestration layer that can pause agent execution, route approval requests to authorized humans, enforce time-boxed decision windows, and log every intervention for audit."

Approval Architecture

flowchart TD subgraph Agent["AI Agent"] Action["Proposed Action"] Pause["Pause Execution"] Resume["Resume / Halt"] end subgraph Policy["Policy Engine"] Evaluate["Evaluate Action"] Tier1["Allow
(safe)"] Tier2["Deny
(dangerous)"] Tier3["Escalate
(needs human)"] end subgraph Approval["Approval Workflow"] Create["Create Request"] Notify1["Notify Primary
Reviewer"] Wait["Wait for Response"] Escalate["Escalate to
Secondary"] Timeout["Timeout
Auto-Deny"] end subgraph Reviewer["Human Reviewer"] Context["Review Context
(action, reasoning, impact)"] Decision["Approve / Reject"] end subgraph Audit["Audit Trail"] Log["Tamper-Evident
Decision Records"] end Action --> Evaluate Evaluate --> Tier1 Evaluate --> Tier2 Evaluate --> Tier3 Tier1 --> Action Tier2 --> Log Tier3 --> Create Create --> Notify1 Notify1 --> Context Context --> Decision Decision -->|approve| Resume Decision -->|reject| Log Notify1 --> Wait Wait -->|no response| Escalate Escalate --> Wait Wait -->|timeout| Timeout Timeout --> Log Resume --> Log

Three-Tier Policy

Tier Outcome When to Use Example
Allow Proceed immediately Safe, read-only, within SOP search_documents, get_document
Deny Blocked, no approval possible Irreversibly destructive delete_database, drop_table
Escalate Pause for human review Ambiguous, high-impact transfer_funds > $10K, send_external_email

Approval Decision Matrix

Action Type Risk Level Auto-Approve Require Approval Always Deny
Read/query Low ✅ All
Search KB Low ✅ All
Send internal Slack Low ✅ Business hours Off-hours
Send external email Medium ✅ Always
Transfer < $1K Medium ✅ Business hours Off-hours
Transfer $1K-$10K High ✅ Manager
Transfer > $10K Critical ✅ Treasury + Compliance
Delete document High ✅ Data steward
Delete database Critical ✅ Always
Execute code High ✅ Engineering lead
Deploy to production Critical ✅ DevOps + Manager
Access PII High ✅ Compliance

Implementation

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
import json

class ApprovalState(Enum):
    PENDING = "pending"
    APPROVED = "approved"
    DENIED = "denied"
    TIMEOUT = "timeout"
    ERROR = "error"

class TimeoutAction(Enum):
    DENY = "deny"  # Safe default
    ALLOW = "allow"  # Rare — only for low-criticality

@dataclass
class EscalationRequest:
    id: str
    agent_id: str
    action: str
    action_type: str
    context: dict
    reasoning: str  # Why the agent wants this action
    potential_impact: str  # What happens if approved
    approvers: list[str]
    escalation_chain: list[str]
    timeout_seconds: int = 300
    escalation_window: int = 120  # Escalate to secondary after 2 min
    state: ApprovalState = ApprovalState.PENDING
    created_at: datetime = field(default_factory=datetime.utcnow)
    resolved_at: datetime = None
    approver: str = None
    approval_reason: str = None

class ApprovalEngine:
    def __init__(self, notifier, audit_logger, redis_client=None):
        self.notifier = notifier  # Slack, email, dashboard
        self.audit = audit_logger
        self.redis = redis_client  # Production: Redis for distributed queue
        self.pending = {}  # In-memory fallback

    async def request_approval(
        self,
        agent_id: str,
        action: str,
        action_type: str,
        context: dict,
        reasoning: str,
        impact: str,
        approvers: list[str],
        escalation_chain: list[str] = None,
        timeout: int = 300,
    ) -> EscalationRequest:
        """Create approval request and notify reviewers."""
        request = EscalationRequest(
            id=str(uuid4()),
            agent_id=agent_id,
            action=action,
            action_type=action_type,
            context=context,
            reasoning=reasoning,
            potential_impact=impact,
            approvers=approvers,
            escalation_chain=escalation_chain or [],
            timeout_seconds=timeout,
        )

        self.pending[request.id] = request

        # Notify primary reviewers
        await self.notifier.send(
            channels=approvers,
            message=f"⚠️ Approval Required: {action_type}",
            details={
                "action": action,
                "reasoning": reasoning,
                "impact": impact,
                "context": context,
                "request_id": request.id,
                "timeout": f"{timeout}s",
            },
        )

        # Audit log
        self.audit.log(
            event="approval_requested",
            agent_id=agent_id,
            action=action,
            approvers=approvers,
            request_id=request.id,
        )

        return request

    async def wait_for_decision(
        self,
        request: EscalationRequest,
    ) -> ApprovalState:
        """Wait for human decision with escalation and timeout."""
        deadline = request.created_at + timedelta(seconds=request.timeout_seconds)
        escalation_deadline = request.created_at + timedelta(seconds=request.escalation_window)

        while datetime.utcnow() < deadline:
            # Check if decision was made
            if request.state in (ApprovalState.APPROVED, ApprovalState.DENIED):
                return request.state

            # Escalate to secondary if primary hasn't responded
            if datetime.utcnow() > escalation_deadline and request.escalation_chain:
                if not request.escalation_chain_notified:
                    await self.notifier.send(
                        channels=request.escalation_chain,
                        message=f"🚨 Escalation: {request.action_type} (primary unresponsive)",
                        details={"request_id": request.id},
                    )
                    request.escalation_chain_notified = True

            await asyncio.sleep(5)  # Poll every 5 seconds

        # Timeout: auto-deny (fail-safe)
        request.state = ApprovalState.TIMEOUT
        request.resolved_at = datetime.utcnow()

        self.audit.log(
            event="approval_timeout",
            request_id=request.id,
            action=request.action,
            agent_id=request.agent_id,
            outcome="auto_denied",
        )

        return ApprovalState.TIMEOUT

    async def resolve(self, request_id: str, approved: bool, approver: str, reason: str):
        """Human reviewer approves or denies."""
        request = self.pending.get(request_id)
        if not request:
            raise ValueError(f"Unknown request: {request_id}")

        request.state = ApprovalState.APPROVED if approved else ApprovalState.DENIED
        request.approver = approver
        request.approval_reason = reason
        request.resolved_at = datetime.utcnow()

        self.audit.log(
            event="approval_decision",
            request_id=request_id,
            approver=approver,
            approved=approved,
            reason=reason,
            action=request.action,
            agent_id=request.agent_id,
        )

Slack Approval Integration

class SlackApprovalNotifier:
    def __init__(self, webhook_url: str, bot_token: str):
        self.webhook_url = webhook_url
        self.bot_token = bot_token

    async def send(self, channels: list[str], message: str, details: dict):
        """Send approval request to Slack channels."""
        blocks = [
            {
                "type": "header",
                "text": {"type": "plain_text", "text": message},
            },
            {
                "type": "section",
                "fields": [
                    {"type": "mrkdwn", "text": f"*Action:* {details['action']}"},
                    {"type": "mrkdwn", "text": f"*Reasoning:* {details['reasoning']}"},
                    {"type": "mrkdwn", "text": f"*Impact:* {details['impact']}"},
                    {"type": "mrkdwn", "text": f"*Timeout:* {details['timeout']}"},
                ],
            },
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "✅ Approve"},
                        "style": "primary",
                        "value": f"approve:{details['request_id']}",
                        "action_id": "approve_action",
                    },
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "❌ Deny"},
                        "style": "danger",
                        "value": f"deny:{details['request_id']}",
                        "action_id": "deny_action",
                    },
                ],
            },
        ]

        for channel in channels:
            await self._post_to_channel(channel, blocks)

Common Pitfalls

Pitfall Why It's Bad Fix
No timeout Agent hangs forever Set 5-min timeout with auto-deny
Timeout = auto-approve Unsafe actions execute Timeout = auto-deny (fail-safe)
All actions require approval Reviewer fatigue, rubber-stamp Tier policy: allow safe, deny dangerous, escalate ambiguous
Insufficient context Review can't make informed decision Show action, reasoning, impact, data sources
No escalation path Stalls when primary unavailable Multi-level: primary → secondary → management
Approval only in UI, not in policy Bypass via direct tool call Policy engine enforces approval at tool gateway
No audit log Can't prove oversight Log every decision with approver, timestamp, reasoning
Blocking worker while waiting Wastes compute resources Async: return pending state, resume on decision

AI Agent Action Approval Checklist

  • [ ] Define three-tier policy: allow, deny, escalate
  • [ ] Classify all agent actions by risk level (low, medium, high, critical)
  • [ ] Configure auto-approve rules for low-risk actions (business hours, below threshold)
  • [ ] Configure approval requirements for high-risk actions
  • [ ] Set always-deny for irreversibly destructive actions (delete database)
  • [ ] Implement approval engine with request creation and notification
  • [ ] Set timeout (default 5 min) with auto-deny fail-safe
  • [ ] Configure escalation window (default 2 min) for secondary reviewers
  • [ ] Define escalation chain: primary → secondary → management
  • [ ] Set up Slack integration with approve/deny buttons
  • [ ] Set up email notifications as backup channel
  • [ ] Create approval dashboard for pending requests
  • [ ] Provide full context to reviewers: action, reasoning, impact, data sources
  • [ ] Implement audit logging for every approval decision
  • [ ] Log: who approved/denied, when, reasoning, request ID
  • [ ] Send audit logs to SIEM (Splunk, ELK, Datadog)
  • [ ] Implement async approval: agent returns pending state, doesn't block worker
  • [ ] Test fail-safe: verify auto-deny on timeout
  • [ ] Test fail-safe: verify auto-deny on handler error
  • [ ] Test fail-safe: verify auto-deny when no handler configured
  • [ ] Test escalation: verify secondary notified after escalation window
  • [ ] Test approval flow: verify agent resumes after approval
  • [ ] Test denial flow: verify agent halts after denial
  • [ ] Integrate with agentic AI governance policy engine
  • [ ] Apply OWASP ASI09 trust exploitation defenses
  • [ ] Comply with EU AI Act Article 14 (human oversight)
  • [ ] Enforce approval at MCP tool gateway, not just in UI
  • [ ] Use for SOP automation edge cases
  • [ ] Apply zero-trust architecture to approval flow
  • [ ] Monitor approval metrics with platform monitoring
  • [ ] Track: approval rate, denial rate, timeout rate, average response time
  • [ ] Alert on timeout spikes (reviewers not responding)
  • [ ] Alert on denial spikes (agent making bad decisions)
  • [ ] Consider AI incident response for approval failures
  • [ ] Quarterly review: update approval thresholds and escalation chains
  • [ ] Train reviewers on human-agent trust risks
  • [ ] Implement "approve forever" for recurring low-risk actions (with admin role)

FAQ

What is AI agent action approval?

AI agent action approval is a governance pattern where certain agent actions require explicit human authorization before execution. The agent can decide an action is needed, but execution pauses until a human approves or rejects. Three-tier policy: allow (safe actions proceed), deny (dangerous actions blocked), escalate (ambiguous actions need human review). Use for: financial transactions above threshold, data deletion, external communications, code execution, and any irreversible action. Implementation: policy engine evaluates action, if escalation required agent pauses and notifies approver, approver reviews context and decides, agent resumes or halts. Fail-safe: auto-deny on timeout (default 5 min) or handler error.

When should AI agent actions require human approval?

Require human approval when: (1) Action is irreversible — data deletion, sending external email, executing code. (2) Financial impact exceeds threshold — transfers > $10K, refunds > $500. (3) Action affects external parties — customer notifications, vendor payments. (4) Action accesses sensitive data — PII, financial records, health data. (5) Action modifies production systems — database schema changes, deployments. (6) Action is outside normal SOP — edge cases the agent hasn't handled before. Don't require approval for: read-only operations (search, query), low-risk actions within SOP, or actions already approved by policy auto-approval rules.

What happens when an approval times out?

When approval times out (default 5 minutes), the action is auto-denied. This is the fail-safe default — if a human is supposed to review an action and nobody does, the safest assumption is that the action should not happen. An unanswered escalation should never silently become an approval. For less critical actions, you can set DefaultTimeoutAction.ALLOW, but this is rare — if the action weren't important, it wouldn't need escalation. Configure escalation paths: if primary reviewer doesn't respond within escalation window (shorter than full timeout), notify secondary reviewers. Log timeout events for audit and process improvement.

How do you implement multi-level approval escalation?

Multi-level escalation: (1) Primary reviewer notified via Slack/email with action context. (2) If no response within escalation window (e.g., 2 min), notify secondary reviewer. (3) If no response within full timeout (e.g., 5 min), auto-deny. (4) Escalation to management for critical actions. Implementation: EscalationHandler with configurable timeout, InMemoryApprovalQueue (or Redis for production), and escalation chain. Each level logs: who was notified, when, response (approve/deny/timeout). Use Slack webhooks for real-time notifications, dashboard for approval queue visibility. Consider business-hours auto-approval for low-risk actions (amount < $1K, 9am-5pm).

How does this integrate with existing governance frameworks?

Action approval integrates with: (1) OWASP ASI09 (Human-Agent Trust Exploitation) — prevents agents from exploiting trust to get unsafe approvals. (2) EU AI Act Article 14 (Human Oversight) — provides the stop mechanism and human intervention capability. (3) NIST AI RMF Manage function — enforce limits and stop agents. (4) MCP security — approval gates sit between agent and MCP tool calls. (5) Policy-as-code — approval rules defined in YAML/OPA, enforced by external engine. (6) Audit logging — every approval decision logged with approver identity, timestamp, reasoning. (7) Circuit breakers — approval failures can trigger circuit breaker to pause all agent actions.


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