Human-in-the-loop patterns·续
Configure the user-facing handlers in onStart():
import { Agent } from "agents";
export class MyAgent extends Agent {
onStart() {
this.mcp.configureElicitationHandlers({
form: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
url: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
});
}
forwardElicitationToUser(request, serverId) {
// Present the request in your UI and resolve after the user responds.
throw new Error(
`Implement elicitation for ${serverId}: ${request.params.message}`,
);
}
}
import { Agent } from "agents";
import type { ElicitRequest, ElicitResult } from "agents/mcp";
export class MyAgent extends Agent<Env> {
onStart() {
this.mcp.configureElicitationHandlers({
form: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
url: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
});
}
private forwardElicitationToUser(
request: ElicitRequest,
serverId: string,
): Promise<ElicitResult> {
// Present the request in your UI and resolve after the user responds.
throw new Error(
`Implement elicitation for ${serverId}: ${request.params.message}`,
);
}
}
For the complete form, URL, and browser forwarding patterns, refer to MCP client elicitation. For the server-side request API, refer to McpAgent elicitation.
Durable Code Mode approvals
Use the durable Code Mode runtime for coding agents and other Agents that use the Code Mode pattern. Mark a connector method with requiresApproval: true to pause model-generated code before it invokes the underlying tool.
The following example marks a GitHub MCP tool as approval-gated, adds the connector to a durable runtime, and exposes methods that a UI can use to inspect, approve, or reject the pending action:
import { AIChatAgent } from "@cloudflare/ai-chat";
import {
createCodemodeRuntime,
DynamicWorkerExecutor,
McpConnector,
} from "@cloudflare/codemode";
import { callable } from "agents";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { model } from "./model";
class GitHubConnector extends McpConnector {
connection;
constructor(ctx, env, connection) {
super(ctx, env);
this.connection = connection;
}
name() {
return "github";
}
createConnection() {
return this.connection;
}
tool(name, tool) {
if (name === "create_issue") {
return { ...tool, requiresApproval: true };
}
return tool;
}
}
export class CodingAgent extends AIChatAgent {
runtime() {
const server = this.mcp
.listServers()
.find((item) => item.name === "GitHub");
if (!server) throw new Error("GitHub MCP server is not registered.");
const connection = this.mcp.mcpConnections[server.id];
if (!connection) throw new Error("GitHub MCP connection is unavailable.");
return createCodemodeRuntime({
ctx: this.ctx,
executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
connectors: [new GitHubConnector(this.ctx, this.env, connection)],
});
}
async onChatMessage() {
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
tools: { codemode: this.runtime().tool() },
stopWhen: stepCountIs(10),
});
return result.toUIMessageStreamResponse();
}
@callable()
async pendingApprovals() {
return this.runtime().pending();
}
@callable()
async approveExecution(executionId) {
return this.runtime().approve({ executionId });
}
@callable()
async rejectExecution(executionId, seq) {
return this.runtime().reject({ executionId, seq });
}
}
import { AIChatAgent } from "@cloudflare/ai-chat";
import {
createCodemodeRuntime,
DynamicWorkerExecutor,
McpConnector,
type CodemodeRuntimeHandle,
type ConnectorTool,
type McpConnectionLike,
type PendingAction,
} from "@cloudflare/codemode";
import { callable } from "agents";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { model } from "./model";
class GitHubConnector extends McpConnector<Env> {
private connection: McpConnectionLike;
constructor(
ctx: DurableObjectState,
env: Env,
connection: McpConnectionLike,
) {
super(ctx, env);
this.connection = connection;
}
override name() {
return "github";
}
protected override createConnection() {
return this.connection;
}
protected override tool(name: string, tool: ConnectorTool): ConnectorTool {
if (name === "create_issue") {
return { ...tool, requiresApproval: true };
}
return tool;
}
}
export class CodingAgent extends AIChatAgent<Env> {
private runtime(): CodemodeRuntimeHandle {
const server = this.mcp
.listServers()
.find((item) => item.name === "GitHub");
if (!server) throw new Error("GitHub MCP server is not registered.");
const connection = this.mcp.mcpConnections[server.id];
if (!connection) throw new Error("GitHub MCP connection is unavailable.");
return createCodemodeRuntime({
ctx: this.ctx,
executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
connectors: [new GitHubConnector(this.ctx, this.env, connection)],
});
}
async onChatMessage() {
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
tools: { codemode: this.runtime().tool() },
stopWhen: stepCountIs(10),
});
return result.toUIMessageStreamResponse();
}
@callable()
async pendingApprovals(): Promise<PendingAction[]> {
return this.runtime().pending();
}
@callable()
async approveExecution(executionId: string) {
return this.runtime().approve({ executionId });
}
@callable()
async rejectExecution(executionId: string, seq: number): Promise<boolean> {
return this.runtime().reject({ executionId, seq });
}
}
When generated code calls github.create_issue(), the runtime records the pending method and arguments, then pauses before the MCP tool executes. Approval starts another pass with the same source code and execution ID. Previously completed calls replay from the durable log, the approved call executes, and the generated code continues.
Pending approvals and execution history survive request completion and Durable Object hibernation. For the execution and replay model, refer to Approvals through abort and replay.
Build Workflow approval UIs
Pending approvals list
Use the agent's state to display pending approvals in your UI:
import { useAgent } from "agents/react";
function PendingApprovals() {
const { state, agent } = useAgent({
agent: "expense-agent",
name: "main",
});
if (!state?.pendingApprovals?.length) {
return <p>No pending approvals</p>;
}
return (
<div className="approval-list">
{state.pendingApprovals.map((item) => (
<div key={item.workflowId} className="approval-card">
<h3>${item.amount}</h3>
<p>{item.description}</p>
<p>Requested by {item.requestedBy}</p>
<div className="actions">
<button
onClick={() => agent.stub.approve(item.workflowId, "admin")}
>
Approve
</button>
<button
onClick={() => agent.stub.reject(item.workflowId, "Declined")}
>
Reject
</button>
</div>
</div>
))}
</div>
);
}
Add multiple Workflow approvers
For sensitive operations requiring multiple approvers:
import { Agent, callable } from "agents";
class MultiApprovalAgent extends Agent {
@callable()
async approveMulti(workflowId, userId) {
const approval = this.state.pendingMultiApprovals.find(
(p) => p.workflowId === workflowId,
);
if (!approval) throw new Error("Approval not found");
// Check if user already approved
if (approval.currentApprovals.some((a) => a.userId === userId)) {
throw new Error("Already approved by this user");
}
// 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.approveWorkflow(workflowId, {
metadata: { approvers: approval.currentApprovals },
});
return true;
}
this.setState({ ...this.state });
return false; // Still waiting for more approvals
}
}
import { Agent, callable } from "agents";
type MultiApproval = {
workflowId: string;
requiredApprovals: number;
currentApprovals: Array<{ userId: string; approvedAt: number }>;
rejections: Array<{ userId: string; rejectedAt: number; reason: string }>;
};
type State = {
pendingMultiApprovals: MultiApproval[];
};
class MultiApprovalAgent extends Agent<Env, State> {
@callable()
async approveMulti(workflowId: string, userId: string): Promise<boolean> {
const approval = this.state.pendingMultiApprovals.find(
(p) => p.workflowId === workflowId,
);
if (!approval) throw new Error("Approval not found");
// Check if user already approved
if (approval.currentApprovals.some((a) => a.userId === userId)) {
throw new Error("Already approved by this user");
}
// 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.approveWorkflow(workflowId, {
metadata: { approvers: approval.currentApprovals },
});
return true;
}
this.setState({ ...this.state });
return false; // Still waiting for more approvals
}
}
Best practices
- Define clear approval criteria — Only require confirmation for actions with meaningful consequences (payments, emails, data changes)
- Provide detailed context — Show users exactly what the action will do, including all arguments
- Implement timeouts — Use
schedule()to escalate or auto-reject after reasonable periods - Maintain audit trails — Use
this.sqlto record all approval decisions for compliance - Handle connection drops — Store pending approvals in agent state so they survive disconnections
- Graceful degradation — Provide fallback behavior if approvals are rejected
Next steps
Run Workflows
Complete waitForApproval() API reference.
MCP clients
Handle elicitation requests in an Agent acting as an MCP client.
MCP servers
Request form or URL elicitation from an MCP server.
Durable Code Mode runtime
Pause model-generated code for approval before connector calls.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/concepts/agentic-patterns/human-in-the-loop/#page","headline":"Human-in-the-loop patterns","description":"Implement human-in-the-loop functionality using Workflow approvals, durable Code Mode approvals, and MCP elicitation.","url":"https://developers.cloudflare.com/agents/concepts/agentic-patterns/human-in-the-loop/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/concepts/agentic-patterns/human-in-the-loop/og.png?v=b6b831b35a9f5372","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Build agents that persist for days, weeks, or months — surviving restarts, waking on demand, and managing work that spans far longer than any single request. title: Long-running agents image: https://developers.cloudflare.com/agents/concepts/agentic-patterns/long-running-agents/og.png?v=276b7d8e629b5bd0
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.
Docs