diff --git a/src/content/docs/agents/api-reference/run-workflows.mdx b/src/content/docs/agents/api-reference/run-workflows.mdx index 3e022a5c372..72512fe241b 100644 --- a/src/content/docs/agents/api-reference/run-workflows.mdx +++ b/src/content/docs/agents/api-reference/run-workflows.mdx @@ -1,5 +1,5 @@ --- -title: Run Workflows +title: Workflows Integration pcx_content_type: concept sidebar: order: 5 @@ -8,101 +8,961 @@ sidebar: import { MetaInfo, Render, Type, TypeScriptExample, WranglerConfig } from "~/components"; -Agents can trigger asynchronous [Workflows](/workflows/), allowing your Agent to run complex, multi-step tasks in the background. This can include post-processing files that a user has uploaded, updating the embeddings in a [vector database](/vectorize/), and/or managing long-running user-lifecycle email or SMS notification workflows. +Integrate [Cloudflare Workflows](/workflows/) with Agents for durable, multi-step background processing while Agents handle real-time communication. -Because an Agent is just like a Worker script, it can create Workflows defined in the same project (script) as the Agent _or_ in a different project. +## Introduction -:::note[Agents vs. Workflows] +### What are Cloudflare Workflows? -Agents and Workflows have some similarities: they can both run tasks asynchronously. For straightforward tasks that are linear or need to run to completion, a Workflow can be ideal: steps can be retried, they can be cancelled, and can act on events. +Cloudflare Workflows provide durable, multi-step execution that survives failures, retries automatically, and can pause to wait for external events. They are ideal for: -Agents do not have to run to completion: they can loop, branch and run forever, and they can also interact directly with users (over HTTP or WebSockets). An Agent can be used to trigger multiple Workflows as it runs, and can thus be used to co-ordinate and manage Workflows to achieve its goals. +- Long-running background tasks (data processing, report generation) +- Multi-step pipelines with retry logic +- Human-in-the-loop approval flows +- Tasks that should not block user requests -::: +### Why Integrate with Agents? -## Trigger a Workflow +Agents excel at real-time communication and state management, while Workflows excel at durable execution. Together they provide: -An Agent can trigger one or more Workflows from within any method, whether from an incoming HTTP request, a WebSocket connection, on a delay or schedule, and/or from any other action the Agent takes. +| Feature | Agent | Workflow | Combined | +| ---------------------- | ------- | -------- | ---------------- | +| Real-time WebSocket | Yes | No | Agent handles | +| Long-running tasks | Limited | Yes | Workflow handles | +| State persistence | Yes | Yes | Both | +| Automatic retries | No | Yes | Workflow handles | +| External event waiting | No | Yes | Workflow handles | -Triggering a Workflow from an Agent is no different from [triggering a Workflow from a Worker script](/workflows/build/trigger-workflows/): +### When to Use What - +| Use Case | Recommendation | +| ----------------------------- | ------------------------------ | +| Chat or messaging | Agent only | +| Quick API calls | Agent only | +| Background processing (< 30s) | Agent `queue()` | +| Long-running tasks (> 30s) | Agent + Workflow | +| Multi-step pipelines | Workflow | +| Human approval flows | Agent + Workflow | +| Scheduled tasks | Agent `schedule()` or Workflow | + +## Quick Start + +### 1. Define Your Workflow + +Create a Workflow that extends `AgentWorkflow` to get typed access to the originating Agent: + + ```ts -interface Env { - MY_WORKFLOW: Workflow; - MyAgent: DurableObjectNamespace; -} +import { AgentWorkflow } from "agents"; +import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents"; +import type { MyAgent } from "../agent"; + +type TaskParams = { + taskId: string; + data: string; +}; + +export class ProcessingWorkflow extends AgentWorkflow { + async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { + const params = event.payload; + + // Step 1: Process data + const result = await step.do("process-data", async () => { + // Durable step - will retry on failure + return processData(params.data); + }); + + // Report progress to Agent (non-durable, lightweight) + await this.reportProgress({ + step: "process", + status: "complete", + percent: 0.5 + }); -export class MyAgent extends Agent { - async onRequest(request: Request) { - let userId = request.headers.get("user-id"); - // Trigger a schedule that runs a Workflow - // Pass it a payload - let { taskId } = await this.schedule(300, "runWorkflow", { id: userId, flight: "DL264", date: "2025-02-23" }); - } + // Step 2: Save results + await step.do("save-results", async () => { + // Call Agent method via RPC + await this.agent.saveResult(params.taskId, result); + }); - async runWorkflow(data) { - let instance = await env.MY_WORKFLOW.create({ - id: data.id, - params: data, - }) + // Broadcast to connected clients (non-durable) + this.broadcastToClients({ + type: "task-complete", + taskId: params.taskId + }); - // Schedule another task that checks the Workflow status every 5 minutes... - await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id }); - } + // Report completion (durable via step) + await step.reportComplete(result); + + return result; + } } +``` + + + +### 2. Start Workflow from Agent + +Use `runWorkflow()` to start a workflow with automatic tracking: + + + +```ts +import { Agent } from "agents"; + +export class MyAgent extends Agent { + async startTask(taskId: string, data: string) { + // Start workflow - automatically tracked in Agent database + const instanceId = await this.runWorkflow("PROCESSING_WORKFLOW", { + taskId, + data + }); + + return { instanceId }; + } -export class MyWorkflow extends WorkflowEntrypoint { - async run(event: WorkflowEvent, step: WorkflowStep) { - // Your Workflow code here - } + // Called when workflow reports progress (progress is typed object) + async onWorkflowProgress( + workflowName: string, + instanceId: string, + progress: unknown + ) { + // Cast to your progress type + const p = progress as { step?: string; status?: string; percent?: number }; + console.log( + `Workflow ${workflowName}/${instanceId}: ${p.step} - ${p.status} (${(p.percent ?? 0) * 100}%)` + ); + + // Broadcast to connected clients + this.broadcast( + JSON.stringify({ + type: "workflow-progress", + workflowName, + instanceId, + progress + }) + ); + } + + // Called when workflow completes + async onWorkflowComplete( + workflowName: string, + instanceId: string, + result?: unknown + ) { + console.log(`Workflow ${workflowName}/${instanceId} completed:`, result); + } + + // Method called by workflow via RPC + async saveResult(taskId: string, result: unknown) { + this + .sql`INSERT INTO results (task_id, data) VALUES (${taskId}, ${JSON.stringify(result)})`; + } } ``` -You'll also need to make sure your Agent [has a binding to your Workflow](/workflows/build/trigger-workflows/#workers-api-bindings) so that it can call it: +### 3. Configure Wrangler ```jsonc { - // ... - // Create a binding between your Agent and your Workflow - "workflows": [ - { - // Required: - "name": "EMAIL_WORKFLOW", - "class_name": "MyWorkflow", - // Optional: set the script_name field if your Workflow is defined in a - // different project from your Agent - "script_name": "email-workflows" - } - ], - // ... + "name": "my-app", + "main": "src/index.ts", + "compatibility_date": "2025-02-11", + "durable_objects": { + "bindings": [{ "name": "MY_AGENT", "class_name": "MyAgent" }] + }, + "workflows": [ + { + "name": "processing-workflow", + "binding": "PROCESSING_WORKFLOW", + "class_name": "ProcessingWorkflow" + } + ], + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }] } ``` -## Trigger a Workflow from another project +## API Reference -You can also call a Workflow that is defined in a different Workers script from your Agent by setting the `script_name` property in the `workflows` binding of your Agent: +### `AgentWorkflow` - +Base class for Workflows that integrate with Agents. -```jsonc -{ - // Required: - "name": "EMAIL_WORKFLOW", - "class_name": "MyWorkflow", - // Optional: set the script_name field if your Workflow is defined in a - // different project from your Agent - "script_name": "email-workflows" +**Type Parameters:** + +- `AgentType` - The Agent class type (for typed RPC) +- `Params` - User params passed to the workflow (optional) +- `ProgressType` - Type for progress reporting (defaults to `DefaultProgress`) +- `Env` - Environment type (defaults to `Cloudflare.Env`) + +**Properties:** + +- `agent` - Typed stub for calling Agent methods via RPC +- `instanceId` - The workflow instance ID +- `workflowName` - The workflow binding name +- `env` - Environment bindings + +**Methods on `this` (non-durable, may repeat on retry):** + +| Method | Description | +| ------------------------------ | --------------------------------------------- | +| `reportProgress(progress)` | Report typed progress object to the Agent | +| `broadcastToClients(message)` | Broadcast message to all WebSocket clients | +| `waitForApproval(step, opts?)` | Wait for approval event (throws on rejection) | + +**Methods on `step` (durable, idempotent, will not repeat on retry):** + +| Method | Description | +| ------------------------------- | ---------------------------------------------- | +| `step.reportComplete(result?)` | Report successful completion | +| `step.reportError(error)` | Report an error | +| `step.sendEvent(event)` | Send a custom event to the Agent | +| `step.updateAgentState(state)` | Replace Agent state (broadcasts to clients) | +| `step.mergeAgentState(partial)` | Merge into Agent state (broadcasts to clients) | +| `step.resetAgentState()` | Reset Agent state to initialState | + +**DefaultProgress Type:** + + + +```ts +type DefaultProgress = { + step?: string; + status?: "pending" | "running" | "complete" | "error"; + message?: string; + percent?: number; + [key: string]: unknown; // extensible +}; +``` + + + +### Agent Workflow Methods + +Methods added to the `Agent` class: + +#### `runWorkflow(workflowName, params, options?)` + +Start a workflow and track it in the Agent database. + + + +```ts +const instanceId = await this.runWorkflow( + "MY_WORKFLOW", + { taskId: "123", data: "process this" }, + { + id: "custom-id", // optional - auto-generated if not provided + metadata: { userId: "user-456", priority: "high" }, // optional - for querying + agentBinding: "MyAgent" // optional - auto-detected from class name if not provided + } +); +``` + + + +**Parameters:** + +- `workflowName` - Workflow binding name from `env` +- `params` - Params to pass to the workflow +- `options.id` - Custom workflow ID (auto-generated if not provided) +- `options.metadata` - Optional metadata stored for querying (not passed to workflow) +- `options.agentBinding` - Agent binding name (auto-detected from class name if not provided) + +**Returns:** Workflow instance ID + +#### `sendWorkflowEvent(workflowName, instanceId, event)` + +Send an event to a running workflow. + + + +```ts +await this.sendWorkflowEvent("MY_WORKFLOW", instanceId, { + type: "approval", + payload: { approved: true } +}); +``` + + + +#### `getWorkflowStatus(workflowName, instanceId)` + +Get the status of a workflow and update tracking record. + + + +```ts +const status = await this.getWorkflowStatus("MY_WORKFLOW", instanceId); +// status: { status: 'running', output: null, error: null } +``` + + + +#### `getWorkflow(instanceId)` + +Get a tracked workflow by ID. + + + +```ts +const workflow = this.getWorkflow(instanceId); +// { instanceId, workflowName, status, metadata, error, createdAt, ... } +``` + + + +#### `getWorkflows(criteria?)` + +Query tracked workflows with cursor-based pagination. Returns a `WorkflowPage` with workflows, total count, and cursor for the next page. + + + +```ts +// Get running workflows (default limit is 50, max is 100) +const { workflows, total } = this.getWorkflows({ status: "running" }); + +// Get workflows by binding name +const { workflows: processing } = this.getWorkflows({ + workflowName: "PROCESSING_WORKFLOW" +}); + +// Filter by metadata +const { workflows: userWorkflows } = this.getWorkflows({ + metadata: { userId: "user-456" } +}); + +// Pagination example +const page1 = this.getWorkflows({ + status: ["complete", "errored"], + limit: 20, + orderBy: "desc" +}); + +console.log(`Showing ${page1.workflows.length} of ${page1.total} workflows`); + +// Get next page using cursor +if (page1.nextCursor) { + const page2 = this.getWorkflows({ + status: ["complete", "errored"], + limit: 20, + orderBy: "desc", + cursor: page1.nextCursor + }); } ``` - + + +The `WorkflowPage` type: + + + +```ts +type WorkflowPage = { + workflows: WorkflowInfo[]; + total: number; // Total matching workflows + nextCursor: string | null; // null when no more pages +}; +``` + + + +#### `deleteWorkflow(instanceId)` + +Delete a single workflow tracking record. + + + +```ts +const deleted = this.deleteWorkflow(instanceId); +// true if deleted, false if not found +``` + + + +#### `deleteWorkflows(criteria?)` + +Delete workflow tracking records matching criteria. Useful for cleanup. + + + +```ts +// Delete all completed workflows older than 7 days +const count = this.deleteWorkflows({ + status: "complete", + createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) +}); + +// Delete all errored and terminated workflows +const count = this.deleteWorkflows({ + status: ["errored", "terminated"] +}); +``` + + + +#### `terminateWorkflow(instanceId)` + +Terminate a running workflow immediately. + + + +```ts +await this.terminateWorkflow(instanceId); +``` + + + +This stops the workflow and sets its status to `"terminated"`. Throws if the workflow is not found in the tracking table. Cloudflare will throw if the workflow is already completed, errored, or terminated. + +:::note + +`terminateWorkflow()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare. Follow [cloudflare/agents#823](https://github.com/cloudflare/agents/issues/823) for details and updates. + +::: + +#### `pauseWorkflow(instanceId)` + +Pause a running workflow. The workflow can be resumed later with `resumeWorkflow()`. + + + +```ts +await this.pauseWorkflow(instanceId); +``` + + + +Throws if the workflow is not running. Cloudflare will throw if the workflow is already paused, completed, errored, or terminated. + +:::note + +`pauseWorkflow()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare. Follow [cloudflare/agents#823](https://github.com/cloudflare/agents/issues/823) for details and updates. + +::: + +#### `resumeWorkflow(instanceId)` + +Resume a paused workflow. + + + +```ts +await this.resumeWorkflow(instanceId); +``` + + + +Throws if the workflow is not paused. Cloudflare will throw if the workflow is already running, completed, errored, or terminated. + +:::note + +`resumeWorkflow()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare. Follow [cloudflare/agents#823](https://github.com/cloudflare/agents/issues/823) for details and updates. + +::: + +#### `restartWorkflow(instanceId, options?)` + +Restart a workflow instance from the beginning with the same ID. + + + +```ts +// Reset tracking (default) - clears timestamps and error fields +await this.restartWorkflow(instanceId); + +// Preserve original timestamps +await this.restartWorkflow(instanceId, { resetTracking: false }); +``` + + + +This is useful for re-running failed workflows or retrying from scratch. The `resetTracking` option (default: `true`) controls whether to reset the `created_at` timestamp and clear error fields. + +:::note + +`restartWorkflow()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare. Follow [cloudflare/agents#823](https://github.com/cloudflare/agents/issues/823) for details and updates. + +::: + +### Lifecycle Callbacks + +Override these methods in your Agent to handle workflow events: + + + +```ts +class MyAgent extends Agent { + // Called when workflow reports progress (progress is typed object) + async onWorkflowProgress( + workflowName: string, + instanceId: string, + progress: unknown + ) { + // Cast to your progress type + const p = progress as { step?: string; percent?: number }; + } + + // Called when workflow completes successfully + async onWorkflowComplete( + workflowName: string, + instanceId: string, + result?: unknown + ) {} + + // Called when workflow encounters an error + async onWorkflowError( + workflowName: string, + instanceId: string, + error: string + ) {} + + // Called when workflow sends a custom event + async onWorkflowEvent( + workflowName: string, + instanceId: string, + event: unknown + ) {} + + // Handle all callbacks in one place (alternative) + async onWorkflowCallback(callback: WorkflowCallback) { + // Called for all callback types - callback includes workflowName + } +} +``` + + + +### Approval Methods + +Convenience methods for human-in-the-loop approval flows: + + + +```ts +class MyAgent extends Agent { + // Approve a waiting workflow + async handleApproval(instanceId: string, userId: string) { + await this.approveWorkflow(instanceId, { + reason: "Approved by admin", + metadata: { approvedBy: userId } + }); + } + + // Reject a waiting workflow + async handleRejection(instanceId: string, reason: string) { + await this.rejectWorkflow(instanceId, { reason }); + } +} +``` + + + +## Workflow Tracking + +Workflows started with `runWorkflow()` are automatically tracked in the Agent SQLite database. + +### `cf_agents_workflows` Table + +| Column | Type | Description | +| --------------- | ------- | ------------------------------- | +| `id` | TEXT | Internal row ID | +| `workflow_id` | TEXT | Cloudflare workflow instance ID | +| `workflow_name` | TEXT | Workflow binding name | +| `status` | TEXT | Current status | +| `metadata` | TEXT | JSON metadata (for querying) | +| `error_name` | TEXT | Error name (if failed) | +| `error_message` | TEXT | Error message (if failed) | +| `created_at` | INTEGER | Unix timestamp | +| `updated_at` | INTEGER | Unix timestamp | +| `completed_at` | INTEGER | Unix timestamp (when done) | + +:::note + +Workflow params and output are not stored by default. Use `metadata` to store queryable information, and store large payloads in your own tables if needed. + +::: + +### Workflow Status Values + +- `queued` - Waiting to start +- `running` - Currently executing +- `paused` - Paused by user +- `waiting` - Waiting for event +- `complete` - Finished successfully +- `errored` - Failed with error +- `terminated` - Manually terminated + +## Patterns + +### Background Processing with Progress + + + +```ts +// Workflow with default progress type +export class DataProcessingWorkflow extends AgentWorkflow< + MyAgent, + ProcessParams +> { + async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { + const params = event.payload; + const items = params.items; + + for (let i = 0; i < items.length; i++) { + await step.do(`process-${i}`, async () => { + await processItem(items[i]); + }); + + // Report progress after each item (non-durable, lightweight) + await this.reportProgress({ + step: `process-${i}`, + status: "complete", + percent: (i + 1) / items.length, + message: `Processed ${i + 1}/${items.length}` + }); + } + + await step.reportComplete({ processed: items.length }); + } +} + +// Agent +class MyAgent extends Agent { + async onWorkflowProgress( + workflowName: string, + instanceId: string, + progress: unknown + ) { + // Broadcast progress to all connected clients + this.broadcast( + JSON.stringify({ + type: "processing-progress", + workflowName, + instanceId, + progress + }) + ); + } +} +``` + + + +### Human-in-the-Loop Approval + + + +```ts +// Workflow using the built-in waitForApproval helper +export class ApprovalWorkflow extends AgentWorkflow { + async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { + const params = event.payload; + + // Prepare request + const request = await step.do("prepare", async () => { + return { ...params, preparedAt: Date.now() }; + }); + + // Wait for approval (throws WorkflowRejectedError if rejected) + await this.reportProgress({ + step: "approval", + status: "pending", + percent: 0.5, + message: "Awaiting approval" + }); + const approvalData = await this.waitForApproval<{ approvedBy: string }>( + step, + { timeout: "7 days" } + ); + + console.log("Approved by:", approvalData?.approvedBy); + + // Execute approved action + const result = await step.do("execute", async () => { + return executeRequest(request); + }); + + await step.reportComplete(result); + return result; + } +} + +// Agent using the built-in approval methods +class MyAgent extends Agent { + // Approve a waiting workflow + async handleApproval(instanceId: string, userId: string) { + await this.approveWorkflow(instanceId, { + reason: "Approved by admin", + metadata: { approvedBy: userId } + }); + } + + // Reject a waiting workflow + async handleRejection(instanceId: string, reason: string) { + await this.rejectWorkflow(instanceId, { reason }); + } +} +``` + + + +### Durable Task Queue with Retries + + + +```ts +// Workflow with built-in retry logic +export class ResilientTaskWorkflow extends AgentWorkflow { + async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { + const params = event.payload; + + const result = await step.do( + "call-external-api", + { + retries: { + limit: 5, + delay: "10 seconds", + backoff: "exponential" + }, + timeout: "5 minutes" + }, + async () => { + const response = await fetch("https://api.example.com/process", { + method: "POST", + body: JSON.stringify(params) + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); + } + ); + + await step.reportComplete(result); + return result; + } +} +``` + + + +### State Synchronization + +Workflows can update the Agent state directly (durably via step), which automatically broadcasts to all connected clients: + + + +```ts +// Workflow that syncs state to Agent +export class ProcessingWorkflow extends AgentWorkflow { + async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { + const params = event.payload; + + // Update Agent state (durable, replaces entire state, broadcasts to clients) + await step.updateAgentState({ + currentTask: { + id: params.taskId, + status: "processing", + startedAt: Date.now() + } + }); + + const result = await step.do("process", async () => { + return processTask(params); + }); + + // Merge partial state (durable, keeps existing fields, broadcasts to clients) + await step.mergeAgentState({ + currentTask: { + status: "complete", + result, + completedAt: Date.now() + } + }); + + await step.reportComplete(result); + return result; + } +} +``` + + + +### Custom Progress Types + +Define custom progress types for domain-specific reporting: + + + +```ts +// Custom progress type for data pipeline +type PipelineProgress = { + stage: "extract" | "transform" | "load"; + recordsProcessed: number; + totalRecords: number; + currentTable?: string; +}; + +// Workflow with custom progress type (3rd type parameter) +export class ETLWorkflow extends AgentWorkflow< + MyAgent, + ETLParams, + PipelineProgress +> { + async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { + const params = event.payload; + + // Report typed progress (non-durable, lightweight for frequent updates) + await this.reportProgress({ + stage: "extract", + recordsProcessed: 0, + totalRecords: 1000, + currentTable: "users" + }); + + // ... processing + } +} + +// Agent receives typed progress +class MyAgent extends Agent { + async onWorkflowProgress( + workflowName: string, + instanceId: string, + progress: unknown + ) { + const p = progress as PipelineProgress; + console.log(`Stage: ${p.stage}, ${p.recordsProcessed}/${p.totalRecords}`); + } +} +``` + + + +## Bidirectional Communication + +### Workflow to Agent + + + +```ts +// Direct RPC call (typed) +await this.agent.updateTaskStatus(taskId, "processing"); +const data = await this.agent.getData(taskId); + +// Non-durable callbacks (may repeat on retry, use for frequent updates) +await this.reportProgress({ + step: "process", + percent: 0.5, + message: "Halfway done" +}); +this.broadcastToClients({ type: "update", data }); + +// Durable callbacks via step (idempotent, will not repeat on retry) +await step.reportComplete(result); +await step.reportError("Something went wrong"); +await step.sendEvent({ type: "custom", data: {} }); + +// Durable state synchronization via step (broadcasts to clients) +await step.updateAgentState({ status: "processing" }); +await step.mergeAgentState({ progress: 0.5 }); +``` + + + +### Agent to Workflow + + + +```ts +// Send event to waiting workflow (generic) +await this.sendWorkflowEvent("MY_WORKFLOW", instanceId, { + type: "custom-event", + payload: { action: "proceed" } +}); + +// Approve or reject workflows using convenience methods +await this.approveWorkflow(instanceId, { + reason: "Approved by admin", + metadata: { approvedBy: userId } +}); + +await this.rejectWorkflow(instanceId, { + reason: "Request denied" +}); + +// The workflow waits for approval with: +const approvalData = await this.waitForApproval(step, { timeout: "7 days" }); +``` + + + +## Best Practices + +1. **Keep workflows focused** - One workflow per logical task +2. **Use meaningful step names** - Helps with debugging and observability +3. **Report progress regularly** - Keeps users informed +4. **Handle errors gracefully** - Use `reportError()` before throwing +5. **Clean up completed workflows** - The `cf_agents_workflows` table can grow unbounded, so implement a retention policy: + + + +```ts +// Option 1: Cleanup immediately on completion +async onWorkflowComplete(workflowName, instanceId, result) { + // Process result first, then delete + this.deleteWorkflow(instanceId); +} + +// Option 2: Scheduled cleanup (keep recent history) +// Call this periodically via a scheduled task or cron +this.deleteWorkflows({ + status: ["complete", "errored"], + createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days +}); + +// Option 3: Keep all history for compliance or auditing +// Do not call deleteWorkflows() - query historical data as needed +``` + + + +6. **Handle workflow binding renames carefully** - If you rename a workflow binding in `wrangler.jsonc`, existing tracked workflows will reference the old name. The agent will warn on startup if it detects this. Use `migrateWorkflowBinding()` to update them: + + + +```ts +// After renaming OLD_WORKFLOW to NEW_WORKFLOW in wrangler.jsonc +async onStart() { + // Migrate any existing tracked workflows to the new binding name + const migrated = this.migrateWorkflowBinding('OLD_WORKFLOW', 'NEW_WORKFLOW'); + // You can remove this code after all agents have migrated +} +``` + + + +## Limitations + +- Workflows can have at most 1,024 steps +- Maximum 10MB state per workflow +- Events wait for at most 1 year +- No direct WebSocket from workflows (use `broadcastToClients()`) +- Workflow execution time: up to 30 minutes per step + +## Related resources -Refer to the [cross-script calls](/workflows/build/workers-api/#cross-script-calls) section of the Workflows documentation for more examples. +- [Cloudflare Workflows documentation](/workflows/) +- [Workflows API](/workflows/workflows-api/) +- [Agent state management](/agents/api-reference/store-and-sync-state/)