JustificationForward direction

xFlow vs open WDK

What the Vercel Workflow Development Kit defines as an open spec, what it leaves on the table, and what xFlow adds on top. The justification for a separate package, and an honest accounting of when WDK alone is enough.

xFlow.WTF is being renamed to xFlow.WTF to reflect its position alongside xSync.WTF, xCoder, and xAgent.WTF. The rename ships incrementally β€” current packages remain on the @decoperations/xflow-* scope until the rename PR lands.

Setup

The question this page answers.

WDK is a small, well-scoped open spec. xFlow is additive. This page strips WDK down to its open spec, names what's missing, and shows what xFlow adds. If WDK alone fits your situation, use WDK alone. If the gaps below describe your reality, xFlow is justified.

What is open WDK?

Workflow grammar, step lifecycle, and the World contract. Three things, well specified.

What's not in the spec?

Distribution, language richness, multi-writer, browser participation, substrates outside a World.

When is WDK alone enough?

Single platform, single team, server-only, flat DAGs, no cross-tool consumption.

Baseline

What open WDK actually defines.

Three things. Everything else in Vercel's Workflow product is platform-specific (region pinning, replay caps, payload limits, bundler integration), not part of the open spec. The spec itself is small β€” and that's both its strength and its weakness.

The open WDK spec β€” three pieces
// What the open WDK spec actually gives you, in three pieces.

// 1. Workflow grammar β€” imperative step boundaries inside a function.
async function checkout(input: CheckoutInput) {
  "use workflow"
  const cart  = await step("load-cart",  () => loadCart(input.userId))
  const total = await step("price",      () => priceCart(cart))
  await        step("charge",            () => charge(input.userId, total))
  return total
}

// 2. Step lifecycle β€” events emitted around each step boundary.
//    started / succeeded / failed / cancelled, plus signals.

// 3. World contract β€” the durability surface (`@workflow/world`).
interface World {
  storage:  Storage    // event journal
  queue:    Queue      // pending work
  streamer: Streamer   // realtime fan-out
}
// Any provider that implements World can host the workflow runtime.

1. Workflow grammar

"use workflow", imperative step() boundaries, signals, waitForEvent, deterministic-replay semantics.

2. Step lifecycle

started / succeeded / failed / cancelled events around each step boundary, plus signal events.

3. World contract

@workflow/world = Storage + Queue + Streamer. Any provider that implements all three can host the runtime durably.

Everything else you might associate with WDK β€” the 240s replay cap, function duration ceilings, payload limits, region pinning, the bundler/build tooling β€” is Vercel-platform-specific, not open spec. The case studies on /docs/wdk-gaps cover those platform pain points. This page is about the spec itself.

Gap analysis

What's outside the open spec β€” ten axes.

Each row names what open WDK does (or doesn't do), the consequence of using only WDK, and what xFlow adds on top. xFlow's additions are designed to coexist with WDK runtimes β€” a stock WDK runtime can ignore xFlow's extension fields and still execute the definition correctly.

Axis

Definition distribution

Open WDK: Definitions are JS functions inside a deploy artifact. There is no notion in the spec for publishing a workflow definition to a shared location and resolving it from elsewhere.

Consequence: Cross-team and cross-org sharing is 'copy the file.' Tenant-provided flow defs require a separate sandboxing product. There's no canonical way for ops tools, agent systems, or other services to consume the same definitions an app uses.

What xFlow adds

Definition distribution

A content-addressed, signed, S3-published registry. Flows resolve by id@version from any runtime. The bucket layout is documented as an open spec β€” Switchboard.WTF, FlowStack.WTF, and xAgent.WTF all read it without coupling to xFlow's release cadence.

xFlow β€” registry resolve
const def = await registry.resolve("shop.checkout@1.0.0")
// Same definition. Any runtime. No rebuild.

Axis

Definition language richness

Open WDK: WDK's grammar is imperative step() calls inside a JavaScript function. Control flow is expressed via JS itself: conditionals, loops, awaits.

Consequence: A flow is a flat DAG of awaits. There's no way to express parallel regions, hierarchical states, or history states. There are no formal transition semantics, no SCXML, no visual editor, no static analysis of the state graph without executing JS.

What xFlow adds

Definition language richness

xState as the canonical language. Statecharts give you parallel, hierarchical, and history states; guards and actions; invoked actors; SCXML serialization for W3C interop; and Stately Studio for visual authoring. xState's machine config maps cleanly to the WDK shape for runtimes that only speak WDK.

xFlow β€” parallel review state
// xFlow add #2 β€” xState as the canonical definition language.
// Hierarchical, parallel, history; SCXML out the door.

const checkout = createMachine({
  id: "shop.checkout",
  initial: "loading",
  states: {
    loading:   { invoke: { src: "loadCart", onDone: "pricing" } },
    pricing:   { invoke: { src: "priceCart", onDone: "charging" } },
    charging:  {
      invoke:  { src: "charge", onDone: "done", onError: "review" }
    },
    review:    {                       // <- WDK can't express this
      type: "parallel",
      states: {
        humanApproval: { /* ... */ },
        fraudCheck:    { /* ... */ },
      },
      onDone: "done",
    },
    done:      { type: "final" },
  },
})

// Same machine compiles to:
//   - JSON registry entry (for distribution)
//   - SCXML (for cross-engine handoff)
//   - WDK-shape workflow (for runtimes that only speak WDK)

Axis

JSON-as-definition

Open WDK: Definitions are functions. To inspect, diff, validate, sign, or version a flow, you have to execute or bundle JS.

Consequence: No build-time validation of flow shape. No content hash for a definition. No signing. No diffing. Tenant-provided flows are unsafe by default β€” you'd be running uploaded code.

What xFlow adds

JSON-as-definition

Flow definitions serialize to canonical JSON (RFC 8785) and SCXML. Each registry entry has a sha256 content hash and an ed25519 signature. A flow can be inspected, diffed, and authorized without executing any of its bodies.

xFlow β€” signed, content-hashed manifest
// <root>/xflow/registry/flows/shop.checkout@1.0.0/manifest.json
{
  "id":           "shop.checkout",
  "version":      "1.0.0",
  "contentHash":  "sha256:9f2c…",
  "format":       "xstate-v5",
  "signers":      ["ed25519:0xacme…"],
  "signature":    "…",
  "extensions":   ["parallel-states"]
}

Axis

Multi-writer / placement

Open WDK: WDK assumes a single orchestrator. One process owns a run. Steps execute on whatever provider the World runs on, with no first-class notion of where a step should run.

Consequence: A browser tab, a Next route, and a long-running worker can't all participate in the same run as typed peers. Human-approval steps need a separate channel. Co-located data steps need a separate fan-out service.

What xFlow adds

Multi-writer / placement

Substrate-pluggable claim modes: authority, lease, deterministic-election, optimistic-idempotent. A placement policy attached to each step says where it can run (browser / server / specific worker class). Multiple writers can be members of the same run; the substrate resolves who wins.

xFlow β€” placement on a step
defineStep({
  id: "summarize",
  type: "ai.summarize",
  placement: { kind: "server", capabilities: ["gpu"] },
  claim:     { mode: "lease", ttlMs: 30_000 },
})

Axis

Browser participation

Open WDK: WDK is server-only. Workflow code runs inside the orchestrator on a World provider. The browser is a client of an API, not a peer.

Consequence: Steps that should run client-side β€” human approval, local IO, offline-first capture, OS keychain access β€” need a separate system. There's no single audit trail across server and client work.

What xFlow adds

Browser participation

A flow can have steps with placement: { kind: "browser" } that execute in a tab as typed peers, append events to the same substrate, and join the same run. xSync transports (BroadcastChannel, WS, SSE) make this real today.

xFlow β€” browser-placed step
defineStep({
  id: "approve-purchase",
  type: "human.approval",
  placement: { kind: "browser", trust: "user" },
  claim:     { mode: "authority", actorClass: "browser-tab" },
})

Axis

Substrate beyond a World

Open WDK: Durability is the World contract: Storage + Queue + Streamer. To get durability, you must run something that implements it.

Consequence: The minimum viable deployment is 'a World provider running somewhere.' That's a real piece of infrastructure to set up, monitor, and pay for. Plain Postgres, plain Redis, plain Cloudflare Durable Objects β€” none of them are a World.

What xFlow adds

Substrate beyond a World

A broader XFlowSubstrate interface (append, subscribeEvents, getRunState, acquireClaim). Postgres, Redis, DO, xSync, and S3WORM are all substrates. World becomes one option among many β€” a stronger one for some workloads, but not the only path to durability.

xFlow β€” substrate-pluggable runtime
import { createXFlowRuntime } from "@decoperations/xflow-runtime"
import { postgresSubstrate } from "@decoperations/xflow-substrate-postgres"

const runtime = createXFlowRuntime({
  node: detectLocalNode({ id: "api" }),
  substrate: postgresSubstrate({ url: process.env.DATABASE_URL! }),
})

Axis

Drop-in TS package

Open WDK: Adopting WDK requires a World provider running somewhere β€” either Vercel's hosted offering or a self-hosted implementation.

Consequence: Not actually drop-in. There's a deployment dependency before the first flow runs. Local dev needs a local World; tests need either a fake or a stood-up provider.

What xFlow adds

Drop-in TS package

pnpm add @decoperations/xflow + a Postgres URL = a working runtime. MemorySubstrate makes tests trivial. The substrate-pluggable rewrite makes the dependency a config knob, not a deployment requirement.

xFlow β€” minimum viable runtime
// One package, one env var, runnable.
import { createXFlowRuntime } from "@decoperations/xflow-runtime"
import { postgresSubstrate } from "@decoperations/xflow-substrate-postgres"

export const runtime = createXFlowRuntime({
  node: detectLocalNode({ id: "api" }),
  substrate: postgresSubstrate({ url: process.env.DATABASE_URL! }),
})

Axis

Cross-runtime portability

Open WDK: A WDK definition lives in its deploy artifact. Running the same flow on Vercel and on a Fly.io Docker worker = two builds with two pipelines.

Consequence: Flows can't move freely across runtimes. Dev/prod parity requires running the same World in both places. Multi-cloud or hybrid setups need bespoke wiring per environment.

What xFlow adds

Cross-runtime portability

Flows are JSON definitions in a bucket. A Next route, a Fly Docker worker, a Bitlaunch worker, and a browser tab all resolve the same id@version and execute the same definition. Capabilities are advertised by the worker; placement matches steps to workers.

xFlow β€” same definition, three runtimes
// In Next handler
const def = await registry.resolve("shop.checkout@1.0.0")
await runtime.run(def, input)

// In Fly.io Docker worker (Nitro + Turbo image)
//   xflow-worker boots, advertises capabilities,
//   pulls the same id@version, joins the same run.

// In a browser tab
//   xflow-react useFlowRun() picks up the run,
//   executes its placement: "browser" steps locally.

Axis

Tenant-provided flows

Open WDK: Letting end-users supply their own flow definitions requires a sandboxed compute product (Vercel Sandbox or equivalent). It's not part of the workflow spec.

Consequence: Multi-tenant platforms that want users to upload custom flows have to bolt on a sandbox layer, decide its trust model, and pay for it separately.

What xFlow adds

Tenant-provided flows

A flow def is signed JSON. Loading a tenant's flow is fetch + signature verify + schema validate. The dangerous part β€” running tenant code β€” is scoped to step bodies, which can be policy-checked, sandboxed, or rejected based on capability advertisement.

xFlow β€” load a tenant flow safely
const def = await registry.resolve(tenantFlowId, {
  trustedSigners: [TENANT_PUBKEY],
  allowExtensions: ["parallel-states"],
})
// def is data; nothing runs until runtime.run() picks an executor.

Axis

Open registry format

Open WDK: There is no canonical bucket format, schema, or signing convention for sharing WDK definitions across tools or organizations.

Consequence: Every consumer (ops tools, agent systems, third-party services) needs a bespoke integration to read your flows. There's no 'package registry for workflows.'

What xFlow adds

Open registry format

An open spec for the bucket layout, manifest schema, canonicalization, and signature format. Any HTTP/S3 client can read an xFlow registry. Switchboard.WTF, FlowStack.WTF, xAgent.WTF, and xCoder are all expected consumers.

xFlow β€” bucket layout (excerpt)
s3://acme/<root>/
  xflow/
    registry/
      index.json                              ← signed, lists everything
      flows/<id>@<version>/
        definition.json                       ← canonical JSON
        definition.scxml                      ← W3C interop
        manifest.json                         ← contentHash + signers
      actions/<id>@<version>/...
      services/<id>@<version>/...
    runs/<runId>/
      events/...                              ← live run state

The two new layers

What xFlow adds β€” at a glance.

The ten gaps above collapse into two structural additions on top of open WDK, plus a handful of ergonomic ones that fall out of those two.

Add #1 β€” Registry

Definitions become data. Content-addressed, signed, S3-published, resolvable by id@version from any runtime.

Definition lifecycle
// xFlow add #1 β€” definitions are data. Published once, resolved anywhere.

// Authored as TS (or xState, or hand-written JSON):
const checkout = defineFlow({
  id: "shop.checkout",
  version: "1.0.0",
  // ...statechart goes here
})

// Built and published to a bucket (S3WORM over Storj):
//   <root>/xflow/registry/flows/shop.checkout@1.0.0/definition.json
//   <root>/xflow/registry/flows/shop.checkout@1.0.0/manifest.json
//                                                   ^ signed, content-hashed

// Resolved by any runtime β€” no rebuild, no redeploy:
const def = await registry.resolve("shop.checkout@1.0.0")
runtime.run(def, input)

Add #2 β€” xState as canonical language

Statecharts replace flat DAGs as the definition format. Hierarchical, parallel, history states; SCXML for cross-engine portability; visual editor for free.

Statechart-shaped flow
// xFlow add #2 β€” xState as the canonical definition language.
// Hierarchical, parallel, history; SCXML out the door.

const checkout = createMachine({
  id: "shop.checkout",
  initial: "loading",
  states: {
    loading:   { invoke: { src: "loadCart", onDone: "pricing" } },
    pricing:   { invoke: { src: "priceCart", onDone: "charging" } },
    charging:  {
      invoke:  { src: "charge", onDone: "done", onError: "review" }
    },
    review:    {                       // <- WDK can't express this
      type: "parallel",
      states: {
        humanApproval: { /* ... */ },
        fraudCheck:    { /* ... */ },
      },
      onDone: "done",
    },
    done:      { type: "final" },
  },
})

// Same machine compiles to:
//   - JSON registry entry (for distribution)
//   - SCXML (for cross-engine handoff)
//   - WDK-shape workflow (for runtimes that only speak WDK)

Registry

Content-addressed, signed, S3-published flows / actions / services. Documented as an open spec. WDK definitions can be republished into the registry without re-authoring.

xState as canonical language

Statecharts with full SCXML interop. Stately Studio for design. Codecs for round-tripping into the WDK workflow shape.

Substrate-pluggable runtime

Postgres, Redis, Durable Objects, xSync, S3WORM β€” all viable durability backends. World becomes one option, not the requirement.

Multi-writer + placement

Browsers, Next routes, Docker workers, and edge workers all participate as typed peers in the same run. Steps declare where they can run.

Drop-in TS package

pnpm add and go. Memory substrate for tests, Postgres for hot data, S3WORM for cold. No required infra.

Family interop

Switchboard.WTF, FlowStack.WTF, xAgent.WTF, xCoder all read the same registry. xFlow is the publisher; everyone else is a consumer.

Honest accounting

When WDK alone is sufficient.

If all five conditions hold, xFlow is overkill. Just use WDK. The value xFlow adds is real, but it's not free β€” there's a runtime, a registry, and a substrate to reason about. Don't take that on if you don't need it.

Single-platform Vercel app

You ship to Vercel, full stop. Your workflows fit comfortably under the platform limits. No federation, no other clouds.

Server-only flows

Nothing runs in the browser. No human-approval steps that need a tab. No offline capture. The orchestrator can own everything.

One team, one repo

Definition distribution doesn't matter when there's one consumer. The repo is the registry.

Linear DAG workflows

Your flows are awaits in a row. No parallel regions, no hierarchical states, no history. WDK's grammar fits exactly.

No cross-tool consumption

Flows aren't fed into Switchboard, an agent system, a CI pipeline, or a third party. Your app is the only thing that runs them.

Why this tranche

When xFlow is justified.

The decision isn't 'WDK or xFlow' in the abstract. It's whether the additions in the previous section describe your situation. For this ecosystem tranche β€” multi-product family, cross-runtime, code-first, agent-integrated β€” they do.

Multi-product family

xFlow + Switchboard.WTF + FlowStack.WTF + xAgent.WTF all need to read the same flow definitions. A registry is mandatory, not optional.

Cross-runtime story

Flows must run on Vercel + Fly.io / Bitlaunch Docker workers (Nitro + Turbo) + browser tabs. Single-orchestrator, server-only WDK can't carry this.

Storj + S3WORM as canonical storage

Flow defs and audit logs live in S3WORM-over-Storj buckets. WDK has no bucket-publishing concept; the storage shape is xFlow-shaped, not WDK-shaped.

Code-first authoring with TS-native ergonomics

Drop a package in, define a flow next to the code it orchestrates, ship. No World provider to stand up first.

Agent skill consumption

xCoder consumes flow definitions as agent skills. That requires JSON / SCXML, signed, fetchable, and policy-checkable β€” none of which open WDK provides.

Statechart semantics actually used

Parallel regions, hierarchical states, history states, guards. If your flows are flat DAGs forever you don't need this β€” but the family premise is that they aren't.

Recap

One sentence.

Open WDK defines a small, well-scoped spec for durable workflow execution: a grammar, a step lifecycle, and a World contract.

xFlow adds the layer the spec doesn't cover β€” a portable, content-addressed, signed registry of statechart-shaped flow definitions, runnable on any substrate (World included), consumable by any tool that speaks the open registry format.

For projects in the xFlow / xFlow tranche β€” multi-product, cross-runtime, code-first, agent-integrated β€” that addition pays for itself. For single-platform Vercel apps with flat-DAG workflows, WDK alone is the right call.