Approval

Admin-mediated HITL workflows with persistent records and audit trails.

Approval enables a "User Triggers, Admin Authorizes" workflow. When an agent (or team member) hits a protected tool during a run, the run pauses and persists a pending record to your database. Continuation by run ID without explicit requirements checks that the stored approval has been resolved. Your application must authorize who can resolve and continue runs; a trusted SDK caller can also supply resolved requirements directly.

Approvals are built on HITL primitives (requires_confirmation, requires_user_input, or external_execution). Your tool must implement at least one. Bare @approval sets requires_confirmation=True automatically if none is set.

Approvals work at both the agent and team level. When a member agent in a team calls an @approval tool, the team run pauses with the same flow shown below. See the Team approval example.

Setup

Create and activate a virtual environment, then install the dependencies and set your key:

uv pip install -U agno openai sqlalchemy
export OPENAI_API_KEY="your_openai_api_key"
mkdir -p tmp

The examples use a writable SQLite database for continuation by run ID. Retain both run_id and session_id and use the same database when resuming in a new process. Application code must authorize the caller's access to that session and its pending requirements.

Quick start

import time

from agno.models.openai import OpenAIResponses
from agno.approval import approval
from agno.tools import tool
from agno.db.sqlite import SqliteDb
from agno.agent import Agent

@approval
@tool(requires_confirmation=True)
def delete_user_data(user_id: str) -> str:
    """Demonstrate an approved deletion request without deleting data."""
    return f"Demo: deletion approved for user {user_id}; no data was deleted."

db = SqliteDb(db_file="app.db", approvals_table="approvals")
agent = Agent(model=OpenAIResponses(id="gpt-5.2"), tools=[delete_user_data], db=db)

run = agent.run("Delete all data for user U-100")

When the user asks for something that uses this tool, the run pauses and a pending approval is written to the database. An admin resolves it; then you continue the run.

Approval Types

TypeBehaviorUse Case
@approval(type="required") or @approvalRun pauses and stores a pending record. ID-only continuation applies the stored admin resolution.Critical actions such as deletion, payments, bulk emails.
@approval(type="audit")Run pauses for its normal HITL interaction. Resolution creates an audit record without an additional admin gate.Compliance and activity auditing purposes.

Blocking

By default, @approval needs HITL approval and requires_confirmation=True is set.

Audit mode

To record the outcome of the normal Human-in-the-Loop pause without a separate admin-resolution step, use @approval(type="audit"). This will create an audit log after the HITL interaction is resolved.

@approval(type="audit") requires at least one HITL flag (requires_confirmation=True, requires_user_input=True, or external_execution=True) on the @tool() decorator.

See User Confirmation for details.

Execution Flow

The required-approval flow has three phases. The following fragments reuse db, agent, and the paused run from the quickstart:

  • The Pause: When a user triggers an @approval tool, the SDK automatically pauses the run and inserts a pending record into your database.

  • Admin Approval: Admin views the list of pending requests. Then update the record status via the DB provider. Use expected_status="pending" to prevent race conditions.

    pending, _ = db.get_approvals(run_id=run.run_id, status="pending")
    if not pending:
        raise RuntimeError("This run has no pending approval")
    approval_id = pending[0]["id"]
    # This demo has one protected tool. Review each pending record in a multi-tool run.
    db.update_approval(
        approval_id,
        expected_status="pending",
        status="approved",   # or "rejected"
        resolved_by="admin_user_id",
        resolved_at=int(time.time()),
        # For requires_user_input or external_execution: pass resolution_data
        # (e.g. values for user input, result for external execution); SDK applies it on continue_run.
    )
  • Resuming the Run: Continue the run using the run_id and session_id. When called without requirements, the SDK verifies the resolution and applies it before proceeding. If the record is missing or still pending, continue_run raises a ValueError.

    run = agent.continue_run(run_id=run.run_id, session_id=run.session_id)

Examples

Developer Resources