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.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
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-deniedtool 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
needsApprovalin your tool definitions. - Move each rule to
toolApprovalon 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-requestparts 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
toolApprovalandneedsApproval.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.