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 != "finance"| 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.