AuthoringxState v5SCXML W3C

xState as the canonical authoring surface

Statecharts replace flat DAGs. Parallel regions, hierarchical states, history, formal transition semantics, SCXML for cross-engine portability, and a free visualizer (Stately) โ€” all without building any of it ourselves.

Why

WDK's grammar is a flat DAG of awaits.

Each step is an await; control flow is JS itself. That works for linear pipelines and breaks down everywhere else โ€” parallel regions, hierarchical states, history, formal verification. xFlow adopts xState as the canonical inner shape so we get those for free.

What xState gets you

  • ยท Parallel regions (multiple states active simultaneously)
  • ยท Hierarchical states (compound/atomic/final/history)
  • ยท Formal transition semantics (SCXML-aligned)
  • ยท Stately Studio for visual authoring + simulation
  • ยท SCXML serialization for cross-engine handoff
  • ยท An ecosystem of test utilities and inspectors

What xFlow adds

  • ยท Statecharts become content-addressed registry entries
  • ยท Round-trip codec to/from WorkflowDefinition
  • ยท Per-step claim/placement/retry config via meta.xflow
  • ยท Substrate-pluggable execution; no xstate runtime dep

Codec

fromXState โ€” author once, run anywhere.

Take any xstate v5 machine config and produce a WorkflowDefinition the runtime can drive.

fromXState
import { fromXState } from "@decoperations/xflow-xstate"

const machine = {
  id: "shop.checkout",
  initial: "load",
  states: {
    load:    { invoke: { src: "cart.load",       onDone: "price"  } },
    price:   { invoke: { src: "cart.price",      onDone: "charge" } },
    charge:  { invoke: { src: "payments.charge", onDone: "done", onError: "review" } },
    review:  { invoke: { src: "human.review",    onDone: "done"   } },
    done:    { type: "final" },
  },
}

const workflow = fromXState(machine, { id: "shop.checkout", version: "1.0.0" })
// โ†’ WorkflowDefinition with steps + links derived from invoke / onDone / onError

Codec (inverse)

toXState โ€” round-trip preserves the topology.

The WorkflowDefinition coming out of fromXState round-trips back to a machine config that you can hand to Stately's visualizer or to another engine.

toXState
import { toXState } from "@decoperations/xflow-xstate"

const machine = toXState(workflow)
// Round-trip preserves: step ids, types, link topology,
// and per-step config (claim, placement, retry, sideEffects, timeoutMs)
// via meta.xflow on each state.

Parallel

Parallel regions become concurrent steps.

A `type: 'parallel'` region flattens into independent steps that share a workflow.start link. xFlow's runtime claims each on whichever node is eligible โ€” no coordination required.

Parallel region โ†’ flat steps
// Parallel regions flatten into independent steps that fire concurrently.
const machine = {
  id: "review",
  initial: "review",
  states: {
    review: {
      type: "parallel",
      states: {
        human: { invoke: { src: "review.human" } },
        fraud: { invoke: { src: "review.fraud" } },
      },
    },
    done: { type: "final" },
  },
}

const wf = fromXState(machine, { id: "review", version: "1.0.0" })
// โ†’ wf.steps:  { "review.human": {...}, "review.fraud": {...} }
// โ†’ wf.links:  workflow.start โ†’ review.human   AND   workflow.start โ†’ review.fraud
// Both branches start together; the runtime claims each on whichever node is eligible.

Hierarchical

Compound states namespace their children.

Nested states get path-namespaced step ids ('main.step1'). Incoming links resolve through to the compound state's initial child automatically.

Compound state expansion
// Compound (nested) states delegate to their initial substate.
const machine = {
  states: {
    before: { invoke: { src: "noop", onDone: "main" } },
    main: {
      type: "compound",
      initial: "step1",
      states: {
        step1: { invoke: { src: "main.step1", onDone: "step2" } },
        step2: { invoke: { src: "main.step2" } },
      },
    },
  },
}
// Step ids namespace by path: "main.step1", "main.step2".
// "before.onDone: main" resolves through to "main.step1" (the initial child).

Per-step config

meta.xflow rides alongside each state.

Claim mode, placement policy, retry policy, side-effect kind, timeout โ€” anything xFlow needs that xstate doesn't model. Standard xstate consumers ignore it; xFlow's codec extracts it.

State-level config via meta.xflow
// Per-step xFlow config rides as meta.xflow on each xstate state.
const machine = {
  id: "deploy",
  initial: "build",
  states: {
    build: {
      invoke: { src: "ci.build", onDone: "deploy" },
      meta: {
        xflow: {
          claim: { mode: "lease", ttlMs: 30_000 },
          retry: { maxAttempts: 3, backoff: { kind: "exponential", initialMs: 200, maxMs: 5000 } },
          timeoutMs: 120_000,
        },
      },
    },
    deploy: { invoke: { src: "ci.deploy", onDone: "done" } },
    done:   { type: "final" },
  },
}

Portability

SCXML 1.0 round-trip.

Any xstate machine serializes to W3C SCXML 1.0 and back. atomic / compound / parallel / final states + invoke onDone/onError + named transitions + meta.xflow all survive. Use it to hand off flows to non-TS engines or archive them in a portable format.

toSCXML / fromSCXML
import { toSCXML, fromSCXML } from "@decoperations/xflow-xstate"

const xml = toSCXML(machine)
// <?xml version="1.0" encoding="UTF-8"?>
// <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" name="shop.checkout" initial="load">
//   <state id="load">
//     <invoke type="action" src="cart.load"/>
//     <transition event="done.invoke" target="price"/>
//   </state>
//   ...
//   <final id="done"></final>
// </scxml>

const back = fromSCXML(xml)
// Round-trip preserves: states (atomic / compound / parallel / final),
// invoke + onDone/onError, named transitions, meta.xflow overrides
// (carried under <meta xmlns:xflow="..."><xflow:overrides>{json}</xflow:overrides></meta>).

Registry

xstate-v5 as a canonical registry format.

When the xstate machine *is* the definition, skip the WorkflowDefinition shape entirely. xstateToFlowDef wraps a machine as a FlowDef with format='xstate-v5'; extensions (parallel-states, history-states) are inferred from the machine shape.

xstate-v5 registry entries
import { xstateToFlowDef, flowDefToXState } from "@decoperations/xflow-xstate"
import { contentHash } from "@decoperations/xflow-registry"

// Wrap an xstate machine as a registry-shaped FlowDef.
// format: "xstate-v5"  โ†’  the machine *is* the canonical definition.
const draft = xstateToFlowDef(machine, { id: "shop.checkout", version: "1.0.0" })
draft.manifest.contentHash = await contentHash(draft)

// Publish via fsRegistry / s3wormRegistry / chainResolvers โ€” same as any other entry.
// Extensions (parallel-states, history-states) auto-inferred from the machine shape.

// Resolve later:
const back = flowDefToXState(entry)  // โ†’ XStateMachineLike, ready for Stately Studio.

Next

Read on.