Skip to main content

Human-in-the-Loop Approval

AI agents perform actions with real-world consequences -- executing trades, deleting records, sending emails, modifying production systems. Many of these actions require human oversight before proceeding.

Without a schema-level approval mechanism:

  • Approval configuration lives in opaque SDK-level metadata
  • Different SDKs implement incompatible approval conventions
  • The schema cannot validate approval configuration; typos are silent
  • There is no portable way to express "this tool requires human approval"

Agent Format solves this by making approval a first-class schema concept with rich support for message templates, conditional evaluation, and governance integration.

The Three Forms of Approval

The approval field is a union type that accepts three forms:

FormMeaningExample
approval: trueAlways require approval. Runtime generates a default message.High-risk irreversible actions.
approval: falseExplicitly exempt from inherited approval.Safe read-only tools opted out of a blanket server-level approval.
approval: { ... }Detailed configuration with optional message template and/or condition.Conditional approval based on argument values.

When the approval field is omitted entirely, no approval is required (unless inherited from a blanket server-level or governance policy).

Empty object: approval: {} (an ApprovalConfig with no fields set) is equivalent to approval: true -- approval is always required and the runtime generates a default message. This is the safe interpretation: an explicit approval object, even if empty, signals the author's intent to require approval.

Message Templates

Agent Owners provide human-readable message templates with mustache-style placeholders that the runtime resolves at invocation time:

approval:
message_template: "Approve {{tool_args.order_type}} order: {{tool_args.action}} {{tool_args.quantity}} shares of {{tool_args.symbol}} at ${{tool_args.price}}?"

Available Template Variables

VariableDescriptionExample Value
{{tool_name}}Alias of the tool being invokedexecute_trade
{{tool_args.<key>}}A specific argument valueAAPL, 100, 150.25
{{tool_args}}Full tool arguments as JSON string{"symbol": "AAPL", ...}
{{agent_alias}}Alias of the agent executing the tooltrading_agent
{{agent_id}}The agent's metadata IDfinancial_analyst_v2
{{skill_id}}A2A skill ID (for remote agent skills)process-payment
{{skill_args.<key>}}Skill input field (for remote agent skills)500.00

For the full list of available variables, see the Template Variable Catalog.

Template Rendering Rules

  1. Missing variables render as empty string (no error).
  2. Nested access is supported: {{tool_args.order.details.amount}}.
  3. Templates are pure interpolation -- no logic. Conditional logic belongs in the condition block.

When message_template is omitted, the runtime constructs a default message including the tool name and arguments.

Conditional Approval

The condition field makes approval conditional -- required only when the condition evaluates to true at invocation time.

Single Condition Group (AND Semantics)

approval:
message_template: "Approve transfer of ${{tool_args.amount}}?"
condition:
args_match:
amount: { gt: 10000 }
currency: "USD"
# Both must be true: amount > 10000 AND currency == "USD"

Multiple Condition Groups (OR-of-ANDs)

approval:
message_template: "Approve transfer of ${{tool_args.amount}}?"
condition:
- args_match:
amount: { gt: 10000 }
- args_match:
recipient_type: "external"
# Approval if amount > 10000 OR recipient is external

Match Expression Syntax

The following match expressions are available in args_match blocks. For the complete reference, see Condition Matcher Reference.

ExpressionMeaningExample
Literal valueExact matchcountry: "US"
{ gt: N }Greater thanamount: { gt: 10000 }
{ gte: N }Greater than or equalamount: { gte: 100 }
{ lt: N }Less thanrisk_score: { lt: 0.5 }
{ lte: N }Less than or equalrisk_score: { lte: 0.5 }
{ ne: V }Not equal (string, number, or boolean)status: { ne: "approved" }
{ pattern: "..." }Regex matchemail: { pattern: ".*@external\\.com$" }
{ in: [...] }Value in listcategory: { in: ["delete", "modify"] }
{ not_in: [...] }Value not in listregion: { not_in: ["restricted", "embargoed"] }

MCP Server Approval: Blanket and Per-Tool

MCP servers support two levels of approval that compose:

mcp_servers:
- alias: external_api
server_ref: partner.example.api-v2
approval: true # blanket: all tools require approval
allowed_tools:
- name: health_check
approval: false # exempt from blanket
- name: create_resource
approval:
message_template: "Approve creating '{{tool_args.resource_type}}'?"
- list_resources # inherits blanket approval: true

Resolution Rules

  1. String entry (e.g., "read_table") -- inherits server-level approval (if present) or no approval.
  2. Object entry with approval -- overrides server-level for that tool.
  3. Object entry with approval: false -- exempted from blanket.
  4. Governance policies can add approval regardless of what the Agent Owner declares (union semantics).

Approval and Governance: Union Semantics

Approval requirements can come from two independent sources:

SourceAuthored ByConfigured In
Action space declarationAgent Owner.agf.yaml action_space entries
Governance policyGovernance TeamExternal Policy Registry

The composition rule is simple: if ANY source requires approval, approval is required.

Agent OwnerGovernance PolicyEffective
No approval fieldNo approval policyNo approval
approval: trueNo approval policyApproval required
No approval fieldPolicy requires approvalApproval required
approval: falsePolicy requires approvalApproval required (governance wins)

The Agent Owner's approval: false is a preference (primarily for per-tool exemption from blanket server-level approval), not an override of governance. The tighten_only_invariant applies: governance can only add restrictions, never remove them.

Approval on Agent Delegation

The approval field on local_agents[] and remote_agents[] gates the delegation decision -- the runtime requests approval before invoking the sub-agent. This is distinct from tool-level approval within the sub-agent: a sub-agent may have its own approval requirements on its tools, which are evaluated independently during the sub-agent's execution.

local_agents:
- alias: financial_executor
source: agents/executor.agf.yaml
approval:
message_template: "Delegate to financial executor for {{parent.input.action}}?"
condition:
args_match:
parent.input.risk_level: { in: ["high", "critical"] }

In this example, the runtime requests approval before delegating to financial_executor when the risk level is high or critical. The sub-agent's own tool-level approvals (e.g., on execute_trade) are evaluated separately during its execution.

What the Runtime Handles

The schema declares what requires approval. The following are runtime concerns, not schema concerns:

ConcernWhy It's Runtime
Approval delivery mechanism (WebSocket, Slack, email, CLI)Depends on Runtime Owner's infrastructure
Approval timeoutDepends on approval channel; governed by governance policy
Timeout fallback behavior (deny, approve, escalate)Governance/Runtime Owner decision
What additional context to attach to the requestRuntime decides based on UI capabilities
How "approve with modifications" worksRuntime/SDK implementation