Skip to content

Handlers

Some things can happen at any point of an instance, not at one step. The requester withdraws the request, whatever step it waits at. The whole request must be answered within 30 days. Someone asks how far it has got, and the answer must not stop the work.

A handler is a step that waits for the whole time its instance runs. No flow leads to it. The engine starts a token on it when the instance starts, beside the instance's first token. When the step goes on, the handler fires, and its token goes down the step's flows.

An example: withdrawing a request

A request waits for a review. The requester can withdraw it at any moment, and is then told it was withdrawn:

nodes:
  n_start: { id: n_start, label: Start, type: start }
  n_review: { id: n_review, label: Review, type: user }
  n_end: { id: n_end, label: End, type: end }
  n_withdrawn:
    id: n_withdrawn
    label: Withdrawn
    type: event
    config:
      event: request_withdrawn
    features:
      handler:
        mode: interrupting
  n_tell: { id: n_tell, label: Tell the requester, type: notify }
  n_end_withdrawn: { id: n_end_withdrawn, label: Withdrawn, type: end }
flows:
  - { id: f1, from: n_start, to: n_review }
  - { id: f2, from: n_review, to: n_end }
  - { id: f3, from: n_withdrawn, to: n_tell }
  - { id: f4, from: n_tell, to: n_end_withdrawn }

n_withdrawn is an ordinary Wait for an event step, with the Handler feature set. When the instance starts, its first token heads for n_review, and another one waits on n_withdrawn. A source delivers request_withdrawn the way it delivers any event (see Waiting for an event):

\Drupal::service(EventDeliveryInterface::class)->deliverEvent(
  new InstanceCriteria(tenant: $tenant, correlationKey: $request_number),
  new IncomingEvent('request_withdrawn'),
);

The handler fires. Because it is set to interrupt, it first cancels the review, its task and its timers with it, then goes on to n_tell. The instance ends at n_end_withdrawn.

In the editors, the setting is Wait for the whole instance, in the step's Handler section:

  • No, an ordinary step (the default).
  • Yes, and cancel everything else when it goes on: mode: interrupting.
  • Yes, and leave the rest running: mode: non_interrupting.

What a handler waits for

A handler is any step that waits: whatever makes that step go on fires the handler.

The handler waits for Its step
An event Wait for an event, naming the event. The event is delivered by its criteria, as to any step waiting for it.
A deadline Wait, with a timeout. The timeout is counted from when the instance starts.
An incident Wait for an incident. Any step of the instance that stops on an incident sends it on.
A person A human task. The task is offered from the start, beside the rest.

A cancel button for the requester

A handler that is a human task offered to the requester is a button they can press at any time. Here the requester cancels their own request:

n_cancel:
  id: n_cancel
  label: Cancel my request
  type: user_operation
  config:
    outcomes: canceled
  features:
    assignments:
      - { plugin: initiator }
    handler:
      mode: interrupting

The Initiator audience offers the step to the person who started the instance, so the starter has nothing to pass (see Task assignment). The requester finds the step on their My requests card, where it is the card's action while the rest waits at its own step. Pressing it cancels the rest and follows the step's flows. Like any task, it takes the process orchestra tasks permission.

A deadline for the whole instance

A request must be answered within 30 days, whatever step it is at:

n_expired:
  id: n_expired
  label: 30 days passed
  type: wait
  features:
    timeout:
      deadline: relative
      deadline_settings: { duration: P30D }
    handler:
      mode: interrupting

The timeout is counted from when the handler starts waiting, which is when the instance starts. The cron sweep fires it on the first run after the 30 days, as it fires any timeout. The handler cancels everything else and goes on to its flows. An absolute deadline works too: wait until a date a variable holds (see Wait until a date).

The site-wide default timeout never applies to a handler. A handler waits for as long as the instance runs, so it has only the timeout it sets itself.

An incident

Operators should hear of every step that stops on an incident:

n_alert:
  id: n_alert
  label: A step stopped
  type: incident
  config:
    payload_variable: incident
  features:
    handler:
      mode: non_interrupting
      repeat: true
      on_end: cancel

When a step of the instance stops on an incident (see Incidents), incident holds what it says, and the handler's flows go on, to a step mailing the operators for example:

{"node": "n_register", "message": "The remote system refused the request."}

The message is written for developers and may name parts of the site. Only name a payload variable where it may be shown.

The handler goes on as a step of its own, once the step that stopped has been dealt with: in the same request for an instance that runs synchronously, with the next run of the queue for one that runs queued.

The halted step is left as it is, waiting for an operator. An interrupting handler waiting for an incident cancels it instead, and resolves its incident as canceled, so the instance can end with the handler's branch.

A Wait for an incident step is always a handler: a workflow with one that is not a handler is refused when it is saved, since nothing would ever send it on.

An incident in a branch a handler waiting for incidents sent out does not send any such handler on again. Otherwise a handler whose own branch fails would go on for ever.

Interrupting or not

An interrupting handler cancels every other token of the instance when it fires: the steps the instance waits at, their tasks and timers, the branches waiting at a join, the other handlers. A branch halted on an incident is canceled too, and its incident resolved as canceled. Then the handler's own branch goes on, and is all that is left.

A non-interrupting handler leaves everything else running. Its branch runs beside the rest.

A status request

Someone asks how far the request has got. The answer must not stop the review, and the question can come several times:

n_status:
  id: n_status
  label: Status requested
  type: event
  config:
    event: status_requested
  features:
    handler:
      mode: non_interrupting
      repeat: true
      on_end: cancel
n_answer: { id: n_answer, label: Answer, type: notify }

Each status_requested event sends a branch to n_answer while the review goes on waiting.

When the rest ends first

A non-interrupting handler's branch can still be running when everything else has ended. When the rest has ended (on_end) says what happens then:

  • The instance waits for it (wait, the default): the branch keeps the instance running, as any branch does. The instance completes when the branch ends too.
  • It is canceled (cancel): the branch does not keep the instance running. Once the rest has ended, it is canceled, and the instance completes.

A reminder sent a week after the start is a good case for cancel: once the request is answered, a reminder still on its way is no longer needed.

n_reminder:
  id: n_reminder
  label: A week passed
  type: wait
  features:
    timeout:
      deadline: relative
      deadline_settings: { duration: P7D }
    handler:
      mode: non_interrupting
      on_end: cancel

An interrupting handler's branch is all that is left once it fires, so the instance always waits for it.

Firing more than once

A handler fires once by default: once its step has gone on, it waits no more. Tick Can go on more than once (repeat: true) and it waits again as soon as it has fired. A status request is answered each time it comes.

A timeout counted from each wait (relative, the default anchor) starts again with each wait: a reminder every seven days. A date, or a budget (a duration counted from the start of the instance, or from the step's first wait), stays where it is. A handler that also waits for an event goes on for each event while that deadline is ahead, then fires a last time when it passes, and waits no more: waiting again would fire it at once, on every sweep.

Only while a condition holds

A handler can wait for a condition first. An expert review is offered only while the amount is over 10,000:

n_expert:
  id: n_expert
  label: Request an expert review
  type: user_operation
  features:
    assignments:
      - { plugin: roles, settings: { roles: [claims_handler] } }
    handler:
      mode: non_interrupting
      repeat: true
      condition:
        plugin: comparison
        settings: { variable: amount, operator: '>', value: 10000 }

The condition is a flow condition, chosen and set as a flow's is. In the editors, it is Only while, in the step's Handler section.

  • While it does not hold, the handler waits for nothing. Its token is held before the step: no task is offered, no deadline runs, no event is waited for. The trace shows it as Waiting for its condition.
  • When it starts to hold, the step waits as a handler does. The task is offered, the timeout starts, the event is waited for.
  • When it stops holding, the step stops waiting. Its task and its timers are canceled, its condition stopped holding, and a new token is held until the condition holds again. A timeout counted from the wait starts again then.

The condition is tested when the handler starts waiting, then each time something it reads changes: a variable it reads gets a new value, whether a step writes it, a module writes it from outside a step, or an administrator changes it on the instance page (Changing a variable), or a step it reads goes on (see step_completed in Flow conditions). Each condition says what it reads, so a change none of them reads tests no handler and reads nothing, and an instance whose handlers have no condition pays nothing. A condition that cannot say what it reads is tested on any change. The condition reads the instance's variables: a handler belongs to no branch, so a variable local to a branch is not there.

Time passing is not a change: a condition comparing a date with today, such as the expression date(due) < today(), is tested again only when due changes. For a handler that waits until a date, give its step a timeout instead.

A condition that cannot be evaluated, such as an expression naming a function that does not exist, halts its handler on an incident, as a flow's condition halts its token. The step whose change tested it is not affected. Retrying the incident, or resuming it with corrected variables, tests the condition again: the handler reaches its step only once the condition holds.

A handler held before its step waits for no event. An event delivered to it then is kept as any event no step waits for yet: for as long as its source asks, an hour by default, and taken if the condition holds before that (see An event that arrives early). A broadcast event is never kept, so it does not reach a held handler.

A step that does not wait

A handler with a condition can be a step that does not wait, a pass-through step for instance. It goes on as soon as its condition holds. Set to interrupt, it is how a case closes itself once its work is done (see Running a workflow as a case):

n_done:
  id: n_done
  label: Everything is done
  type: passthrough
  features:
    handler:
      mode: interrupting
      condition:
        plugin: comparison
        settings: { variable: decided, operator: '==', value: true }

It is also how a case closes once its required items are done: a step_completed condition per required step, in an all. Such a handler needs a condition, and goes on once: without a condition it would go on as the instance starts, and set to go on more than once it would go on again at once, for as long as its condition holds. The workflow is refused when it is saved with either.

A handler that is still waiting

A handler waiting for its trigger, or held until its condition holds, does not keep its instance running. Once every other token of the instance has ended, the handler's token is canceled, and the instance completes.

The same goes for a handler that never fires: a request reviewed in time completes, and its withdrawal handler is canceled with the reason the rest has ended.

A handler waiting for its trigger is not where the instance is: the current step a list shows is the step the rest waits at. Nor does it change the instance's status. Neither does a branch set to be canceled once the rest has ended.

Handlers of an embedded subprocess

A subprocess that runs its child inside its parent's instance (see Subprocesses) starts the child's handlers when the child starts. They cover the child only:

  • An interrupting handler of the child cancels the child's tokens, not the parent's.
  • A handler of the child still waiting is canceled once the rest of the child has ended, and the subprocess goes on.
  • When the rest has ended reads the same way inside the child: a branch set to wait keeps the subprocess waiting, one set to cancel is canceled when the rest of the child ends.

A handler of the parent covers the child too: an interrupting one cancels the subprocess, and the child with it. A handler of the parent waiting for an incident hears of an incident in the child.

A subprocess that starts its child as an instance of its own starts the child's handlers in that instance, which they cover.

Moving to another version

An instance moved to another version of its workflow (see Versioning) follows that version's handlers. A handler the version no longer has is canceled, with the reason its handler was removed, and never holds the instance back on its version. A handler the version adds starts waiting. A handler that already fired once does not start again: the version still has it, and it has done its work.

With versioning off, an instance runs the workflow as it is now. A handler added to it waits only in the instances started after it. A handler removed from it no longer fires, and its token is canceled once the rest has ended, by the next cron run at the latest.

Rules

  • No flow may lead to a handler, and a handler cannot be the start node. A workflow breaking either rule is refused when it is saved.
  • A handler is a step that waits, or a step that does not wait with a condition, which goes on once (see A step that does not wait). A start or an end step cannot be one.
  • A handler's flows are followed as any step's are: conditions, splits and joins included. A split in the handler's branch is joined as one after the start is.
  • A handler's branch can lead into the steps of the rest, but not into a join waiting for the branches of a split it did not go through: that join could not tell which branches to wait for, and the workflow is refused when it is saved.

In the trace and the audit trail

The instance page's trace shows each handler's token from the start, with Started as a handler as the way it arrived. Once it fires, its branch follows, each token naming the one it came from.

Each fire is recorded in the audit trail as Handler went on, naming the handler's node and whether it interrupts. Each token an interrupting handler cancels is recorded as canceled, interrupted by a handler.