Skip to content

Expressions

The optional Orchestra Expression submodule (orchestra_expression) lets a workflow compute values and test conditions with short expressions, without writing a plugin.

An example

An expense is routed to the person who approves it. A step computes the total with VAT, and whether it is over budget. The flows after it route on those values:

flowchart LR
  s([Start]) --> c["Compute the total<br/>total = amount * 1.2<br/>over_budget = total > budget"]
  c --> g{Who approves?}
  g -->|over_budget and department != &quot;finance&quot;| d([Director approval])
  g -->|total < 500| a([Approved directly])
  g -->|otherwise| m([Manager approval])
n_compute:
  type: passthrough
  features:
    variables:
      -
        plugin: expression
        settings:
          assignments:
            - { name: total, expression: 'amount * 1.2' }
            - { name: over_budget, expression: 'total > budget' }
        scope: instance
- id: f_director
  from: g_route
  to: n_director
  condition:
    plugin: expression
    settings:
      expression: 'over_budget and department != "finance"'

An expense of 1000 against a budget of 1000 has a total of 1200, so it goes to the director, unless it comes from finance. An expense of 400 has a total of 480, and is approved directly. The orchestra_examples submodule ships this workflow as example_expense_routing.

A decision taken on several inputs at once reads better as a rule table: see Decision tables.

Installing

The expressions are evaluated with Symfony's ExpressionLanguage library. Orchestra does not require it, so add it first:

composer require symfony/expression-language
drush pm:install orchestra_expression

The module refuses to install without the library. If the library is removed later, the status report says so, before a workflow fails on it.

The expression condition

The Expression flow condition holds when its expression is true. Choose it on a flow, as you would a comparison, and write the expression.

It can do what a comparison cannot:

  • compare two variables: amount > budget;
  • compare a date with today: date(due_date) < today();
  • compute before comparing: amount * 1.2 > 5000.

Tests joined with and and or fit in one expression, where comparisons need an all or any condition around them.

A value is read the way PHP reads it in an if. So 0, an empty text, an empty list and a variable nothing wrote are all false.

The expression variables

The Expressions variable provider computes variables. Add it to a step's Computed variables, and write one variable per line:

total = amount * 1.2
over_budget = total > budget
late = days_between(due_date, today()) > 0

The lines run in order. Each line sees the variables the lines above it wrote, so over_budget reads total. A name is made of letters, digits and underscores, with a dot between two parts, so a later line can read it. Like every computed variable, the values are there before the step's task runs, and for its outgoing flows. Set the row's When to Step ends to compute them after the task instead, from what it wrote, such as a person's answer. See When the rows run.

Writing an expression

A name reads a process variable. A dotted name reads into a structured one: decision.comment reads the comment of the decision variable. It reads a variable whose own name has dots first, as every other part of Orchestra does (see Flow conditions).

In the condition, a name reads a variable as the other conditions do, so decision == "approved" matches the outcome of a structured answer. In the variable provider, a name reads the whole value: write decision.outcome to read the outcome of a structured answer.

A dot and brackets with the same key mean the same thing: lines[0].label and lines[0]["label"] both read lines.0.label, as a dotted name. An item whose key is computed, lines[i], is read from the value of lines, and lines[i].label then reads its label, as lines[i]["label"] does.

A name that nothing wrote reads null. discount ?? 0 gives 0 instead.

Write For
+ - * / % ** arithmetic
== != < <= > >= comparison
and or not logic
~ joining two texts: first_name ~ " " ~ last_name
in, not in list membership: "urgent" in tags
contains, starts with, ends with text tests: email ends with "@example.com"
matches a regular expression: code matches "/^[A-Z]{3}$/"
a ? b : c a choice
a ?? b a fallback when a is not set, or is null
[1, 2], {"a": 1} a list, a map
items[0], map["key"] an item of a list or a map

in compares types too: "3" in [3] is false.

Functions

Function Gives
now() The current time, as a Unix timestamp.
today() Midnight today in the site timezone, as a Unix timestamp.
date(value) An ISO 8601 date or date-time, such as 2026-11-02 or 2026-11-02T14:30, as a Unix timestamp. A timestamp stays as it is. null when the value is neither.
days_between(from, to) The days from one date to the other on the site's calendar, counted as the midnights crossed, negative when to comes first: 23:00 to 01:00 the next day is one day. Each side is a date or a timestamp.
lower(text), upper(text), trim(text) The text in lower case, in upper case, or without the spaces around it.
count(list) How many items the list holds. A list nothing wrote holds none.
min(a, b, ...), max(a, b, ...) The smallest or the largest value.

Dates are read as timers read them: without an offset, in the site timezone, and with one, at that offset (see Comparing dates). 2026-02-30 is not a date.

So on 2 November 2026, date("2026-11-02") == today() is true, from midnight to midnight on the site's clock. days_between() counts days on that calendar too, so a day on which the clocks change still counts as one.

What an expression cannot do

An expression only reads the variables it names and calls the functions above. It cannot call a method, read a PHP constant, or call any other PHP function.

When an expression is wrong

A syntax error is refused when the workflow is saved: a missing operand, an unknown function, a function given too many or too few arguments, a method call, a regular expression written after matches that PHP cannot read, a text or a number written after in where a list belongs. The editors show the error on the condition, or on the line of the provider.

An expression can still fail while it runs: a division by zero, or lower() given a list. In a provider, the step then fails, and its failure setting decides what happens next, as for any other error. See Incidents. A failing provider writes none of its lines.

A condition is evaluated after the step's work. When it fails, the token stops on an incident, and the step's work is kept: a person's answer stays saved. See A condition that cannot be evaluated.

In the BPMN.io editor, the providers of a step are one JSON field, so a wrong line there is only found when the step runs. Write each variable with its name and its expression:

[{"plugin": "expression", "scope": "instance", "settings": {"assignments": [
  {"name": "total", "expression": "amount * 1.2"}
]}}]

A row whose variables are not written that way fails the step, with a message saying so, rather than computing nothing.

Which modules a workflow depends on

A workflow using an expression condition or the expression provider depends on orchestra_expression. So the module cannot be uninstalled while a workflow still uses it.