Workflows & definitions
A workflow definition is a plain data structure. The same shape is what you
build in code, what php artisan workflow:build writes, and what the storage
driver persists.
[
'id' => 'send-welcome-email', // stable, chosen by you
'name' => 'Send Welcome Email',
'description' => '',
'version' => 1,
'trigger' => ['type' => '@wf::trigger.manual', 'config' => []],
'nodes' => [
['id' => 'greet', 'type' => '@wf::action.log', 'config' => ['message' => 'Hi {{ trigger.name }}']],
],
'edges' => [
['from' => 'trigger', 'to' => 'greet', 'branch' => 'main'],
],
'meta' => [],
]
The pieces
trigger
{ type, config }. The type is a trigger identifier (see Triggers);
config is the trigger's saved settings. When an execution starts, the engine
fires the trigger against the incoming payload — the trigger decides whether an
execution should run at all.
nodes
An ordered list of { id, type, config }. The id is yours and must be unique
within the workflow; it's how edges refer to the node and how expressions read
its output ({{ nodes.<id>.* }}). type selects the node class. config is the
node's settings, and its string values may contain {{ }} expressions that are
resolved just before the node runs.
A node's config may also carry connection and __advanced__ keys — see
advanced settings below.
edges
An ordered list of { from, to, branch }. from and to are node ids (or the
literal 'trigger' for the entry edge). branch names which of the source
node's outgoing branches this edge belongs to.
Most nodes have a single main branch. Branching nodes declare more:
| Node | Branches |
|---|---|
| Condition | true, false |
| Switch | default, plus one branch per configured case |
| Loop | body (once per item), main (after the last item) |
| Scope | body (the grouped run), then continues on main |
Working with definitions in code
Workflow::fromArray() / Workflow::fromJson() build a Workflow model.
->toArray() / ->toJson() serialise it back. ->toDefinition() produces the
immutable Definition value object the engine runs against.
use Qanna\WorkflowEngine\Models\Workflow;
$workflow = Workflow::fromArray($data);
$workflow->nodeById('greet'); // ['id' => 'greet', 'type' => ..., 'config' => ...]
$workflow->edgesFrom('greet'); // outgoing edges
Persist through the repository contract — never write storage files yourself:
use Qanna\WorkflowEngine\Storage\Contracts\WorkflowRepositoryContract;
$repo = app(WorkflowRepositoryContract::class);
$repo->create($workflow); // throws if the id already exists
$repo->update($workflow); // throws if it doesn't
$repo->upsert($workflow); // either way
$repo->find('send-welcome-email'); // ?Workflow
$repo->all(); // Workflow[]
$repo->allActive(); // active only
$repo->findByTriggerType('@wf::trigger.webhook');
$repo->delete('send-welcome-email');
Versioning
A workflow carries an integer version. The interactive builder bumps it each
time you edit and save an existing workflow. When an execution starts, the engine
captures a snapshot of the whole definition into the execution record — so
later edits to the workflow never rewrite what a past run actually executed, and a
suspended execution resumes against the definition it started with.
Node types can also be versioned independently with a {vN} suffix so a node's
schema can change without breaking saved workflows. See
Node versioning.
Advanced settings
Every node can declare extra field groups beyond its main config:
connection— connection-style settings (Node::connection()).advanced— settings that matter but would clutter the main form (Node::advanced()).
The engine reads a handful of advanced keys itself, on every node, to govern
retries and timeouts:
| Key | Default | Effect |
|---|---|---|
max_retries | 0 | If the node's handle() throws, it is attempted again, up to max_retries total attempts (minimum one). A value of 1 behaves the same as 0. |
retry_delay | 1 | Seconds to sleep() between attempts. |
retry_backoff | fixed | fixed keeps retry_delay constant; exponential uses retry_delay * 2^(n-1). |
timeout | 0 | Seconds. When a node runs longer, a workflow.node-slow hook fires and a warning log row is written. On the CLI (and where pcntl is available) a hard alarm is also armed. |
A retried node emits a workflow.node-retrying hook and a retrying log row for
each extra attempt. If every attempt throws, the node's result is an error and
the workflow fails with the last exception message.
In a stored definition these live under the node's config as __advanced__ /
connection. The interactive builder prompts for them as separate "Advanced
settings" / "Connection settings" steps when a node declares them.