branch:
BasicDemo.tsx
15229 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import {
  CheckIcon,
  CircleIcon,
  XIcon,
  PlayIcon,
  TrashIcon,
  ArrowsClockwiseIcon
} from "@phosphor-icons/react";
import { Loader } from "@cloudflare/kumo";
import {
  Button,
  Input,
  Surface,
  Badge,
  Empty,
  Text,
  Meter
} from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
  LogPanel,
  ConnectionStatus,
  CodeExplanation,
  type CodeSection
} from "../../components";
import { useLogs, useUserId } from "../../hooks";
import type {
  BasicWorkflowAgent,
  BasicWorkflowState,
  WorkflowWithProgress
} from "./basic-workflow-agent";

const codeSections: CodeSection[] = [
  {
    title: "Define a workflow with AgentWorkflow",
    description:
      "Extend AgentWorkflow instead of WorkflowEntrypoint to get typed access to the originating agent. You get this.agent for RPC, this.reportProgress() for live updates, and this.broadcastToClients() to push messages over WebSocket.",
    code: `import { AgentWorkflow } from "agents/workflows";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";

class ProcessingWorkflow extends AgentWorkflow<MyAgent, TaskParams> {
  async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {
    const params = event.payload;

    const result = await step.do("process-data", async () => {
      return processData(params.data);
    });

    // Report progress back to the agent (non-durable, lightweight)
    await this.reportProgress({
      step: "process",
      status: "complete",
      percent: 0.5,
    });

    // Call agent methods via typed RPC
    await this.agent.saveResult(params.taskId, result);

    // Broadcast to all connected WebSocket clients
    this.broadcastToClients({ type: "task-complete", taskId: params.taskId });

    // Mark completion (durable via step)
    await step.reportComplete(result);
    return result;
  }
}`
  },
  {
    title: "Start and track workflows from the agent",
    description:
      "Use this.runWorkflow() to start a workflow with automatic tracking in the agent's database. Override onWorkflowProgress and onWorkflowComplete to react to workflow events and broadcast them to connected clients.",
    code: `class MyAgent extends Agent {
  @callable()
  async startTask(taskId: string, data: string) {
    const instanceId = await this.runWorkflow("PROCESSING_WORKFLOW", {
      taskId,
      data,
    });
    return { instanceId };
  }

  async onWorkflowProgress(workflowName: string, instanceId: string, progress: unknown) {
    this.broadcast(JSON.stringify({
      type: "workflow-progress",
      instanceId,
      progress,
    }));
  }

  async onWorkflowComplete(workflowName: string, instanceId: string, result?: unknown) {
    console.log("Workflow completed:", instanceId);
  }
}`
  },
  {
    title: "Durable step helpers",
    description:
      "Steps have built-in helpers for common patterns: step.reportComplete() and step.reportError() for status, step.updateAgentState() and step.mergeAgentState() to durably update the agent's state (which broadcasts to all clients), and step.sendEvent() for custom events.",
    code: `  async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
    // Durably update agent state (broadcasts to WebSocket clients)
    await step.updateAgentState({ status: "processing", startedAt: Date.now() });

    const result = await step.do("process", async () => {
      return processTask(event.payload);
    });

    // Merge partial state (keeps existing fields)
    await step.mergeAgentState({ status: "complete", result });

    // Report completion
    await step.reportComplete(result);
  }`
  }
];

function WorkflowCard({ workflow }: { workflow: WorkflowWithProgress }) {
  const name = workflow.name || workflow.workflowName;

  const statusVariant: Record<
    string,
    "beta" | "primary" | "destructive" | "outline" | "secondary"
  > = {
    queued: "beta",
    running: "primary",
    complete: "primary",
    errored: "destructive",
    waiting: "beta"
  };

  const statusIcons: Record<string, React.ReactNode> = {
    queued: <CircleIcon size={14} />,
    running: <Loader size="sm" />,
    complete: <CheckIcon size={14} />,
    errored: <XIcon size={14} />,
    waiting: <Loader size="sm" />
  };

  return (
    <Surface className="p-4 rounded-lg ring ring-kumo-line">
      <div className="flex items-center justify-between mb-3">
        <div>
          <Text bold>{name}</Text>
          <p className="text-xs text-kumo-subtle">
            ID: {workflow.workflowId.slice(0, 8)}...
          </p>
        </div>
        <Badge variant={statusVariant[workflow.status] || "outline"}>
          <span className="flex items-center gap-1">
            {statusIcons[workflow.status] || statusIcons.queued}
            {workflow.status}
          </span>
        </Badge>
      </div>

      {/* Progress Bar */}
      {workflow.progress && (
        <div className="mb-3">
          <div className="flex justify-between text-xs text-kumo-subtle mb-1">
            <span>{workflow.progress.message}</span>
            <span>
              {workflow.progress.step} / {workflow.progress.total}
            </span>
          </div>
          <Meter
            label="Progress"
            value={workflow.progress.step}
            max={workflow.progress.total}
            showValue={false}
          />
        </div>
      )}

      {/* Error */}
      {workflow.error && (
        <div className="mb-3 p-2 bg-kumo-danger-tint rounded text-sm">
          <div className="text-kumo-danger">{workflow.error.message}</div>
        </div>
      )}

      {/* Timestamps */}
      <div className="pt-3 border-t border-kumo-fill text-xs text-kumo-subtle">
        <div>Started: {new Date(workflow.createdAt).toLocaleTimeString()}</div>
        {workflow.completedAt && (
          <div>
            Completed: {new Date(workflow.completedAt).toLocaleTimeString()}
          </div>
        )}
      </div>
    </Surface>
  );
}

export function WorkflowBasicDemo() {
  const userId = useUserId();
  const { logs, addLog, clearLogs } = useLogs();
  const [workflowName, setWorkflowName] = useState("Data Processing");
  const [stepCount, setStepCount] = useState(4);
  const [isStarting, setIsStarting] = useState(false);
  const [workflows, setWorkflows] = useState<WorkflowWithProgress[]>([]);

  const agent = useAgent<BasicWorkflowAgent, BasicWorkflowState>({
    agent: "basic-workflow-agent",
    name: `workflow-basic-${userId}`,
    onStateUpdate: (newState) => {
      if (newState) {
        addLog("in", "state_update", {
          progress: Object.keys(newState.progress).length
        });
        refreshWorkflows();
      }
    },
    onOpen: () => {
      addLog("info", "connected");
      refreshWorkflows();
    },
    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("workflow_")) {
            refreshWorkflows();
          }
        }
      } catch {
        // ignore
      }
    }
  });

  const refreshWorkflows = async () => {
    try {
      const list = await (
        agent.call as (m: string) => Promise<WorkflowWithProgress[]>
      )("listWorkflows");
      setWorkflows(list);
    } catch {
      // ignore - might not be connected yet
    }
  };

  const handleStartWorkflow = async () => {
    if (!workflowName.trim()) return;

    setIsStarting(true);
    addLog("out", "startWorkflow", { name: workflowName, stepCount });

    try {
      await agent.call("startWorkflow", [workflowName, stepCount]);
      await refreshWorkflows();
    } catch (e) {
      addLog("error", "error", e instanceof Error ? e.message : String(e));
    } finally {
      setIsStarting(false);
    }
  };

  const handleClearWorkflows = async () => {
    addLog("out", "clearWorkflows");
    try {
      const result = await agent.call("clearWorkflows");
      addLog("in", "cleared", { count: result });
      await refreshWorkflows();
    } catch (e) {
      addLog("error", "error", e instanceof Error ? e.message : String(e));
    }
  };

  const activeWorkflows = workflows.filter(
    (w) =>
      w.status === "queued" || w.status === "running" || w.status === "waiting"
  );
  const completedWorkflows = workflows.filter(
    (w) =>
      w.status === "complete" ||
      w.status === "errored" ||
      w.status === "terminated"
  );

  return (
    <DemoWrapper
      title="Multi-Step Workflows"
      description={
        <>
          Extend{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            AgentWorkflow
          </code>{" "}
          to build durable multi-step workflows that integrate tightly with your
          agent. Each step runs exactly once — if the Worker crashes
          mid-execution, the workflow resumes from the last completed step. Use{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            this.runWorkflow()
          </code>{" "}
          to start a workflow with automatic tracking, and{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            this.reportProgress()
          </code>{" "}
          to stream live updates back to connected clients.
        </>
      }
      statusIndicator={
        <ConnectionStatus
          status={
            agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
          }
        />
      }
    >
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Left Panel - Controls */}
        <div className="space-y-6">
          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-4">
              <Text variant="heading3">Start Workflow</Text>
            </div>
            <div className="space-y-4">
              <Input
                label="Workflow Name"
                type="text"
                value={workflowName}
                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                  setWorkflowName(e.target.value)
                }
                className="w-full"
                placeholder="Enter workflow name"
              />
              <div>
                <label
                  htmlFor="step-count"
                  className="text-xs text-kumo-subtle block mb-1"
                >
                  Number of Steps: {stepCount}
                </label>
                <input
                  id="step-count"
                  type="range"
                  min={2}
                  max={6}
                  value={stepCount}
                  onChange={(e) => setStepCount(Number(e.target.value))}
                  className="w-full"
                />
                <div className="flex justify-between text-xs text-kumo-inactive mt-1">
                  <span>2</span>
                  <span>6</span>
                </div>
              </div>
              <Button
                variant="primary"
                onClick={handleStartWorkflow}
                disabled={isStarting || !workflowName.trim()}
                className="w-full"
                icon={<PlayIcon size={16} />}
              >
                {isStarting ? "Starting..." : "Start Workflow"}
              </Button>
            </div>
          </Surface>

          <Surface className="p-4 rounded-lg bg-kumo-elevated">
            <div className="mb-2">
              <Text variant="heading3">How it Works</Text>
            </div>
            <ul className="text-sm text-kumo-subtle space-y-1">
              <li>
                1.{" "}
                <code className="text-xs bg-kumo-control px-1 rounded text-kumo-default">
                  runWorkflow()
                </code>{" "}
                starts a durable workflow
              </li>
              <li>
                2. Workflow executes steps with{" "}
                <code className="text-xs bg-kumo-control px-1 rounded text-kumo-default">
                  step.do()
                </code>
              </li>
              <li>
                3.{" "}
                <code className="text-xs bg-kumo-control px-1 rounded text-kumo-default">
                  getWorkflows()
                </code>{" "}
                tracks all workflows
              </li>
              <li>
                4. Progress via{" "}
                <code className="text-xs bg-kumo-control px-1 rounded text-kumo-default">
                  onWorkflowProgress()
                </code>
              </li>
            </ul>
          </Surface>
        </div>

        {/* Center Panel - Workflows */}
        <div className="space-y-6">
          {/* Active Workflows */}
          <div>
            <div className="flex items-center justify-between mb-3">
              <Text variant="heading3">Active ({activeWorkflows.length})</Text>
              <Button
                variant="ghost"
                size="xs"
                onClick={refreshWorkflows}
                icon={<ArrowsClockwiseIcon size={12} />}
              >
                Refresh
              </Button>
            </div>
            {activeWorkflows.length > 0 ? (
              <div className="space-y-3">
                {activeWorkflows.map((workflow) => (
                  <WorkflowCard key={workflow.workflowId} workflow={workflow} />
                ))}
              </div>
            ) : (
              <Surface className="p-6 rounded-lg ring ring-kumo-line">
                <Empty title="No active workflows" size="sm" />
              </Surface>
            )}
          </div>

          {/* Completed Workflows */}
          <div>
            <div className="flex items-center justify-between mb-3">
              <Text variant="heading3">
                History ({completedWorkflows.length})
              </Text>
              {completedWorkflows.length > 0 && (
                <Button
                  variant="ghost"
                  size="xs"
                  onClick={handleClearWorkflows}
                  icon={<TrashIcon size={12} />}
                  className="text-kumo-danger"
                >
                  Clear
                </Button>
              )}
            </div>
            {completedWorkflows.length > 0 ? (
              <div className="space-y-3 max-h-64 overflow-y-auto">
                {completedWorkflows.map((workflow) => (
                  <WorkflowCard key={workflow.workflowId} workflow={workflow} />
                ))}
              </div>
            ) : (
              <Surface className="p-6 rounded-lg ring ring-kumo-line">
                <Empty title="No completed workflows" size="sm" />
              </Surface>
            )}
          </div>
        </div>

        {/* Right Panel - Logs */}
        <div className="space-y-6">
          <LogPanel logs={logs} onClear={clearLogs} maxHeight="500px" />
        </div>
      </div>

      <CodeExplanation sections={codeSections} />
    </DemoWrapper>
  );
}