Get Started
Tool approval in the Vercel AI SDK with Approval Gate
The AI SDK can hold a tool call until someone says yes. It hands useChat the call and an approval id, and
waits for addToolApprovalResponse. The adapter fills the gap between that and Approval Gate: it
turns the waiting call into an ActionRequest, and the person's ApprovalDecision into
the response the AI SDK expects. Approve, deny, edit and expiry all map.
Install
npm install [email protected] @ai-sdk/[email protected] npx shadcn@latest add https://agentsandhumans.ai/api/registry/approval-gate.json npx shadcn@latest add https://agentsandhumans.ai/api/registry/ai-sdk-approval.json
The adapter lands in lib/ai-sdk-approval.ts and brings the Interaction Contract with it. It was
written against ai 7.0.127 and @ai-sdk/react 4.0.130 and reads the shapes those
versions document. It does not import ai, so it adds no package of its own.
1. Ask for approval on the server
Set the tool to 'user-approval' in toolApproval. The reason you return shows in the gate
under "What happens".
// app/api/chat/route.ts
const result = streamText({
model,
messages: await convertToModelMessages(messages, { tools }),
tools: { sendInvoice },
toolApproval: {
sendInvoice: () => ({
type: 'user-approval',
reason: 'The customer gets this invoice by email as soon as you approve.',
}),
},
// Signs each approval request, so a client cannot invent an approval.
experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream, tools, originalMessages: messages }),
}); 2. Feed the gate from useChat
'use client';
import { useChat } from '@ai-sdk/react';
import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
import { useAiSdkApproval } from '@/lib/ai-sdk-approval';
import { ApprovalGate } from '@/components/approval-gate';
export default function Chat() {
const { id, messages, addToolApprovalResponse } = useChat({
// Send the turn back once every approval in it has an answer.
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});
const { gateProps } = useAiSdkApproval({
messages,
addToolApprovalResponse,
runId: id,
describe: (tool, input) =>
tool === 'sendInvoice' ? { resources: ['billing'], reversible: false } : undefined,
});
return gateProps && (
<ApprovalGate key={gateProps.request.id} {...gateProps} config={{ expiresInMs: 120_000 }} />
);
} gateProps is null until a call waits on the person. Key the gate by the request id so each call
gets a fresh gate. describe tells the gate what the AI SDK does not know: what the tool touches
and whether it can be undone. Left out, the gate says the call cannot be undone.
Each decision, mapped
| The person | The gate records | The AI SDK receives |
|---|---|---|
| Approves | granted, version 1 | { approved: true }. The tool runs on the server. |
| Rejects | denied | { approved: false, reason }. The tool does not run, and the model reads the reason. |
| Lets the timer run out | expired, actor system | { approved: false, reason }. The AI SDK has no expiry of its own, so the gate's timer decides. |
| Edits, then approves | granted, version 2, with editedArgs | { approved: false, reason } naming the edited input. If the model then calls the tool with that input, the adapter approves it without a second ask. |
An edit declines the first call for a reason: the AI SDK runs an approved call with the input it asked about and no other. So the adapter declines that call and gives the model the input the person approved. Any other input from the model opens a new gate.
A session or standing grant approves later calls to the same tool while the page is open.
To make a standing grant outlast a reload, set the tool to 'approved' in toolApproval on the server.
Run the example
Node 22.6 or later. From a clone of the repository:
cd examples/ai-sdk-approval npm ci npm test # approve, deny, expire and edit through the real chat route npm run dev # http://localhost:3000
Without ANTHROPIC_API_KEY the example runs a scripted model, so the whole flow works offline. Set
the key to talk to Claude instead. Source and tests: examples/ai-sdk-approval. The AI SDK's own
guide: Tool Approvals.
Before you ship it
In the useChat pattern the browser sends the whole conversation back each turn, so a changed client could claim
an approval the person did not give. Set experimental_toolApprovalSecret on the server, and the AI SDK checks
that it issued each approval request it sees. The gate's receipt reads "Recorded on this device" until your
server stores the decision and passes back a serverReceiptId.