LayerOpen spec

The xFlow registry

Definitions are data. Content-addressed, signed, and publishable to any S3-compatible bucket — or to disk, or behind plain HTTP. The registry is xFlow's first additive layer over open WDK: the part the spec leaves out, made into a standard format any tool can read.

Premise

Workflow definitions deserve a package registry.

WDK definitions live in deploy artifacts. Sharing one across teams, agents, or runtimes means copying source. xFlow flips it: definitions are JSON in a bucket, addressed by id@version, signed, and resolvable from anywhere.

Content-addressed

Each entry's payload is canonicalized (RFC 8785 JCS) and hashed with sha256. Identical entries hash identically, anywhere.

Signed

ed25519 over the canonical manifest. Verifiers carry a trust list; untrusted entries fall through the resolver chain.

Portable

The bucket layout is documented; any HTTP/S3 client can read a registry. Switchboard, FlowStack, and xAgent are all expected consumers.

Grammar

The manifest envelope.

Every flow / action / service ships with the same metadata header. Codec packages own the inner definition; the grammar package owns the envelope, the canonicalization, the hashing, and the signature.

@decoperations/xflow-registry — types
// Every entry shares the same envelope.
interface RegistryManifest {
  id:           string                 // "shop.checkout"
  version:      string                 // semver
  kind:         "flow" | "action" | "service"
  format:       "xflow-v1" | "xstate-v5" | "scxml-1.0"
  contentHash:  `sha256:${string}`     // covers the definition payload
  exposure:     "private" | "internal" | "published"
  extensions:   RegistryExtension[]    // opt-in features (parallel-states, …)
  createdAt:    string                 // ISO 8601
  signers:      `ed25519:${string}`[]  // public keys
  signature?:   `ed25519:${string}`    // covers the canonical manifest
}

type RegistryEntry = FlowDef | ActionDef | ServiceDef

Adapters

Three storage backings, one resolver interface.

Adapter packages implement RegistryResolver. The runtime composes them; nothing else changes.

Pick your storage
// fs — local disk; the local-only / CLI use case
import { fsRegistry } from "@decoperations/xflow-registry-fs"
const local = fsRegistry({ root: "./xflow-registry" })

// s3worm — S3-compatible bucket; Storj reference target
import { s3wormRegistry } from "@decoperations/xflow-registry-s3worm"
const cloud = s3wormRegistry({
  bucket: "acme-flows",
  s3: { endpoint: "https://gateway.storjshare.io", region: "auto", forcePathStyle: true },
})

// http — read-only fetch; works in browsers + edge runtimes
import { httpRegistry } from "@decoperations/xflow-registry-http"
const remote = httpRegistry({ baseUrl: "https://flows.acme.com" })

@decoperations/xflow-registry-fs

Disk-backed. Single-process; multi-writer is the substrate's concern. Doubles as a cache layer in chained resolvers.

@decoperations/xflow-registry-s3worm

S3-compatible. WORM enforced via If-None-Match: *— content-addressed entries can never be silently overwritten. Reference target: Storj.

@decoperations/xflow-registry-http

Read-only HTTPS fetch. Browser-safe, edge-runtime-safe, no AWS SDK. Pair with s3worm on the publishing side.

Composition

Layered resolution — npm-for-workflows.

Chain resolvers and the registry behaves like a package manager: local first, then a cache, then origin. With trustedSigners + writeThrough you get authenticated, cached, and self-healing.

chainResolvers
import { chainResolvers } from "@decoperations/xflow-registry"

// Layered: try local first, fall through to a cache, then origin.
// trustedSigners gates remote hits — untrusted entries fall through.
// writeThrough caches successful resolves into earlier layers.
const registry = chainResolvers([local, fsCache, remote], {
  trustedSigners: [`ed25519:${TENANT_PUBKEY}`],
  writeThrough: true,
})

const flow = await registry.resolve({
  kind: "flow-ref",
  id: "shop.checkout",
  version: "1.0.0",
})

trustedSigners.When the chain hits an entry whose signature doesn't verify against any trusted public key, the hit falls through. Untrusted layers can't poison the resolver.

writeThrough. When a later layer answers, the entry is written back into earlier layers via their optional add() method. Cache failures are silent — they never fail the resolve.

Cross-references

Flows can invoke other flows by id@version.

A FlowDef carries inline step bodies for everything it owns. Anything it doesn't own becomes a content-addressable reference resolved at runtime.

Cross-flow invocation
// Inside a FlowDef.definition you can invoke another flow / action / service
// by id@version. The reference is content-addressable.
const flow = defineFlow({
  steps: {
    review: {
      type: "human.review",
      // Cross-reference to a versioned action published elsewhere.
      // The runtime resolves it through the active resolver chain.
      ref: { kind: "action-ref", id: "compliance.review", version: "^2.0.0" },
    },
  },
})

Visibility

Three exposure levels gate which registries host which entries.

private

Local or private bucket only. Not for redistribution. The default for xflow init.

internal

Org-wide; signed; lives in a private bucket consumed by org-internal services.

published

Public-readable; signed; can live in a public bucket. Other tools (Switchboard, xAgent, xCoder) consume these.

On disk

The bucket layout.

Documented as an open spec. Same shape for fs, s3worm, and http — every adapter agrees on where things live.

Layout under any registry root
<root>/
  xflow/
    registry/
      flows/
        shop.checkout@1.0.0/
          definition.json    ← canonical JSON (RFC 8785-style)
          manifest.json      ← contentHash + signature + extensions
        media.pipeline@2.1.0/
          ...
      actions/
        payments.charge@1.4.0/
          ...
      services/
        scheduler@0.1.0/
          ...
      index.json             ← optional, signed; lists everything

Next

Read on.