Skip to main content

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:

NodeBranches
Conditiontrue, false
Switchdefault, plus one branch per configured case
Loopbody (once per item), main (after the last item)
Scopebody (the grouped run), then continues on main

See Control flow & branching.

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:

KeyDefaultEffect
max_retries0If 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_delay1Seconds to sleep() between attempts.
retry_backofffixedfixed keeps retry_delay constant; exponential uses retry_delay * 2^(n-1).
timeout0Seconds. 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.