Skip to content

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.