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:
| Form | Meaning | Example |
|---|---|---|
approval: true | Always require approval. Runtime generates a default message. | High-risk irreversible actions. |
approval: false | Explicitly 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
| Variable | Description | Example Value |
|---|---|---|
{{tool_name}} | Alias of the tool being invoked | execute_trade |
{{tool_args.<key>}} | A specific argument value | AAPL, 100, 150.25 |
{{tool_args}} | Full tool arguments as JSON string | {"symbol": "AAPL", ...} |
{{agent_alias}} | Alias of the agent executing the tool | trading_agent |
{{agent_id}} | The agent's metadata ID | financial_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
- Missing variables render as empty string (no error).
- Nested access is supported:
{{tool_args.order.details.amount}}. - Templates are pure interpolation -- no logic. Conditional logic belongs in the
conditionblock.
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.
| Expression | Meaning | Example |
|---|---|---|
| Literal value | Exact match | country: "US" |
{ gt: N } | Greater than | amount: { gt: 10000 } |
{ gte: N } | Greater than or equal | amount: { gte: 100 } |
{ lt: N } | Less than | risk_score: { lt: 0.5 } |
{ lte: N } | Less than or equal | risk_score: { lte: 0.5 } |
{ ne: V } | Not equal (string, number, or boolean) | status: { ne: "approved" } |
{ pattern: "..." } | Regex match | email: { pattern: ".*@external\\.com$" } |
{ in: [...] } | Value in list | category: { in: ["delete", "modify"] } |
{ not_in: [...] } | Value not in list | region: { 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
- String entry (e.g.,
"read_table") -- inherits server-levelapproval(if present) or no approval. - Object entry with
approval-- overrides server-level for that tool. - Object entry with
approval: false-- exempted from blanket. - 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:
| Source | Authored By | Configured In |
|---|---|---|
| Action space declaration | Agent Owner | .agf.yaml action_space entries |
| Governance policy | Governance Team | External Policy Registry |
The composition rule is simple: if ANY source requires approval, approval is required.
| Agent Owner | Governance Policy | Effective |
|---|---|---|
No approval field | No approval policy | No approval |
approval: true | No approval policy | Approval required |
No approval field | Policy requires approval | Approval required |
approval: false | Policy requires approval | Approval 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:
| Concern | Why It's Runtime |
|---|---|
| Approval delivery mechanism (WebSocket, Slack, email, CLI) | Depends on Runtime Owner's infrastructure |
| Approval timeout | Depends on approval channel; governed by governance policy |
Timeout fallback behavior (deny, approve, escalate) | Governance/Runtime Owner decision |
| What additional context to attach to the request | Runtime decides based on UI capabilities |
| How "approve with modifications" works | Runtime/SDK implementation |