| name | workflows |
| description | Implement Cloudflare Workflows for durable execution, ordered multi-step jobs, retries, checkpointing, sleeps, long-running processes, imports, billing flows, AI pipelines, and sagas. Use when a Worker needs reliable progress over multiple steps instead of manual retry state.
|
| compatibility | Cloudflare Workers TypeScript projects using Wrangler; verify current Cloudflare APIs, limits, and pricing before production use. |
| metadata | {"source":"Architecting on Cloudflare plus official Cloudflare Developer Platform docs","generated":"2026-04-28"} |
Workflows
Use this skill for ordered, durable, multi-step processes that need retries, checkpointing, sleeps, human/time delays, or progress across failures.
When to use
Use Workflows for:
- Order fulfillment.
- Multi-step imports/exports.
- Billing, provisioning, and cleanup flows.
- AI pipelines that fetch, transform, embed, store, and notify.
- Delayed retries and durable polling.
Prefer alternatives when:
- Work is a simple fire-and-forget task: use Queues.
- Many clients need live shared state: use Durable Objects.
- Logic is purely stateless and immediate: use Workers.
Coding rules
- Put durable side effects inside
step.do().
- Give steps deterministic, stable names.
- Make every step idempotent; assume retries can happen.
- Store large outputs externally in D1/R2/KV and return small IDs from steps.
- Do not depend on mutable module/global state between steps.
- Pass immutable parameters at workflow creation; do not expect event payloads to change after start.
Workflow skeleton
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from "cloudflare:workers";
export interface Env {
ONBOARDING: Workflow;
DB: D1Database;
JOBS: Queue<{ tenantId: string }>;
}
type Params = {
tenantId: string;
requestedBy: string;
};
export class OnboardingWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const tenant = await step.do("create tenant record", {
retries: { limit: 3, delay: "5 seconds", backoff: "exponential" }
}, async () => {
return this.env.DB.prepare(
"INSERT INTO tenants (id, requested_by) VALUES (?, ?) ON CONFLICT(id) DO NOTHING RETURNING id"
).bind(event.payload.tenantId, event.payload.requestedBy).first<{ id: string }>();
});
await step.do("enqueue provisioning", async () => {
await this.env.JOBS.send({ tenantId: event.payload.tenantId });
});
await step.sleep("wait before verification", "30 seconds");
return { ok: true, tenantId: event.payload.tenantId, tenant };
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const params = await request.json<Params>();
const instance = await env.ONBOARDING.create({
id: `tenant-${params.tenantId}`,
params
});
return Response.json({ id: instance.id });
}
} satisfies ExportedHandler<Env>;
Wrangler binding
{
"workflows": [
{
"name": "onboarding",
"binding": "ONBOARDING",
"class_name": "OnboardingWorkflow"
}
]
}
Idempotency examples
- Use natural primary keys such as
tenantId, orderId, or importId.
- Use
INSERT ... ON CONFLICT DO NOTHING or a processed-events table.
- Use external API idempotency keys when calling payment, email, or provisioning systems.
- Store progress records so manual support can inspect workflow state.
Anti-patterns
- One giant step that hides all failure points.
- Non-deterministic step names based on timestamps/random IDs.
- Returning large blobs from steps instead of R2/D1 references.
- Using Workflows to hold live WebSocket room state.