Why Wiggle — the workflow as data, not code§

Durable execution — the promise that a long-running business process survives crashes, restarts, and redeployments, resuming exactly where it left off — has largely converged on one implementation strategy: event-sourced replay. Engines in the Temporal/Cadence lineage persist a history of events for each workflow; to make progress after a crash, a worker re-executes the workflow function from the beginning, feeding recorded results back into it until the code catches up with history.

It's a genuinely elegant trick, and it buys enormous expressiveness. But it has a price every operating team eventually pays.

The determinism tax§

For replay to work, the workflow function must be deterministic: given the same history, it must make exactly the same decisions, in exactly the same order, every time. That outlaws — inside workflow code — random numbers, clock reads, iteration over unordered collections, config lookups, library calls that might change behavior between versions, and, most painfully, ordinary code changes. Deploying a modified workflow while instances are in flight requires version-patching APIs and discipline, because the new code must still replay old histories faithfully.

The failure mode is what makes it a tax rather than a fee: non-determinism does not fail at the point of the mistake. The workflow runs fine — until a worker somewhere replays it and the re-execution diverges from history. The bug detonates during recovery, in production, possibly weeks after it was introduced, in a workflow that "worked in every test."

The question worth asking: what exactly is being replayed, and why? The answer is control flow. The engine replays your code because your code is the only representation of the workflow's structure it has. Which suggests an alternative: give the engine the structure directly.

Wiggle's bet: the graph is the workflow§

A business process is described as a graph — named steps and how they chain, branch, and rejoin — and that graph, not any function, is what the server owns:

FlowSpec orders = FlowSpec.define("order-fulfilment", 1, Order.class, OrderSteps.class, (f, s) -> {
    var checked = f.thenApply(s::validate)
            .thenFilter(s::inStock);         // false ⇒ the instance ends cleanly

    // continuing `checked` twice is the fan-out; the arms run on isolated copies
    var payment  = checked.thenApply(s::authorise, RetryPolicy.exponential(5, Duration.ofMillis(100)))
                          .thenApply(s::capture);
    var shipping = checked.thenApply(s::reserveStock)
                          .thenApply(s::printLabel);

    return Wiggle.allOf(payment, shipping)
            .combine(s::merge)    // mandatory — there is no implicit join
            .thenApply(s::notify);
});

Nothing in that snippet executes. The chain is walked once at definition time and compiled to a graph; s is an inert stand-in that only lets the body name its steps, and registering the result ships the graph to the server as data. A running instance is a set of tokens positioned on the graph, in the spirit of a Petri net — and every token is a row in a database.

Recovery, in this model, is almost embarrassingly boring. There is no history to replay and no function to re-execute, because the engine never lost the state in the first place — the state is the rows. A crashed server restarts and reads where every instance stands. A crashed worker is handled by leases: when a worker dies mid-step, the lease expires and the step is redelivered. Execution is at-least-once at the step level, dispatch is exactly-once, and no code anywhere is subject to a determinism contract — because no code is ever executed twice for the engine's benefit.

Handlers are ordinary methods matched to the graph by name. They can use clocks, randomness, any library. They can be redeployed at will. And because binding is by activity name over a language-neutral graph, the same workflow's steps can be served by Java, Go, and Python workers simultaneously.

What it costs — stated plainly§

Your control flow is bounded by the DSL's operators. Wiggle's vocabulary — sequential steps, gates, exclusive choice, parallel fork/combine, dynamic fan-out over a collection, timers, external signals with deadlines and escalation, sub-workflows, do-while loops — covers a large share of real business processes. It does not cover all of them. If your orchestration logic is genuinely algorithmic — control flow computed at runtime in ways no fixed operator set expresses — then a replay engine's Turing-complete workflow code is the right tool, and its determinism tax is the fair price.

That's the honest framing: replay engines make code durable and tax it with determinism; graph engines make data durable and tax it with a vocabulary. Neither tax is avoidable. You choose the one your workloads can afford.

Explicitness as a design principle§

Once state transitions are first-class, you must decide exactly what each one means — and every softness in that definition becomes a bug factory. Wiggle's rules are strict:

The through-line: in a data-first engine, the state transition rules are the product. Every place an implicit behavior became an explicit one, a category of "why does my context look like this?" question disappeared.

Versioning without patch APIs§

A definition's version is the content hash of its graph. Re-registering an identical graph is a no-op; registering a changed one creates a new version, and in-flight instances continue on the version they started with — both graphs coexist in the database. No patched() calls, no version branches inside workflow code, no migration windows.

The second bet: one database, not a distributed system§

A wiggle deployment is a cluster of stateless server nodes over one database. That database is the only source of truth, so there is no consensus protocol to operate, no quorum to lose, and no second store to keep in sync. Any node can drive any instance; a leader is elected over the same table to run timers and recovery, and its duties are idempotent, so a failover duplicates work at worst and never corrupts state.

The operational meaning is that scaling and availability are things you already know how to do: add server nodes for capacity, and give the database the availability story your organisation already runs.

Choosing§

If your processes are expressible as steps, branches, waits, and fan-outs — and in our experience the overwhelming majority of order flows, onboarding pipelines, approval chains, and document processes are — a data-first engine gives you durable execution with no determinism contract, trivially redeployable handlers, polyglot workers, content-hash versioning, and an engine small enough to read in an afternoon and embed in a test. If your orchestration is irreducibly algorithmic, pay the replay tax knowingly and take the expressiveness.

The deeper lesson generalizes: when a system replays code to reconstruct state, every property of that code becomes a correctness constraint. Moving the authoritative representation out of code and into data shrinks the constrained surface to a vocabulary you control — and makes everything the engine does to your state something you can point at, name, and test.


Ready to try it? Start with the docs or copy a shape from the patterns library.