Starting a workflow on an event¶
A workflow can say for itself when it starts: when an event arrives. An order paid on a provider's site starts the invoicing workflow and the shipping workflow, each knowing nothing of the other. The source only reports what happened.
A workflow can also start at set times: see Starting a workflow on a schedule.
An example¶
The invoicing workflow starts on order_paid. Its start node is of the type
Start when an event arrives:
n_start:
type: event_start
config:
event: order_paid
payload_variable: order
A source reports the event, with the order number as the correlation key the instances are known by:
$result = \Drupal::service(EventDeliveryInterface::class)->startByEvent(
'acme',
new IncomingEvent('order_paid', ['id' => 42, 'total' => 4200], id: $provider_event_id, source: 'shop'),
'order-42',
);
// $result->started holds the ids of the instances it started.
The invoicing instance starts with order = {"id": 42, "total": 4200} and
the correlation key order-42. A shipping workflow whose start node also names
order_paid starts an instance of its own, with the same data and correlation
key.
The correlation key is optional. An instance started without one is still
reached by a later event through its other criteria: its workflow, or a
variable it holds, such as order.id when the start node stores the data
field by field.
The workflow list names the event in the workflow's components:
Start (event_start: order_paid).
The start node stores the event's data the way the Wait for an event step
does: whole in the payload variable, or
field by field (order.id,
order.total), with the same settings.
Starting is not resuming¶
A source says which it does. startByEvent() starts the workflows whose start
node names the event, and never resumes a step. deliverEvent() and
broadcastEvent() resume steps, and never start an instance. The same event
name can do both: order_paid can start the invoicing workflow and, delivered,
resume a step of a running order workflow waiting for it. The source calls
the one it means.
An incoming webhook does one of the three, as its Delivery
setting says: Start the workflows starting on it calls startByEvent(), with
the correlation key the call carries, if any. So does a
Send an event step, with the correlation key a variable of
its instance holds.
Which workflows start¶
Every enabled workflow of the tenant whose start node names the event starts
one instance. Event names belong to the tenant, not to a workflow: the source
announces what happened, and each workflow declares whether it starts on it.
Adding a workflow that starts on order_paid takes no change to the source.
In manual versioning mode, the event is read from the published version: renaming it in an edit changes nothing until the workflow is published.
The instances started on one event carry the same correlation key. Delivering a later event by that key reaches the one step waiting for it, in whichever instance it waits; when two of them wait for the same event, a delivery is refused as ambiguous and a broadcast reaches both. To keep them apart, select by the workflow too:
$delivery->deliverEvent(
new InstanceCriteria(tenant: 'acme', workflow: 'invoicing', correlationKey: 'order-42'),
new IncomingEvent('payment_received', $data),
);
One instance per start data¶
A provider may report the same order twice, with two different event ids. The duplicate id check does not catch that, so a start can be marked unique: it is refused while a running instance of the same workflow holds the same start data. The start data is every start variable, and the correlation key when one is given.
On a start node, tick One instance per start data:
n_start:
type: event_start
config:
event: order_paid
payload_variable: order
store_fields_separately: true
fields_to_store: |
order.id
unique: true
Only order.id is stored, so only the order's id is compared: a second
order_paid for order 42 starts no invoicing instance while the first one
runs, and startByEvent() answers not_unique. Once that instance ends, the
next event for order 42 starts one again.
All the start data counts. Store only what names the case: a field that differs on every event, such as when it was sent, makes every start unique. And a start with no data at all, no variable and no correlation key, matches every running instance of its workflow: marked unique, the workflow runs one instance at a time.
The same flag exists on every way in:
| Start | Unique when |
|---|---|
ProcessControlInterface::start() |
unique: TRUE |
| The API's start call | "unique": true in the body |
| A subprocess step, local or remote | One child per start data is ticked; a refused child fails the step |
| A start node starting on an event | One instance per start data is ticked |
| A start node starting on a schedule | One instance at a time is ticked; see One instance at a time |
$engine->start('invoicing', ['order_id' => 42], correlation_key: 'order-42', unique: TRUE);
The check and the creation run under one lock, held until the start commits, so two unique starts of the same data at once never both start an instance. A start not marked unique takes no lock and is never refused.
The check looks at every running instance of the workflow in the tenant, whoever started it. So a refusal tells the caller that such an instance exists, even one started by another caller of the tenant over the API. The tenant is what keeps runs apart: callers that must not learn about each other's runs belong in separate tenants.
What the source is told¶
startByEvent() returns an EventDeliveryResult. started lists the
instances it started. When it started none, refusal says why: no workflow
of the tenant starts on the event (no_workflow), each workflow starting on
it refused it as unique (not_unique) or could not store its data
(invalid), the event id was already used (duplicate), or the event itself
is not valid (invalid, too_large). An event that started nothing leaves
its id free, so a retry can still succeed. When one start fails after others
went through, the workflows started are kept with the event's id: the
source's retry starts the others and skips those, so a workflow that keeps
failing does not hold back the ones that can start. The failure is logged
with what it started, under the orchestra channel.