Back to skills
SkillHub ClubShip Full StackFull Stack

cloudflare-workflows

Cloudflare Workflows durable execution playbook: multi-step orchestration, state persistence, retries, sleep/scheduling, waitForEvent, external events, bindings, lifecycle management, limits, pricing. Keywords: Cloudflare Workflows, durable execution, WorkflowEntrypoint, step.do, step.sleep, waitForEvent, sendEvent, retries, NonRetryableError, Workflow binding.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
10
Hot score
84
Updated
March 20, 2026
Overall rating
C1.4
Composite score
1.4
Best-practice grade
C64.8

Install command

npx @skill-hub/cli install itechmeat-llm-code-cloudflare-workflows
cloudflareworkfloworchestrationserverlessautomation

Repository

itechmeat/llm-code

Skill path: skills/cloudflare-workflows

Cloudflare Workflows durable execution playbook: multi-step orchestration, state persistence, retries, sleep/scheduling, waitForEvent, external events, bindings, lifecycle management, limits, pricing. Keywords: Cloudflare Workflows, durable execution, WorkflowEntrypoint, step.do, step.sleep, waitForEvent, sendEvent, retries, NonRetryableError, Workflow binding.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: itechmeat.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install cloudflare-workflows into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/itechmeat/llm-code before adding cloudflare-workflows to shared team environments
  • Use cloudflare-workflows for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: cloudflare-workflows
description: "Cloudflare Workflows durable execution playbook: multi-step orchestration, state persistence, retries, sleep/scheduling, waitForEvent, external events, bindings, lifecycle management, limits, pricing. Keywords: Cloudflare Workflows, durable execution, WorkflowEntrypoint, step.do, step.sleep, waitForEvent, sendEvent, retries, NonRetryableError, Workflow binding."
---

# Cloudflare Workflows

Workflows provide durable multi-step execution for Workers. Steps persist state, survive restarts, support retries, and can sleep for days.

---

## Quick Start

### Create Workflow

```typescript
// src/index.ts
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from "cloudflare:workers";

interface Env {
  MY_WORKFLOW: Workflow;
}

interface Params {
  userId: string;
  action: string;
}

export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    const user = await step.do("fetch user", async () => {
      const resp = await fetch(`https://api.example.com/users/${event.payload.userId}`);
      return resp.json();
    });

    await step.sleep("wait before processing", "1 hour");

    const result = await step.do("process action", async () => {
      return { processed: true, user: user.id };
    });

    return result; // Available in instance.status().output
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const instance = await env.MY_WORKFLOW.create({
      params: { userId: "123", action: "activate" },
    });
    return Response.json({ instanceId: instance.id });
  },
};
```

### wrangler.jsonc

```jsonc
{
  "name": "my-workflow-worker",
  "main": "src/index.ts",
  "workflows": [
    {
      "name": "my-workflow",
      "binding": "MY_WORKFLOW",
      "class_name": "MyWorkflow"
    }
  ]
}
```

### Deploy

```bash
npx wrangler deploy
```

---

## Core Concepts

### WorkflowEntrypoint

```typescript
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    // Workflow logic with steps
    return optionalResult;
  }
}
```

### WorkflowEvent

```typescript
interface WorkflowEvent<T> {
  payload: Readonly<T>; // Immutable input params
  timestamp: Date; // Creation time
  instanceId: string; // Unique instance ID
}
```

**Warning**: Event payload is immutable. Changes are NOT persisted across steps. Return state from steps instead.

### WorkflowStep

```typescript
interface WorkflowStep {
  do<T>(name: string, callback: () => Promise<T>): Promise<T>;
  do<T>(name: string, config: StepConfig, callback: () => Promise<T>): Promise<T>;
  sleep(name: string, duration: Duration): Promise<void>;
  sleepUntil(name: string, timestamp: Date | number): Promise<void>;
  waitForEvent<T>(name: string, options: WaitOptions): Promise<T>;
}
```

See [api.md](references/api.md) for full type definitions.

---

## Steps

### Basic Step

```typescript
const result = await step.do("step name", async () => {
  const response = await fetch("https://api.example.com/data");
  return response.json(); // State persisted
});
```

### Step with Config

```typescript
const data = await step.do(
  "call external API",
  {
    retries: {
      limit: 10,
      delay: "30 seconds",
      backoff: "exponential",
    },
    timeout: "5 minutes",
  },
  async () => {
    return await externalApiCall();
  }
);
```

### Default Step Config

```typescript
const defaultConfig = {
  retries: {
    limit: 5,
    delay: 10000, // 10 seconds
    backoff: "exponential",
  },
  timeout: "10 minutes",
};
```

| Option            | Type          | Default     | Description                                 |
| ----------------- | ------------- | ----------- | ------------------------------------------- |
| `retries.limit`   | number        | 5           | Max attempts (use `Infinity` for unlimited) |
| `retries.delay`   | string/number | 10000       | Delay between retries                       |
| `retries.backoff` | string        | exponential | `constant`, `linear`, `exponential`         |
| `timeout`         | string/number | 10 min      | Per-attempt timeout                         |

---

## Sleep & Scheduling

### Relative Sleep

```typescript
await step.sleep("wait before retry", "1 hour");
await step.sleep("short pause", 5000); // 5 seconds (ms)
```

**Duration units**: `second`, `minute`, `hour`, `day`, `week`, `month`, `year`.

### Sleep Until Date

```typescript
const targetDate = new Date("2024-12-31T00:00:00Z");
await step.sleepUntil("wait until new year", targetDate);

// Or with timestamp
await step.sleepUntil("wait until launch", Date.parse("24 Oct 2024 13:00:00 UTC"));
```

**Maximum sleep**: 365 days.

**Note**: `step.sleep` and `step.sleepUntil` do NOT count towards the 1024 steps limit.

---

## Wait for Events

### Wait in Workflow

```typescript
const approval = await step.waitForEvent<{ approved: boolean }>("wait for approval", {
  type: "user_approval",
  timeout: "7 days",
});

if (approval.approved) {
  await step.do("proceed", async () => {
    /* ... */
  });
}
```

**Default timeout**: 24 hours.

**Timeout behavior**: Throws error and fails instance. Use try-catch to continue:

```typescript
try {
  const event = await step.waitForEvent("optional event", { type: "update", timeout: "1 hour" });
} catch (e) {
  // Continue without event
}
```

### Send Event from Worker

```typescript
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { instanceId, approved } = await request.json();
    const instance = await env.MY_WORKFLOW.get(instanceId);

    await instance.sendEvent({
      type: "user_approval", // Must match waitForEvent type
      payload: { approved },
    });

    return new Response("Event sent");
  },
};
```

**Event buffering**: Events can be sent before Workflow reaches `waitForEvent`. They are buffered and delivered when the matching step executes.

See [events.md](references/events.md) for REST API.

---

## Error Handling

### NonRetryableError

```typescript
import { NonRetryableError } from "cloudflare:workflows";

await step.do("validate input", async () => {
  if (!event.payload.data) {
    throw new NonRetryableError("Missing required data");
  }
  return event.payload.data;
});
```

### Catch and Continue

```typescript
try {
  await step.do("risky operation", async () => {
    await riskyApiCall();
  });
} catch (error) {
  await step.do("handle failure", async () => {
    await sendAlertEmail(error.message);
  });
}
```

### Workflow States

| Status            | Description                                |
| ----------------- | ------------------------------------------ |
| `queued`          | Waiting to start                           |
| `running`         | Actively executing                         |
| `paused`          | Manually paused                            |
| `waiting`         | Sleeping or waiting for event              |
| `waitingForPause` | Pause requested, waiting to take effect    |
| `complete`        | Successfully finished                      |
| `errored`         | Failed (uncaught exception or retry limit) |
| `terminated`      | Manually terminated                        |

---

## Workflow Bindings

### Create Instance

```typescript
const instance = await env.MY_WORKFLOW.create({
  id: "order-12345", // Optional custom ID (max 100 chars)
  params: { orderId: 12345 },
});
console.log(instance.id);
```

### Create Batch

```typescript
const instances = await env.MY_WORKFLOW.createBatch([{ params: { userId: "1" } }, { params: { userId: "2" } }, { params: { userId: "3" } }]);
// Up to 100 instances per batch
```

### Get Instance

```typescript
const instance = await env.MY_WORKFLOW.get("order-12345");
```

### Instance Methods

```typescript
await instance.pause(); // Pause execution
await instance.resume(); // Resume paused
await instance.terminate(); // Stop permanently
await instance.restart(); // Restart from beginning

const status = await instance.status();
console.log(status.status); // "running", "complete", etc.
console.log(status.output); // Return value from run()
console.log(status.error); // { name, message } if errored

await instance.sendEvent({
  type: "approval",
  payload: { approved: true },
});
```

---

## Rules of Workflows

### ✅ DO

- Make steps granular and self-contained
- Return state from steps (only way to persist)
- Name steps deterministically
- `await` all step calls
- Keep step return values under 1 MiB
- Use idempotent operations in steps
- Base conditions on `event.payload` or step returns

### ❌ DON'T

- Store state outside steps (lost on restart)
- Mutate `event.payload` (changes not persisted)
- Use non-deterministic step names (`Date.now()`, `Math.random()`)
- Skip `await` on step calls
- Put entire logic in one step
- Call multiple unrelated services in one step
- Do heavy CPU work in single step

### Step State Persistence

```typescript
// ✅ Good: Return state from step
const userData = await step.do("fetch user", async () => {
  return await fetchUser(userId);
});

// ❌ Bad: State stored outside step (lost on restart)
let userData;
await step.do("fetch user", async () => {
  userData = await fetchUser(userId); // Will be lost!
});
```

---

## Wrangler Commands

```bash
# List instances
wrangler workflows instances list my-workflow

# Describe instance
wrangler workflows instances describe my-workflow --id <instance-id>

# Terminate instance
wrangler workflows instances terminate my-workflow --id <instance-id>

# Trigger new instance
wrangler workflows trigger my-workflow --params '{"key": "value"}'

# Delete Workflow
wrangler workflows delete my-workflow
```

---

## Limits

| Feature                  | Free      | Paid               |
| ------------------------ | --------- | ------------------ |
| CPU time per step        | 10 ms     | 30 sec (max 5 min) |
| Wall clock per step      | Unlimited | Unlimited          |
| State per step           | 1 MiB     | 1 MiB              |
| Event payload            | 1 MiB     | 1 MiB              |
| Total state per instance | 100 MB    | 1 GB               |
| Max sleep duration       | 365 days  | 365 days           |
| Max steps per Workflow   | 1024      | 1024               |
| Concurrent instances     | 25        | 10,000             |
| Instance creation rate   | 100/sec   | 100/sec            |
| Queued instances         | 100,000   | 1,000,000          |
| Subrequests per instance | 50/req    | 1000/req           |
| Instance ID length       | 100 chars | 100 chars          |
| Retention (completed)    | 3 days    | 30 days            |

**Note**: Instances in `waiting` state (sleeping, waiting for event) do NOT count against concurrency limits.

### Increase CPU Limit

```jsonc
{
  "limits": {
    "cpu_ms": 300000 // 5 minutes
  }
}
```

---

## Pricing

Based on Workers Standard pricing:

| Metric   | Free              | Paid                            |
| -------- | ----------------- | ------------------------------- |
| Requests | 100K/day (shared) | 10M/mo included, +$0.30/M       |
| CPU time | 10 ms/invocation  | 30M ms/mo included, +$0.02/M ms |
| Storage  | 1 GB              | 1 GB included, +$0.20/GB-mo     |

**Storage notes**:

- Calculated across all instances (running, sleeping, completed)
- Deleting instances frees storage (updates within minutes)
- Free plan: instance errors if storage limit reached

See [pricing.md](references/pricing.md) for details.

---

## Prohibitions

- ❌ Do not mutate `event.payload` (changes not persisted)
- ❌ Do not store state outside steps
- ❌ Do not use non-deterministic step names
- ❌ Do not exceed 1 MiB per step return
- ❌ Do not skip `await` on step calls
- ❌ Do not rely on in-memory state between steps

---

## References

- [api.md](references/api.md) — Full API reference
- [events.md](references/events.md) — Event handling and REST API
- [patterns.md](references/patterns.md) — Saga, approval, reminders
- [pricing.md](references/pricing.md) — Billing details

## Cross-References (Skills)

- `cloudflare-workers` — Worker development
- `cloudflare-queues` — Message queue integration
- `cloudflare-r2` — Large state storage
- `cloudflare-kv` — Key-value references


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/api.md

```markdown
# Workflows API Reference

## WorkflowEntrypoint

```typescript
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from "cloudflare:workers";

export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<T> {
    // Workflow logic
    return optionalResult;
  }
}
```

## WorkflowEvent

```typescript
interface WorkflowEvent<T> {
  payload: Readonly<T>; // Immutable params passed on create
  timestamp: Date; // Instance creation timestamp
  instanceId: string; // Unique instance identifier
}
```

## WorkflowStep

```typescript
interface WorkflowStep {
  // Execute durable step
  do<T>(name: string, callback: () => Promise<T>): Promise<T>;
  do<T>(name: string, config: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>;

  // Sleep for duration
  sleep(name: string, duration: WorkflowDuration): Promise<void>;

  // Sleep until timestamp
  sleepUntil(name: string, timestamp: Date | number): Promise<void>;

  // Wait for external event
  waitForEvent<T>(name: string, options: WaitForEventOptions): Promise<T>;
}
```

## WorkflowStepConfig

```typescript
interface WorkflowStepConfig {
  retries?: {
    limit: number; // Max attempts (supports Infinity)
    delay: string | number; // Delay between retries
    backoff?: "constant" | "linear" | "exponential";
  };
  timeout?: string | number; // Per-attempt timeout
}
```

**Defaults**:

```typescript
{
  retries: { limit: 5, delay: 10000, backoff: 'exponential' },
  timeout: '10 minutes'
}
```

## WorkflowDuration

Accepts `number` (milliseconds) or human-readable string:

```typescript
type WorkflowDuration = number | string;

// Examples
("1 second");
("30 seconds");
("5 minutes");
("2 hours");
("1 day");
("1 week");
("1 month");
("1 year");
```

## WaitForEventOptions

```typescript
interface WaitForEventOptions {
  type: string; // Event type (max 100 chars)
  timeout?: WorkflowDuration; // Default: 24 hours (max: 365 days)
}
```

## NonRetryableError

```typescript
import { NonRetryableError } from "cloudflare:workflows";

// Immediately fail step without retries
throw new NonRetryableError("Validation failed", "ValidationError");
```

## Workflow Binding

```typescript
interface Workflow<Params = unknown> {
  // Create single instance
  create(options?: WorkflowInstanceCreateOptions<Params>): Promise<WorkflowInstance>;

  // Create up to 100 instances
  createBatch(batch: WorkflowInstanceCreateOptions<Params>[]): Promise<WorkflowInstance[]>;

  // Get existing instance
  get(id: string): Promise<WorkflowInstance>;
}

interface WorkflowInstanceCreateOptions<Params> {
  id?: string; // Custom ID (max 100 chars)
  params?: Params; // JSON-serializable params
}
```

## WorkflowInstance

```typescript
interface WorkflowInstance {
  readonly id: string;

  pause(): Promise<void>;
  resume(): Promise<void>;
  terminate(): Promise<void>;
  restart(): Promise<void>;
  status(): Promise<InstanceStatus>;
  sendEvent(options: SendEventOptions): Promise<void>;
}

interface SendEventOptions {
  type: string; // Must match waitForEvent type
  payload?: unknown; // JSON-serializable data
}
```

## InstanceStatus

```typescript
interface InstanceStatus {
  status: "queued" | "running" | "paused" | "waiting" | "waitingForPause" | "complete" | "errored" | "terminated" | "unknown";
  error?: {
    name: string;
    message: string;
  };
  output?: unknown; // Return value from run()
}
```

## wrangler.jsonc

```jsonc
{
  "name": "my-worker",
  "main": "src/index.ts",
  "workflows": [
    {
      "name": "my-workflow", // Workflow name
      "binding": "MY_WORKFLOW", // Env binding name
      "class_name": "MyWorkflow" // Exported class name
    }
  ],
  "limits": {
    "cpu_ms": 300000 // Max 5 minutes CPU (optional)
  }
}
```

## Step Name Rules

- Max 256 characters
- Must be deterministic (no `Date.now()`, `Math.random()`)
- Acts as cache key for step state
- Duplicate names reuse cached result

```

### references/events.md

```markdown
# Workflow Events

## Overview

Workflows can wait for external events using `step.waitForEvent`. Events can be sent via Worker bindings or REST API.

## Wait for Event

```typescript
const result = await step.waitForEvent<PayloadType>("event name", {
  type: "approval", // Event type to match (max 100 chars)
  timeout: "7 days", // Optional, default: 24 hours
});
```

## Send Event via Binding

```typescript
const instance = await env.MY_WORKFLOW.get(instanceId);

await instance.sendEvent({
  type: "approval", // Must match waitForEvent type
  payload: {
    approved: true,
    approvedBy: "[email protected]",
  },
});
```

## Send Event via REST API

```bash
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/events" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "approval",
    "payload": {
      "approved": true
    }
  }'
```

## Event Buffering

Events can be sent before Workflow reaches `waitForEvent`:

```typescript
// 1. Create instance
const instance = await env.MY_WORKFLOW.create({ params: { orderId: 123 } });

// 2. Send event immediately (before workflow reaches waitForEvent)
await instance.sendEvent({
  type: "payment_confirmed",
  payload: { transactionId: "abc123" },
});

// 3. Workflow will receive event when it reaches matching waitForEvent
```

## Timeout Handling

Default timeout is 24 hours. On timeout, Workflow throws error and fails.

### Continue on Timeout

```typescript
try {
  const event = await step.waitForEvent("optional approval", {
    type: "manager_approval",
    timeout: "1 hour",
  });
  await processApproval(event);
} catch (e) {
  // Timeout occurred, continue with default behavior
  await processDefault();
}
```

## Multiple Events

Wait for different event types in sequence:

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  // Wait for first event
  const orderConfirmed = await step.waitForEvent("order confirmation", {
    type: "order_confirmed",
    timeout: "1 day"
  });

  // Wait for second event
  const paymentReceived = await step.waitForEvent("payment", {
    type: "payment_received",
    timeout: "7 days"
  });

  // Continue processing
  await step.do("fulfill order", async () => {
    // ...
  });
}
```

## Webhook Integration

```typescript
// Webhook handler Worker
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { instanceId, status } = await request.json();

    const instance = await env.MY_WORKFLOW.get(instanceId);
    await instance.sendEvent({
      type: "webhook_callback",
      payload: { status },
    });

    return new Response("OK");
  },
};
```

## Event Payload Limits

- Maximum event payload size: 1 MiB
- Event type: max 100 characters
- Payload must be JSON-serializable

## Best Practices

1. Use descriptive, unique event types
2. Always handle timeout cases with try-catch
3. Include correlation IDs in payloads for debugging
4. Send events as soon as external action completes
5. Use buffering for async event sources

```

### references/pricing.md

```markdown
# Workflows Pricing

## Plan Requirement

Workflows requires **Workers Paid** plan ($5/month).

## Billing Dimensions

| Metric                 | Free                           | Paid                            |
| ---------------------- | ------------------------------ | ------------------------------- |
| Requests (invocations) | 100K/day (shared with Workers) | 10M/mo included, +$0.30/M       |
| CPU time               | 10 ms/invocation               | 30M ms/mo included, +$0.02/M ms |
| Storage                | 1 GB                           | 1 GB included, +$0.20/GB-mo     |

## CPU Time

- Billed on total CPU milliseconds consumed
- Each step consumes CPU time independently
- Default limit: 30 seconds per step (configurable to 5 min)

**Example**: Workflow with 5 steps, each using 100ms CPU

- Total CPU: 500 ms per invocation
- 60K invocations = 30M ms (included in Paid)

## Requests (Invocations)

- Each `create()` call = 1 invocation
- Subrequests from within Workflow do NOT count extra
- `createBatch()` counts as N invocations (one per instance)

**Example**:

- 1M Workflow invocations/month
- Cost: (1M - 10M) = within included, **$0**
- 15M invocations: (15M - 10M) = 5M × $0.30/M = **$1.50**

## Storage

Billed as GB-months across all instances:

- Running instances
- Sleeping instances
- Waiting instances
- Completed instances (until retention expires)

**Storage per instance**:

- Step return values (up to 1 MiB each)
- Event payloads
- Instance metadata

**Retention periods**:
| Plan | Completed Instance Retention |
|------|------------------------------|
| Free | 3 days |
| Paid | 30 days |

**Example**:

- 10K instances, average 10 KB state each
- Total: 100 MB
- Monthly cost: 0.1 GB × $0.20 = **$0.02**

## Cost Optimization

1. **Minimize step return sizes** — return only what you need
2. **Delete completed instances** — reduces storage immediately
3. **Use external storage** — store large data in R2/KV, return only references
4. **Batch instance creation** — reduces API overhead

## Free Plan Limits

- CPU: 10 ms per step invocation
- Concurrent instances: 25
- Storage limit: 1 GB (errors if exceeded)
- Daily requests: 100K (shared with Workers)

## Paid Plan Defaults

- CPU: 30 sec per step (max 5 min with config)
- Concurrent instances: 10,000
- Storage: 1 GB included, expandable

## Storage Calculation

```
storage_gb_month = sum(instance_state_bytes) / (1024^3) × days_stored / 30
```

Deleting instances via API, Wrangler, or dashboard frees storage within minutes.

## No Additional Charges

- ✓ No fees for sleeping instances
- ✓ No fees for waiting instances
- ✓ No per-step fees
- ✓ No egress fees

```

### references/patterns.md

```markdown
# Workflow Patterns

## Saga Pattern (Compensating Transactions)

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  const reservation = await step.do("reserve inventory", async () => {
    return await reserveItems(event.payload.items);
  });

  try {
    const payment = await step.do("charge payment", async () => {
      return await chargeCard(event.payload.paymentMethod);
    });

    await step.do("confirm order", async () => {
      return await confirmOrder(reservation.id, payment.id);
    });
  } catch (error) {
    // Compensating transaction
    await step.do("release inventory", async () => {
      await releaseReservation(reservation.id);
    });
    throw error;
  }
}
```

## Approval Workflow

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  await step.do("send approval request", async () => {
    await sendSlackMessage(event.payload.approverEmail);
  });

  const approval = await step.waitForEvent<{ approved: boolean }>("wait for approval", {
    type: "manager_approval",
    timeout: "7 days"
  });

  if (approval.approved) {
    await step.do("execute action", async () => { /* ... */ });
  } else {
    await step.do("notify rejection", async () => { /* ... */ });
  }
}
```

## Scheduled Reminders

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  for (let i = 0; i < 3; i++) {
    await step.do(`send reminder ${i + 1}`, async () => {
      await sendReminder(event.payload.userId);
    });
    await step.sleep(`wait ${i + 1}`, "1 day");
  }
}
```

## Retry with Exponential Backoff

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  const result = await step.do("call flaky API", {
    retries: {
      limit: 10,
      delay: "1 second",
      backoff: "exponential"  // 1s, 2s, 4s, 8s...
    },
    timeout: "30 seconds"
  }, async () => {
    return await flakyApiCall();
  });
}
```

## Fan-Out / Fan-In

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  const items = event.payload.items;

  // Fan-out: process each item
  const results = [];
  for (const item of items) {
    const result = await step.do(`process ${item.id}`, async () => {
      return await processItem(item);
    });
    results.push(result);
  }

  // Fan-in: aggregate
  const summary = await step.do("aggregate results", async () => {
    return { total: results.length, success: results.filter(r => r.ok).length };
  });

  return summary;
}
```

## Webhook Callback

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  // Start async job
  const jobId = await step.do("start job", async () => {
    const resp = await fetch("https://api.example.com/jobs", {
      method: "POST",
      body: JSON.stringify({ callbackInstanceId: event.instanceId })
    });
    return (await resp.json()).jobId;
  });

  // Wait for callback
  const result = await step.waitForEvent<{ status: string }>("wait for job", {
    type: "job_complete",
    timeout: "1 hour"
  });

  return { jobId, status: result.status };
}

// Webhook handler Worker
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { instanceId, status } = await request.json();
    const instance = await env.MY_WORKFLOW.get(instanceId);
    await instance.sendEvent({ type: "job_complete", payload: { status } });
    return new Response("OK");
  }
};
```

## Timeout with Fallback

```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
  let response;

  try {
    response = await step.waitForEvent<ApiResponse>("wait for API", {
      type: "api_response",
      timeout: "5 minutes"
    });
  } catch (e) {
    // Timeout: use cached/default value
    response = await step.do("get fallback", async () => {
      return await getCachedValue(event.payload.key);
    });
  }

  return response;
}
```

```

cloudflare-workflows | SkillHub