AI agents

Structured Output for Approval: A Schema Reviewers Read

Antonio Evans9 min read

A single monitor on a plain desk shows a card titled 'Approval request'.
Key points

Key points

  1. 1The approval payload is a fixed schema with six required blocks (action, reason, evidence, risk, reversibility, rollback) so every queued request reads the same way.
  2. 2The action block names the tool and lists every parameter with its type and current value, so the reviewer checks facts instead of trusting a summary.
  3. 3The evidence block lists the record IDs, document names and query results the agent relied on, each with a timestamp, which turns a claim into something a reviewer can click and verify.
  4. 4Approved decisions should be stored against a specific call ID and resumed from a saved state, so a later edit or a replayed run cannot silently change what was approved.

Start from the framework primitive, not from prose

Every serious agent framework already has a pause-and-resume concept, and the shape of your approval JSON should follow it. Microsoft's agent framework wraps a function in an approval-required wrapper, and the run completes with a ToolApprovalRequestContent object whose ToolCall property exposes the function name and its arguments, which the caller can show to a person before deciding the Microsoft Learn approval guide. The OpenAI Agents SDK does the same thing with needs_approval: the runner pauses, surfaces interruptions, and lets you serialize the paused run with RunState and resume after a decision the OpenAI Agents SDK HITL docs. LangChain's deep agent stack exposes an interrupt_on map that configures which tools pause and which decisions are allowed for each, with a checkpointer that is required for the flow to work at all the LangChain HITL docs.

The lesson for schema design: the framework gives you the tool name and the argument object for free. Do not restate them in a paragraph. Pass them through as structured fields and spend your own schema budget on what the framework does not know: why the agent wants this, what it read, how bad it could get, and how to undo it.

A second consequence is that the decision itself must be a first class object. The OpenAI SDK scopes per-call approvals to the specific call ID and only persists a decision to future calls when you explicitly ask for it the OpenAI Agents SDK HITL docs. That matches what audits need: an approval is valid for one call, one set of arguments, at one time.

The six blocks and what belongs in each

The short answer is six required blocks. Here is the field level detail, with the failure each field prevents.

BlockRequired fieldsPrevents
actiontool, params[] with name, type, value, call_idApproving "a refund" when the amount is different from what the reviewer assumed
reasonsummary (one sentence), goal_id, expected_outcomeSign-off on an action nobody can connect to a business goal
evidenceitems[] with source_type, id, retrieved_at, excerptApproving a claim the agent invented with no document behind it
risktier, blast_radius, reversible, confidenceTreating a one-record write and a table-wide update as the same thing
rollbackplan, tested, ownerDiscovering after the fact that nobody knows how to undo it
dry_runstatus, result_url, ran_atApproving code paths that were never executed

Two design rules make the difference between a schema reviewers read and one they scroll past. First, every block is required and validated at submission time, so an agent that cannot produce evidence cannot file the request at all. Second, free text is allowed only in reason.summary and rollback.plan, both capped in length. Everything else is an identifier, an enum, a timestamp or a typed value. That is what makes the payload renderable as a card instead of a wall of text, and it is what lets you diff two requests mechanically.

The action block: types, values and a worked diff

The action block should carry a JSON Schema of the parameter set alongside the actual values. Schema plus values means the reviewer can see that amount is a decimal with two places and currently equals 480.00, not that the agent "wants to refund a customer."

{
  "action": {
    "call_id": "call_01J8Z9K3",
    "tool": "billing.refund",
    "params_schema_ref": "billing.refund@v2",
    "params": [
      {"name": "invoice_id", "type": "string", "value": "INV-88213"},
      {"name": "amount", "type": "decimal(10,2)", "value": "480.00"},
      {"name": "currency", "type": "string", "value": "USD"},
      {"name": "reason_code", "type": "enum", "value": "DUPLICATE_CHARGE"},
      {"name": "notify_customer", "type": "boolean", "value": true}
    ]
  }
}

The params_schema_ref matters more than it looks. It pins the review to the exact contract the tool validates against, so if the tool later gains a batch parameter, old approvals stay tied to the old contract. It also gives you a cheap check: reject any payload whose declared types do not match the tool's registered schema before a human ever sees it.

A worked comparison shows why scale belongs next to the parameters. Suppose two queued refunds both read as approved-in-principle under a policy that allows refunds. The first is 480.00 to one invoice with no downstream notification. The second, built from the same schema, carries amount 480.00, a batch list of 340 invoice IDs, and notify_customer true. The policy says yes to both; only the payload tells the reviewer that the second touches 340 customers in one call. That gap is exactly what blast-radius gates exist to close, because a rule permitting refunds says nothing about how many of them can stack up before someone looks the blast-radius gate analysis.

The evidence block: making claims clickable

The evidence block is where most approval designs quietly fail. An agent writes "per the customer email, they were double charged," and the reviewer has no way to check that without opening a separate search tool. The fix is to require a list of typed, timestamped references rather than prose the structured approval payload walkthrough.

{
  "evidence": {
    "items": [
      {"source_type": "ticket", "id": "ZD-44120", "field": "body",
       "retrieved_at": "2026-02-11T09:14:02Z", "excerpt": "charged twice in February"},
      {"source_type": "ledger_query", "query_id": "q_7731",
       "retrieved_at": "2026-02-11T09:14:40Z", "row_count": 2},
      {"source_type": "policy_doc", "id": "FIN-REFUND-POLICY", "section": "4.2",
       "retrieved_at": "2026-02-10T22:00:00Z"}
    ],
    "missing": ["customer_confirmation_of_second_charge"]
  }
}

Two fields do heavy lifting. The retrieved_at timestamp bounds staleness: a policy document read two weeks ago may since have changed. And the missing array lets an agent declare what it could not verify, which is far more useful to a reviewer than a confident sentence hiding the gap. A reviewer scanning for risk reads the missing array first. If the agent's plan depends on a fact that is not in the evidence list, the request is not ready to submit.

This is also the field that keeps the human decision meaningful. The wider human-in-the-loop literature is blunt that a human who cannot see the basis of a decision is not really in the loop, and the reliance on explainability to make oversight useful is well documented across the review of the field the Springer survey of human-in-the-loop machine learning. Evidence references are the cheapest form of explainability an agent can produce, because they cost one lookup each and no extra generation.

The risk and reversibility blocks: a tier table you can adopt

The risk block should encode a decision you have already made elsewhere, not an opinion the model produces at submission time. A four-tier classification is the common pattern in production practice: reads pass freely, reversible production changes need one approver, destructive or irreversible changes need two, and security-sensitive changes are refused outright at the gate the nofire runtime enforcement guide. A parallel treatment for AI SRE work uses the same four tiers and pairs reversible production changes with a single on-call approver and destructive changes with two approvals including the service owner the production access analysis.

Encode that as an enum plus the facts the tier was computed from:

FieldValuesReviewer use
tierread, reversible_narrow, reversible_wide, irreversible, security_sensitiveDecides who must approve and how many
blast_radius.recordsintegerSpots a batch hiding behind a single-tool call
blast_radius.systemslist of service namesTwo services means a cross-team approval
reversibleboolean plus a noteSeparates "undo in one call" from "undo by hand"
confidence0 to 1, with the rule that produced itFlag only; never the sole gate

Put a warning on the last row. Confidence from a language model measures token likelihood, not factual accuracy, and teams have reported incorrect agent actions carrying high confidence scores, which is why confidence cannot be the only routing signal the HITL patterns field report. Use it to color the card, not to skip the gate.

The tier table is also how you prevent confirmation fatigue. Requiring a human to approve reads and reversible internal writes trains reviewers to click through without reading, and the point of a risk tier is to reserve interruptions for the small set of actions where the answer could be no the SaaS approval workflow guide. Fewer, better requests is a security property, not a convenience.

The rollback and dry-run blocks: the two fields reviewers ask for

Rollback is one sentence and three facts: the undo operation, whether it has been tested, and who owns it if it needs a human at 3 a.m.

{
  "rollback": {
    "plan": "issue compensating reversal for INV-88213 amount 480.00 USD",
    "operation": "billing.reverse_refund",
    "tested": true,
    "owner": "billing-platform",
    "time_to_undo_minutes": 5
  },
  "dry_run": {
    "status": "passed",
    "ran_at": "2026-02-11T09:15:12Z",
    "result_url": "https://ops.internal/dryruns/01J8Z9K3",
    "diff_recorded": true
  }
}

If the dry run failed or was skipped, the request should not be submittable at the tier that auto-approves on a clean dry run. If tested is false on an irreversible action, the payload should demand a second approver. These are schema-level rules, enforced before a human is interrupted, and they remove the reviewer's need to interrogate the agent for obvious gaps.

A dry-run link is worth more than a paragraph about what would happen. It is the same principle as an approval gate placing evidence in front of the approver before a production change runs the production access analysis. The reviewer opens the diff, sees the affected rows, and closes the tab. That is a decision in seconds, not a research project.

Where do you run the dry run when the target system has no sandbox? Two options that scale: snapshot the target records into a scratch store and execute the tool against it, or compile the action into a reversible diff (a JSON patch, a SQL transaction that ends in rollback) and attach that. Either way, the artifact is a URL, and the URL goes in the payload.

Rendering the payload: one screen, top to bottom

Schema design and screen design are the same project. If the blocks exist, the card writes itself. A layout that reviewers can scan in under a minute:

  1. Header: tool name, tier badge, one-sentence reason, request timestamp.
  2. Action table: parameter, type, value, with any value outside a policy bound highlighted.
  3. Evidence list: source, ID, retrieved time, excerpt, plus the missing array at the top if non-empty.
  4. Risk strip: records affected, systems touched, reversible yes or no, confidence as a neutral number.
  5. Rollback line: plan, tested flag, owner, minutes to undo.
  6. Dry run: status badge and link.
  7. Decision: approve, edit then approve, reject, request more evidence, each recorded against call_id.

Review queues work better when they are a shared inbox with a clear evaluation question attached, rather than a spreadsheet of transcripts, and the same discipline applies here: one request per ticket, a fixed question, and a recorded judgment that later becomes training data for better triage the MLflow review queue writeup. The four decision buttons matter too. Approve and reject alone force a reviewer to choose between a bad action and a stalled workflow; edit-then-approve lets them fix a wrong parameter without sending the agent back to plan from scratch, and that option is explicitly configurable in the LangChain interrupt model the LangChain HITL docs.

If you are building this surface as an internal tool and would rather describe it than hand-roll it, Quanta can take the description and build the review console alongside your schema: describe the tool you need.

Testing and evolving the schema

Validate the payload before it reaches a human. A JSON Schema check catches missing blocks and wrong types, and a second pass runs your policy checks: irreversible actions missing a rollback owner, batch parameters exceeding the tier's record bound, evidence items older than the allowed staleness window. Anything that fails is returned to the agent as a validation error, not shown to a reviewer.

Then log the decisions. Every approval, edit and rejection should be stored with the payload that produced it, because those pairs are the most valuable dataset you will generate: real agent requests with human verdicts attached, which is exactly the raw material for tuning the triage that decides which requests need a person at all the MLflow review queue writeup. Treat schema versions as contracts. Adding a required field is a breaking change for every agent that files requests, so version the schema (schema_version in the root) and keep old versions valid until their queued requests drain.

Two metrics tell you whether the schema is working. Time-to-decision on tier 2 and tier 3 requests tells you whether the card is readable; if median time creeps up, a block is probably hiding the fact reviewers need. Edit rate tells you whether the agent's parameters are trustworthy; a high edit rate on one tool means that tool's argument generation needs work, not that reviewers need more training.

FAQ

FAQ

Do I need all six blocks for every request?

For anything above a read, yes. Below that, you should be auto-approving in code rather than filing a request a human has to clear, which is the whole point of risk tiering the nofire runtime enforcement guide. A schema with optional blocks invites agents to omit the one that would have caught the mistake.

How do I stop agents from writing persuasive but unverifiable reasons?

Make the reason one sentence with a length cap, and make the evidence block the only place a factual claim can live. If a claim is not backed by an item with an ID and a timestamp, it does not count. Adding a missing array gives the agent an honest place to record gaps instead of smoothing over them the structured approval payload walkthrough.

What happens to a queued request if the agent process restarts?

That is a persistence question, and the answer is to store the pause state, not to hold an HTTP connection open. The OpenAI SDK's RunState exists for serializing paused runs, and LangChain requires a checkpointer for interrupts to survive the OpenAI Agents SDK HITL docs the LangChain HITL docs. A blocking request that waits on a reviewer will eventually time out at some gateway.

Can two agents file conflicting requests?

Yes, and the payload should make the conflict visible rather than trusting the agents to coordinate. Include a resource key (the invoice ID, the record ID, the service name) at the root of the payload and let the queue group requests that share a key. Reviewers then see duplicates and contradictions in one place, which is also a useful signal that the tool needs an idempotency key.

Where should the approval decision be enforced?

Outside the model. A rule in the system prompt is text the model reads and a prompt injection can override, whereas a check at the tool boundary sits outside the model's reach the nofire runtime enforcement guide. Framework hooks such as needs_approval are the first layer, and a policy gate in front of the tool call is the backstop.

Sources

Sources

  1. Using function tools with human in the loop approvals - Microsoft Learn
  2. Human-in-the-loop - OpenAI Agents SDK
  3. Human-in-the-loop - Docs by LangChain
  4. Blast-Radius Gates: Limiting What a Single Agent Action ...
  5. Implementing Human-in-the-Loop Approval JSON Evidence in ...
  6. Human-in-the-loop machine learning: a state of the art - Springer Nature
  7. How to implement runtime policy enforcement for AI agents
  8. should an AI agent have production access?
  9. Human-in-the-Loop (HITL) for AI Agents: Patterns and Best ...
  10. Implementing Human-in-the-Loop Approval Workflows for ...
  11. Review Queues: The Human Step Towards Better AI - MLflow
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles