import { useAgent } from "agents/react"; import { useState } from "react"; import { ClockIcon, CheckCircleIcon, XCircleIcon, PaperPlaneTiltIcon, TrashIcon, WarningCircleIcon, ArrowsClockwiseIcon } from "@phosphor-icons/react"; import { Button, Input, InputArea, Surface, Empty, Text } from "@cloudflare/kumo"; import { DemoWrapper } from "../../layout"; import { LogPanel, ConnectionStatus, CodeExplanation, type CodeSection } from "../../components"; import { useLogs, useUserId } from "../../hooks"; import type { ApprovalAgent, ApprovalAgentState, ApprovalRequest } from "./approval-agent"; const codeSections: CodeSection[] = [ { title: "Wait for human approval in a workflow", description: "AgentWorkflow provides a built-in waitForApproval() helper. The workflow suspends durably — it can wait for minutes, hours, or days — and resumes when the agent calls approveWorkflow() or rejectWorkflow(). If rejected, it throws a WorkflowRejectedError.", code: `import { AgentWorkflow } from "agents/workflows"; import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows"; class ApprovalWorkflow extends AgentWorkflow { async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { const request = await step.do("prepare", async () => { return { ...event.payload, preparedAt: Date.now() }; }); await this.reportProgress({ step: "approval", status: "pending", message: "Awaiting approval", }); // Suspends until approved — throws WorkflowRejectedError if rejected const approvalData = await this.waitForApproval<{ approvedBy: string }>( step, { timeout: "7 days" } ); const result = await step.do("execute", async () => { return executeRequest(request); }); await step.reportComplete(result); return result; } }` }, { title: "Approve or reject from the agent", description: "The agent has built-in approveWorkflow() and rejectWorkflow() convenience methods. Both accept optional metadata — for approvals, this data is returned to the waiting workflow.", code: `class MyAgent extends Agent { @callable() async approve(instanceId: string, userId: string) { await this.approveWorkflow(instanceId, { reason: "Approved by admin", metadata: { approvedBy: userId }, }); } @callable() async reject(instanceId: string, reason: string) { await this.rejectWorkflow(instanceId, { reason }); } }` } ]; function ApprovalCard({ request, onApprove, onReject }: { request: ApprovalRequest; onApprove: (id: string) => void; onReject: (id: string, reason?: string) => void; }) { const [rejectReason, setRejectReason] = useState(""); const [showRejectForm, setShowRejectForm] = useState(false); const statusIcons = { pending: , approved: , rejected: }; const statusBorder = { pending: "border-l-4 border-l-kumo-warning", approved: "border-l-4 border-l-green-500", rejected: "border-l-4 border-l-kumo-danger" }; const timeAgo = (date: string) => { const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); return `${hours}h ago`; }; return (
{statusIcons[request.status]} {request.title}
{timeAgo(request.createdAt)}

{request.description}

{request.status === "pending" && (
{!showRejectForm ? (
) : (
) => setRejectReason(e.target.value) } placeholder="Reason for rejection (optional)" className="w-full" />
)}
)} {request.status !== "pending" && request.resolvedAt && (
{request.status === "approved" ? "Approved" : "Rejected"} at{" "} {new Date(request.resolvedAt).toLocaleTimeString()}
{request.reason &&
Reason: {request.reason}
}
)}
); } export function WorkflowApprovalDemo() { const userId = useUserId(); const { logs, addLog, clearLogs } = useLogs(); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [requests, setRequests] = useState([]); const agent = useAgent({ agent: "approval-agent", name: `workflow-approval-${userId}`, onStateUpdate: () => { refreshRequests(); }, onOpen: () => { addLog("info", "connected"); refreshRequests(); }, onClose: () => addLog("info", "disconnected"), onError: () => addLog("error", "error", "Connection error"), onMessage: (message) => { try { const data = JSON.parse(message.data as string); if (data.type) { addLog("in", data.type, data); if (data.type.startsWith("approval_")) { refreshRequests(); } } } catch { // ignore } } }); const refreshRequests = async () => { try { const list = await ( agent.call as (m: string) => Promise )("listRequests"); setRequests(list); } catch { // ignore - might not be connected yet } }; const handleSubmitRequest = async () => { if (!title.trim() || !description.trim()) return; setIsSubmitting(true); addLog("out", "requestApproval", { title, description }); try { await agent.call("requestApproval", [title, description]); setTitle(""); setDescription(""); await refreshRequests(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } finally { setIsSubmitting(false); } }; const handleApprove = async (requestId: string) => { addLog("out", "approve", { requestId }); try { await agent.call("approve", [requestId]); await refreshRequests(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const handleReject = async (requestId: string, reason?: string) => { addLog("out", "reject", { requestId, reason }); try { await agent.call("reject", [requestId, reason]); await refreshRequests(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const handleClearApprovals = async () => { addLog("out", "clearApprovals"); try { const result = await agent.call("clearApprovals"); addLog("in", "cleared", { count: result }); await refreshRequests(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const pendingRequests = requests.filter((r) => r.status === "pending"); const resolvedRequests = requests.filter((r) => r.status !== "pending"); const presetRequests = [ { title: "Deploy to Production", description: "Release v2.3.0 with new features" }, { title: "Access Request - Admin Panel", description: "Need admin access for debugging" }, { title: "Expense Report - $450", description: "Team offsite dinner and supplies" } ]; return ( Workflows can pause and wait for human input using{" "} waitForApproval() . The workflow suspends durably — it can wait for minutes, hours, or days — and resumes when someone calls{" "} approveWorkflow() {" "} or{" "} rejectWorkflow() . Submit a request below and then approve or reject it. } statusIndicator={ } >
{/* Left Panel - Create Request */}
Submit Request
) => setTitle(e.target.value) } className="w-full" placeholder="What needs approval?" /> setDescription(e.target.value)} className="w-full" rows={3} placeholder="Provide details..." />
{/* Quick Presets */}
Quick Presets
{presetRequests.map((preset) => ( ))}
{/* Center Panel - Approval Queue */}
{/* Pending Requests */}
Pending Approval ({pendingRequests.length})
{pendingRequests.length > 0 ? (
{pendingRequests.map((request) => ( ))}
) : ( )}
{/* History */}
History ({resolvedRequests.length}) {resolvedRequests.length > 0 && ( )}
{resolvedRequests.length > 0 ? (
{resolvedRequests.map((request) => ( ))}
) : ( )}
{/* Right Panel - Logs */}
); }