NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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.

TermMeaning
DefinitionA named workflow of one owner (key unique per owner) for one subject type (or none). Status draft / active / retired. Points at its current version.
VersionOne numbered snapshot of a graph, trigger, actor mode and settings. draft (editable, at most one per definition) → published (immutable; the newest is current) → superseded.
InstanceOne 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 / edgeA graph vertex (id, type, config, ui, optional retry and escalations) and a directed link (from, to, optional outcome).
Step typeWhat a node does: a catalogue entry (StepType class) with a config JSON Schema, outcomes and a handler.
Step runOne 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 codeWhy an instance or attempt failed: a FailureCode case for the engine, built-ins and bridges, or a host step’s own string.
VisitThe n-th time an instance enters a node (loops). Visit and node give the idempotency key; attempts don’t.
WaitA row the engine wakes on: a timer (due_at), an event key, a condition poll, a retry or a timeout.
EscalationA row per step-run visit and level: due time + actions. Cancelled when the step finishes.
SignalWhat resumes a wait: timer, event (key + payload), condition, probe, retry, timeout.
PartyA person or group a step addresses: an approver, a recipient, an escalation target.
ActorWho a version runs as: system, or on_behalf_of the triggering user.
OwnerThe tenant a definition belongs to (e.g. a team). Null = app-wide.
SubjectThe model an instance is about (a purchase request). Registered under a key; fields are declared.
BridgeOptional 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 crypto

By 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.