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

# Human in the Loop

> Pause agent execution and wait for human approval, confirmation, or input

Human-in-the-loop (HITL) patterns allow agents to pause execution and wait for human approval, confirmation, or input before proceeding. This is essential for compliance, safety, and oversight in agentic systems.

## Overview

### Why Human in the Loop?

* **Compliance**: Regulatory requirements may mandate human approval for certain actions
* **Safety**: High-stakes operations (payments, deletions, external communications) need oversight
* **Quality**: Human review catches errors AI might miss
* **Trust**: Users feel more confident when they can approve critical actions

### Common Use Cases

| Use Case            | Example                                  |
| ------------------- | ---------------------------------------- |
| Financial approvals | Expense reports, payment processing      |
| Content moderation  | Publishing, email sending                |
| Data operations     | Bulk deletions, exports                  |
| AI tool execution   | Confirming LLM tool calls before running |
| Access control      | Granting permissions, role changes       |

## Choosing an Approach

Agents SDK supports multiple human-in-the-loop patterns. Choose based on your use case:

| Use Case               | Pattern           | Best For                                           |
| ---------------------- | ----------------- | -------------------------------------------------- |
| Long-running workflows | Workflow Approval | Multi-step processes, durable approval gates       |
| AIChatAgent tools      | `needsApproval`   | Chat-based tool calls with `@cloudflare/ai-chat`   |
| OpenAI Agents SDK      | `needsApproval`   | Using OpenAI's agent SDK with conditional approval |
| Client-side tools      | `onToolCall`      | Tools that need browser APIs or user interaction   |
| MCP Servers            | Elicitation       | MCP tools requesting structured user input         |

### Decision Guide

```
Is this part of a multi-step workflow?
├── Yes → Use Workflow Approval (waitForApproval)
└── No → Are you building an MCP server?
         ├── Yes → Use MCP Elicitation (elicitInput)
         └── No → Is this an AI chat interaction?
                  ├── Yes → Does the tool need browser APIs?
                  │        ├── Yes → Use onToolCall (client-side execution)
                  │        └── No → Use needsApproval (server-side with approval)
                  └── No → Use State + WebSocket for simple confirmations
```

## Workflow-Based Approval

For durable, multi-step processes, use Cloudflare Workflows with the `waitForApproval()` helper. The workflow pauses until a human approves or rejects.

### Basic Pattern

```typescript theme={null}
import { Agent, AgentWorkflow, callable } from "agents";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents";

// Workflow that pauses for approval
export class ExpenseWorkflow extends AgentWorkflow<
  ExpenseAgent,
  ExpenseParams
> {
  async run(event: AgentWorkflowEvent<ExpenseParams>, step: AgentWorkflowStep) {
    const expense = event.payload;

    // Step 1: Validate the expense
    const validated = await step.do("validate", async () => {
      return validateExpense(expense);
    });

    // Step 2: Wait for manager approval
    await this.reportProgress({
      step: "approval",
      status: "pending",
      message: `Awaiting approval for $${expense.amount}`
    });

    // This pauses the workflow until approved/rejected
    const approval = await this.waitForApproval<{ approvedBy: string }>(step, {
      timeout: "7 days"
    });

    console.log(`Approved by: ${approval.approvedBy}`);

    // Step 3: Process the approved expense
    const result = await step.do("process", async () => {
      return processExpense(validated);
    });

    await step.reportComplete(result);
    return result;
  }
}
```

### Agent Methods for Approval

The agent provides methods to approve or reject waiting workflows:

```typescript theme={null}
export class ExpenseAgent extends Agent<Env, ExpenseState> {
  initialState: ExpenseState = {
    pendingApprovals: [],
    status: "idle"
  };

  // Approve a waiting workflow
  @callable()
  async approve(workflowId: string, approvedBy: string): Promise<void> {
    await this.approveWorkflow(workflowId, {
      reason: "Expense approved",
      metadata: { approvedBy, approvedAt: Date.now() }
    });

    // Update state to reflect approval
    this.setState({
      ...this.state,
      pendingApprovals: this.state.pendingApprovals.filter(
        (p) => p.workflowId !== workflowId
      )
    });
  }

  // Reject a waiting workflow
  @callable()
  async reject(workflowId: string, reason: string): Promise<void> {
    await this.rejectWorkflow(workflowId, { reason });

    this.setState({
      ...this.state,
      pendingApprovals: this.state.pendingApprovals.filter(
        (p) => p.workflowId !== workflowId
      )
    });
  }

  // Track workflow progress
  async onWorkflowProgress(
    workflowName: string,
    workflowId: string,
    progress: unknown
  ): Promise<void> {
    const p = progress as { step: string; status: string };

    if (p.step === "approval" && p.status === "pending") {
      // Add to pending approvals list
      this.setState({
        ...this.state,
        pendingApprovals: [
          ...this.state.pendingApprovals,
          { workflowId, requestedAt: Date.now() }
        ]
      });
    }
  }
}
```

### Timeout Handling

Set timeouts to prevent workflows from waiting indefinitely:

```typescript theme={null}
const approval = await this.waitForApproval(step, {
  timeout: "7 days" // or "1 hour", "30 minutes", etc.
});
```

If the timeout expires, the workflow continues without approval data. Handle this case:

```typescript theme={null}
const approval = await this.waitForApproval<{ approvedBy: string }>(step, {
  timeout: "24 hours"
});

if (!approval) {
  // Timeout expired - escalate or auto-reject
  await step.reportError("Approval timeout - escalating to manager");
  throw new Error("Approval timeout");
}
```

## AI Tool Approval with `needsApproval`

When building AI chat agents, you often want humans to approve certain tool calls before execution. The AI SDK's `needsApproval` option pauses tool execution until the user approves or rejects.

### Server

Define tools with `needsApproval` to require human confirmation:

```typescript theme={null}
import { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, tool, convertToModelMessages } from "ai";
import { z } from "zod";

export class MyAgent extends AIChatAgent {
  async onChatMessage() {
    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      messages: await convertToModelMessages(this.messages),
      tools: {
        // Tool with conditional approval
        processPayment: tool({
          description: "Process a payment",
          inputSchema: z.object({
            amount: z.number(),
            recipient: z.string()
          }),
          // Approval required for amounts over $100
          needsApproval: async ({ amount }) => amount > 100,
          execute: async ({ amount, recipient }) => {
            return await chargeCard(amount, recipient);
          }
        }),

        // Tool that always requires approval
        deleteAccount: tool({
          description: "Delete a user account",
          inputSchema: z.object({ userId: z.string() }),
          needsApproval: true,
          execute: async ({ userId }) => {
            return await deleteUser(userId);
          }
        }),

        // Tool that executes automatically (no approval)
        getWeather: tool({
          description: "Get weather for a city",
          inputSchema: z.object({ city: z.string() }),
          execute: async ({ city }) => fetchWeather(city)
        })
      },
      maxSteps: 5
    });

    return result.toUIMessageStreamResponse();
  }
}
```

### Client

Handle approval requests with `addToolApprovalResponse`:

```tsx theme={null}
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
import { isToolUIPart, getToolName } from "ai";

function Chat() {
  const agent = useAgent({ agent: "MyAgent" });
  const { messages, sendMessage, addToolApprovalResponse } = useAgentChat({
    agent
  });

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts?.map((part, i) => {
            if (part.type === "text") {
              return <p key={i}>{part.text}</p>;
            }

            if (isToolUIPart(part)) {
              // Tool waiting for approval
              if ("approval" in part && part.state === "approval-requested") {
                const approvalId = part.approval?.id;
                return (
                  <div key={part.toolCallId} className="approval-card">
                    <p>
                      Approve <strong>{getToolName(part)}</strong> with{" "}
                      {JSON.stringify(part.input)}?
                    </p>
                    <button
                      onClick={() =>
                        addToolApprovalResponse({
                          id: approvalId,
                          approved: true
                        })
                      }
                    >
                      Approve
                    </button>
                    <button
                      onClick={() =>
                        addToolApprovalResponse({
                          id: approvalId,
                          approved: false
                        })
                      }
                    >
                      Reject
                    </button>
                  </div>
                );
              }

              // Tool was denied
              if (part.state === "output-denied") {
                return (
                  <div key={part.toolCallId}>{getToolName(part)}: Denied</div>
                );
              }

              // Tool completed
              if (part.state === "output-available") {
                return (
                  <div key={part.toolCallId}>
                    {getToolName(part)}: {JSON.stringify(part.output)}
                  </div>
                );
              }
            }

            return null;
          })}
        </div>
      ))}
    </div>
  );
}
```

### Custom denial messages with `addToolOutput`

When a user rejects a tool, `addToolApprovalResponse({ id, approved: false })` sets the tool state to `output-denied` with a generic "Tool execution denied." message. If you need to give the LLM a more specific reason for the denial, use `addToolOutput` with `state: "output-error"` instead:

```tsx theme={null}
const { addToolOutput } = useAgentChat({ agent });

// Reject with a custom error message
addToolOutput({
  toolCallId: part.toolCallId,
  state: "output-error",
  errorText: "User declined: insufficient budget for this quarter"
});
```

This sends a `tool_result` to the LLM with your custom error text, so it can respond appropriately (e.g. suggest an alternative, ask clarifying questions). The `addToolOutput` function also works for tools in `approval-requested` or `approval-responded` states, not just `input-available`.

## Client-Side Tool Execution with `onToolCall`

For tools that need browser APIs (geolocation, camera, clipboard) or user interaction, define the tool on the server without an `execute` function and handle it on the client with `onToolCall`:

### Server

```typescript theme={null}
export class MyAgent extends AIChatAgent {
  async onChatMessage() {
    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      messages: await convertToModelMessages(this.messages),
      tools: {
        // No execute function - client handles via onToolCall
        getUserLocation: tool({
          description: "Get the user's current location from their browser",
          inputSchema: z.object({})
        })
      },
      maxSteps: 3
    });

    return result.toUIMessageStreamResponse();
  }
}
```

### Client

```tsx theme={null}
const { messages, sendMessage } = useAgentChat({
  agent,
  onToolCall: async ({ toolCall, addToolOutput }) => {
    if (toolCall.toolName === "getUserLocation") {
      const position = await new Promise((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(resolve, reject);
      });
      addToolOutput({
        toolCallId: toolCall.toolCallId,
        output: {
          lat: position.coords.latitude,
          lng: position.coords.longitude
        }
      });
    }
  }
});
```

The server receives the tool output via `CF_AGENT_TOOL_RESULT` and can auto-continue the conversation (with `maxSteps > 1`), letting the LLM respond to the location data in the same turn.

## State Patterns for Approvals

Track pending approvals in agent state for UI rendering and persistence:

```typescript theme={null}
type PendingApproval = {
  id: string;
  workflowId?: string;
  type: "expense" | "publish" | "delete";
  description: string;
  amount?: number;
  requestedBy: string;
  requestedAt: number;
  expiresAt?: number;
};

type ApprovalRecord = {
  id: string;
  approvalId: string;
  decision: "approved" | "rejected";
  decidedBy: string;
  decidedAt: number;
  reason?: string;
};

type ApprovalState = {
  pending: PendingApproval[];
  history: ApprovalRecord[];
};
```

### Multi-Approver Patterns

For sensitive operations requiring multiple approvers:

```typescript theme={null}
type MultiApproval = {
  id: string;
  requiredApprovals: number;  // e.g., 2
  currentApprovals: Array<{
    userId: string;
    approvedAt: number;
  }>;
  rejections: Array<{
    userId: string;
    rejectedAt: number;
    reason: string;
  }>;
};

@callable()
async approveMulti(approvalId: string, userId: string): Promise<boolean> {
  const approval = this.state.pending.find(p => p.id === approvalId);
  if (!approval) throw new Error("Approval not found");

  // Add this user's approval
  approval.currentApprovals.push({ userId, approvedAt: Date.now() });

  // Check if we have enough approvals
  if (approval.currentApprovals.length >= approval.requiredApprovals) {
    // Execute the approved action
    await this.executeApprovedAction(approval);
    return true;
  }

  this.setState({ ...this.state });
  return false; // Still waiting for more approvals
}
```

## Timeouts and Escalation

### Setting Approval Timeouts

```typescript theme={null}
const approval = await this.waitForApproval(step, {
  timeout: "24 hours"
});
```

### Escalation with Scheduling

Use `schedule()` to set up escalation reminders:

```typescript theme={null}
@callable()
async submitForApproval(request: ApprovalRequest): Promise<string> {
  const approvalId = crypto.randomUUID();

  // Add to pending
  this.setState({
    ...this.state,
    pending: [...this.state.pending, { id: approvalId, ...request }]
  });

  // Schedule reminder after 4 hours
  await this.schedule(
    Date.now() + 4 * 60 * 60 * 1000,
    "sendReminder",
    { approvalId }
  );

  // Schedule escalation after 24 hours
  await this.schedule(
    Date.now() + 24 * 60 * 60 * 1000,
    "escalateApproval",
    { approvalId }
  );

  return approvalId;
}
```
