There's a fundamental tension when building AI agents: the more power you give them, the more useful they become, but the more likely they are to make costly mistakes. A destructive action like sending an email can't be undone.
The solution is human-in-the-loop approval. Before the LLM executes risky actions, it asks for your permission first. Fortunately, the AI SDK has built-in support for this pattern, so you don't need to implement it from scratch.
In this exercise, you'll add approval workflows to a sendEmail tool, creating a UI that lets users review and approve emails before they're sent.
This tells the AI SDK that this tool requires user approval before execution.
const tools = {sendEmail: tool({description: 'Send an email to a recipient',inputSchema: z.object({to: z.string().describe('The email address of the recipient'),subject: z.string
This prop is a function that takes an id (string) and approved (boolean).
export const Message = ({role,parts,}: // TODO: Add addToolApprovalResponse prop, a function which takes in:// - id: string// - approved: boolean{role: string;parts: MyUIMessage['parts'];})
Check for the approval-requested state and display the email details (to, subject, body) with approve and reject buttons.
if (part.type === 'tool-sendEmail') {// TODO: Check if part.state === 'approval-requested'// If so, render the email preview with approve/reject buttons// Use addToolApprovalResponse({ id: part.approval.id, approved: true/false })
When the user clicks approve, call addToolApprovalResponse({ id: part.approval.id, approved: true }). When they click reject, use approved: false.
The useChat hook returns this function directly.
Import lastAssistantMessageIsCompleteWithApprovalResponses from the AI SDK and pass it to the hook. This automatically sends responses once all approvals are handled.
The LLM should call the sendEmail tool and display your approval UI.
Click approve and check your browser console. You should see Sending email to bob@example.com: hello.
Reject an email and verify the LLM asks for follow-up information instead of sending it.
const { messages, sendMessage, addToolApprovalResponse } =useChat<MyUIMessage>({// TODO: Get addToolApprovalResponse from useChat// TODO: Add sendAutomaticallyWhen option using// lastAssistantMessageIsCompleteWithApprovalResponses});
{messages.map((message) => (<Messagekey={message.id}role={message.role}parts={message.parts}// TODO: Pass addToolApprovalResponse to Message/>))}
import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';const { messages, sendMessage, addToolApprovalResponse } =useChat<MyUIMessage>({sendAutomaticallyWhen:lastAssistantMessageIsCompleteWithApprovalResponses,});