Approval Request
The agent pauses before one specific action and asks permission, with its scope, cost and consequences visible.
≈ 1,849 tokens per use (tool 1,310 · guidance 302 · typical payload 237)How token costs are estimated
Example: Approval Request
Send the renewal email to all customers whose plan ends this month.
Send renewal email
Medium riskExpires in 14 min
October renewal reminder to customers whose plan ends this month.. Requested by Billing agent
When to use
- The agent is about to take one specific action that sends, charges, grants access, deletes or runs something, and the user should decide first.
- The action has no before/after diff worth showing: sending an email, paying, running a job, granting a role.
When not to use
- The agent is changing field values the user should see before and after. Use the Change Preview.
- The action is read-only or trivially reversible, and the user already asked the agent to go ahead.
- The agent hasn't read the real targets yet. Fetch the recipients, account or environment first.
How the agent should use it
- Wait for the result. Never act before receiving
approve, and apply the returnedadjustmentsexactly. Anything not listed stays as proposed. - Scope comes from tool results: real counts, names and environments, never estimates from memory.
- Set
riskhonestly. Usehighplus aconfirmPhrase(such as the environment name) for production, deletes and large payments. - High risk means press and hold. At
risk: "high"with noconfirmPhrase, Approve becomes Hold to approve: the user holds the button (or Enter or Space) for about 1.2 seconds, and releasing early cancels. Setholdyourself to force it on or off. AconfirmPhrasealready asks for a deliberate step, so it replaces the hold unless you sethold: true. - Offer
alternativeswhen a safer path exists, like "Send to a test list first" or "Run on staging first". - Lifecycle: set
expiresAton the call when the approval only holds for a while (a quote, a maintenance window, a held price), and pass the same value as theexpiresAtprop so the card shows "Expires in 14 min". After that moment the Renderer turns the card inert. Use supersede when the agent issues a revised request for the same action.fetchedAtisn't needed.
Interaction
| Control | Sends | Notes |
|---|---|---|
| Approve | approve | Press and hold when hold is on (see above): the fill grows, then it sends. A screen reader's "activate" approves at once, since it can't hold. Reads Approve with changes once an adjustable field differs from the proposal. Disabled until the confirm phrase matches (trimmed, case-insensitive) and every field is valid |
| Reject | reject | Asks for an optional reason inline before sending |
| Request changes | revise | The message icon. Opens a text box for a question or a requested change |
| An alternative | alternative | Sends the alternative's id right away. Its description is the button's accessible description |
The card leads with the cost when there is one, then the scope, the reversibility line, "What happens", any adjustable fields and the alternatives. The risk level and the expiry share the one line under the title; summary, requestedBy and each field's help are read to screen readers but not shown, to keep the card short.
After a decision the card locks and the footer shows a compact record, such as "Approved at 10:42 · with 1 change". Focus moves to that record, and the footer announces it politely.
Installation
npx shadcn@latest add https://your-site.com/r/approval-request.json
Usage
import { tool } from "ai"
import { approvalRequestTool } from "@/components/genui/approval-request.schema"
const tools = {
// No execute(): the call is completed by the user's decision in the UI.
[approvalRequestTool.name]: tool({
description: approvalRequestTool.description,
inputSchema: approvalRequestTool.inputSchema,
}),
}<ApprovalRequest
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({ tool: "request_approval", toolCallId: part.toolCallId, output: result })
}
/>The agent receives one of:
{ action: "approve", decidedAt: "2026-10-08T10:42:00.000Z" }
{ action: "approve", adjustments: { recipient_cap: 50 }, decidedAt: "…" }
{ action: "reject", reason: "Wait until the pricing page is updated", decidedAt: "…" }
{ action: "revise", feedback: "Exclude customers who already renewed", decidedAt: "…" }
{ action: "alternative", id: "test_list", decidedAt: "…" }adjustments holds only the fields the user changed, typed (numbers as numbers, toggles as booleans). On revise, do nothing: answer the question or send a new request. On reject, stop.
The helpers in approval-request-utils.ts (diffAdjustments, matchesConfirmPhrase, formatCost, expiryText, riskMeta) are pure, so the server can reuse them, for example to check a returned adjustment against the field's min and max before acting.
With LangGraph
Push it from a graph node with ui.push({ name: "request_approval", props: { toolCallId, ...args } }, { message }), then interrupt() until the user's result arrives. Render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored result back as result. The card renders the decision record (with the adjusted values) and never calls onResult again, so a decision can't be made twice. The record's time comes from decidedAt.
<ApprovalRequest
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
action | string | Imperative: "Send renewal email" |
summary | string? | One sentence on what will happen. Read to screen readers |
risk | "low" | "medium" | "high" | Shown as an icon and as text ("High risk") |
reversible | boolean | "partially" | "Can't be undone", "Can be undone" or "Partly reversible: …" |
undoWindow | string? | "Undo within 30 seconds" |
reversibleDetail | string? | What can be reversed: "Refunds are prorated within 14 days" |
scope | { label, value, detail? }[] | Data-bound. 1–8 rows: recipients, account, environment |
consequences | string[]? | Up to 5 plain sentences |
cost | { amount, currency, period? } | Data-bound. period: one-time, day, week, month, quarter, year |
requestedBy | string? | "Billing agent". Read to screen readers |
expiresAt | string? | ISO 8601. Shown after mount as "Expires in 14 min" |
adjustable | AdjustableField[]? | Up to 6 fields the user may change before approving |
hold | boolean? | Press and hold Approve. Default: on for risk: "high" without a confirmPhrase, off otherwise |
confirmPhrase | string? | The user must type it to unlock Approve |
alternatives | { id, label, description? }[]? | Up to 3 safer options |
AdjustableField
| Field | Type | Notes |
|---|---|---|
id | string | Key in adjustments |
label | string | |
type | number | text | select | toggle | Input, Input, Select or Switch |
value | string | number | boolean | The proposed value |
options | string[]? | Required for select |
min, max | number? | For number |
help | string? | One line, read as the field's description |
Host props: status (streaming, ready, submitting, approved, rejected, revised, alternative), onResult, result, now (a fixed time for the expiry text) and className.
Source
Music Player
What's playing on the user's music app, with remote controls, or a playlist the agent built for them to preview, reorder and save.
Design Review
A designer reviews a design-system change the AI proposed (a rule, component, token, pattern or artifact) and approves, declines or sends it back before it enters the system.