A workflow is a graph stored as JSON (nodes, edges, settings), versioned per definition. Published versions are immutable; every instance runs the version it started on. Steps run one at a time on your queue (or inline), waits are rows, the scheduler wakes timers, and every state transition happens under row locks with compare-and-set claims.
| Term | Meaning |
|---|---|
| Definition | A named workflow of one owner (key unique per owner) for one subject type (or none). Status draft / active / retired. Points at its current version. |
| Version | One numbered snapshot of a graph, trigger, actor mode and settings. draft (editable, at most one per definition) → published (immutable; the newest is current) → superseded. |
| Instance | One run of one version for one subject (or none), started by one trigger. Status running / waiting / completed / failed / cancelled. Keeps its version_id forever. |
| Node / edge | A graph vertex (id, type, config, ui, optional retry and escalations) and a directed link (from, to, optional outcome). |
| Step type | What a node does: a catalogue entry (StepType class) with a config JSON Schema, outcomes and a handler. |
| Step run | One attempt of one visit of one node in one instance — the step log. Records its input (the node config with every reference resolved), output, outcome and error. |
| Failure code | Why an instance or attempt failed: a FailureCode case for the engine, built-ins and bridges, or a host step’s own string. |
| Visit | The n-th time an instance enters a node (loops). Visit and node give the idempotency key; attempts don’t. |
| Wait | A row the engine wakes on: a timer (due_at), an event key, a condition poll, a retry or a timeout. |
| Escalation | A row per step-run visit and level: due time + actions. Cancelled when the step finishes. |
| Signal | What resumes a wait: timer, event (key + payload), condition, probe, retry, timeout. |
| Party | A person or group a step addresses: an approver, a recipient, an escalation target. |
| Actor | Who a version runs as: system, or on_behalf_of the triggering user. |
| Owner | The tenant a definition belongs to (e.g. a team). Null = app-wide. |
| Subject | The model an instance is about (a purchase request). Registered under a key; fields are declared. |
| Bridge | Optional code for another Roundly package, active only when that package is installed. |
The life of an instance
- A trigger starts it: start() (manual), emit() (event), the scheduler (schedule) or the webhook route / triggers()->webhook() (webhook). The trigger step completes at once with the trigger payload as its output.
- The engine follows the edge for the step’s outcome, creates the next step run and dispatches it.
- A step either completes with an outcome, waits (timer, event, condition), or fails (retryable or not).
- A waiting step resumes when its wait fires; an escalation may fire while it waits.
- Reaching an end node completes (or fails) the instance.
Every transition happens under a row lock on the instance, then the step run, then the wait or escalation. Only one step of an instance runs at a time.
Exactly once, at least once
- State transitions are exactly-once: claims are compare-and-set, results of a lost lease are discarded, a wait fires once.
- Handlers are at-least-once: a handler may run again after a crash or retry, always with the same idempotencyKey() for the same visit. $context->once() makes database effects exactly-once.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.