Short answer: set needsApproval on a Vercel AI SDK tool, show the proposed arguments to your user, add the approval response to the messages, and call the model again. The tool does not execute during the first call. Use an authorization layer around execute when the decision also depends on your signed-in user, a database resource, a shared budget, or policy that must work outside the AI SDK.
The current AI SDK tool-calling guide documents the full two-call flow. generateText and streamText return a tool-approval-request; your application supplies a tool-approval-response before the approved tool can run.
Start with the native approval flag
A native approval rule fits a tool whose risk is easy to express next to its schema:
const sendEmail = tool({
description: 'Send an email',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
}),
needsApproval: async ({ to }) => !to.endsWith('@acme.com'),
execute: async ({ to, subject }) => mailer.send({ to, subject }),
})
Keep this version when one AI SDK agent owns the tool, the rule is local, and your application already stores the approval state you need. The SDK gives you the pause and resume protocol. You do not need another library to render a confirmation button.
The rule starts to strain when two tools implement different checks for the same business action, or when “may this run?” requires data the model must not supply. A recipient domain in model output is inspectable. The user’s tenant membership and current permission on a customer record must come from your application.
Put shared policy around the execute callback
guardTools preserves the AI SDK tool shape and routes each server-side execute through the same Nominee instance. The object key becomes the policy action name:
import { openai } from '@ai-sdk/openai'
import { generateText, tool } from 'ai'
import { Nominee, allow, ask } from 'nominee'
import { guardTools } from 'nominee-ai'
import { z } from 'zod'
const nominee = new Nominee({
policy: {
rules: [
allow('sendEmail', {
when: ({ input }) => input.to.endsWith('@acme.com'),
}),
ask('sendEmail'),
],
fallback: 'deny',
},
onApprovalRequest: (request) => approvalUi.show({
input: request.detail,
approve: () => request.approve(),
deny: () => request.deny(),
}),
})
const rawTools = {
sendEmail: tool({
description: 'Send an email',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
}),
execute: ({ to, subject }) => mailer.send({ to, subject }),
}),
}
const result = await generateText({
model: openai('gpt-5-mini'),
prompt: 'Email the release note to pat@example.com',
tools: guardTools(nominee, rawTools, { user: session.userId }),
})
The callback runs after Nominee reaches allow or after a human resolves ask. A denial rejects before mailer.send receives control. For an approval that may outlive the request, use Nominee’s durable action flow or a framework-native resumable adapter instead of keeping one promise in memory.
Choose the smaller boundary that meets your requirements
- Use AI SDK approval alone for a local confirmation rule and an approval UI you already own.
- Add shared action policy when several frameworks or workers must apply the same user, resource, argument, and budget rules.
Approval answers whether a person accepted one proposed call. Application authorization answers whether that person and agent may perform the action at all. The deeper distinction is covered in the AI SDK can pause a tool call; it cannot define your application policy.
See the boundary run
Approve one tool call and inspect the receipt chain.