📦 HÖMSTYLE · E-commerce

Order Fulfillment Pipeline

A real checkout — cart, email, shipping address — that runs a multi-step order pipeline on Cloudflare Workflows: validate, charge, invoice, render a real PDF, wait for the warehouse, email a confirmation with that PDF attached. Switch to Step-by-step mode and the instance genuinely pauses after every step (a real step.waitForEvent()) until you click Continue — with a verbose 'why this Cloudflare product' explainer for each one. A live System Log shows every real timestamp behind the scenes.

The Problem

""Our order pipeline is a chain of calls — check stock, charge the card, generate an invoice PDF, wait for the warehouse, email the customer with that PDF attached. Any one of those can fail transiently (the payment gateway times out, say), and today that means the whole request fails and we have to build our own retry queue, our own state machine, our own PDF rendering setup, and our own way to resume a half-finished order after a server restart.""

The Outcome

1 class

One Workflow class replaces a hand-rolled retry queue + state machine. Each step.do() call is independently retried with backoff on failure, step.sleep() pauses for free (no compute billed while waiting), and the whole instance survives a Worker restart mid-pipeline — you get this by writing the steps in order, not by building infrastructure for it.

Live demo below
📦

HÖMSTYLE Order Fulfillment

Cloudflare Workflows · durable execution

Subtotal: $899.00

Place an order to see each step's real Worker code, live, as it runs.

Productionising this

What changes when you ship this for real

Workflows is not a native Pages Functions binding

Unlike D1/KV/R2/Durable Objects, [[workflows]] is absent from the Pages Functions supported-bindings list (confirmed against current Cloudflare docs). This demo hosts the Workflow class in the existing cf-demo-agent sidecar Worker and reaches it from Pages via a Service binding + fetch() — see agent.wrangler.toml and src/pages/api/workflow/*.ts.

instance.status() is coarse-grained by design

The JS binding API only reports overall status (queued/running/errored/complete) plus top-level output/error — not per-step detail. Per-step visibility (what this demo's UI shows) needs a side channel; this demo writes a step-progress JSON blob to KV from inside each step.do() callback and polls it alongside instance.status(). The alternative — wrangler workflows instances describe — is a CLI-only view, not something a Worker can call at runtime.

NonRetryableError vs. a plain thrown Error

The "validate order" step throws NonRetryableError for a bad SKU because no amount of retrying fixes a SKU that does not exist — Workflows stops immediately instead of burning through the retry budget. The "charge payment" step throws a plain Error because a gateway timeout genuinely might succeed on retry — that distinction is a real design decision every step needs, not a default to leave unexamined.

Idempotency of side effects — this demo has a real, unresolved gap

A retried step re-runs its entire callback from the top on failure. The payment step is simulated, so a double-charge risk there is moot. The confirmation email is a real Cloudflare Email Service send, though — if env.EMAIL.send() succeeds but the step throws before returning (a network blip, a Workflows-side retry decision, etc.), the retry will send the email again. This demo does not add an idempotency key or a "was this already sent" check before calling send() — a production version must, before going live, exactly because this step is real, not simulated.

Step and instance limits

Workers Paid supports 10,000 steps per Workflow by default (up to 25,000 via workflows.limits.steps in Wrangler config); Workers Free caps at 1,024. Up to 10,000 concurrent *running* instances per account — instances that are waiting (asleep, or waiting on a retry/event) do not count against that limit, which is why step.sleep() for hours/days/weeks is a normal pattern, not something to avoid.

Observability beyond this demo's UI

For real operations, wrangler workflows instances describe <name> <id> gives step-by-step CLI output (status, retries, sleep state, errors) without needing the custom KV side channel this demo built for its own UI.

PDF + email attachment size limits are real constraints

The invoice PDF travels through the workflow as base64 (JSON step return values and the KV step log are both text) — fine for a one-page invoice, but base64 inflates size by ~33%, and Cloudflare Email Service caps total email size (body + attachments) at 25 MiB. A production version generating larger PDFs (multi-page catalogs, embedded images) should watch that ceiling, and consider linking to a PDF hosted in R2 instead of attaching it directly once documents get large.

Browser Rendering is a shared, metered resource

The /pdf Quick Action spins up a real headless browser session per call — it is not free-standing local rendering. High-volume invoice generation should be rate-limited and monitored the same way any other Workers AI or external-API call would be, not treated as a zero-cost local function.

step.waitForEvent() has a real timeout — decide what happens on expiry

This demo's pause points use a 30-minute timeout — if nobody clicks Continue in that window, waitForEvent throws and the instance errors out rather than waiting forever. A production approval-gate workflow needs an explicit decision here: auto-approve after a timeout, escalate to someone else, or fail the whole run — "wait forever" is not a real option, and the default timeout is 24 hours if you do not set one.