> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool Approval

> Human-in-the-loop tool approval for the Agent component

Tool approval lets you require human confirmation before a tool call is
executed. This is useful for dangerous or irreversible operations — deleting
data, spending money, sending emails — where you want a person to review the
action before it happens.

## Defining tools with approval

Add `needsApproval` to any tool created with `createTool`. It can be a boolean
or an async function that receives the tool context and input:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { createTool } from "@bijection-dev/agent";
import { z } from "zod/v4";

// Always requires approval
const deleteFileTool = createTool({
  description: "Delete a file from the system",
  inputSchema: z.object({
    filename: z.string().describe("The name of the file to delete"),
  }),
  needsApproval: () => true,
  execute: async (_ctx, input) => {
    return `Deleted file: ${input.filename}`;
  },
});

// Conditionally requires approval (only for large amounts)
const transferMoneyTool = createTool({
  description: "Transfer money to an account",
  inputSchema: z.object({
    amount: z.number().describe("The amount to transfer"),
    toAccount: z.string().describe("The destination account"),
  }),
  needsApproval: async (_ctx, input) => {
    return input.amount > 100;
  },
  execute: async (_ctx, input) => {
    return `Transferred $${input.amount} to ${input.toAccount}`;
  },
});
```

Tools without `needsApproval` (or with `needsApproval` returning `false`)
execute immediately as usual.

## Server-side flow

The typical approval flow involves four server functions:

1. **Save the user's message** and schedule generation.
2. **Generate a response.** If the model calls a tool that needs approval,
   generation pauses and the `tool-approval-request` is persisted in the thread.
3. **Submit an approval or denial** for each pending tool call.
   `approveToolCall` and `denyToolCall` work from both mutations and actions.
4. **Continue generation** once all pending approvals have been resolved.

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { approvalAgent } from "../agents/approval";

// 1. Save message and schedule generation
export const sendMessage = mutation({
  args: { prompt: v.string(), threadId: v.string() },
  handler: async (ctx, { prompt, threadId }) => {
    const { messageId } = await approvalAgent.saveMessage(ctx, {
      threadId,
      prompt,
    });
    await ctx.scheduler.runAfter(0, internal.chat.approval.generateResponse, {
      threadId,
      promptMessageId: messageId,
    });
    return { messageId };
  },
});

// 2. Generate (stops if approval is needed)
export const generateResponse = internalAction({
  args: { promptMessageId: v.string(), threadId: v.string() },
  handler: async (ctx, { promptMessageId, threadId }) => {
    const result = await approvalAgent.streamText(
      ctx,
      { threadId },
      { promptMessageId },
    );
    await result.consumeStream();
  },
});

// 3. Submit an approval decision (can be a mutation or action)
export const submitApproval = mutation({
  args: {
    threadId: v.string(),
    approvalId: v.string(),
    approved: v.boolean(),
    reason: v.optional(v.string()),
  },
  returns: v.object({ messageId: v.string() }),
  handler: async (ctx, { threadId, approvalId, approved, reason }) => {
    const { messageId } = approved
      ? await approvalAgent.approveToolCall(ctx, {
          threadId,
          approvalId,
          reason,
        })
      : await approvalAgent.denyToolCall(ctx, {
          threadId,
          approvalId,
          reason,
        });
    return { messageId };
  },
});

// 4. Continue generation after all approvals resolved.
//    Pass the last approval message ID as promptMessageId so the agent
//    resumes generation from where the approval was issued.
export const continueAfterApprovals = internalAction({
  args: { threadId: v.string(), lastApprovalMessageId: v.string() },
  handler: async (ctx, { threadId, lastApprovalMessageId }) => {
    const result = await approvalAgent.streamText(
      ctx,
      { threadId },
      { promptMessageId: lastApprovalMessageId },
    );
    await result.consumeStream();
  },
});
```

<Tip>
  You can approve a tool call and continue generation in the same server function.
  For example, an action can call `approveToolCall` and then immediately call
  `streamText` to resume — no separate scheduling step needed.
</Tip>

## Handling multiple tool calls

When the model calls several tools in a single step, some or all of them may
require approval. **Every** pending approval must be resolved (approved or
denied) before you continue generation.

If a new generation starts while approvals are still unresolved, the unresolved
approvals are **automatically denied** with the reason
`"auto-denied: new generation started"`. This prevents broken message histories
where tool calls lack results.

## Client-side flow

On the client, use `useUIMessages` to detect pending approvals and show
Approve/Deny buttons. Tool parts with `state === "approval-requested"` are
waiting for a decision.

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { useEffect, useRef } from "react";
import { useMutation } from "bijection/react";
import { useUIMessages, type UIMessage } from "@bijection-dev/agent/react";
import type { ToolUIPart } from "ai";

function Chat({ threadId }: { threadId: string }) {
  const lastApprovalMessageIdRef = useRef<string | null>(null);

  const { results: messages } = useUIMessages(
    api.chat.approval.listThreadMessages,
    { threadId },
    { initialNumItems: 10, stream: true },
  );

  const submitApproval = useMutation(api.chat.approval.submitApproval);
  const triggerContinuation = useMutation(
    api.chat.approval.triggerContinuation,
  );

  const hasPendingApprovals = messages.some((m) =>
    m.parts.some(
      (p) =>
        p.type.startsWith("tool-") &&
        (p as ToolUIPart).state === "approval-requested",
    ),
  );

  // When all approvals are resolved, trigger continuation
  useEffect(() => {
    if (!hasPendingApprovals && lastApprovalMessageIdRef.current) {
      void triggerContinuation({
        threadId,
        lastApprovalMessageId: lastApprovalMessageIdRef.current,
      });
      lastApprovalMessageIdRef.current = null;
    }
  }, [hasPendingApprovals, threadId, triggerContinuation]);

  // Render approval buttons for tool parts with state "approval-requested"
  // ...
}
```

The `ToolUIPart` states relevant to approval are:

| State | Meaning |
| - | - |
| `approval-requested` | Waiting for the user to approve or deny |
| `approval-responded` | User responded; tool is being executed (if approved) |
| `output-available` | Tool executed successfully |
| `output-denied` | Tool was denied |
| `output-error` | Tool execution failed |

## Example files

For a complete working example, see:

* **Agent definition:**
  `example/bijection/agents/approval.ts`
* **Server functions:**
  `example/bijection/chat/approval.ts`
* **React UI:**
  `example/ui/chat/ChatApproval.tsx`
