Add human approval workflows to AI agents using World ID.
AI agent pauses for World ID approval before booking a flight
Human-in-the-loop lets an AI agent pause mid-execution and wait for a real, verified human to approve an action before continuing. Every approval is cryptographically bound to the action via World ID — no bots, no spoofing, no replay.Built on the Workflow SDK and the Vercel AI SDK.
# Server — used by @worldcoin/human-in-the-loopWORLD_RP_ID=your_rp_idWORLD_SIGNING_KEY=your_signing_key# Client — used by the <HumanApproval> component (optional if passing appId prop)NEXT_PUBLIC_WORLD_APP_ID=app_...
// src/workflows/chat/index.tsimport { DurableAgent } from 'workflow/ai'import { getWritable } from 'workflow'import { openai } from '@workflow/ai/openai'import { tools } from './steps/tools'export async function chatWorkflow(messages: ModelMessage[]) { // Durable workflow — can pause for hours/days and resume where it left off 'use workflow' const writable = getWritable<UIMessageChunk>() const agent = new DurableAgent({ model: openai('gpt-5.4'), tools, system: 'You are a helpful assistant. Before performing any sensitive action, use the approveAction tool.', }) await agent.stream({ messages, writable })}
// src/workflows/chat/steps/tools.tsimport { requestHumanAuthorization } from '@worldcoin/human-in-the-loop/workflows'import { z } from 'zod'export const tools = { approveAction: { description: 'Request human approval via World ID before a sensitive action.', inputSchema: z.object({ summary: z.string() }), // Pauses the workflow, streams approval context to the client, // waits for World ID proof, verifies it, then resumes. // Action defaults to toolCallId; pass a function to bind to input fields: // action: ({ input }) => `booking:${input.flightNumber}` execute: requestHumanAuthorization(), }, // ...your other tools}
This example uses the <HumanApproval> component, if you want to customize the UI you can use the useHumanApproval hook instead.
import { HumanApproval } from '@worldcoin/human-in-the-loop-react'// Match on the tool name from Step 2. <HumanApproval> renders the World ID// widget and POSTs the proof back to the server automatically.{message.parts.map(part => { if (part.type === 'tool-approveAction' && 'toolCallId' in part) { return ( <HumanApproval key={part.toolCallId} message={message} part={part} /> ) } // ...your other part renderers})}