A domain-agnostic engine that turns a directed graph of steps into a recorded, resumable run. Hand it a trigger — it plans, claims, executes, transitions, recovers.
Five mechanisms turn in the same sequence on every run — deterministic, ordered, recorded end to end. Each fires when its phase comes due.
due. The run carries its own copy of the graph forever after.FOR UPDATE … SKIP LOCKED lets any number of workers drain the queue with no collisions. Postgres is the lock manager.{{ }} templates against it, run the step's code, and persist its outputs for the steps downstream.Three tables, zero foreign keys to your data. Durability, idempotency and per-subject supersession are machined in — not bolted on after.
One row per execution. A worker can die mid-step; stale recovery resurrects the orphaned row, retries, and converges. The run is never lost.
A unique key over workflow · source · subject · event dedupes triggers. Deliver an event twice; the database guarantees exactly one run.
A newer run for the same subject supersedes the older in-flight one when their change-sets overlap. No racing duplicates, no stale outcome.
JSONB definitions on a draft / active / archived lifecycle. Each run executes its frozen snapshot — editing never touches runs already moving.
A workflow is a DAG of typed steps in JSON. The engine ships no domain knowledge — you register it behind five extension points, as Spring beans, never as edits to the core.
// wait → branch → notify → terminal { "entryNode": "wait", "nodes": [ { "id":"wait", "type":"delay", "config":{ "delayMinutes": 5 }, "transitions":{ "DONE":["check"] } }, { "id":"check", "type":"branch_on_field", "config":{ "field":"event.priority", "op":"equalsAny", "values":["high"] }, "transitions":{ "HIGH":["notify"], "NORMAL":{ "terminal":"COMPLETED" } } }, { "id":"notify", "type":"http_request", "config":{ "method":"POST", "url":"https://hooks.example/alert" }, "transitions":{ "SUCCESS":{ "terminal":"COMPLETED" } } } ] }