# Tool Approval (Human-in-the-Loop)
v6 patterns for requiring user approval before tool execution.
## When to Use
| Scenario | Approval Type |
|----------|---------------|
| Always dangerous (delete, payment) | Static: `needsApproval: true` |
| Conditionally risky (large amounts) | Dynamic: `needsApproval: async (args) => boolean` |
| User preference | Dynamic based on user settings |
## Static Approval
Tool always requires approval:
```typescript
import { tool } from 'ai';
import { z } from 'zod';
const deleteUserTool = tool({
description: 'Permanently delete a user account',
inputSchema: z.object({
userId: z.string(),
reason: z.string(),
}),
needsApproval: true, // Always require
execute: async ({ userId, reason }) => {
await deleteUser(userId, reason);
return { success: true };
},
});
```
## Dynamic Approval
Approval based on input:
```typescript
const paymentTool = tool({
description: 'Process a payment',
inputSchema: z.object({
amount: z.number(),
recipient: z.string(),
currency: z.string().default('USD'),
}),
needsApproval: async ({ amount }) => amount > 1000,
execute: async ({ amount, recipient, currency }) => {
return await processPayment(amount, recipient, currency);
},
});
```
## Complex Approval Logic
```typescript
const externalApiTool = tool({
description: 'Call external API',
inputSchema: z.object({
endpoint: z.string(),
method: z.enum(['GET', 'POST', 'DELETE']),
body: z.any().optional(),
}),
needsApproval: async ({ method, endpoint }) => {
// Approve all non-GET requests
if (method !== 'GET') return true;
// Approve requests to sensitive endpoints
if (endpoint.includes('/admin')) return true;
// No approval needed for safe reads
return false;
},
execute: async ({ endpoint, method, body }) => {
return await fetch(endpoint, { method, body: JSON.stringify(body) });
},
});
```
## Client-Side Handling
### useChat with Approval
```tsx
'use client';
import { useChat } from '@ai-sdk/react';
function Chat() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
return (
{messages.map((message) => (
addToolApprovalResponse({ id, approved: true })}
onDeny={(id) => addToolApprovalResponse({ id, approved: false })}
/>
))}
);
}
```
### Approval UI Component
```tsx
function ToolInvocation({ invocation, onApprove, onDeny }) {
switch (invocation.state) {
case 'approval-requested':
return (
Approval Required
Tool: {invocation.toolName}
{JSON.stringify(invocation.input, null, 2)}
);
case 'pending':
return Waiting for tool execution...
;
case 'output-available':
return (
Tool Result
{JSON.stringify(invocation.output, null, 2)}
);
default:
return null;
}
}
```
### Rendering Tool Parts in Messages
```tsx
function Message({ message, onApprove, onDeny }) {
return (
{message.parts.map((part, i) => {
if (part.type === 'text') {
return
{part.text}
;
}
if (part.type === 'tool-invocation') {
return (
);
}
return null;
})}
);
}
```
## Server-Side Processing
For complex approval workflows with server-side tool execution:
```typescript
import { processToolCalls } from './tool-processor';
export async function POST(req: Request) {
const { messages } = await req.json();
// Check for pending approvals
const lastMessage = messages.at(-1);
const hasPendingApprovals = lastMessage?.parts?.some(
(p) => p.type === 'tool-invocation' && p.state === 'output-available'
);
if (hasPendingApprovals) {
// Process approved tools
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const processed = await processToolCalls({
writer,
messages,
tools: myTools,
}, toolExecuteFunctions);
// Continue conversation with tool results
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(processed),
});
writer.merge(result.toUIMessageStream());
},
originalMessages: messages,
});
return createUIMessageStreamResponse({ stream });
}
// Normal flow
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
tools: myTools,
});
return result.toUIMessageStreamResponse({ originalMessages: messages });
}
```
## Best Practices
**Security:**
- Always require approval for destructive operations
- Use dynamic approval for operations with varying risk levels
- Log all approval decisions for audit trails
**UX:**
- Show clear context for what the tool will do
- Display input parameters so users can make informed decisions
- Provide cancel/timeout options for pending approvals
**Error Handling:**
- Handle denied approvals gracefully
- Provide alternative actions when tools are denied
- Don't retry denied tools automatically