AI agents

Approval Prompt Fields That Map to a Schema

Antonio Evans8 min read

A four-column approval card drawn as a wireframe, with each field labeled and connected by thin lines to a matching JSON object beside it: a name string, an action enum, an affected resource object, and a reason string.
Key points

Key points

  1. 1Model every approval prompt as one JSON object with four properties: requester, action, target and justification, each with an explicit type.
  2. 2Keep requester, action and target in the required list, and keep justification optional so a card can render without it.
  3. 3Put rendering rules such as maxLength, default and enum values in the schema so every surface truncates and labels the same way.
  4. 4Validate each payload against the schema before it reaches the card, and render a fixed fallback string for anything missing.

Why approval payloads need a schema at all

Human-in-the-loop approval is a runtime control pattern: the agent pauses, a person decides, and the run resumes. In the Microsoft Agent Framework, a function tool is wrapped so that the agent returns a ToolApprovalRequestContent instead of executing, and the caller passes an approval response back in a new run (Microsoft Learn). LangChain's Deep Agents does the same thing through interrupt_on and allowed_decisions, which limits a reviewer to approve, edit, reject or respond per tool (LangChain docs). Once a design like that exists, the approval card is no longer a decorative UI detail. It is the only place where a person sees what the agent wants to do before the side effect lands.

That makes the payload a contract. Without a schema, three things break. First, the card drifts: one team truncates long target names, another does not, and reviewers start approving text they cannot see. Second, automated pre-checks have nothing to compare against, so a missing target id passes silently. Third, downstream systems that log approvals (audit, review queues, ticketing) receive inconsistent records. MLflow's review queues make the point indirectly: human judgments become useful only when they are captured as structured traces with a defined evaluation question, not as free text in a spreadsheet (MLflow). A schema is the small piece of discipline that makes an approval record usable later.

Guardrail literature places this at the technical layer: schema validation and output formatting rules sit alongside action authorization as the mechanisms that check a proposed action before it runs, rather than after (Salesforce). Treat the four approval fields as that layer for your approval cards.

Choosing types for the four fields

Start with the working definition of each field and the type that makes the least sense to violate.

Requester: string. The person or service asking for approval. A string with minLength: 1 matching an identifier pattern is enough. Do not use an object here unless you genuinely need display name, email and role separately; the cheapest fix for most teams is a stable principal id, with the card resolving a friendly name from it.

Action: enum. This is the field that decides how the approval gets routed, so free text is a liability. An enum of allowed verbs (update_firewall_rule, send_email, refund_payment) means the card can pick an icon, the router can pick an approver, and a typo fails validation instead of producing an unlabeled card. Change management guidance for firewall rules is a useful model: the request carries a defined action (allow or deny) rather than a sentence (fwchange).

Target: object with id and type. The first search result on "who approves what" will rarely tell you this, but the decision rule matters: a plain string target forces the card to parse meaning out of text, while an object with id and type lets you render "Firewall rule r-1183" and link to it. Require both subfields.

Justification: string. The business reason. It is the field most likely to be missing, vague or long, which is exactly why it needs bounds. Require a minLength (so "ok" fails) and a maxLength (so a pasted stack trace does not blow up the card).

FieldTypeRequiredKey constraintsWhy
requesterstringyesminLength 1, pattern for principal idRouting and audit need a stable identity
actionstring enumyesclosed list of verbsDrives icon, routing and risk tier
targetobjectyesid string, type string, both requiredLets the card link to the real object
justificationstringnominLength 8, maxLength 500, defaultMost-frequently-missing field; must not block the card
proposedAtstring (date-time)noformat date-timeUseful for SLA and staleness checks

Required versus optional, and conditional rules

The simple split from the short answer holds: requester, action and target are required; justification is optional. Three heuristics keep that split honest.

  1. Required if the card cannot be rendered without it. If the card's headline is "requester wants to action target", all three belong in required.
  2. Required if the decision is unsafe without it. A refund without an amount, or a firewall change without a port, is not reviewable. Model that as a nested required list on target or as a conditional.
  3. Optional if a sensible default exists. Justification is the classic case: the card shows a default string and a prompt, not an error.

Conditional requirements are where teams get into trouble. JSON Schema supports dependentRequired to say "if this property is present, these must also be present", and if/then/else for more general cases (JSON Schema reference). A long-running Stack Overflow thread documents the common mistake: people reach for conditionals when what they actually wanted was a plain required list, and then debug why the else branch never fires (Stack Overflow). Practitioners hit the same wall in tooling: a request to generate conditional required fields from annotations is still open because only unconditional requiredness is expressible today (cozyvalues-gen issue), and users of form renderers have reported failing if/then/else rules in nested objects (JSON Forms forum).

The decision rule: use dependentRequired only for real couplings (for example, if estimatedImpact is present, impactUnit must be too). Do not make justification conditionally required based on action. It sounds clever and it produces cards that fail validation for reasons the agent cannot fix at generation time.

The schema, in full

Here is a schema you can adopt directly. It uses draft-style keywords that most validators in common languages support.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "ApprovalRequest",
  "type": "object",
  "additionalProperties": false,
  "required": ["requester", "action", "target"],
  "properties": {
    "requester": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "^[a-z0-9._-]+$",
      "description": "Stable principal id of the person or service asking for approval."
    },
    "action": {
      "type": "string",
      "enum": [
        "update_firewall_rule",
        "delete_record",
        "send_customer_email",
        "refund_payment",
        "provision_access"
      ],
      "description": "Allowed verb. Unknown verbs fail validation."
    },
    "target": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "type"],
      "properties": {
        "id": { "type": "string", "minLength": 1, "maxLength": 128 },
        "type": {
          "type": "string",
          "enum": ["firewall_rule", "database_row", "email_draft", "payment", "user_account"]
        },
        "label": { "type": "string", "maxLength": 80 }
      }
    },
    "justification": {
      "type": "string",
      "minLength": 8,
      "maxLength": 500,
      "default": "No justification supplied."
    },
    "proposedAt": {
      "type": "string",
      "format": "date-time"
    }
  }
}

Two design notes. additionalProperties: false keeps agents from smuggling extra keys into the card, which is how "why" fields end up duplicated with one of them stale. And default on justification is a rendering hint, not a value the validator inserts; your renderer must apply it.

If you want a tighter coupling between action and target type, express it once, outside the schema, as a lookup table in code. Nested conditionals in the schema are valid but hard to debug and unevenly supported by generators, which is exactly the failure mode reported in the cozyvalues-gen and JSON Forms threads above.

Rendering rules inside the schema

The second insight most approval prompts miss: the schema is also the place to encode how the card looks, so every surface agrees.

  • Truncation. maxLength is doing double duty. If justification allows 500 characters and the card shows 140, put the display limit in the renderer config keyed to the schema, not in each component. The card should truncate with an explicit expand control, not silently.
  • Labels and order. Keep a small x-ui block (or a side config) that maps each property to a display label and sort order. The four fields should always render in the same order: who, what, what it affects, why.
  • Enum presentation. Give the enum a parallel label map so update_firewall_rule reads as "Update firewall rule". Never show raw snake_case to a reviewer.
  • Defaults. Render the justification default in muted text with a prompt such as "No reason given". Do not hide the row when the field is absent; a missing reason is information the reviewer needs.
  • Risk styling. Map each action to a risk tier and color the card edge accordingly. This is the same escalation logic guardrail vendors recommend, where high blast-radius actions require named approvers and evidence before execution (Aembit).

Rendering rules in the schema are not cosmetic. They are how the reviewer's "approve" becomes a decision made against the same information on every screen, including the mobile one and the chat integration.

Validation and fallbacks when a field is missing

Validate at one boundary: between the agent and the approval store. Not in the agent prompt, not in the card component. A short Python example with jsonschema:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)

def gate(payload: dict) -> dict:
    errors = sorted(validator.iter_errors(payload), key=lambda e: list(e.path))
    if not errors:
        return {"ok": True, "payload": payload}
    return {"ok": False, "errors": [
        {"path": list(e.path), "message": e.message} for e in errors
    ]}

Then apply a fallback policy. The decision rule that works: fail closed on required fields, degrade gracefully on optional ones.

SituationBehaviorReviewer sees
Wrapper exists but target id missingReject payload, do not create cardNothing (agent told to retry)
Action not in enumReject payloadNothing
Justification missingAccept, apply default"No reason given" plus prompt
Justification too shortAccept but flagAmber warning on the reason row
Target label missingAcceptShow type:id
Payload is not an objectReject and logNothing, alert raised

The fail-closed choice is supported by how established approval systems behave. LangGraph's human-in-the-loop requires a checkpointer precisely because an interrupted run has to be resumable with intact state (LangChain docs). If a card is created for a payload the agent cannot resume from, the reviewer approves something that then fails, and the trust cost is higher than a retry. The same logic applies to malformed payloads: an interrupted or cancelled run needs its message history repaired before the next turn, as the PatchToolCalls middleware does in that stack.

One more fallback worth writing down: what the card shows when the requester is a service account whose display name cannot be resolved. Show the id. Never show "Unknown".

If you are wiring several model providers into one agent platform and want the same approval envelope across all of them, Quanta Inference exposes text, image, audio and embedding models through one OpenAI-compatible key (https://quantaintelligence.ai/inference), which keeps the payload shape stable while the model behind it changes.

Testing the schema and reading the results

Write three test fixtures and keep them in the repo: one fully valid payload, one missing justification, one with an unknown action. Add a fourth with a target missing id, because that is the case that silently produces an unlinkable card. A test suite of four cases catches most drift.

Then instrument the boundary. Count validations by error path, and review weekly. The percent of approvals arriving without justification is the single most useful number here: if it is high, the upstream prompt is not asking for a reason, and you should fix generation rather than relax the schema.

Approval latency is the second metric. Time spent waiting for a decision is usually blamed on the human step, but it often comes from incomplete context or bad routing, and it can be measured separately from execution time (Atlassian community). A schema gives you the fields to route on and the fields to measure. Teams that skip it end up with an approval card that is technically working and a review process nobody can improve.

If your bottleneck is the design work rather than the validation code, a consulting call is a fast way to get the four fields, the enums and the fallback policy agreed across the teams that own each card.

FAQ

FAQ

Why does the action field need an enum instead of free text?

An enum makes routing and styling deterministic and turns typos into validation errors rather than unlabeled cards. It also lets you attach a risk tier and a default approver per verb. Free text forces the card to guess meaning.

Should justification ever be required?

Only if the decision cannot be made without it, which is rare, because the other three fields usually carry the decision. Making it required causes agents to invent filler to pass validation. Use a minimum length instead and show a clear "No reason given" state.

How do I handle a missing target id?

Fail closed: reject the payload before the card is created and return the validation error to the agent so it regenerates. An approval card without a target id cannot be acted on safely, and letting it through trains reviewers to approve incomplete context.

Where should validation run?

At one boundary, between the agent and the approval store, so that every renderer (web, mobile, chat) receives only validated payloads. Putting checks in the UI duplicates logic and lets a second surface accept what the first rejects.

Can I express "if action is X then target type must be Y" in the schema?

Yes, with dependentSchemas or if/then keywords, but support across validators and form generators varies and debugging is harder. For a four-field approval payload, a lookup table in code is usually the more maintainable choice.

Sources

Sources

  1. Using function tools with human in the loop approvals - Microsoft Learn
  2. Human-in-the-loop - Docs by LangChain
  3. Review Queues: The Human Step Towards Better AI - MLflow
  4. AI Guardrails: A Guide to Responsible AI - Salesforce
  5. Firewall Change Management: Process, Tools & Best ...
  6. Conditional schema validation - JSON Schema
  7. jsonSchema attribute conditionally required - Stack Overflow
  8. Support JSON Schema conditional validation (if/then/else) for required ...
  9. Json Schema validation using If-Else - "conditionally choosing required ...
  10. Agentic AI Guardrails: What They Are and How to Implement Them - Aembit
  11. How to Measure Approval Time in Jira
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