Concepts

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:

ModeBehavior
trueAlways ask for approval before executing
falseNever ask (default)
(input, options) => boolean | Promise<boolean>Decide per call based on input

Agent-Level Rules

Configure approval rules on the agent via needsApproval:

src/agent.ts
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:

src/agent.ts
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

  1. Model calls a tool — The LLM emits a tool call.
  2. needsApproval check runs — Via RPC (tools.needsApproval).
  3. If approval needed — Stream emits tool-approval-request, workflow suspends.
  4. User approves or denies — Call session.send({ type: "approval", approval: { approvalId, approved } }).
  5. 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 — From part.approval.id on the tool part
  • approvedtrue to execute, false to skip
  • reason — Optional explanation

Throws on failure. See the API reference for full details.

Tool Invocation States

Tool parts progress through these states:

StateMeaning
input-availableTool called, executing normally (no approval needed)
approval-requestedWaiting for human approval
approval-respondedUser approved/denied, tool executing or skipped
output-availableTool finished, result available
output-errorTool 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:

app/api/chat/[chatId]/approval/route.ts
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:

components/ToolApproval.tsx
"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