MallaDocs

Workflow engine

execSteps, the browser, sync and durable adapters, and the replay rules.

Developer guide

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

ClientServer
Steps per run2001,000, lowered by the plan
Nesting depth88
Loop iterations1,0001,000
Retries per step—5
Sync run timeout—30 s
Parallel lanes / switch cases8 / 328 / 32