Client and server workflows share one JSON format and one interpreter, @malla/engine. Only the adapter that runs each step changes. Workflows are interpreted, never compiled: publishing stores an immutable version and one runner serves every site.
Three adapters
BrowserPublished sites and canvas preview. sleep is a timer; waitForEvent is unavailable.
SyncInline in the runner for endpoints and short work. Retries in process; durable steps throw DurableRequired.
DurableCloudflare Workflows (SiteWorkflow): sleep, waitForEvent, retries with backoff, replay.
ts
interface StepAdapter {
do<T>(name: string, retry: RetryPolicy | undefined, fn: () => Promise<T>): Promise<T>
sleep(name: string, duration: string): Promise<void>
waitForEvent(name: string, opts: { type: string; timeout?: string }): Promise<unknown>
}
Step format
ts
interface WorkflowStep {
id: string
action: string // core.<name> or <pluginId>.<name>
params?: Record<string, unknown> // values may be Bindings at any depth
branches?: Record<string, WorkflowStep[]> // then/else, body, try/catch, cases, lanes a…h
disabled?: boolean
retry?: { limit: number; delay?: string; backoff?: 'constant' | 'linear' | 'exponential' }
}
Each step’s result is stored in steps[id]. Control flow nests step lists in branches. A server workflow is durable exactly when it contains an enabled core.sleep or core.waitForEvent.
Replay rules
Cloudflare Workflows re-execute code and return cached results for completed steps, so:
- Step names are deterministic paths,
<branch>/<loop index>/<step>, so loops and branches replay correctly; parallel lanes add<name>/<lane>/<step>. - Everything outside a step is deterministic;
fn.now()runs inside steps. - The workflow version is pinned in the run params, so in-flight runs never see edits.
- Step results kept in history are capped at 16 KB each.
Runner wiring
text
apps/runner (Worker)
├─ POST /run/:siteId/:workflowId test and editor runs (api)
├─ ALL /api/* endpoints + webhooks (sites)
├─ Workflows binding → SiteWorkflow durable runs
├─ Durable Objects → SiteScheduler (cron alarms), RunStream (live test runs),
│ SiteDatabase (tables, visitors)
└─ Worker Loader → runJs isolates (no bindings, no secrets)
Published server workflows live in KV at wf:<environment>:<siteId> with their HTTP routes and cron schedules. A publish reschedules the site’s crons.
Limits
| Client | Server | |
|---|---|---|
| Steps per run | 200 | 1,000, lowered by the plan |
| Nesting depth | 8 | 8 |
| Loop iterations | 1,000 | 1,000 |
| Retries per step | — | 5 |
| Sync run timeout | — | 30 s |
| Parallel lanes / switch cases | 8 / 32 | 8 / 32 |