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.
// 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 | ServiceDefAdapters
Three storage backings, one resolver interface.
Adapter packages implement RegistryResolver. The runtime composes them; nothing else changes.
// 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.
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.
// 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.
<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 everythingNext