Approvals
Gate tools behind human approval. Always, never, or conditionally. Workflow suspends and resumes on user response.
When approval is needed, the agent workflow suspends and waits. The user approves or denies via session.send(), and the workflow resumes. Works with built-in tools, custom tools, or both.
Approval Modes
needsApproval accepts three modes per tool:
| Mode | Behavior |
|---|---|
true | Always ask for approval before executing |
false | Never ask (default) |
(input, options) => boolean | Promise<boolean> | Decide per call based on input |
Agent-Level Rules
Configure approval rules on the agent via needsApproval:
import { agent } from "experimental-agent";
const myAgent = agent("my-agent", {
model: "anthropic/claude-opus-4.6",
needsApproval: {
Bash: (input) => input.command.startsWith("rm"),
Write: true,
},
});- Bash — Only ask when the command starts with
rm - Write — Always ask before any write
The function receives the tool input and { toolCallId, messages, experimental_context }, matching the AI SDK's needsApproval signature.
Per-Tool Approval
Custom tools can define needsApproval directly on the tool. Per-tool config takes priority over the agent-level map:
import { agent } from "experimental-agent";
import { tool } from "ai";
import { z } from "zod";
const Deploy = tool({
description: "Deploy to production",
parameters: z.object({ env: z.string() }),
needsApproval: async (input) => input.env === "production",
execute: async (input) => {
// deploy logic
},
});
const myAgent = agent("my-agent", {
model: "anthropic/claude-opus-4.6",
tools: { Deploy },
needsApproval: {
Bash: true,
// Deploy uses its own needsApproval
},
});Here, Deploy only requires approval when env === "production". The agent-level needsApproval applies to built-in tools like Bash.
How the Flow Works
- Model calls a tool — The LLM emits a tool call.
needsApprovalcheck runs — Via RPC (tools.needsApproval).- If approval needed — Stream emits
tool-approval-request, workflow suspends. - User approves or denies — Call
session.send({ type: "approval", approval: { approvalId, approved } }). - Workflow resumes — Tool executes (or is skipped if denied).
If a new message arrives while waiting for approval, pending approvals are auto-rejected and the workflow moves on.
Resolving Approvals
Use session.send with type: "approval" to respond to a pending approval:
await session.send({
type: "approval",
approval: { approvalId, approved: true, reason: "Looks good" },
});approvalId— Frompart.approval.idon the tool partapproved—trueto execute,falseto skipreason— Optional explanation
Throws on failure. See the API reference for full details.
Tool Invocation States
Tool parts progress through these states:
| State | Meaning |
|---|---|
input-available | Tool called, executing normally (no approval needed) |
approval-requested | Waiting for human approval |
approval-responded | User approved/denied, tool executing or skipped |
output-available | Tool finished, result available |
output-error | Tool execution failed |
Check part.state in your UI to render the right UI (e.g. approve/deny buttons when approval-requested).
Frontend Integration
Approval API Route
When the browser sends approval decisions, handle them in an API route:
import { myAgent } from "@/agent";
export async function POST(
req: Request,
{ params }: { params: Promise<{ chatId: string }> }
) {
const { chatId } = await params;
const { approvalId, approved, reason } = await req.json();
const session = myAgent.session(chatId);
await session.send({
type: "approval",
approval: { approvalId, approved, reason },
});
return Response.json({ success: true });
}React Component
Render approve/deny buttons when a tool part is in approval-requested state:
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
function ToolApproval({
chatId,
approvalId,
}: {
chatId: string;
approvalId: string;
}) {
const router = useRouter();
const [pending, setPending] = useState(false);
const respond = async (approved: boolean) => {
setPending(true);
await fetch(`/api/chat/${chatId}/approval`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ approvalId, approved }),
});
setPending(false);
router.refresh();
};
return (
<div className="flex gap-2">
<button disabled={pending} onClick={() => respond(true)}>
Approve
</button>
<button disabled={pending} onClick={() => respond(false)}>
Deny
</button>
</div>
);
}
function ToolPart({
part,
chatId,
}: {
part: { state?: string; approval?: { id: string } };
chatId: string;
}) {
if (part.state === "approval-requested" && part.approval) {
return (
<ToolApproval chatId={chatId} approvalId={part.approval.id} />
);
}
return null;
}Next Steps
- Tools — Built-in and custom tools
- Sessions — Session lifecycle
- API Reference — Full session API