# 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