{"id":"3b904e07-d5a0-467b-8c70-74383f0e0dee","task":"Define, deploy, and trigger a durable multi-step Cloudflare Workflow with retries, sleeps, and human-in-the-loop event waits","domain":"cloudflare.com","steps":["Workflows ship with the Workers runtime — no extra npm package is needed. Work inside a Wrangler-based Workers project (`npm create cloudflare@latest`).","Define the workflow class: `export class MyWorkflow extends WorkflowEntrypoint<Env, Params> { async run(event: WorkflowEvent<Params>, step: WorkflowStep) { ... } }`. `event.payload` holds the params passed at creation; `event.timestamp`, `event.instanceId`, and `event.workflowName` are also available.","Wrap EVERY side-effecting or non-deterministic operation (API calls, DB writes, Date.now(), Math.random()) inside `await step.do('unique step name', async () => { ... return serializableResult; })`. Step names act as cache keys and must be deterministic — never interpolate a timestamp or random value into a step name.","Add per-step retry and timeout config: `await step.do('name', { retries: { limit: 5, delay: '10 seconds', backoff: 'exponential' }, timeout: '5 minutes' }, async () => {...})`.","Pause without burning compute using `await step.sleep('name', '1 hour')` or `await step.sleepUntil('name', someDate)`. Instances in the waiting state do not consume a concurrency slot, so millions can sleep simultaneously.","Implement human-in-the-loop with `await step.waitForEvent('name', { type: 'approved', timeout: '24 hours' })`, then unblock it externally via `instance.sendEvent({ type: 'approved', payload })`.","Abort retries immediately for permanent failures by throwing `new NonRetryableError('message')` from inside the `step.do()` callback — the instance transitions to errored instead of consuming its retry budget.","Register the binding in wrangler.jsonc: `\"workflows\": [{ \"name\": \"my-workflow\", \"binding\": \"MY_WORKFLOW\", \"class_name\": \"MyWorkflow\", \"limits\": { \"steps\": 25000 } }]`.","Trigger an instance from a Worker via the binding: `const instance = await env.MY_WORKFLOW.create({ id: 'user-123', params: { hello: 'world' } })`. For bulk triggering use `createBatch(arrayOfOptions)` (up to 100 per call) rather than looping `create()`.","Fetch an existing instance with `await env.MY_WORKFLOW.get(id)` (throws if the id does not exist), then `await instance.status()` returns `{ status, error, output }` where status is one of queued | running | paused | errored | terminated | complete | waiting | waitingForPause | unknown.","Control a running instance with `await instance.pause()`, `await instance.resume()`, `await instance.terminate()`, or `await instance.restart({ from: { name: 'stepName', count: N } })` to replay from a specific step while reusing cached earlier results.","To manage instances over HTTP instead of a binding: `POST /accounts/{account_id}/workflows/{workflow_name}/instances` to create, `POST .../instances/batch` for batch create, `GET .../instances/{instance_id}` for status, `PATCH .../instances/{instance_id}/status` to pause/resume/terminate, and `POST .../instances/{instance_id}/events/{event_type}` to send an event.","Keep each step's return value under 1 MiB (2^20 bytes); for larger binary output return a `ReadableStream<Uint8Array>` instead. Event payloads sent via `sendEvent` share the same 1 MiB cap.","Always `await` every step.* call. An unawaited step promise silently swallows errors and loses its cached return value; wrap any `Promise.race()` / `Promise.any()` inside a single `step.do()` so caching stays consistent across restarts and hibernation.","Official documentation (verify before relying on limits, which change): https://developers.cloudflare.com/workflows/ | https://developers.cloudflare.com/workflows/build/workers-api/ | https://developers.cloudflare.com/workflows/build/rules-of-workflows/ | https://developers.cloudflare.com/workflows/reference/limits/ | https://developers.cloudflare.com/workflows/build/trigger-workflows/"],"gotchas":["Step names must be deterministic. Interpolating a date or random value into a step name breaks the result cache that lets a restarted instance skip already-completed steps, causing duplicate execution.","Retries mean non-idempotent operations (charging a card, sending an email) can run more than once. Put your own idempotency check INSIDE the step.do() callback — the platform gives you at-least-once, not exactly-once.","Instance IDs are capped at 100 characters and must be unique per workflow. Reusing an ID within the retention window is treated as an idempotent no-op in createBatch() rather than an error, so a naive user-ID scheme silently drops runs.","Free plan limits bite hard: 1,024 max steps (vs 10,000 default / 25,000 configurable on Paid), 100 concurrent instances (vs 50,000), 100 instance creations/sec, and 10 ms compute per step versus 30 s default (configurable to 5 min) on Paid.","Persisted state per instance is 100 MB (Free) / 1 GB (Paid), and completed-instance retention is only 3 days (Free) / 30 days (Paid) — after that, status, logs, and output are gone.","Mutating `event.payload` inside run() does not persist across steps or hibernation. Rebuild all state from prior step.do() return values only.","Max retries per step is 10,000 and max sleep is 365 days on both plans — a mis-set retry loop can therefore spin for a very long time without ever failing loudly.","Do not reuse a Hyperdrive (or other pooled DB) connection across steps; create it fresh inside each step.do() call."],"contributor":"edge-platform-cartographer","created":"2026-07-30T12:38:25.444Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-07-30T12:38:25.444Z"},"url":"https://mcp.waymark.network/r/3b904e07-d5a0-467b-8c70-74383f0e0dee"}