
Cloudflare Workflows
- 136 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Helps with automation & workflows tasks during AI-assisted development.
About
cloudflare-workflows is a Claude Code skill for automation & workflows. It helps solo builders move faster with AI-assisted coding.
- cloudflare-workflows
- Automation & Workflows
- AI-coding skill
Cloudflare Workflows by the numbers
- 136 all-time installs (skills.sh)
- +10 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #683 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-workflowsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 136 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Helps with automation & workflows tasks during AI-assisted development.
Files
Cloudflare Workflows
Status: Production Ready ✅ | Last Verified: 2025-12-27 | Version: 3.0.0
Dependencies: cloudflare-worker-base (for Worker setup)
Contents: Quick Start • Commands • Agents • Core Concepts • Critical Rules • Top Errors • Common Patterns • When to Load References • Limits
---
Quick Start (10 Minutes)
1. Create a Workflow
Use the Cloudflare Workflows starter template:
npm create cloudflare@latest my-workflow -- --template cloudflare/workflows-starter --git --deploy false
cd my-workflowWhat you get:
- WorkflowEntrypoint class template
- Worker to trigger workflows
- Complete wrangler.jsonc configuration
2. Basic Workflow Structure
src/index.ts:
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
type Env = {
MY_WORKFLOW: Workflow;
};
type Params = {
userId: string;
email: string;
};
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const { userId, email } = event.payload;
// Step 1: Do work with automatic retries
const result = await step.do('process user', async () => {
return { processed: true, userId };
});
// Step 2: Wait before next step
await step.sleep('wait 1 hour', '1 hour');
// Step 3: Continue workflow
await step.do('send email', async () => {
return { sent: true, email };
});
return { completed: true, userId };
}
}
// Worker to trigger workflow
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const instance = await env.MY_WORKFLOW.create({
params: { userId: '123', email: 'user@example.com' }
});
return Response.json({
id: instance.id,
status: await instance.status()
});
}
};Template: See templates/basic-workflow.ts for complete example
3. Configure wrangler.jsonc
{
"name": "my-workflow",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
"workflows": [
{
"binding": "MY_WORKFLOW",
"name": "my-workflow",
"class_name": "MyWorkflow"
}
]
}Template: See templates/wrangler-workflows-config.jsonc
4. Deploy
npm run deploy---
Commands
Interactive slash commands for workflow development:
| Command | Description | Use When |
|---|---|---|
/workflow-setup | Complete wizard for new workflow projects | Starting new project, need full setup |
/workflow-create | Quick scaffolding for workflow classes | Adding workflow to existing project |
/workflow-debug | Interactive debugging with error patterns | Troubleshooting workflow issues |
/workflow-test | Test workflows locally and remotely | Validating workflow behavior |
Example Usage:
/workflow-setup # Full guided setup wizard
/workflow-create # Quick workflow scaffolding
/workflow-debug # Debug workflow issues
/workflow-test # Test workflow execution---
Agents
Autonomous agents for complex workflow tasks:
| Agent | Description | Triggers |
|---|---|---|
workflow-debugger | Auto-detects and fixes configuration/runtime errors | "debug workflow", "fix workflow errors" |
workflow-optimizer | Analyzes performance, cost, and reliability | "optimize workflow", "improve performance" |
workflow-setup-assistant | Autonomous project scaffolding | "setup workflow", "create first workflow" |
Key Capabilities:
- Debugger: 6-phase analysis, auto-fix for I/O context, serialization, export issues
- Optimizer: Cost analysis, reliability scoring, actionable recommendations
- Setup Assistant: Project detection, automatic scaffolding, validation
---
Scripts
Automation scripts in scripts/ directory:
| Script | Purpose |
|---|---|
validate-workflow-config.sh | Validate wrangler.jsonc configuration |
test-workflow.sh | Create and test workflow instances |
benchmark-workflow.sh | Measure performance and cost |
generate-workflow.sh | Scaffold new workflows from templates |
check-workflow-limits.sh | Validate against Cloudflare limits |
Usage:
./scripts/validate-workflow-config.sh # Check config
./scripts/test-workflow.sh my-workflow # Test workflow
./scripts/benchmark-workflow.sh my-workflow 10 # Benchmark 10 runs
./scripts/generate-workflow.sh MyWorkflow # Generate scaffold
./scripts/check-workflow-limits.sh src/workflows/my-workflow.ts---
Core Concepts
WorkflowEntrypoint
Every workflow must extend WorkflowEntrypoint:
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// Workflow logic here
}
}Key Points:
Env: Environment bindings (KV, D1, etc.)Params: Typed payload passed when creating workflow instanceevent: Containsid,payload,timestampstep: Methods for durable execution
Step Methods
All workflow work MUST be done in steps for durability:
// step.do - Execute work with automatic retries
await step.do('step name', async () => {
return { result: 'data' };
});
// step.sleep - Wait for duration
await step.sleep('wait', '1 hour');
// step.sleepUntil - Wait until timestamp
await step.sleepUntil('wait until', Date.now() + 3600000);
// step.waitForEvent - Wait for external event
const event = await step.waitForEvent('payment received', 'payment.completed', {
timeout: '30 minutes'
});CRITICAL: All I/O (fetch, KV, D1, R2) must happen inside step.do() callbacks!
Reference: See references/workflow-patterns.md for all patterns
---
Critical Rules
Always Do ✅
✅ Perform all I/O inside step.do() - Required for durability ✅ Use named steps - Makes debugging easier ✅ Return JSON-serializable data from steps - Required for state persistence ✅ Use step.sleep() for delays - Don't use setTimeout() ✅ Handle errors explicitly - Use try/catch in step callbacks ✅ Use NonRetryableError for permanent failures - Stops retries
Workflow Patterns: See references/workflow-patterns.md for:
- Sequential workflows
- Parallel execution
- Event-driven workflows
- Scheduled workflows
- Human-in-the-loop workflows
Never Do ❌
❌ Never do I/O outside step.do() - Will fail with "I/O context" error ❌ Never use setTimeout() or setInterval() - Use step.sleep() instead ❌ Never return non-serializable data - Functions, Promises, etc. will fail ❌ Never hardcode timeouts - Use workflow config ❌ Never ignore NonRetryableError - Indicates permanent failure
---
Top 5 Critical Errors
Error #1: I/O Context Error ⚠️
Error:
Cannot perform I/O on behalf of a different requestCause: Performing I/O outside step.do() callback
Solution:
// ❌ WRONG
const data = await fetch('https://api.example.com');
await step.do('use data', async () => {
return data; // Error!
});
// ✅ CORRECT
const data = await step.do('fetch data', async () => {
const response = await fetch('https://api.example.com');
return await response.json();
});Error #2: Serialization Error
Error:
Cannot serialize workflow stateCause: Returning non-JSON-serializable data from step
Solution:
// ❌ WRONG
await step.do('process', async () => {
return { fn: () => {} }; // Functions not serializable
});
// ✅ CORRECT
await step.do('process', async () => {
return { result: 'data' }; // JSON-serializable
});Error #3: NonRetryableError Not Thrown
Error: Workflow retries forever on permanent failures
Solution:
import { NonRetryableError } from 'cloudflare:workers';
await step.do('validate', async () => {
if (!isValid) {
throw new NonRetryableError('Invalid input'); // Stop retries
}
return { valid: true };
});Error #4: WorkflowEvent Not Found
Error:
WorkflowEvent 'payment.completed' not foundCause: Event name mismatch between waitForEvent and trigger
Solution:
// Workflow waits for event
const event = await step.waitForEvent('wait payment', 'payment.completed', {
timeout: '30 minutes'
});
// Trigger event with EXACT same name
await instance.trigger('payment.completed', { amount: 100 });Error #5: Workflow Execution Failed
Error:
Workflow execution failed: Step timeout exceededCause: Step exceeds maximum CPU time (30 seconds)
Solution:
// ❌ WRONG
await step.do('long task', async () => {
for (let i = 0; i < 1000000; i++) {
// Long computation
}
});
// ✅ CORRECT - Break into smaller steps
for (let i = 0; i < 100; i++) {
await step.do(`batch ${i}`, async () => {
// Process batch
});
}---
All Issues: See references/common-issues.md for complete documentation
---
Common Patterns
Sequential Workflow
Basic workflow with steps executing in order. Each step completes before the next begins.
Use cases: Order processing, user onboarding, data pipelines
Load `templates/basic-workflow.ts` for complete example
Scheduled Workflow
Workflow with time delays between steps using step.sleep() or step.sleepUntil().
Use cases: Reminder sequences, scheduled tasks, delayed notifications
Load `templates/scheduled-workflow.ts` for complete example
Event-Driven Workflow
Wait for external events with step.waitForEvent(). Always set timeout and handle with NonRetryableError:
const payment = await step.waitForEvent('wait payment', 'payment.completed', {
timeout: '30 minutes'
});
if (!payment) throw new NonRetryableError('Payment timeout');Load `templates/workflow-with-events.ts` for complete example
Workflow with Retries
Use NonRetryableError for permanent failures (404), regular Error for transient failures (5xx):
const data = await step.do('fetch', async () => {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) throw new NonRetryableError('Not found');
throw new Error('Temporary failure'); // Will retry
}
return await response.json();
});Load `templates/workflow-with-retries.ts` for complete example with retry configuration
---
Triggering Workflows
From Worker: Create instances via env.MY_WORKFLOW.create(), get status with instance.status(), trigger events with instance.trigger().
From Cron: Use scheduled() handler to create workflow instances on schedule.
Load `templates/worker-trigger.ts` for complete Worker trigger example Load `templates/scheduled-workflow.ts` for complete Cron trigger example
---
When to Load References
`references/common-issues.md`: Encountering I/O context, serialization, NonRetryableError, event naming, or timeout errors; troubleshooting workflow failures.
`references/workflow-patterns.md`: Building complex orchestration, approval workflows, idempotency patterns, or circuit breaker patterns.
`references/wrangler-commands.md`: Need CLI commands for managing workflow instances, debugging stuck workflows, or monitoring production.
`references/production-checklist.md`: Preparing for deployment, need pre-deployment verification, setting up monitoring/error handling.
`references/limits-quotas.md`: Hitting instance/step/payload limits, optimizing for cost, designing high-volume workflows.
`references/2025-features.md`: Using events system, enhanced retries, instance lifecycle control, or latest Workflows features.
`references/metrics-analytics.md`: Setting up monitoring, custom metrics, external logging integration, or workflow dashboards.
`references/troubleshooting.md`: Complex debugging scenarios, stuck instances, systematic diagnosis, performance issues.
`templates/`: basic-workflow.ts (sequential), scheduled-workflow.ts (delays/sleep), workflow-with-events.ts (waitForEvent), workflow-with-retries.ts (custom retry), worker-trigger.ts (Worker triggers), wrangler-workflows-config.jsonc (Wrangler config), parallel-execution-workflow.ts (batched parallel processing), circuit-breaker-workflow.ts (resilient external calls)
---
Wrangler Commands
Key Commands: wrangler workflows create, wrangler workflows instances list/describe/terminate, wrangler deploy
Load `references/wrangler-commands.md` for complete CLI reference with all workflow management commands, monitoring workflows, and debugging stuck instances.
---
State Persistence
Workflows automatically persist state between steps. No manual state management needed:
export class StatefulWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// Step 1 result is automatically persisted
const result1 = await step.do('step 1', async () => {
return { data: 'value' };
});
// Even if workflow crashes here, step 1 won't re-run
await step.sleep('wait', '1 hour');
// Step 2 can use step 1's result (still available after sleep)
await step.do('step 2', async () => {
console.log(result1.data); // 'value' - persisted!
});
}
}Key Points:
- Step results automatically persisted
- Completed steps never re-run (even after crash/restart)
- State available throughout workflow lifetime
---
Limits
| Resource | Limit |
|---|---|
| Step CPU Time | 30 seconds |
| Workflow Duration | 30 days |
| Step Payload Size | 128 KB |
| Workflow Payload Size | 128 KB |
| Steps per Workflow | 1,000 |
| Concurrent Instances | 1,000 per workflow |
| Event Payload Size | 128 KB |
Workarounds:
- Large data: Store in KV/R2, pass key in step
- Long CPU: Break into smaller steps
- Many steps: Consider sub-workflows
Pricing
- Duration: $0.02 per million GB-s (same as Workers)
- Requests: $0.15 per million (workflow creation + step execution)
- State Storage: Included (no additional cost)
- Sleep: Free (no CPU usage during sleep)
Example Cost (1M workflow runs):
- 5 steps each = 5M requests = $0.75
- 10ms per step = 50GB-s = $0.001
- Total: ~$0.75 per million workflows
---
Troubleshooting
"I/O context" error
Solution: Move all I/O into step.do() callbacks → See references/common-issues.md #1
"Serialization error"
Solution: Return only JSON-serializable data from steps → See references/common-issues.md #2
Workflow retries forever
Solution: Throw NonRetryableError for permanent failures → See references/common-issues.md #3
"WorkflowEvent not found"
Solution: Ensure event names match exactly → See references/common-issues.md #4
"Step timeout exceeded"
Solution: Break long computations into smaller steps → See references/common-issues.md #5
---
Production Checklist
10-Point Pre-Deployment Checklist: I/O context isolation, JSON serialization, NonRetryableError usage, event name consistency, step duration limits, error handling, retry configuration, timeouts, workflow naming, and monitoring.
Load `references/production-checklist.md` for complete checklist with detailed explanations, code examples, verification steps, and deployment workflow.
---
Official Documentation
Workflows: https://developers.cloudflare.com/workflows/ • API Reference: https://developers.cloudflare.com/workflows/reference/ • Examples: https://developers.cloudflare.com/workflows/examples/ • Blog: https://blog.cloudflare.com/cloudflare-workflows/
Cloudflare Workflows 2025 Features
Recent features, updates, and improvements to Cloudflare Workflows as of 2025.
New in 2025
1. Enhanced Retry Configuration (January 2025)
Granular control over retry behavior with exponential backoff:
await step.do('api call', {
retries: {
limit: 5,
delay: '10 seconds',
backoff: 'exponential' // 10s, 20s, 40s, 80s, 160s
}
}, async () => {
return await callExternalAPI();
});Key Features:
limit: Number of retry attempts (0 to disable)delay: Initial delay between retriesbackoff: 'linear' | 'exponential' | 'constant'- Max backoff capped at 1 hour
2. Workflow Events System (Late 2024/2025)
Send events to running workflow instances:
// In Worker: Send event to workflow
const instance = await env.MY_WORKFLOW.get(instanceId);
await instance.sendEvent('payment.received', {
amount: 99.99,
transactionId: 'txn_123'
});
// In Workflow: Wait for event
const event = await step.waitForEvent('wait for payment', 'payment.received', {
timeout: '24 hours'
});
if (event) {
console.log('Payment received:', event.payload);
}Use Cases:
- Human-in-the-loop approvals
- External webhook integration
- Multi-system coordination
- Payment confirmation flows
3. Instance Lifecycle Management
New methods for controlling workflow instances:
// Get instance by ID
const instance = await env.MY_WORKFLOW.get(instanceId);
// Check status
const status = await instance.status();
// Returns: 'queued' | 'running' | 'paused' | 'complete' | 'errored' | 'terminated'
// Pause running instance
await instance.pause();
// Resume paused instance
await instance.resume();
// Terminate instance
await instance.terminate();4. Improved Wrangler CLI Commands
New commands for workflow management:
# List all instances
wrangler workflows instances list my-workflow
# Filter by status
wrangler workflows instances list my-workflow --status running
# Describe specific instance
wrangler workflows instances describe my-workflow abc-123
# Terminate instance
wrangler workflows instances terminate my-workflow abc-123
# Pause instance
wrangler workflows instances pause my-workflow abc-123
# Resume instance
wrangler workflows instances resume my-workflow abc-1235. Custom Instance IDs
Specify your own instance IDs for idempotency:
// Create with custom ID
const instance = await env.MY_WORKFLOW.create({
id: `order-${orderId}`, // Custom, unique ID
params: { orderId }
});
// Prevents duplicate processing
try {
await env.MY_WORKFLOW.create({
id: `order-${orderId}`, // Same ID
params: { orderId }
});
} catch (e) {
// Instance already exists - check status instead
const existing = await env.MY_WORKFLOW.get(`order-${orderId}`);
return await existing.status();
}6. Enhanced Error Information
More detailed error objects with stack traces:
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
try {
await step.do('risky operation', async () => {
throw new Error('Something went wrong');
});
} catch (error) {
// Enhanced error object
console.log({
message: error.message,
step: error.step, // Which step failed
attempt: error.attempt, // Which retry attempt
stack: error.stack // Full stack trace
});
}
}7. Workflow Metrics & Analytics
Access workflow metrics through Workers Analytics Engine:
// Query workflow metrics
const metrics = await env.ANALYTICS.query({
dimensions: ['workflow_name', 'status'],
metrics: ['instance_count', 'avg_duration', 'error_rate'],
timeRange: { hours: 24 }
});Available Metrics:
- Instance count by status
- Average/P50/P95/P99 duration
- Step execution counts
- Error rates and types
- Retry statistics
---
Compatibility Date Requirements
Different features require specific compatibility dates:
| Feature | Min Compatibility Date |
|---|---|
| Basic Workflows | 2024-01-01 |
| Events System | 2024-10-01 |
| Enhanced Retries | 2024-12-01 |
| Instance Control | 2024-12-01 |
Update your wrangler.jsonc:
{
"compatibility_date": "2025-01-01" // Use latest for all features
}---
Migration Guide
From Pre-2025 Workflows
Old retry syntax (deprecated):
// Old way - still works but limited
await step.do('api call', async () => {
return await callAPI();
});
// Uses default 3 retriesNew retry syntax (recommended):
// New way - full control
await step.do('api call', {
retries: {
limit: 5,
delay: '10s',
backoff: 'exponential'
}
}, async () => {
return await callAPI();
});Adding Events to Existing Workflows
// Before: Polling for status changes
for (let i = 0; i < 60; i++) {
const status = await step.do(`poll ${i}`, () => checkPaymentStatus());
if (status === 'complete') break;
await step.sleep('wait', '1 minute');
}
// After: Event-driven (recommended)
await step.do('initiate payment', () => initiatePayment());
const event = await step.waitForEvent('payment confirmation', 'payment.complete', {
timeout: '1 hour'
});
if (!event) {
throw new NonRetryableError('Payment timeout');
}---
Best Practices for 2025
1. Always Set Event Timeouts
// Always include timeout
const event = await step.waitForEvent('user action', 'user.confirmed', {
timeout: '24 hours' // Don't wait forever
});
if (!event) {
// Handle timeout gracefully
await step.do('escalate', () => notifyTimeout());
}2. Use Custom IDs for Idempotency
// Generate deterministic ID from input
const instanceId = `process-${hashInput(params)}`;
// Check if already processed
try {
const existing = await env.MY_WORKFLOW.get(instanceId);
const status = await existing.status();
if (status.status === 'complete') {
return { alreadyProcessed: true, result: status.output };
}
} catch {
// Instance doesn't exist, create new
}
await env.MY_WORKFLOW.create({
id: instanceId,
params
});3. Leverage Free Sleep for Cost Optimization
// Sleep is FREE - use it liberally
await step.sleep('rate limit cooldown', '1 minute'); // $0
await step.sleep('wait for next business day', '16 hours'); // $0
await step.sleep('monthly check', '30 days'); // $0
// Steps cost money - minimize when possible
await step.do('process', async () => { // $0.00000015
return await process();
});4. Use Enhanced Retries for External APIs
await step.do('call external api', {
retries: {
limit: 5,
delay: '5 seconds',
backoff: 'exponential' // 5s, 10s, 20s, 40s, 80s
}
}, async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
if (response.status === 429) {
// Rate limited - retry will handle backoff
throw new Error('Rate limited');
}
if (response.status >= 500) {
// Server error - worth retrying
throw new Error(`Server error: ${response.status}`);
}
// Client error - don't retry
throw new NonRetryableError(`Client error: ${response.status}`);
}
return await response.json();
});---
Upcoming Features (Roadmap)
Based on Cloudflare's public roadmap:
1. Workflow Versioning: Deploy new versions without affecting running instances 2. Conditional Branching: Built-in if/else step logic 3. Parallel Steps: Execute multiple steps concurrently 4. Sub-workflows: Call workflows from workflows 5. Dashboard UI: Visual workflow monitoring and management 6. Workflow Templates: Pre-built patterns for common use cases
---
When to Load This Reference
Load this file when:
- User asks about new Workflow features
- Migrating older workflows to new patterns
- Implementing events, retries, or instance control
- Checking compatibility date requirements
- Planning workflow architecture with latest capabilities
Cloudflare Workflows - Common Issues
Last Updated: 2025-10-22
This document details all known issues with Cloudflare Workflows and their solutions.
---
Issue #1: I/O Context Error
Error Message:
Cannot perform I/O on behalf of a different requestDescription: When trying to use I/O objects (like fetch responses, file handles, etc.) created in one request context from a different request's handler, Cloudflare Workers throws this error. This is a fundamental Workers platform limitation.
Root Cause: I/O objects are bound to the request context that created them. Workflows create a new execution context for each step, so I/O must happen within the step's callback.
Prevention:
❌ Bad - I/O outside step:
// This will fail!
const response = await fetch('https://api.example.com/data');
const data = await response.json();
await step.do('use data', async () => {
// Trying to use data from outside step's context
return data; // ❌ Error!
});✅ Good - I/O inside step:
const data = await step.do('fetch data', async () => {
const response = await fetch('https://api.example.com/data');
return await response.json(); // ✅ Correct
});Workaround: Always perform all I/O operations (fetch, KV reads, D1 queries, R2 operations) within step.do() callbacks.
Source: Cloudflare Workers platform limitation
---
Issue #2: NonRetryableError Behaves Differently in Dev vs Production
Error Message:
(No specific error - workflow retries when it shouldn't)Description: When throwing a NonRetryableError with an empty message in development mode (wrangler dev), the workflow incorrectly retries the failed step. In production, it correctly exits without retrying.
Root Cause: Bug in the development environment handling of empty NonRetryableError messages.
Prevention:
❌ Bad - Empty message:
import { NonRetryableError } from 'cloudflare:workflows';
// May retry in dev mode
throw new NonRetryableError();✅ Good - Always provide message:
import { NonRetryableError } from 'cloudflare:workflows';
// Works consistently in dev and production
throw new NonRetryableError('User not found');
throw new NonRetryableError('Invalid authentication credentials');
throw new NonRetryableError('Amount exceeds limit');Workaround: Always provide a descriptive message when throwing NonRetryableError.
Source: cloudflare/workers-sdk#10113 Status: Reported July 2025, not yet fixed
---
Issue #3: WorkflowEvent Export Not Found
Error Message:
The requested module 'cloudflare:workers' does not provide an export named 'WorkflowEvent'Description: TypeScript cannot find the WorkflowEvent export from the cloudflare:workers module. This usually happens with outdated type definitions.
Root Cause:
- Outdated
@cloudflare/workers-typespackage - Incorrect import statement
- Missing types in tsconfig.json
Prevention:
✅ Ensure latest types installed:
npm install -D @cloudflare/workers-types@latest✅ Correct import:
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';✅ Correct tsconfig.json:
{
"compilerOptions": {
"types": ["@cloudflare/workers-types/2023-07-01"],
"moduleResolution": "bundler"
}
}Workaround: 1. Update workers types: npm install -D @cloudflare/workers-types@latest 2. Run type generation: npx wrangler types 3. Restart TypeScript server in your editor
Source: Community reports, package versioning issues Latest Working Version: @cloudflare/workers-types@4.20251014.0 (verified 2025-10-22)
---
Issue #4: Serialization Error - Non-Serializable Return Values
Error Message:
Error: Could not serialize return value
(or workflow hangs without clear error)Description: Attempting to return non-serializable values from step.do() or run() methods causes serialization failures. The workflow instance may error or hang.
Root Cause: Workflows persist state between steps by serializing return values. Only JSON-serializable types are supported.
Prevention:
❌ Bad - Non-serializable types:
// ❌ Function
await step.do('bad example', async () => {
return {
data: [1, 2, 3],
transform: (x) => x * 2 // ❌ Function not serializable
};
});
// ❌ Circular reference
await step.do('bad example 2', async () => {
const obj: any = { name: 'test' };
obj.self = obj; // ❌ Circular reference
return obj;
});
// ❌ Symbol
await step.do('bad example 3', async () => {
return {
id: Symbol('unique'), // ❌ Symbol not serializable
data: 'test'
};
});
// ❌ undefined (use null instead)
await step.do('bad example 4', async () => {
return {
value: undefined // ❌ undefined not serializable
};
});✅ Good - Only serializable types:
await step.do('good example', async () => {
return {
// ✅ Primitives
string: 'value',
number: 123,
boolean: true,
nullValue: null,
// ✅ Arrays
array: [1, 2, 3],
// ✅ Objects
nested: {
data: 'test',
items: [{ id: 1 }, { id: 2 }]
}
};
});✅ Convert class instances to plain objects:
class User {
constructor(public id: string, public name: string) {}
toJSON() {
return { id: this.id, name: this.name };
}
}
await step.do('serialize class', async () => {
const user = new User('123', 'Alice');
// ✅ Convert to plain object
return user.toJSON(); // { id: '123', name: 'Alice' }
});Workaround:
- Only return primitives, arrays, and plain objects
- Convert class instances to plain objects before returning
- Use
nullinstead ofundefined - Avoid circular references
Source: Cloudflare Workflows documentation Reference: Workflows Workers API
---
Issue #5: Testing Workflows in CI Environments
Error Message:
(Tests pass locally but fail in CI)Description: Tests that use vitest-pool-workers to test workflows work reliably in local development but fail inconsistently in CI environments (GitHub Actions, GitLab CI, etc.).
Root Cause:
- Timing issues in CI environments
- Resource constraints in CI runners
- Race conditions in test setup/teardown
Prevention:
✅ Increase timeouts in CI:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
testTimeout: 30000, // Increase from default 5000ms
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' },
},
},
},
});✅ Add retry logic for flaky tests:
describe('Workflow tests', () => {
it.retry(3)('should complete workflow', async () => {
// Test code
});
});✅ Use proper test isolation:
import { beforeEach, afterEach } from 'vitest';
let instance: WorkflowInstance;
beforeEach(async () => {
instance = await env.MY_WORKFLOW.create({
params: { userId: '123' }
});
});
afterEach(async () => {
if (instance) {
try {
await instance.terminate();
} catch (error) {
// Instance may already be terminated
}
}
});Workaround: 1. Increase test timeouts for CI 2. Add retry logic for flaky tests 3. Use proper test isolation 4. Consider mocking workflows in unit tests, testing real workflows in integration tests
Source: cloudflare/workers-sdk#10600 Status: Ongoing investigation
---
Additional Troubleshooting Tips
Workflow Instance Stuck in "Running" State
Possible Causes: 1. Step is sleeping for long duration 2. Step is waiting for event that never arrives 3. Step is retrying with long backoff
Solution:
# Check detailed instance status
npx wrangler workflows instances describe my-workflow <instance-id>
# Look for:
# - Sleep state (shows wake time)
# - waitForEvent state (shows event type and timeout)
# - Retry history (shows attempts and delays)---
Step Returns Undefined
Cause: Missing return statement in step callback
Solution:
// ❌ Bad - no return
const result = await step.do('get data', async () => {
const data = await fetchData();
// Missing return!
});
console.log(result); // undefined
// ✅ Good - explicit return
const result = await step.do('get data', async () => {
const data = await fetchData();
return data; // ✅ Return the value
});---
Payload Too Large Error
Error:
Payload size exceeds limitCause: Workflow parameters or step outputs exceed 128 KB
Solution:
// ❌ Bad - large payload
await env.MY_WORKFLOW.create({
params: {
largeData: hugeArray // >128 KB
}
});
// ✅ Good - store in R2/KV, pass reference
const key = `workflow-data/${crypto.randomUUID()}`;
await env.MY_BUCKET.put(key, JSON.stringify(hugeArray));
await env.MY_WORKFLOW.create({
params: {
dataKey: key // Just pass the key
}
});---
Getting Help
If you encounter issues not listed here:
1. Check Cloudflare Status: https://www.cloudflarestatus.com/ 2. Search GitHub Issues: https://github.com/cloudflare/workers-sdk/issues 3. Cloudflare Discord: https://discord.gg/cloudflaredev 4. Cloudflare Community: https://community.cloudflare.com/ 5. Official Docs: https://developers.cloudflare.com/workflows/
When reporting issues, include:
- Workflow code (sanitized)
- Wrangler configuration
- Error messages and stack traces
- Workflow instance ID
- Steps to reproduce
- Expected vs actual behavior
---
Last Updated: 2025-10-22 Maintainer: Claude Skills Maintainers | maintainers@example.com
Cloudflare Workflows Limits & Quotas
Complete reference for all Cloudflare Workflows limits with workarounds and optimization strategies.
Instance Limits
| Limit | Value | Notes |
|---|---|---|
| Max running instances | 100 per workflow | Queued instances wait |
| Max queued instances | 10,000 per workflow | Oldest dropped if exceeded |
| Instance retention | 7 days | After completion/failure |
| Instance ID length | 64 characters | Custom IDs must fit |
| Max instances per Worker request | No hard limit | Rate limits apply |
Workarounds for Instance Limits
High Volume Processing:
// Bad: Create many instances at once
for (const item of items) {
await env.MY_WORKFLOW.create({ params: { item } });
}
// Good: Batch processing in single workflow
await env.MY_WORKFLOW.create({
params: { items: items.slice(0, 1000) }
});Instance Management:
// Check running instances before creating new ones
const instances = await env.MY_WORKFLOW.list();
const running = instances.filter(i => i.status === 'running');
if (running.length >= 90) {
// Wait or queue externally
throw new Error('Too many running instances');
}---
Step Limits
| Limit | Value | Notes |
|---|---|---|
| Max steps per instance | 1,000 | Across all step types |
| Step name length | 256 characters | Must be unique per instance |
| Step CPU time | 30 seconds | Per step.do() execution |
| Step wall-clock time | 15 minutes | Including I/O wait |
| Max concurrent steps | 1 | Sequential only |
Step Limit Workarounds
Breaking Large Operations into Batches:
// Bad: Single step with many items (may hit 30s limit)
await step.do('process all', async () => {
for (const item of items) {
await processItem(item); // 1000 items × 0.1s = 100s
}
});
// Good: Batched steps
const batchSize = 100;
for (let i = 0; i < items.length; i += batchSize) {
await step.do(`batch ${Math.floor(i/batchSize)}`, async () => {
const batch = items.slice(i, i + batchSize);
return Promise.all(batch.map(processItem));
});
}Avoiding Step Limit Exhaustion:
// Bad: One step per item (hits 1000 step limit)
for (const item of items) {
await step.do(`process ${item.id}`, () => process(item));
}
// Good: Batch items into fewer steps
const chunks = chunkArray(items, 10);
for (let i = 0; i < chunks.length; i++) {
await step.do(`batch ${i}`, async () => {
return Promise.all(chunks[i].map(process));
});
}---
Payload & Data Limits
| Limit | Value | Notes |
|---|---|---|
| Step return value | 128 KB | JSON serialized |
| Workflow params | 128 KB | Input payload |
| Event payload | 128 KB | sendEvent data |
| Total state per instance | 1 MB | Sum of all step results |
Payload Limit Workarounds
Large Data Handling with KV/R2:
// Bad: Return large data from step
await step.do('fetch data', async () => {
const data = await fetchLargeDataset(); // 500KB
return data; // ERROR: exceeds 128KB
});
// Good: Store in KV, return key
await step.do('fetch and store', async () => {
const data = await fetchLargeDataset();
const key = `workflow-${event.instanceId}-data`;
await env.KV.put(key, JSON.stringify(data));
return { dataKey: key };
});
// Later step retrieves if needed
await step.do('process data', async () => {
const key = previousResult.dataKey;
const data = JSON.parse(await env.KV.get(key));
return processData(data);
});Streaming Large Responses:
// For very large data, use R2
await step.do('store to r2', async () => {
const data = await fetchHugeDataset(); // 10MB
await env.R2.put(`workflow/${event.instanceId}/data.json`, JSON.stringify(data));
return { stored: true, key: `workflow/${event.instanceId}/data.json` };
});---
Duration Limits
| Limit | Value | Notes |
|---|---|---|
| Max workflow duration | 1 year | Total wall-clock time |
| Sleep duration | No limit | Can sleep for months |
| waitForEvent timeout | No default | Set explicitly |
| Step execution timeout | 15 minutes | Wall-clock time |
Duration Best Practices
Long-Running Workflows:
// Workflow can run for months
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// Start immediately
await step.do('start', () => notifyStart());
// Wait for a month
await step.sleep('wait for billing cycle', '30 days');
// Continue processing
await step.do('monthly task', () => runMonthlyTask());
// This is fine - workflows can last up to 1 year
}waitForEvent with Timeout:
// Always set timeout to prevent indefinite waiting
const approval = await step.waitForEvent('wait for approval', 'user.approved', {
timeout: '7 days'
});
if (!approval) {
throw new NonRetryableError('Approval timeout - escalating');
}---
Retry Limits
| Limit | Default | Max |
|---|---|---|
| Retry attempts | 3 | Configurable |
| Retry delay | Exponential | Configurable |
| Max retry delay | 1 hour | Capped |
Retry Configuration
// Configure retries per step
await step.do('flaky api', {
retries: {
limit: 5, // Max 5 retries
delay: '10 seconds', // Initial delay
backoff: 'exponential' // 10s, 20s, 40s, 80s, 160s
}
}, async () => {
return await callFlakyAPI();
});
// Disable retries for idempotency-unsafe operations
await step.do('one-time action', {
retries: { limit: 0 }
}, async () => {
return await nonIdempotentAction();
});---
Rate Limits
| Operation | Limit | Period |
|---|---|---|
| Instance creation | 1,000/min | Per workflow |
| sendEvent | 1,000/min | Per instance |
| Status queries | 10,000/min | Per account |
| List instances | 100/min | Per workflow |
Rate Limit Handling
// Implement rate limiting for bulk operations
async function createInstancesWithRateLimit(
workflow: Workflow,
params: any[],
ratePerMinute = 500
) {
const delayMs = 60000 / ratePerMinute;
const results = [];
for (const param of params) {
const instance = await workflow.create({ params: param });
results.push(instance);
await new Promise(r => setTimeout(r, delayMs));
}
return results;
}---
Cost Considerations
| Resource | Price | Notes |
|---|---|---|
| Requests | $0.15/million | Instance creation + each step |
| Duration | $0.02/million GB-s | CPU time only |
| Sleep | FREE | No CPU usage |
| waitForEvent | FREE | While waiting |
Cost Optimization Examples
Use Sleep Instead of Polling:
// Expensive: Polling loop (10 steps = 10 requests)
for (let i = 0; i < 10; i++) {
const ready = await step.do(`check ${i}`, () => checkReady());
if (ready) break;
await step.sleep('wait', '1 minute'); // Free
}
// Cheaper: Single sleep then check
await step.sleep('wait for processing', '10 minutes'); // Free
await step.do('verify', () => verifyComplete()); // 1 requestConsolidate Steps:
// Expensive: 3 steps = 3 requests
await step.do('validate', () => validate(data));
await step.do('transform', () => transform(data));
await step.do('save', () => save(data));
// Cheaper: 1 step = 1 request
await step.do('process', async () => {
const validated = await validate(data);
const transformed = await transform(validated);
return await save(transformed);
});---
Quota Check Script
Use this script to check if your workflow is within limits:
./scripts/check-workflow-limits.sh src/workflows/my-workflow.tsOutput example:
Workflow Limits Check: my-workflow
==================================
Steps Analysis:
- Total step.do() calls: 15
- Status: ✅ Within limit (1000 max)
Payload Analysis:
- Largest return statement: ~2KB estimated
- Status: ✅ Within limit (128KB max)
Duration Analysis:
- Estimated max duration: 5 minutes
- Status: ✅ Within limit (1 year max)
Cost Estimate (per 1000 workflows):
- Requests: 16,000 × $0.15/M = $0.0024
- Duration: ~0.005 GB-s × $0.02/M = ~$0
- Total: ~$0.0024 per 1000 workflows---
Quick Reference Card
INSTANCE LIMITS:
- Running: 100 max
- Queued: 10,000 max
- Retention: 7 days
STEP LIMITS:
- Steps/instance: 1,000 max
- CPU time/step: 30 seconds
- Wall time/step: 15 minutes
PAYLOAD LIMITS:
- Step return: 128 KB
- Params: 128 KB
- Event: 128 KB
- Total state: 1 MB
DURATION:
- Max workflow: 1 year
- Sleep: unlimited (free)
COST:
- Requests: $0.15/million
- Duration: $0.02/million GB-s
- Sleep/wait: FREE---
When to Load This Reference
Load this file when:
- User asks about workflow limits or quotas
- Designing workflows that may hit limits
- Optimizing workflow costs
- Debugging "payload too large" or "step limit exceeded" errors
- Planning high-volume workflow deployments
Cloudflare Workflows Metrics & Analytics
Comprehensive guide to monitoring, observability, and analytics for Cloudflare Workflows.
Built-in Monitoring
Wrangler CLI Monitoring
List Running Instances:
# List all instances
wrangler workflows instances list my-workflow
# Filter by status
wrangler workflows instances list my-workflow --status running
wrangler workflows instances list my-workflow --status errored
wrangler workflows instances list my-workflow --status complete
# Limit results
wrangler workflows instances list my-workflow --limit 50Instance Details:
# Get detailed instance information
wrangler workflows instances describe my-workflow <instance-id>
# Output includes:
# - Instance ID
# - Status (queued, running, paused, complete, errored, terminated)
# - Created timestamp
# - Completed timestamp (if finished)
# - Steps executed
# - Current step (if running)
# - Error details (if errored)
# - Output (if complete)Real-time Logs:
# Stream workflow logs
wrangler tail my-worker --format pretty
# Filter for workflow events
wrangler tail my-worker | grep -E "(Workflow|step|instance)"---
Custom Metrics Implementation
Step-Level Metrics
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
type Env = {
MY_WORKFLOW: Workflow;
ANALYTICS: AnalyticsEngineDataset;
};
type Metrics = {
stepDurations: Record<string, number>;
totalDuration: number;
retryCount: number;
};
export class InstrumentedWorkflow extends WorkflowEntrypoint<Env, any> {
async run(event: WorkflowEvent<any>, step: WorkflowStep) {
const startTime = Date.now();
const metrics: Metrics = {
stepDurations: {},
totalDuration: 0,
retryCount: 0
};
// Instrumented step wrapper
const timedStep = async <T>(
name: string,
fn: () => Promise<T>,
options?: { retries?: { limit: number } }
): Promise<T> => {
const stepStart = Date.now();
const result = await step.do(name, options || {}, fn);
metrics.stepDurations[name] = Date.now() - stepStart;
// Log step completion
console.log(JSON.stringify({
type: 'step_complete',
workflow: 'my-workflow',
instanceId: event.instanceId,
step: name,
duration: metrics.stepDurations[name]
}));
return result;
};
try {
// Use instrumented steps
await timedStep('validate', async () => {
return await validate(event.payload);
});
await timedStep('process', async () => {
return await process(event.payload);
});
await timedStep('complete', async () => {
return await finalize(event.payload);
});
metrics.totalDuration = Date.now() - startTime;
// Write final metrics
this.env.ANALYTICS.writeDataPoint({
blobs: [event.instanceId, 'complete'],
doubles: [metrics.totalDuration],
indexes: ['my-workflow']
});
return { status: 'complete', metrics };
} catch (error) {
metrics.totalDuration = Date.now() - startTime;
this.env.ANALYTICS.writeDataPoint({
blobs: [event.instanceId, 'error', error.message],
doubles: [metrics.totalDuration],
indexes: ['my-workflow']
});
throw error;
}
}
}Analytics Engine Integration
Setup Analytics Engine:
1. Enable in Cloudflare Dashboard > Analytics > Analytics Engine 2. Add binding to wrangler.jsonc:
{
"analytics_engine_datasets": [
{
"binding": "ANALYTICS",
"dataset": "workflow_metrics"
}
]
}Write Metrics:
// Write workflow completion metric
env.ANALYTICS.writeDataPoint({
blobs: [
instanceId, // blob1: instance identifier
workflowName, // blob2: workflow name
status // blob3: completion status
],
doubles: [
duration, // double1: total duration (ms)
stepCount, // double2: steps executed
retryCount // double3: total retries
],
indexes: [workflowName] // For efficient querying
});Query Metrics (via GraphQL API):
query WorkflowMetrics($date: Date!) {
viewer {
accounts(filter: { accountTag: "your-account-id" }) {
workflowMetrics: analyticsEngineDatasets(
dataset: "workflow_metrics"
filter: { date: $date }
limit: 1000
) {
dimensions {
blob1 # instanceId
blob2 # workflowName
blob3 # status
}
avg {
double1 # avg duration
}
sum {
double2 # total steps
double3 # total retries
}
count
}
}
}
}---
Structured Logging
Log Format Standard
interface WorkflowLogEntry {
timestamp: string;
level: 'debug' | 'info' | 'warn' | 'error';
type: 'workflow_start' | 'step_start' | 'step_complete' | 'step_error' | 'workflow_complete' | 'workflow_error';
workflow: string;
instanceId: string;
step?: string;
duration?: number;
error?: {
message: string;
code?: string;
retryable: boolean;
};
metadata?: Record<string, unknown>;
}
function log(entry: WorkflowLogEntry) {
console.log(JSON.stringify({
...entry,
timestamp: entry.timestamp || new Date().toISOString()
}));
}Example Logging Implementation
export class LoggingWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const workflowName = 'order-processing';
// Log workflow start
log({
level: 'info',
type: 'workflow_start',
workflow: workflowName,
instanceId: event.instanceId,
metadata: { params: event.payload }
});
try {
// Log step start
log({
level: 'debug',
type: 'step_start',
workflow: workflowName,
instanceId: event.instanceId,
step: 'validate'
});
const startTime = Date.now();
const result = await step.do('validate', async () => {
return await validate(event.payload);
});
// Log step complete
log({
level: 'info',
type: 'step_complete',
workflow: workflowName,
instanceId: event.instanceId,
step: 'validate',
duration: Date.now() - startTime
});
// ... more steps
// Log workflow complete
log({
level: 'info',
type: 'workflow_complete',
workflow: workflowName,
instanceId: event.instanceId,
duration: Date.now() - workflowStartTime
});
return result;
} catch (error) {
// Log workflow error
log({
level: 'error',
type: 'workflow_error',
workflow: workflowName,
instanceId: event.instanceId,
error: {
message: error.message,
retryable: !(error instanceof NonRetryableError)
}
});
throw error;
}
}
}---
External Monitoring Integration
Logpush to External Services
Configure Cloudflare Logpush for workflow logs:
1. Go to Cloudflare Dashboard > Logs > Logpush 2. Create job for Workers logs 3. Select destination (S3, Datadog, Splunk, etc.)
Example Datadog Integration:
// Log in Datadog-compatible format
console.log(JSON.stringify({
ddsource: 'cloudflare-workflows',
ddtags: `workflow:${workflowName},env:production`,
hostname: 'cloudflare-workers',
service: workflowName,
status: 'info',
message: `Step ${stepName} completed`,
duration: stepDuration,
workflow: {
instanceId,
step: stepName,
params: event.payload
}
}));Custom Webhook Notifications
async function notifyExternal(event: {
type: string;
workflow: string;
instanceId: string;
data: Record<string, unknown>;
}) {
await fetch('https://your-webhook.com/workflow-events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...event,
timestamp: new Date().toISOString(),
source: 'cloudflare-workflows'
})
});
}
// Use in workflow
await step.do('notify start', async () => {
await notifyExternal({
type: 'workflow.started',
workflow: 'order-processing',
instanceId: event.instanceId,
data: { orderId: event.payload.orderId }
});
});---
Dashboard Queries
Instance Status Overview
-- Analytics Engine SQL API (if enabled)
SELECT
blob2 AS workflow_name,
blob3 AS status,
COUNT(*) AS instance_count,
AVG(double1) AS avg_duration_ms,
SUM(double3) AS total_retries
FROM workflow_metrics
WHERE timestamp >= NOW() - INTERVAL '24 hours'
GROUP BY blob2, blob3
ORDER BY instance_count DESCError Rate Tracking
SELECT
blob2 AS workflow_name,
COUNT(*) FILTER (WHERE blob3 = 'error') AS errors,
COUNT(*) FILTER (WHERE blob3 = 'complete') AS successes,
ROUND(
COUNT(*) FILTER (WHERE blob3 = 'error') * 100.0 / COUNT(*),
2
) AS error_rate_pct
FROM workflow_metrics
WHERE timestamp >= NOW() - INTERVAL '1 hour'
GROUP BY blob2Performance Percentiles
SELECT
blob2 AS workflow_name,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY double1) AS p50_ms,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY double1) AS p95_ms,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY double1) AS p99_ms
FROM workflow_metrics
WHERE blob3 = 'complete'
AND timestamp >= NOW() - INTERVAL '24 hours'
GROUP BY blob2---
Alerting Patterns
Error Rate Alert
// Check error rate periodically
async function checkErrorRate(env: Env, threshold = 5) {
const stats = await getWorkflowStats(env, '1 hour');
const errorRate = (stats.errors / stats.total) * 100;
if (errorRate > threshold) {
await sendAlert({
severity: 'critical',
message: `Workflow error rate ${errorRate.toFixed(1)}% exceeds ${threshold}%`,
workflow: 'order-processing',
metrics: stats
});
}
}Latency Alert
async function checkLatency(env: Env, thresholdMs = 30000) {
const p95 = await getP95Latency(env, '1 hour');
if (p95 > thresholdMs) {
await sendAlert({
severity: 'warning',
message: `Workflow P95 latency ${p95}ms exceeds ${thresholdMs}ms`,
workflow: 'order-processing'
});
}
}Stuck Instance Detection
async function detectStuckInstances(env: Env) {
const instances = await env.MY_WORKFLOW.list();
const stuckThreshold = 60 * 60 * 1000; // 1 hour
const now = Date.now();
for (const instance of instances) {
if (instance.status === 'running') {
const age = now - new Date(instance.created).getTime();
if (age > stuckThreshold) {
await sendAlert({
severity: 'warning',
message: `Instance ${instance.id} running for ${Math.round(age / 60000)} minutes`,
workflow: 'order-processing'
});
}
}
}
}---
Monitoring Script
Use the included monitoring script:
# Basic status check
./scripts/benchmark-workflow.sh my-workflow 1
# Performance benchmark
./scripts/benchmark-workflow.sh my-workflow 10
# Output:
# Workflow Benchmark: my-workflow
# ===============================
# Iterations: 10
#
# Results:
# - Min Duration: 1.2s
# - Max Duration: 2.1s
# - Avg Duration: 1.5s
# - Success Rate: 100%
#
# Cost Estimate:
# - Requests: 60 × $0.15/M = $0.000009
# - Duration: 0.015 GB-s × $0.02/M = ~$0
# - Total: ~$0.00001 per run---
When to Load This Reference
Load this file when:
- Setting up workflow monitoring
- Implementing custom metrics
- Integrating with external monitoring tools
- Creating dashboards for workflow visibility
- Debugging performance issues
- Setting up alerts for workflow health
Cloudflare Workflows Production Deployment Checklist
Last Updated: 2025-11-26
Complete this checklist before deploying workflow consumers to production to ensure reliability, proper error handling, and optimal performance.
---
Pre-Deployment Checklist
1. I/O Context Isolation ✅
Requirement: ALL I/O must happen inside step.do() callbacks
Why: Each step runs in a separate request context. I/O outside step.do() causes the error: "Cannot perform I/O on behalf of different request"
Verification:
- [ ] Database calls inside
step.do() - [ ] API requests inside
step.do() - [ ] KV/R2/D1 operations inside
step.do() - [ ] No I/O in
run()method outside steps
Example (Correct):
async run(event: WorkflowEvent, step: WorkflowStep) {
// ✅ All I/O inside step.do()
const user = await step.do('fetch user', async () => {
return await env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(event.payload.userId)
.first();
});
// ✅ API call inside step.do()
const payment = await step.do('process payment', async () => {
return await fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { 'Authorization': `Bearer ${env.STRIPE_KEY}` },
body: JSON.stringify({ amount: 1000, currency: 'usd' })
});
});
}---
2. JSON Serialization Validation ✅
Requirement: Steps must return only JSON-serializable data
Why: State persists between steps and must be serialized. Non-serializable data causes "serialization error"
Non-Serializable Types:
- Functions
- Symbols
- undefined (use null instead)
- Circular references
- Class instances (use
.toJSON()or plain objects) - BigInt (convert to string)
- Date objects (convert to ISO string)
Verification:
- [ ] All step return values are JSON-serializable
- [ ] Class instances converted to plain objects
- [ ] Dates converted to ISO strings
- [ ] No functions or Symbols in return values
Example (Correct):
async run(event: WorkflowEvent, step: WorkflowStep) {
// ❌ Wrong: Returns Date object
const wrongData = await step.do('get timestamp', async () => {
return { createdAt: new Date() }; // Not JSON-serializable
});
// ✅ Correct: Returns ISO string
const correctData = await step.do('get timestamp', async () => {
return { createdAt: new Date().toISOString() }; // JSON-serializable
});
}---
3. NonRetryableError for Permanent Failures ✅
Requirement: Throw NonRetryableError for permanent failures to prevent infinite retries
Why: By default, all errors cause retries. Permanent failures (validation errors, missing resources) should not retry.
Verification:
- [ ]
NonRetryableErrorthrown for validation failures - [ ]
NonRetryableErrorthrown for 404/410 responses - [ ]
NonRetryableErrorincludes descriptive message - [ ] Regular errors used for transient failures (network, 5xx)
Important: Always include a message with NonRetryableError. Empty messages cause dev/prod inconsistency.
Example (Correct):
import { NonRetryableError } from 'cloudflare:workers';
async run(event: WorkflowEvent, step: WorkflowStep) {
await step.do('validate order', async () => {
if (!event.payload.orderId) {
// ✅ Permanent failure - don't retry
throw new NonRetryableError('orderId is required');
}
const order = await fetchOrder(event.payload.orderId);
if (!order) {
// ✅ Order doesn't exist - don't retry
throw new NonRetryableError(`Order ${event.payload.orderId} not found`);
}
return order;
});
}---
4. Event Name Consistency ✅
Requirement: Event names in waitForEvent() must exactly match workflow.trigger() calls
Why: Mismatched event names cause "WorkflowEvent not found" errors
Verification:
- [ ] Event names match exactly (case-sensitive)
- [ ] No typos in event names
- [ ] Event names documented in README
- [ ] Timeout configured for
waitForEvent()
Example (Correct):
// In workflow
await step.waitForEvent('payment.completed', { timeout: '1 hour' });
// In trigger Worker
await env.MY_WORKFLOW.get(instanceId).trigger('payment.completed', {
paymentId: 'ch_123',
amount: 1000
});---
5. Step Duration Limits ✅
Requirement: Break long computations into <30 second steps
Why: Steps have a 30-second CPU limit by default. Exceeding this causes "Step timeout exceeded"
Verification:
- [ ] No single step exceeds 30 seconds
- [ ] Large data processing split into batches
- [ ] Long computations use
step.sleep()between batches - [ ] CPU-intensive tasks optimized
Example (Correct):
async run(event: WorkflowEvent, step: WorkflowStep) {
const items = event.payload.items; // 10,000 items
// ❌ Wrong: Process all items in one step (>30s)
// await step.do('process all', async () => {
// return items.map(item => processItem(item));
// });
// ✅ Correct: Process in batches
const batchSize = 100;
for (let i = 0; i < items.length; i += batchSize) {
await step.do(`process batch ${i / batchSize}`, async () => {
const batch = items.slice(i, i + batchSize);
return await Promise.all(batch.map(processItem));
});
// Optional: Sleep between batches
if (i + batchSize < items.length) {
await step.sleep('wait between batches', '1 second');
}
}
}---
6. Error Handling in All Steps ✅
Requirement: Implement proper error handling with try-catch in steps
Why: Unhandled errors cause workflow to retry unexpectedly
Verification:
- [ ] Try-catch blocks in all steps
- [ ] Errors logged with context
- [ ] Transient errors allowed to retry
- [ ] Permanent errors throw NonRetryableError
Example (Correct):
async run(event: WorkflowEvent, step: WorkflowStep) {
await step.do('call external API', async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
if (response.status === 404) {
// Permanent failure - don't retry
throw new NonRetryableError(`Resource not found: ${response.status}`);
}
// Transient failure (5xx) - will retry
throw new Error(`API error: ${response.status}`);
}
return await response.json();
} catch (error) {
// Log error for debugging
console.error('API call failed:', error);
throw error; // Re-throw for retry logic
}
});
}---
7. Retry Configuration ✅
Requirement: Configure appropriate retry behavior for your use case
Why: Default retry behavior may not suit all workflows
Verification:
- [ ] Max retries set appropriately
- [ ] Retry delays configured if needed
- [ ] Circuit breaker pattern for external APIs
- [ ] Exponential backoff implemented
Example (with Exponential Backoff):
async run(event: WorkflowEvent, step: WorkflowStep) {
let attempt = 0;
const maxAttempts = 5;
while (attempt < maxAttempts) {
try {
return await step.do(`api call attempt ${attempt}`, async () => {
return await callRateLimitedAPI();
});
} catch (error) {
attempt++;
if (attempt >= maxAttempts) {
throw new NonRetryableError('Max retry attempts exceeded');
}
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const delaySec = Math.pow(2, attempt);
await step.sleep(`backoff ${delaySec}s`, `${delaySec} seconds`);
}
}
}---
8. Timeout Configuration ✅
Requirement: Set timeouts for external dependencies and event waits
Why: Workflows can run indefinitely without timeouts, consuming resources
Verification:
- [ ]
waitForEvent()has timeout configured - [ ] External API calls have timeout
- [ ] Long-running operations have max duration
- [ ] Timeout errors handled gracefully
Example (Correct):
async run(event: WorkflowEvent, step: WorkflowStep) {
// ✅ Timeout on waitForEvent
try {
const approval = await step.waitForEvent('approval.decision', {
timeout: '24 hours'
});
// Process approval
} catch (error) {
if (error.message.includes('timeout')) {
// Handle timeout - escalate or auto-reject
throw new NonRetryableError('Approval timeout - auto-rejected');
}
throw error;
}
// ✅ Timeout on fetch
await step.do('fetch with timeout', async () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com', {
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('API timeout');
}
throw error;
}
});
}---
9. Workflow Naming Convention ✅
Requirement: Use unique, descriptive workflow names
Why: Workflow names must be unique within an account
Verification:
- [ ] Workflow names follow convention (e.g.,
order-processing,user-onboarding) - [ ] Names describe the workflow's purpose
- [ ] No conflicts with existing workflows
- [ ] Names match between
wrangler.jsoncand code
---
10. Monitoring & Logging ✅
Requirement: Configure monitoring and structured logging
Why: Production workflows need observability for debugging and alerting
Verification:
- [ ] Console.log statements in all critical steps
- [ ] Error logging includes context (instanceId, stepName, payload)
- [ ] Wrangler tail configured for monitoring
- [ ] Alerts set up for workflow failures
Example (Correct):
async run(event: WorkflowEvent, step: WorkflowStep) {
console.log('Workflow started', {
instanceId: event.instanceId,
payload: event.payload
});
const result = await step.do('critical operation', async () => {
try {
const data = await performOperation();
console.log('Operation succeeded', {
instanceId: event.instanceId,
result: data
});
return data;
} catch (error) {
console.error('Operation failed', {
instanceId: event.instanceId,
error: error.message,
stack: error.stack
});
throw error;
}
});
console.log('Workflow completed', {
instanceId: event.instanceId
});
}---
Deployment Workflow
Step 1: Pre-Deployment Testing
# Run local tests
npm test
# Test in dev environment
wrangler dev
# Verify wrangler.jsonc configuration
cat wrangler.jsoncStep 2: Deploy to Production
# Deploy workflow
wrangler deploy
# Verify deployment
wrangler workflows instances list --workflow-name my-workflowStep 3: Monitor Initial Instances
# Watch for errors
wrangler tail my-worker --status error
# Check instance status
wrangler workflows instances list --workflow-name my-workflow --status runningStep 4: Gradual Rollout
- Start with low traffic
- Monitor error rates
- Gradually increase traffic
- Roll back if issues detected
---
Post-Deployment Monitoring
Daily Checks
- [ ] Check workflow failure rate
- [ ] Review error logs
- [ ] Monitor workflow duration
- [ ] Check for stuck instances
Weekly Reviews
- [ ] Analyze workflow performance
- [ ] Optimize slow steps
- [ ] Review retry patterns
- [ ] Update timeout configurations
---
Additional Resources
- Common Issues Guide:
references/common-issues.md - Workflow Patterns:
references/workflow-patterns.md - Wrangler Commands:
references/wrangler-commands.md - Official Docs: https://developers.cloudflare.com/workflows/
---
Remember: Workflows are powerful but require careful attention to I/O context, serialization, and error handling. Take time to complete this checklist—it prevents costly production issues!
Cloudflare Workflows Troubleshooting
Advanced debugging techniques, diagnostic procedures, and solutions for complex workflow issues.
Diagnostic Decision Tree
Workflow Issue
│
├─ Deployment fails?
│ ├─ "Class not found" → Check exports (Section 1)
│ ├─ "Invalid configuration" → Validate wrangler.jsonc (Section 2)
│ └─ TypeScript errors → Check types (Section 3)
│
├─ Runtime error?
│ ├─ "I/O in run() method" → Move to step.do() (Section 4)
│ ├─ "Non-serializable" → Fix return values (Section 5)
│ ├─ "Step timeout" → Optimize or batch (Section 6)
│ └─ "NonRetryableError" → Check error handling (Section 7)
│
├─ Instance stuck?
│ ├─ In "queued" → Check instance limits (Section 8)
│ ├─ In "running" → Check for blocking operations (Section 9)
│ └─ In "paused" → Resume or investigate (Section 10)
│
└─ Performance issue?
├─ Slow execution → Optimize steps (Section 11)
├─ High cost → Reduce requests (Section 12)
└─ High retry rate → Fix flaky operations (Section 13)---
Section 1: Class Export Issues
Symptom
Error: Workflow class "MyWorkflow" not foundDiagnosis
# Check if class is exported from main entry
grep "export.*MyWorkflow" src/index.ts
# Check if class extends WorkflowEntrypoint
grep "class MyWorkflow extends WorkflowEntrypoint" src/**/*.tsSolution
1. Export from main entry file:
// src/index.ts
export { MyWorkflow } from './workflows/my-workflow';2. Verify wrangler.jsonc matches:
{
"workflows": [
{
"class_name": "MyWorkflow" // Must match exported class name
}
]
}3. Check for typos:
- Class name is case-sensitive
- File name doesn't need to match class name
- But export statement must use exact class name
---
Section 2: Configuration Issues
Symptom
Error: Invalid wrangler.jsonc configurationDiagnosis
# Validate JSON syntax (strip comments)
grep -v '^[[:space:]]*//' wrangler.jsonc | jq '.'
# Check workflow configuration
./scripts/validate-workflow-config.shCommon Configuration Errors
Missing required fields:
// Wrong - missing required fields
{
"workflows": [{ "class_name": "MyWorkflow" }]
}
// Correct - all required fields
{
"workflows": [
{
"binding": "MY_WORKFLOW", // Required
"name": "my-workflow", // Required
"class_name": "MyWorkflow" // Required
}
]
}Duplicate workflow names:
// Wrong - duplicate names
{
"workflows": [
{ "binding": "WF1", "name": "workflow", "class_name": "Workflow1" },
{ "binding": "WF2", "name": "workflow", "class_name": "Workflow2" }
]
}
// Correct - unique names
{
"workflows": [
{ "binding": "WF1", "name": "workflow-1", "class_name": "Workflow1" },
{ "binding": "WF2", "name": "workflow-2", "class_name": "Workflow2" }
]
}Invalid binding names:
// Wrong - lowercase binding
{ "binding": "myWorkflow" }
// Correct - SCREAMING_SNAKE_CASE
{ "binding": "MY_WORKFLOW" }---
Section 3: TypeScript Errors
Symptom
Type 'X' is not assignable to type 'Y'Diagnosis
# Run TypeScript check
npx tsc --noEmitCommon Type Issues
Missing Env interface:
// Wrong - Workflow binding not in Env
interface Env {
KV: KVNamespace;
}
// Correct - include workflow binding
interface Env {
KV: KVNamespace;
MY_WORKFLOW: Workflow; // Add this
}Incorrect generic types:
// Wrong - missing generics
export class MyWorkflow extends WorkflowEntrypoint {
// Correct - specify Env and Params
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {Missing workers-types:
# Install latest types
npm install -D @cloudflare/workers-types@latest---
Section 4: I/O Context Errors
Symptom
Error: Cannot perform I/O outside of a stepDiagnosis
Look for await statements in run() method that are not inside step.do():
// Wrong - I/O outside step.do()
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const data = await fetch('https://api.example.com'); // Error!
await step.do('process', async () => {
return process(data);
});
}Solution
Move ALL I/O inside step.do():
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const data = await step.do('fetch data', async () => {
const response = await fetch('https://api.example.com');
return response.json();
});
await step.do('process', async () => {
return process(data);
});
}Also applies to:
env.KV.get/putenv.D1.prepare().run()env.R2.put/get- Any external API calls
- Database operations
---
Section 5: Serialization Errors
Symptom
Error: Cannot serialize step resultDiagnosis
Check for non-JSON-serializable values in step returns:
# Find potential issues
grep -n "return.*new Date()\|return.*function\|return.*undefined" src/workflows/*.tsNon-Serializable Types
| Type | Problem | Solution |
|---|---|---|
Date | Object, not primitive | Use .toISOString() |
undefined | Not JSON-valid | Use null or omit |
Function | Cannot serialize | Remove or use identifier |
Symbol | Cannot serialize | Use string |
BigInt | Not JSON-native | Convert to string |
| Circular refs | Infinite recursion | Break cycles |
Solution
// Wrong
await step.do('get data', async () => {
return {
createdAt: new Date(), // Object
callback: () => {}, // Function
value: undefined, // undefined
bigNum: 12345678901234567890n // BigInt
};
});
// Correct
await step.do('get data', async () => {
return {
createdAt: new Date().toISOString(), // String
callbackId: 'process-callback', // Identifier
value: null, // Explicit null
bigNum: '12345678901234567890' // String
};
});---
Section 6: Step Timeout Issues
Symptom
Error: Step exceeded 30 second CPU limitDiagnosis
Find long-running operations:
# Find loops inside steps
grep -B5 -A10 "step\.do" src/workflows/*.ts | grep -E "for|while"Solution
Batch large operations:
// Wrong - may timeout
await step.do('process all', async () => {
for (const item of largeArray) {
await processItem(item); // 1000 items × 0.1s = 100s
}
});
// Correct - batched
const batchSize = 50;
for (let i = 0; i < largeArray.length; i += batchSize) {
await step.do(`batch-${i}`, async () => {
const batch = largeArray.slice(i, i + batchSize);
return Promise.all(batch.map(processItem));
});
}Use parallel processing within batches:
await step.do('process batch', async () => {
return Promise.all(items.map(async (item) => {
return await processItem(item);
}));
});---
Section 7: NonRetryableError Issues
Symptom
- Errors retry forever
- Expected failures don't stop workflow
- Missing error context
Diagnosis
# Check NonRetryableError usage
grep -n "NonRetryableError" src/workflows/*.ts
# Check for empty constructors
grep -n "new NonRetryableError()" src/workflows/*.tsSolution
Import NonRetryableError:
import { NonRetryableError } from 'cloudflare:workflows';Use descriptive messages:
// Wrong - no message
throw new NonRetryableError();
// Correct - descriptive
throw new NonRetryableError('Order validation failed: missing customer email');Categorize errors correctly:
await step.do('call api', async () => {
const response = await fetch(url);
if (!response.ok) {
// 4xx = permanent failure
if (response.status >= 400 && response.status < 500) {
throw new NonRetryableError(`Client error: ${response.status}`);
}
// 5xx = transient, will retry
throw new Error(`Server error: ${response.status}`);
}
return response.json();
});---
Section 8: Queued Instances
Symptom
Instances stuck in "queued" status
Diagnosis
# Check running instances
wrangler workflows instances list my-workflow --status running
# Count running (max 100)
wrangler workflows instances list my-workflow --status running | wc -lCauses
1. 100 running instances limit: New instances wait in queue 2. Slow step execution: Instances don't complete fast enough 3. Waiting for events: Instances blocked on waitForEvent
Solution
Optimize workflow speed:
- Reduce step count
- Batch operations
- Use parallel processing
Handle backpressure:
// Check queue before creating new instances
export default {
async fetch(req: Request, env: Env) {
const instances = await env.MY_WORKFLOW.list();
const running = instances.filter(i => i.status === 'running');
if (running.length >= 90) {
return Response.json({
error: 'Queue full',
queuePosition: running.length
}, { status: 429 });
}
const instance = await env.MY_WORKFLOW.create({ params });
return Response.json({ id: instance.id });
}
};---
Section 9: Stuck Running Instances
Symptom
Instances stay in "running" status indefinitely
Diagnosis
# Find long-running instances
wrangler workflows instances list my-workflow --status runningCauses
1. Infinite loop in step.do(): Step never completes 2. waitForEvent without timeout: Waiting forever 3. Deadlock: Waiting for external event that won't arrive
Solution
Always set waitForEvent timeout:
// Wrong - waits forever
const event = await step.waitForEvent('approval', 'user.approved');
// Correct - with timeout
const event = await step.waitForEvent('approval', 'user.approved', {
timeout: '24 hours'
});
if (!event) {
throw new NonRetryableError('Approval timeout');
}Terminate stuck instances:
wrangler workflows instances terminate my-workflow <instance-id>Add circuit breaker:
await step.do('call api', async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 25000);
try {
const response = await fetch(url, { signal: controller.signal });
return response.json();
} finally {
clearTimeout(timeout);
}
});---
Section 10: Paused Instances
Symptom
Instances in "paused" status
Causes
1. Explicit pause: instance.pause() was called 2. System pause: Rate limiting or maintenance
Solution
Resume manually:
wrangler workflows instances resume my-workflow <instance-id>Resume programmatically:
const instance = await env.MY_WORKFLOW.get(instanceId);
await instance.resume();---
Section 11: Slow Execution
Diagnosis
# Benchmark workflow
./scripts/benchmark-workflow.sh my-workflow 10Optimization Strategies
1. Reduce step count:
// Slow: 3 steps
await step.do('validate', () => validate(data));
await step.do('transform', () => transform(data));
await step.do('save', () => save(data));
// Fast: 1 step
await step.do('process', async () => {
const validated = await validate(data);
const transformed = await transform(validated);
return await save(transformed);
});2. Use Promise.all():
await step.do('fetch all', async () => {
const [users, orders, products] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/orders').then(r => r.json()),
fetch('/products').then(r => r.json())
]);
return { users, orders, products };
});3. Cache expensive operations:
await step.do('get config', async () => {
// Check cache first
const cached = await env.KV.get('config', 'json');
if (cached) return cached;
// Fetch and cache
const config = await fetchConfig();
await env.KV.put('config', JSON.stringify(config), { expirationTtl: 3600 });
return config;
});---
Section 12: High Cost
Diagnosis
# Check step count
grep -c "step\.do" src/workflows/*.ts
# Check for polling patterns
grep -B5 -A5 "for.*step\.do\|while.*step\.do" src/workflows/*.tsCost Reduction Strategies
1. Replace polling with sleep:
// Expensive: 10 steps
for (let i = 0; i < 10; i++) {
const done = await step.do(`check-${i}`, () => checkStatus());
if (done) break;
}
// Cheap: 2 steps + free sleep
await step.sleep('wait', '5 minutes'); // FREE
const done = await step.do('check', () => checkStatus());2. Replace polling with events:
// Expensive: polling
for (let i = 0; i < 60; i++) {
const status = await step.do(`poll-${i}`, () => getStatus());
if (status === 'complete') break;
await step.sleep('wait', '1 minute');
}
// Cheap: event-driven
await step.do('initiate', () => startProcess());
const event = await step.waitForEvent('complete', 'process.complete', {
timeout: '1 hour'
});---
Section 13: High Retry Rate
Diagnosis
Check logs for retry patterns:
wrangler tail my-worker | grep -i "retry"Solutions
1. Configure appropriate retry strategy:
await step.do('flaky api', {
retries: {
limit: 5,
delay: '5 seconds',
backoff: 'exponential'
}
}, async () => {
return await callFlakyAPI();
});2. Add circuit breaker:
const FAILURE_THRESHOLD = 5;
const RESET_TIMEOUT = 60000;
let failures = 0;
let lastFailure = 0;
await step.do('protected call', async () => {
// Check circuit breaker
if (failures >= FAILURE_THRESHOLD) {
if (Date.now() - lastFailure < RESET_TIMEOUT) {
throw new NonRetryableError('Circuit breaker open');
}
failures = 0; // Reset after timeout
}
try {
const result = await riskyOperation();
failures = 0; // Reset on success
return result;
} catch (error) {
failures++;
lastFailure = Date.now();
throw error;
}
});3. Use idempotency keys:
await step.do('payment', async () => {
const idempotencyKey = `${event.instanceId}-payment`;
return await fetch('https://payment.api/charge', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({ amount })
});
});---
Debugging Tools Quick Reference
| Tool | Command | Purpose |
|---|---|---|
| Validate config | ./scripts/validate-workflow-config.sh | Check configuration |
| Test workflow | ./scripts/test-workflow.sh | Run test instance |
| Benchmark | ./scripts/benchmark-workflow.sh | Measure performance |
| Check limits | ./scripts/check-workflow-limits.sh | Validate against limits |
| List instances | wrangler workflows instances list | See all instances |
| Describe instance | wrangler workflows instances describe | Get instance details |
| View logs | wrangler tail | Stream real-time logs |
| TypeScript check | npx tsc --noEmit | Validate types |
---
When to Load This Reference
Load this file when:
- Debugging complex workflow issues
- Errors don't match common patterns
- Need systematic diagnostic approach
- Performance optimization needed
- High retry or error rates observed
Cloudflare Workflows - Production Patterns
Last Updated: 2025-10-22
This document provides battle-tested patterns for building production-ready Cloudflare Workflows.
---
Table of Contents
1. Idempotency Patterns 2. Error Handling Patterns 3. Long-Running Process Patterns 4. Human-in-the-Loop Patterns 5. Workflow Chaining Patterns 6. Testing Patterns 7. Monitoring Patterns
---
Idempotency Patterns
Pattern 1: Idempotency Keys
Problem: Workflow steps may execute multiple times due to retries.
Solution: Use idempotency keys to ensure operations execute only once.
export class PaymentWorkflow extends WorkflowEntrypoint<Env, PaymentParams> {
async run(event: WorkflowEvent<PaymentParams>, step: WorkflowStep) {
const { orderId, amount } = event.payload;
// Generate idempotency key from workflow instance ID + step name
const idempotencyKey = `${event.instanceId}-charge-payment`;
const paymentResult = await step.do('charge payment', async () => {
// Check if already processed
const existing = await this.env.KV.get(`payment:${idempotencyKey}`);
if (existing) {
console.log('Payment already processed, returning cached result');
return JSON.parse(existing);
}
// Process payment
const response = await fetch('https://payment-gateway.example.com/charge', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey // Payment gateway checks this
},
body: JSON.stringify({ orderId, amount })
});
const result = await response.json();
// Cache result
await this.env.KV.put(
`payment:${idempotencyKey}`,
JSON.stringify(result),
{ expirationTtl: 86400 } // 24 hours
});
return result;
});
return { orderId, transactionId: paymentResult.transactionId };
}
}---
Pattern 2: Database Upsert for Idempotency
await step.do('create order', async () => {
// Use INSERT OR REPLACE to make idempotent
await this.env.DB.prepare(`
INSERT INTO orders (id, user_id, amount, status, created_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
user_id = excluded.user_id,
amount = excluded.amount,
updated_at = CURRENT_TIMESTAMP
`).bind(
orderId,
userId,
amount,
'pending',
new Date().toISOString()
).run();
return { orderId };
});---
Error Handling Patterns
Pattern 1: Categorize Errors for Retry Logic
async function shouldRetry(error: Error): Promise<boolean> {
// Don't retry on client errors (4xx)
if (error.message.includes('400') ||
error.message.includes('401') ||
error.message.includes('403') ||
error.message.includes('404')) {
return false;
}
// Retry on server errors (5xx) and network errors
return true;
}
await step.do('call API', async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
const error = new Error(`API error: ${response.status}`);
if (!await shouldRetry(error)) {
throw new NonRetryableError(error.message);
}
throw error; // Will retry
}
return await response.json();
} catch (error) {
if (error instanceof NonRetryableError) {
throw error;
}
// Network error - retry
throw error;
}
});---
Pattern 2: Circuit Breaker
export class CircuitBreaker {
constructor(
private kv: KVNamespace,
private serviceName: string,
private threshold: number = 5,
private resetTime: number = 60000 // 1 minute
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
const key = `circuit:${this.serviceName}`;
const state = await this.kv.get(key, 'json') as {
failures: number;
lastFailure: number;
} | null;
// Check if circuit is open
if (state && state.failures >= this.threshold) {
const elapsed = Date.now() - state.lastFailure;
if (elapsed < this.resetTime) {
throw new NonRetryableError(
`Circuit breaker open for ${this.serviceName}`
);
}
}
try {
const result = await fn();
// Reset on success
await this.kv.delete(key);
return result;
} catch (error) {
// Increment failure count
const newState = {
failures: (state?.failures || 0) + 1,
lastFailure: Date.now()
};
await this.kv.put(key, JSON.stringify(newState), {
expirationTtl: this.resetTime / 1000
});
throw error;
}
}
}
// Usage
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const circuitBreaker = new CircuitBreaker(this.env.KV, 'external-api');
await step.do('call external API', async () => {
return await circuitBreaker.call(async () => {
const response = await fetch('https://external-api.example.com/data');
return await response.json();
});
});
}
}---
Pattern 3: Graceful Degradation
await step.do('fetch user preferences', async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}/preferences`);
if (!response.ok) throw new Error('Failed to fetch preferences');
return await response.json();
} catch (error) {
console.warn('Failed to fetch preferences, using defaults:', error);
// Fallback to defaults
return {
theme: 'light',
language: 'en',
notifications: true
};
}
});---
Long-Running Process Patterns
Pattern 1: Polling with Exponential Backoff
export class VideoProcessingWorkflow extends WorkflowEntrypoint<Env, VideoParams> {
async run(event: WorkflowEvent<VideoParams>, step: WorkflowStep) {
const { videoId } = event.payload;
// Submit video for processing
const jobId = await step.do('submit video', async () => {
const response = await fetch('https://processor.example.com/jobs', {
method: 'POST',
body: JSON.stringify({ videoId })
});
const data = await response.json();
return data.jobId;
});
// Poll for completion with exponential backoff
let complete = false;
let attempt = 0;
const maxAttempts = 20;
while (!complete && attempt < maxAttempts) {
// Wait with exponential backoff: 10s, 20s, 40s, ...
const delay = Math.min(10 * Math.pow(2, attempt), 300); // Max 5 minutes
await step.sleep(`wait attempt ${attempt}`, `${delay} seconds`);
const status = await step.do(`check status attempt ${attempt}`, async () => {
const response = await fetch(
`https://processor.example.com/jobs/${jobId}/status`
);
return await response.json();
});
if (status.state === 'complete') {
complete = true;
} else if (status.state === 'failed') {
throw new NonRetryableError(`Processing failed: ${status.error}`);
}
attempt++;
}
if (!complete) {
throw new Error('Processing timeout after maximum attempts');
}
return { videoId, jobId, status: 'complete' };
}
}---
Pattern 2: Progress Tracking
export class DataMigrationWorkflow extends WorkflowEntrypoint<Env, MigrationParams> {
async run(event: WorkflowEvent<MigrationParams>, step: WorkflowStep) {
const { totalRecords, batchSize } = event.payload;
const batches = Math.ceil(totalRecords / batchSize);
for (let i = 0; i < batches; i++) {
const progress = await step.do(`migrate batch ${i}`, async () => {
const offset = i * batchSize;
// Migrate batch
await this.migrateBatch(offset, batchSize);
// Update progress in DB
const percentage = Math.round(((i + 1) / batches) * 100);
await this.env.DB.prepare(`
UPDATE migration_jobs
SET progress = ?, updated_at = ?
WHERE id = ?
`).bind(percentage, new Date().toISOString(), event.payload.jobId).run();
return { batch: i + 1, total: batches, percentage };
});
console.log(`Progress: ${progress.percentage}%`);
// Small delay between batches to avoid overwhelming database
if (i < batches - 1) {
await step.sleep(`pause before batch ${i + 1}`, '1 second');
}
}
return { status: 'complete', batches };
}
private async migrateBatch(offset: number, limit: number) {
// Migration logic
}
}---
Human-in-the-Loop Patterns
Pattern 1: Approval with Timeout and Escalation
export class ApprovalWorkflow extends WorkflowEntrypoint<Env, ApprovalParams> {
async run(event: WorkflowEvent<ApprovalParams>, step: WorkflowStep) {
const { requestId, amount } = event.payload;
// Send to primary approver
await step.do('notify primary approver', async () => {
await this.sendApprovalRequest('manager@example.com', requestId);
});
// Wait 48 hours for approval
let approved: boolean;
let approver: string;
try {
const decision = await step.waitForEvent<ApprovalEvent>(
'wait for primary approval',
{ type: 'approval-decision', timeout: '48 hours' }
);
approved = decision.approved;
approver = decision.approverId;
} catch (error) {
// Timeout - escalate to senior manager
console.log('Primary approval timeout, escalating');
await step.do('notify senior approver', async () => {
await this.sendApprovalRequest('senior-manager@example.com', requestId);
});
// Wait another 24 hours
const escalatedDecision = await step.waitForEvent<ApprovalEvent>(
'wait for escalated approval',
{ type: 'approval-decision', timeout: '24 hours' }
);
approved = escalatedDecision.approved;
approver = escalatedDecision.approverId;
}
if (approved) {
await step.do('execute approved action', async () => {
// Execute the action
});
}
return { requestId, approved, approver };
}
private async sendApprovalRequest(to: string, requestId: string) {
// Send notification
}
}---
Workflow Chaining Patterns
Pattern 1: Parent-Child Workflows
export class OrderWorkflow extends WorkflowEntrypoint<Env, OrderParams> {
async run(event: WorkflowEvent<OrderParams>, step: WorkflowStep) {
const { orderId } = event.payload;
// Step 1: Process payment (separate workflow)
const paymentWorkflow = await step.do('start payment workflow', async () => {
const instance = await this.env.PAYMENT_WORKFLOW.create({
params: { orderId, amount: event.payload.amount }
});
return { instanceId: instance.id };
});
// Step 2: Wait for payment to complete
let paymentComplete = false;
while (!paymentComplete) {
await step.sleep('wait for payment', '30 seconds');
const paymentStatus = await step.do('check payment status', async () => {
const instance = await this.env.PAYMENT_WORKFLOW.get(
paymentWorkflow.instanceId
);
return await instance.status();
});
if (paymentStatus.status === 'complete') {
paymentComplete = true;
} else if (paymentStatus.status === 'errored') {
throw new Error(`Payment failed: ${paymentStatus.error}`);
}
}
// Step 3: Start fulfillment workflow
await step.do('start fulfillment workflow', async () => {
await this.env.FULFILLMENT_WORKFLOW.create({
params: { orderId }
});
});
return { orderId, status: 'processing' };
}
}---
Testing Patterns
Pattern 1: Mock External APIs
import { describe, it, expect, beforeEach } from 'vitest';
import { unstable_dev } from 'wrangler';
describe('PaymentWorkflow', () => {
let worker;
beforeEach(async () => {
worker = await unstable_dev('src/index.ts', {
experimental: { disableExperimentalWarning: true }
});
});
it('should process payment successfully', async () => {
// Mock fetch to return success
globalThis.fetch = async (url: string) => {
if (url.includes('payment-gateway')) {
return new Response(JSON.stringify({
transactionId: 'TXN-123',
status: 'success'
}));
}
return new Response('Not found', { status: 404 });
};
const response = await worker.fetch('/workflows/create', {
method: 'POST',
body: JSON.stringify({
orderId: 'ORD-123',
amount: 99.99
})
});
const data = await response.json();
expect(data.id).toBeDefined();
});
});---
Monitoring Patterns
Pattern 1: Structured Logging
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
this.log('info', 'Workflow started', {
instanceId: event.instanceId,
params: event.payload
});
try {
await step.do('process data', async () => {
this.log('info', 'Processing data', { userId: event.payload.userId });
// Process
return { processed: true };
});
this.log('info', 'Workflow completed successfully', {
instanceId: event.instanceId
});
} catch (error) {
this.log('error', 'Workflow failed', {
instanceId: event.instanceId,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw error;
}
}
private log(level: string, message: string, data: any) {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...data
}));
}
}---
Pattern 2: Metrics Tracking
await step.do('track metrics', async () => {
const metrics = {
workflowId: event.instanceId,
stepName: 'payment-processing',
duration: performance.now() - startTime,
status: 'success',
timestamp: new Date().toISOString()
};
// Store in Analytics Engine
await this.env.ANALYTICS.writeDataPoint(metrics);
return metrics;
});---
Best Practices Summary
Always Do
1. Use idempotency keys for external API calls 2. Categorize errors - retry on transient failures, fail fast on terminal errors 3. Log structured data - JSON logs for easy querying 4. Track progress - update database for long-running processes 5. Use exponential backoff - for polling and retries 6. Test workflows - unit tests with mocked dependencies 7. Monitor metrics - track success rates, durations, errors
Never Do
1. Don't retry non-idempotent operations infinitely - use retry limits 2. Don't ignore timeout errors - handle gracefully with fallbacks 3. Don't block on external events without timeout - always set timeout 4. Don't assume steps execute in order - each step is independent 5. Don't return non-serializable values - only JSON-compatible types 6. Don't store sensitive data in workflow state - use KV/D1 instead 7. Don't forget to clean up resources - terminate unused workflow instances
---
Last Updated: 2025-10-22 Maintainer: Claude Skills Maintainers | maintainers@example.com
Wrangler Commands for Cloudflare Workflows
Complete reference for managing Cloudflare Workflows via the wrangler CLI.
Last Updated: 2025-11-26 Wrangler Version: 4.50.0+
---
Workflow Management
Create Workflow
wrangler workflows create <WORKFLOW_NAME>Creates a new workflow definition in your account.
Example:
wrangler workflows create order-processing---
Instance Management
List Workflow Instances
wrangler workflows instances list --workflow-name <NAME> [OPTIONS]Options:
--workflow-name <NAME>- Required: Name of the workflow--status <STATUS>- Filter by status:running,complete,failed,terminated--limit <NUMBER>- Number of instances to return (default: 10)
Examples:
# List all instances
wrangler workflows instances list --workflow-name order-processing
# List only running instances
wrangler workflows instances list --workflow-name order-processing --status running
# List last 50 instances
wrangler workflows instances list --workflow-name order-processing --limit 50---
Get Instance Details
wrangler workflows instances describe <INSTANCE_ID> --workflow-name <NAME>Returns detailed information about a specific workflow instance including:
- Instance ID
- Status (running, complete, failed, terminated)
- Start time
- End time (if completed)
- Current step
- Error details (if failed)
Example:
wrangler workflows instances describe 550e8400-e29b-41d4-a716-446655440000 --workflow-name order-processing---
Terminate Instance
wrangler workflows instances terminate <INSTANCE_ID> --workflow-name <NAME>Stops a running workflow instance immediately. The instance will be marked as terminated.
Example:
wrangler workflows instances terminate 550e8400-e29b-41d4-a716-446655440000 --workflow-name order-processingUse cases:
- Canceling long-running workflows
- Stopping stuck workflows
- Manual intervention in error scenarios
---
Trigger Workflow (via HTTP)
While not a direct wrangler command, workflows are typically triggered via HTTP:
# Trigger workflow via Worker API
curl -X POST https://my-worker.example.com/workflow \
-H "Content-Type: application/json" \
-d '{"orderId": "123", "userId": "user-456"}'The Worker then creates the workflow instance:
const instance = await env.MY_WORKFLOW.create({
params: { orderId: "123", userId: "user-456" }
});---
Deployment Commands
Deploy Workflow
wrangler deployDeploys your workflow to Cloudflare Workers. Must be run from the project directory containing wrangler.jsonc.
Pre-deployment checklist:
- [ ]
wrangler.jsoncconfigured with workflow bindings - [ ] All workflow code in
src/directory - [ ] Dependencies installed (
npm install) - [ ] TypeScript compiled (if using TS)
Example deployment flow:
# Install dependencies
npm install
# Build TypeScript (if applicable)
npm run build
# Deploy
wrangler deploy
# Verify deployment
wrangler workflows instances list --workflow-name my-workflow---
Dev Mode (Local Testing)
wrangler devRuns your Worker (including workflows) locally for testing.
Limitations in dev mode:
- Workflow persistence may behave differently
- Some timing features may not work exactly as in production
- Always test in production before full rollout
---
Monitoring & Debugging
Check Workflow Status
# Get specific instance status
wrangler workflows instances describe <INSTANCE_ID> --workflow-name <NAME>
# List recent failures
wrangler workflows instances list --workflow-name <NAME> --status failed --limit 20---
Debug Stuck Workflows
Step 1: Find stuck instances
wrangler workflows instances list --workflow-name my-workflow --status runningStep 2: Get instance details
wrangler workflows instances describe <INSTANCE_ID> --workflow-name my-workflowStep 3: Check logs
wrangler tail my-workerStep 4: Terminate if needed
wrangler workflows instances terminate <INSTANCE_ID> --workflow-name my-workflow---
Monitor Logs
wrangler tail <WORKER_NAME>Streams real-time logs from your Worker, including workflow execution logs.
Filter logs:
# Filter by status
wrangler tail my-worker --status error
# Filter by search term
wrangler tail my-worker --search "workflow"---
Production Workflow
Complete Workflow Lifecycle
# 1. Deploy workflow
wrangler deploy
# 2. Trigger workflow (via HTTP or scheduled)
# (Happens automatically based on triggers)
# 3. Monitor instances
wrangler workflows instances list --workflow-name my-workflow --status running
# 4. Check for failures
wrangler workflows instances list --workflow-name my-workflow --status failed
# 5. Investigate failures
wrangler workflows instances describe <FAILED_INSTANCE_ID> --workflow-name my-workflow
# 6. Terminate if stuck
wrangler workflows instances terminate <STUCK_INSTANCE_ID> --workflow-name my-workflow---
Troubleshooting
Workflow Not Starting
# Check if workflow is deployed
wrangler deploy
# Verify bindings in wrangler.jsonc
cat wrangler.jsonc | grep -A 5 "workflows"
# Check Worker logs for trigger errors
wrangler tail my-worker --status error---
Instance Stuck in "Running" State
# Get instance details
wrangler workflows instances describe <INSTANCE_ID> --workflow-name my-workflow
# Check which step it's stuck on
# Look for "current_step" in output
# Terminate if necessary
wrangler workflows instances terminate <INSTANCE_ID> --workflow-name my-workflow---
High Failure Rate
# List recent failures
wrangler workflows instances list --workflow-name my-workflow --status failed --limit 50
# Investigate first failure
wrangler workflows instances describe <FIRST_FAILED_ID> --workflow-name my-workflow
# Check for common error patterns
wrangler tail my-worker --search "NonRetryableError"---
Configuration Reference
wrangler.jsonc Setup
{
"name": "my-worker",
"main": "src/index.ts",
"workflows": [
{
"name": "my-workflow",
"class_name": "MyWorkflow",
"binding": "MY_WORKFLOW"
}
]
}---
Official Documentation
- Wrangler Commands: https://developers.cloudflare.com/workers/wrangler/commands/#workflows
- Workflows CLI Reference: https://developers.cloudflare.com/workflows/reference/wrangler-commands/
- Debugging Workflows: https://developers.cloudflare.com/workflows/observability/
---
Note: All commands require authentication. Run wrangler login if not already authenticated.
/**
* Basic Cloudflare Workflow Example
*
* Demonstrates:
* - WorkflowEntrypoint class
* - step.do() for executing work
* - step.sleep() for delays
* - Accessing environment bindings
* - Returning state from workflow
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
// Define environment bindings
type Env = {
MY_WORKFLOW: Workflow;
// Add your bindings here:
// MY_KV: KVNamespace;
// DB: D1Database;
// MY_BUCKET: R2Bucket;
};
// Define workflow parameters
type Params = {
userId: string;
email: string;
};
/**
* Basic Workflow
*
* Three-step workflow that:
* 1. Fetches user data
* 2. Processes user data
* 3. Sends notification
*/
export class BasicWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// Access parameters from event.payload
const { userId, email } = event.payload;
console.log(`Starting workflow for user ${userId}`);
// Step 1: Fetch user data
const userData = await step.do('fetch user data', async () => {
// Example: Fetch from external API
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
return {
id: data.id,
name: data.name,
email: data.email,
preferences: data.preferences
};
});
console.log(`Fetched user: ${userData.name}`);
// Step 2: Process user data
const processedData = await step.do('process user data', async () => {
// Example: Perform some computation
return {
userId: userData.id,
processedAt: new Date().toISOString(),
status: 'processed'
};
});
// Step 3: Wait before sending notification
await step.sleep('wait before notification', '5 minutes');
// Step 4: Send notification
await step.do('send notification', async () => {
// Example: Send email or push notification
await fetch('https://api.example.com/notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: email,
subject: 'Processing Complete',
body: `Your data has been processed at ${processedData.processedAt}`
})
});
return { sent: true, timestamp: Date.now() };
});
// Return final state (must be serializable)
return {
userId,
status: 'complete',
processedAt: processedData.processedAt
};
}
}
/**
* Worker that triggers the workflow
*/
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
// Handle favicon
if (url.pathname.startsWith('/favicon')) {
return Response.json({}, { status: 404 });
}
// Get instance status if ID provided
const instanceId = url.searchParams.get('instanceId');
if (instanceId) {
const instance = await env.MY_WORKFLOW.get(instanceId);
const status = await instance.status();
return Response.json({
id: instanceId,
status
});
}
// Create new workflow instance
const instance = await env.MY_WORKFLOW.create({
params: {
userId: '123',
email: 'user@example.com'
}
});
return Response.json({
id: instance.id,
details: await instance.status(),
statusUrl: `${url.origin}?instanceId=${instance.id}`
});
}
};
/**
* Circuit Breaker Workflow
*
* Implements the Circuit Breaker pattern for resilient external service calls.
* Prevents cascade failures by detecting unhealthy services and failing fast.
*
* Key Concepts:
* - Circuit states: CLOSED (normal), OPEN (failing fast), HALF_OPEN (testing)
* - Failure threshold before opening circuit
* - Automatic recovery after timeout
* - Fallback behavior when circuit is open
*
* Usage:
* 1. Copy this file to src/workflows/
* 2. Update Env and Params types for your use case
* 3. Configure circuit breaker settings
* 4. Export from src/index.ts
* 5. Add to wrangler.jsonc workflows array
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';
// Environment bindings
interface Env {
CIRCUIT_WORKFLOW: Workflow;
KV: KVNamespace; // For storing circuit state across instances
}
// Workflow input parameters
interface Params {
serviceId: string; // ID of the service to call
endpoint: string; // API endpoint
payload?: unknown; // Request payload
fallbackValue?: unknown; // Value to return when circuit is open
}
// Circuit breaker configuration
interface CircuitConfig {
failureThreshold: number; // Failures before opening (default: 5)
resetTimeoutMs: number; // Time before trying again (default: 60000)
halfOpenSuccesses: number; // Successes to close circuit (default: 3)
monitorWindowMs: number; // Time window for failure counting (default: 60000)
}
// Circuit state stored in KV
interface CircuitState {
status: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
failures: number;
successes: number;
lastFailure: number;
lastSuccess: number;
openedAt?: number;
}
// Default configuration
const DEFAULT_CONFIG: CircuitConfig = {
failureThreshold: 5,
resetTimeoutMs: 60000, // 1 minute
halfOpenSuccesses: 3,
monitorWindowMs: 60000 // 1 minute window for counting failures
};
/**
* Circuit Breaker implementation using KV for distributed state
*/
class CircuitBreaker {
private env: Env;
private serviceId: string;
private config: CircuitConfig;
constructor(env: Env, serviceId: string, config: Partial<CircuitConfig> = {}) {
this.env = env;
this.serviceId = serviceId;
this.config = { ...DEFAULT_CONFIG, ...config };
}
private getKey(): string {
return `circuit:${this.serviceId}`;
}
async getState(): Promise<CircuitState> {
const stored = await this.env.KV.get(this.getKey(), 'json');
if (!stored) {
return {
status: 'CLOSED',
failures: 0,
successes: 0,
lastFailure: 0,
lastSuccess: 0
};
}
return stored as CircuitState;
}
async setState(state: CircuitState): Promise<void> {
await this.env.KV.put(
this.getKey(),
JSON.stringify(state),
{ expirationTtl: 86400 } // 24 hour TTL
);
}
async canExecute(): Promise<{ allowed: boolean; state: CircuitState }> {
const state = await this.getState();
const now = Date.now();
switch (state.status) {
case 'CLOSED':
// Normal operation - allow execution
return { allowed: true, state };
case 'OPEN':
// Check if reset timeout has elapsed
if (state.openedAt && now - state.openedAt >= this.config.resetTimeoutMs) {
// Transition to HALF_OPEN
const newState: CircuitState = {
...state,
status: 'HALF_OPEN',
successes: 0
};
await this.setState(newState);
return { allowed: true, state: newState };
}
// Still open - fail fast
return { allowed: false, state };
case 'HALF_OPEN':
// Allow limited traffic to test recovery
return { allowed: true, state };
default:
return { allowed: true, state };
}
}
async recordSuccess(): Promise<CircuitState> {
const state = await this.getState();
const now = Date.now();
const newState: CircuitState = {
...state,
lastSuccess: now,
successes: state.successes + 1
};
if (state.status === 'HALF_OPEN') {
// Check if we've had enough successes to close
if (newState.successes >= this.config.halfOpenSuccesses) {
newState.status = 'CLOSED';
newState.failures = 0;
newState.openedAt = undefined;
console.log(`Circuit ${this.serviceId}: HALF_OPEN -> CLOSED (recovered)`);
}
} else if (state.status === 'CLOSED') {
// Reset failure count on success
newState.failures = 0;
}
await this.setState(newState);
return newState;
}
async recordFailure(): Promise<CircuitState> {
const state = await this.getState();
const now = Date.now();
// Only count failures within the monitoring window
const recentFailures = state.lastFailure > now - this.config.monitorWindowMs
? state.failures + 1
: 1;
const newState: CircuitState = {
...state,
failures: recentFailures,
lastFailure: now,
successes: 0
};
if (state.status === 'HALF_OPEN') {
// Any failure in HALF_OPEN reopens the circuit
newState.status = 'OPEN';
newState.openedAt = now;
console.log(`Circuit ${this.serviceId}: HALF_OPEN -> OPEN (still failing)`);
} else if (state.status === 'CLOSED') {
// Check if we've exceeded failure threshold
if (newState.failures >= this.config.failureThreshold) {
newState.status = 'OPEN';
newState.openedAt = now;
console.log(`Circuit ${this.serviceId}: CLOSED -> OPEN (threshold exceeded)`);
}
}
await this.setState(newState);
return newState;
}
}
/**
* Call external service with timeout
*/
async function callService(
endpoint: string,
payload?: unknown,
timeoutMs = 10000
): Promise<{ success: boolean; data?: unknown; error?: string }> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(endpoint, {
method: payload ? 'POST' : 'GET',
headers: { 'Content-Type': 'application/json' },
body: payload ? JSON.stringify(payload) : undefined,
signal: controller.signal
});
if (!response.ok) {
return {
success: false,
error: `HTTP ${response.status}: ${response.statusText}`
};
}
const data = await response.json();
return { success: true, data };
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return { success: false, error: 'Request timeout' };
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
} finally {
clearTimeout(timeout);
}
}
export class CircuitBreakerWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const { serviceId, endpoint, payload, fallbackValue } = event.payload;
console.log('Starting circuit breaker workflow', {
instanceId: event.instanceId,
serviceId,
endpoint
});
// Initialize circuit breaker
const circuitBreaker = new CircuitBreaker(this.env, serviceId, {
failureThreshold: 5,
resetTimeoutMs: 60000,
halfOpenSuccesses: 3
});
// Check circuit state
const circuitCheck = await step.do('check circuit', async () => {
const { allowed, state } = await circuitBreaker.canExecute();
console.log(`Circuit ${serviceId} state:`, {
status: state.status,
failures: state.failures,
allowed
});
return { allowed, state };
});
// If circuit is open, return fallback immediately
if (!circuitCheck.allowed) {
console.log(`Circuit ${serviceId} is OPEN - using fallback`);
if (fallbackValue !== undefined) {
return {
status: 'fallback',
circuitState: circuitCheck.state.status,
data: fallbackValue
};
}
throw new NonRetryableError(
`Circuit breaker for ${serviceId} is OPEN. Service unavailable.`
);
}
// Attempt to call the service
const result = await step.do(
'call service',
{
retries: {
limit: 2,
delay: '1 second',
backoff: 'constant'
}
},
async () => {
const response = await callService(endpoint, payload);
if (!response.success) {
// Record failure and throw to trigger retry
await circuitBreaker.recordFailure();
throw new Error(response.error);
}
// Record success
await circuitBreaker.recordSuccess();
return response.data;
}
);
return {
status: 'success',
circuitState: 'CLOSED',
data: result
};
}
}
/**
* Multi-Service Circuit Breaker Workflow
*
* Demonstrates calling multiple services with independent circuit breakers.
*/
interface MultiServiceParams {
services: Array<{
id: string;
endpoint: string;
required: boolean; // If true, workflow fails when circuit is open
}>;
}
export class MultiServiceWorkflow extends WorkflowEntrypoint<Env, MultiServiceParams> {
async run(event: WorkflowEvent<MultiServiceParams>, step: WorkflowStep) {
const { services } = event.payload;
const results: Record<string, { success: boolean; data?: unknown; error?: string }> = {};
for (const service of services) {
const circuitBreaker = new CircuitBreaker(this.env, service.id);
const result = await step.do(`call ${service.id}`, async () => {
const { allowed, state } = await circuitBreaker.canExecute();
if (!allowed) {
if (service.required) {
throw new NonRetryableError(
`Required service ${service.id} unavailable (circuit OPEN)`
);
}
return { success: false, error: 'Circuit open', skipped: true };
}
const response = await callService(service.endpoint);
if (response.success) {
await circuitBreaker.recordSuccess();
} else {
await circuitBreaker.recordFailure();
}
return response;
});
results[service.id] = result;
}
const allRequired = services
.filter(s => s.required)
.every(s => results[s.id]?.success);
return {
allRequiredSucceeded: allRequired,
results
};
}
}
/**
* Retry with Backoff and Circuit Breaker
*
* Combines Cloudflare's built-in retry with circuit breaker pattern.
*/
export class RetryWithCircuitWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const { serviceId, endpoint, payload } = event.payload;
const circuitBreaker = new CircuitBreaker(this.env, serviceId, {
failureThreshold: 3,
resetTimeoutMs: 30000 // 30 seconds
});
// First check: is circuit already open?
const preCheck = await step.do('pre-check circuit', async () => {
const { allowed, state } = await circuitBreaker.canExecute();
return { allowed, status: state.status };
});
if (!preCheck.allowed) {
// Wait for circuit reset timeout
await step.sleep('wait for circuit reset', '30 seconds');
// Re-check after waiting
const reCheck = await step.do('re-check circuit', async () => {
const { allowed, state } = await circuitBreaker.canExecute();
return { allowed, status: state.status };
});
if (!reCheck.allowed) {
throw new NonRetryableError(
`Service ${serviceId} still unavailable after waiting`
);
}
}
// Attempt with Cloudflare's retry + manual circuit breaker updates
const result = await step.do(
'call with retry',
{
retries: {
limit: 5,
delay: '2 seconds',
backoff: 'exponential' // 2s, 4s, 8s, 16s, 32s
}
},
async () => {
const response = await callService(endpoint, payload);
if (!response.success) {
// Record failure for circuit breaker
const state = await circuitBreaker.recordFailure();
// If circuit just opened, throw NonRetryableError to stop retries
if (state.status === 'OPEN') {
throw new NonRetryableError(
`Circuit opened after ${state.failures} failures: ${response.error}`
);
}
// Otherwise throw regular error to continue retrying
throw new Error(response.error);
}
// Success - record it
await circuitBreaker.recordSuccess();
return response.data;
}
);
return {
status: 'success',
data: result
};
}
}
/**
* Parallel Execution Workflow
*
* Demonstrates advanced patterns for executing multiple operations concurrently
* while respecting Cloudflare Workflows' sequential step model.
*
* Key Concepts:
* - Promise.all() for concurrent operations WITHIN a step
* - Batching for large datasets
* - Fan-out/fan-in pattern
* - Aggregation of parallel results
*
* Usage:
* 1. Copy this file to src/workflows/
* 2. Update Env and Params types for your use case
* 3. Export from src/index.ts
* 4. Add to wrangler.jsonc workflows array
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';
// Environment bindings
interface Env {
PARALLEL_WORKFLOW: Workflow;
KV: KVNamespace; // For storing intermediate results
}
// Workflow input parameters
interface Params {
items: string[]; // Items to process in parallel
batchSize?: number; // Items per batch (default: 10)
concurrency?: number; // Parallel requests per batch (default: 5)
}
// Result types
interface ProcessResult {
id: string;
success: boolean;
data?: unknown;
error?: string;
}
interface BatchResult {
batchIndex: number;
results: ProcessResult[];
duration: number;
}
interface FinalResult {
totalItems: number;
successful: number;
failed: number;
batches: number;
totalDuration: number;
results: ProcessResult[];
}
/**
* Process a single item (simulated API call)
*/
async function processItem(itemId: string): Promise<ProcessResult> {
try {
// Simulate API call with random success/failure
const response = await fetch(`https://api.example.com/process/${itemId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: itemId, timestamp: Date.now() })
});
if (!response.ok) {
return {
id: itemId,
success: false,
error: `HTTP ${response.status}`
};
}
const data = await response.json();
return {
id: itemId,
success: true,
data
};
} catch (error) {
return {
id: itemId,
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Process items with concurrency limit
*/
async function processWithConcurrency(
items: string[],
concurrency: number
): Promise<ProcessResult[]> {
const results: ProcessResult[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const promise = processItem(item).then(result => {
results.push(result);
});
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
// Remove completed promises
const completed = executing.findIndex(p =>
p.then(() => true).catch(() => true)
);
if (completed !== -1) {
executing.splice(completed, 1);
}
}
}
// Wait for remaining
await Promise.all(executing);
return results;
}
/**
* Chunk array into batches
*/
function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
export class ParallelExecutionWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<FinalResult> {
const { items, batchSize = 10, concurrency = 5 } = event.payload;
const startTime = Date.now();
console.log('Starting parallel execution workflow', {
instanceId: event.instanceId,
totalItems: items.length,
batchSize,
concurrency
});
// Validate input
await step.do('validate input', async () => {
if (!items || !Array.isArray(items)) {
throw new NonRetryableError('Items must be an array');
}
if (items.length === 0) {
throw new NonRetryableError('Items array cannot be empty');
}
if (items.length > 10000) {
throw new NonRetryableError('Maximum 10000 items allowed');
}
return { valid: true, count: items.length };
});
// Split into batches
const batches = chunkArray(items, batchSize);
const allResults: ProcessResult[] = [];
const batchResults: BatchResult[] = [];
console.log(`Processing ${items.length} items in ${batches.length} batches`);
// Process each batch as a separate step
// This ensures durability - completed batches won't re-run on retry
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const batchResult = await step.do(
`process batch ${i + 1}/${batches.length}`,
{
retries: {
limit: 3,
delay: '5 seconds',
backoff: 'exponential'
}
},
async () => {
const batchStart = Date.now();
// Process items WITHIN this step in parallel
const results = await processWithConcurrency(batch, concurrency);
const duration = Date.now() - batchStart;
console.log(`Batch ${i + 1} complete`, {
items: batch.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
duration
});
return {
batchIndex: i,
results,
duration
};
}
);
batchResults.push(batchResult);
allResults.push(...batchResult.results);
// Optional: Store intermediate results in KV for very large workflows
if (items.length > 1000 && (i + 1) % 10 === 0) {
await step.do(`checkpoint batch ${i + 1}`, async () => {
await this.env.KV.put(
`workflow:${event.instanceId}:progress`,
JSON.stringify({
completedBatches: i + 1,
totalBatches: batches.length,
processedItems: allResults.length
}),
{ expirationTtl: 86400 } // 24 hours
);
return { checkpointed: true };
});
}
}
// Aggregate final results
const finalResult = await step.do('aggregate results', async () => {
const successful = allResults.filter(r => r.success).length;
const failed = allResults.filter(r => !r.success).length;
return {
totalItems: items.length,
successful,
failed,
batches: batches.length,
totalDuration: Date.now() - startTime,
results: allResults
};
});
// Clean up intermediate state
if (items.length > 1000) {
await step.do('cleanup', async () => {
await this.env.KV.delete(`workflow:${event.instanceId}:progress`);
return { cleaned: true };
});
}
console.log('Parallel execution complete', {
instanceId: event.instanceId,
totalItems: finalResult.totalItems,
successful: finalResult.successful,
failed: finalResult.failed,
duration: finalResult.totalDuration
});
return finalResult;
}
}
/**
* Alternative: Fan-Out/Fan-In Pattern
*
* For scenarios where you need to spawn multiple sub-workflows
* and aggregate their results.
*/
export class FanOutFanInWorkflow extends WorkflowEntrypoint<Env, { taskGroups: string[][] }> {
async run(
event: WorkflowEvent<{ taskGroups: string[][] }>,
step: WorkflowStep
) {
const { taskGroups } = event.payload;
// Fan-Out: Create sub-workflow for each task group
const subWorkflowIds = await step.do('fan out', async () => {
const ids: string[] = [];
for (let i = 0; i < taskGroups.length; i++) {
const instance = await this.env.PARALLEL_WORKFLOW.create({
id: `${event.instanceId}-group-${i}`,
params: { items: taskGroups[i] }
});
ids.push(instance.id);
}
return ids;
});
// Wait for all sub-workflows (with timeout)
await step.sleep('wait for sub-workflows', '5 minutes');
// Fan-In: Collect results from all sub-workflows
const aggregatedResults = await step.do('fan in', async () => {
const results: Array<{ groupId: string; status: string }> = [];
for (const id of subWorkflowIds) {
try {
const instance = await this.env.PARALLEL_WORKFLOW.get(id);
const status = await instance.status();
results.push({ groupId: id, status: status.status });
} catch (error) {
results.push({ groupId: id, status: 'error' });
}
}
return results;
});
return {
subWorkflows: subWorkflowIds.length,
results: aggregatedResults,
allComplete: aggregatedResults.every(r => r.status === 'complete')
};
}
}