Vercel AI SDK tool approval: moving from needsApproval to toolApproval

The AI SDK's per-tool needsApproval flag is deprecated; approval now lives in a toolApproval setting on the call. We ran both against a mock model to see exactly when a tool executes.

The short answer

In the AI SDK, the needsApproval property on tool() is deprecated. Existing code keeps working, and new code should use toolApproval, set on generateText, streamText or ToolLoopAgent.1 toolApproval takes one function for all tools or a map per tool, and each entry returns a status: not-applicable (the default, no approval flow), approved, denied or user-approval.1

We ran AI SDK 7.0.153 against its mock model: a refund under the threshold executed with an automatic approval recorded, and one over it stopped at a tool-approval-request without executing. The deprecated needsApproval: true also stopped. Results below.

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

What changed

needsApproval (deprecated) toolApproval
Where On each tool() definition On the call or agent
Shape Per tool One function for all tools, or a map per tool1
Outcomes Ask or not not-applicable, approved, denied, user-approval1
Automatic decisions recorded No Yes: approval request and response parts in the same generation1
Reasons No Optional reason on the status1

The practical gain is the middle of the table. Approval policy moves out of tool definitions, which are often shared across apps, into the call site, where you know who the user is and what they’re allowed to do. And automatic decisions leave a trace in the result, which is what you want when you later ask “why did this refund go through?”

The four statuses

From the docs:1

  • not-applicable: the tool runs with no approval flow. This is the default.
  • approved: the SDK records an automatic approval, then executes.
  • denied: the SDK records an automatic denial and returns a denied tool output.
  • user-approval: the SDK emits an approval request and waits for an explicit response.

A per-tool entry can be a status string or a callback that decides from the tool’s input. The docs’ example requires approval only for transactions over 1,000.1

A run against a mock model

AI SDK 7 ships test helpers in ai/test. We used MockLanguageModelV4 to make the model call a refund tool with a chosen amount, then a plain text reply. The approval rule:

Code Program code, for developers. The text around it says what it does.

const result = await generateText({
  model: new MockLanguageModelV4({ doGenerate: [callRefund(amount), done] }),
  prompt: 'refund order 42',
  tools: { refund },
  toolApproval: { refund: (input) => (input.amount > 100 ? 'user-approval' : 'approved') },
});

The per-tool callback receives the tool’s input as its first argument. (Our first attempt destructured { input } and failed with Cannot read properties of undefined; the callback’s signature is (input, options).)

Output, listing whether execute ran and which parts came back:

Code What the screen or file shows, copied as is.

amount=40 executed=[40] parts=tool-call, tool-approval-request(automatic=true), tool-approval-response, tool-result
amount=250 executed=[] parts=tool-call, tool-approval-request

The 40 refund ran, and the result still carries an approval request marked automatic plus its response: the audit trail the docs describe.1 The 250 refund didn’t run. The call returned with a pending request instead of pausing, which matches the docs: approval doesn’t block the call; it returns tool-approval-request parts for you to act on.1

Then the deprecated form, needsApproval: true on the tool and no toolApproval:

Code What the screen or file shows, copied as is.

needsApproval: executed=0 parts=tool-call, tool-approval-request

It still stops before executing, so migrating isn’t urgent for correctness. It’s worth doing for the amount-based rules and the recorded automatic decisions.

Answering an approval request

Find requests with part.type === 'tool-approval-request' && !part.isAutomatic. Each has an approvalId, the toolCall and a reason. To answer, add a tool-approval-response with the approvalId, approved: true or false and an optional reason to the messages, and call the model again.1

What happens to the reason depends on the decision:1

  • Denied: the reason goes into the execution-denied tool result, so the model sees why.
  • Approved: the reason stays in your messages for the UI or audit log; the model doesn’t see it, and it isn’t a way to pass data to the tool.

After a denial, the model sees it and responds. The docs suggest a system instruction telling it not to retry tool calls that weren’t approved.1 Without one, a model may try the same refund again with slightly different arguments.

Migration checklist

  • Find every needsApproval in your tool definitions.
  • Move each rule to toolApproval on the call or agent; turn “always ask” into 'user-approval'.
  • Turn hand-written thresholds into per-tool callbacks: (input) => input.amount > LIMIT ? 'user-approval' : 'approved'.
  • Use 'denied' with a reason for calls that should never run from this surface.
  • Filter non-automatic tool-approval-request parts into your approval UI.
  • Add the “don’t retry denied calls” instruction.
  • Log automatic approvals too; they’re in the result content.
  • Re-run your tests with the mock model; the callback signature is (input, options).

The human-in-the-loop approval entry covers the pattern in general, and requiring approval for MCP tool calls shows the same idea for MCP.

What approval doesn’t cover

  • Provider-executed tools. Approval applies only to tools the SDK executes locally; provider-executed tools ignore both toolApproval and needsApproval.1
  • What the tool can reach. An approved refund still uses whatever key the tool holds. Give it a narrow one; our post on Stripe restricted keys for agents shows how, including Stripe’s own approval rules as a second check.
  • Other clients. Approval lives in your SDK code. Another agent calling the same API directly skips it.

To see what the agents configured on your machine can change without a prompt, paste the config into the check page or run npx deedglass scan (how the grade works); to try approval rules on tool calls in your browser, open the playground.

Questions people ask

Is needsApproval removed in AI SDK 7?

Deprecated, not removed. The docs say existing code still works and new code should use toolApproval.1 In our run, needsApproval: true still stopped the call.

Does toolApproval pause generateText?

No. The call returns tool-approval-request parts; you respond with tool-approval-response and call again.1

Can approval depend on the arguments?

Yes. A per-tool callback receives the input and returns a status, for example approval only above an amount.1

Written with AI assistance and checked against our claims register.

The takeaway

With toolApproval, a 40 refund ran with an automatic approval and a 250 refund stopped at a tool-approval-request; deprecated needsApproval still stops too.

Read nextStripe restricted keys for AI agents: let an agent handle refunds without a secret key Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1AI SDK docs: Tools and tool calling · ai-sdk.dev · checked 26 October 2026
  2. 2npm: ai · npmjs.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing