Decision tables¶
A decision table holds the rules of a decision in a grid: input columns on the
left, output columns on the right, one rule per row. A step decides with it,
and the flows after the step route on what it decided. It ships in the
optional Orchestra Expression submodule (orchestra_expression), with
Expressions.
An example¶
Who approves an expense:
| Amount | Department | Delegated | → approver |
|---|---|---|---|
> 5000 |
!= "finance" |
false |
"director" |
< 500 or > 1000000 |
"auto" |
||
"manager" |
The input columns test amount, department and delegated. The output
column writes approver. The rules are tried from the top, and the first that
matches answers:
- 6000 from sales, not delegated: the first rule matches, so
director. - 6000 from finance: the first rule does not, nor the second, so the last one,
manager. - Two million from finance: the first rule does not match, the second does, so
auto. From sales, the first rule would already have matched: the order of the rules decides.
A step decides with the table under its Computed variables:
n_decide:
type: passthrough
features:
variables:
-
plugin: decision_table
settings:
table: example_expense_approver
scope: instance
And the flows after it route on approver, with a comparison each:
approver == director, approver == auto, approver == manager. The
orchestra_examples submodule ships this table and this workflow, as
example_expense_approver and example_expense_table.
Rows, cells and columns¶
A rule matches when every input cell it fills holds: the cells of a row are joined by and. An empty cell matches anything.
An input cell is a test on its column, written without the column:
| Cell | Reads |
|---|---|
> 5000 |
amount > 5000 |
!= "finance" |
department != "finance" |
in ["hr", "it"] |
department in ["hr", "it"] |
"finance" |
department == "finance" |
false |
delegated == false |
< 500 or > 1000000 |
amount < 500 or amount > 1000000 |
"hr" or "it" |
department == "hr" or department == "it" |
A cell starting with a comparison (==, !=, <, <=, >, >=, in,
not in, matches, contains, starts with, ends with) tests the column
with it. Any other cell must equal the column. Tests can be joined with and
or or in one cell: the cell is cut there, outside brackets and texts, so
== "this or that" stays one test.
A test may name another variable: > budget in the Amount column reads
amount > budget. But a cell that does not start with a comparison must equal
its column, so a whole test written there, such as amount > budget under
Department, would compare the department with it: the editor refuses it. To
test something else, give it a column of its own: an input column
amount > budget, with true in the rules that need it.
An input column is an expression, usually a variable name, but it can be any
expression: amount * 1.2 tests the total with tax. Names are read as the
Expressions provider reads them.
An output cell is an expression too. Write a text with its quotes:
"director". Without them, director would read a variable called
director. An output can compute: amount * 0.1. An empty output cell
writes nothing for that rule.
Each output column writes the variable it names. The names follow the rules of the Expressions provider: letters, digits and underscores, with a dot between two parts, and none starting with two underscores.
Which rules answer¶
The hit policy of the table says:
- First matching rule: the rules are tried from the top, and the first that matches writes its outputs. The order matters, and a last rule with every input cell empty answers when no other does. Such a rule anywhere but last would leave the rules after it never reached, so the editor refuses it there.
- Collect every matching rule: every rule that matches answers, and each
output is written as the list of their values, in the order of the rules.
For example, the teams to notify about an expense: a rule per team, and
teamsends up as["finance", "legal"]. A flow routes on such a list with an expression condition, such as"legal" in teams: a comparison with one value would never match a list.
When no rule matches, the table writes nothing, so the flows can route on the output being not set.
Editing a table¶
Decision tables are listed under Configuration › Workflow › Orchestra › Decision tables, for the people with the Administer Orchestra decision tables permission. The editor has four parts:
- Inputs: each column's label, its heading in the rules, and its expression, which every cell of the column tests.
- Outputs: each column's variable, the one it writes.
- Rules: one row per rule, one cell per column. Drag the rows to reorder them.
- Try it: sample values, to see what the table decides. See Trying a table.
Below them, Writing an expression shows the most used operators and functions, each with an example. Expressions lists them all.
Save and keep editing saves and opens the table again, rather than the list, to go on editing it. Try it needs no save: it decides with the table as it stands in the form.
Adding or removing a column or a rule rebuilds the grid without losing what was typed. Removing a column whose cells some rules filled asks for a second click, saying how many cells would go. Saving checks every cell, and shows the error on the cell, with its rule and column. A rule with every cell empty is refused too.
A table is shared, so every workflow can use it, or kept to the workflows of one tenant. A shared workflow can only use shared tables. The step's editor offers only the tables the workflow's tenant can use. A table that workflows of another scope already use cannot be kept to one tenant: the editor refuses, naming them.
Removing or renaming an output column is allowed, but the workflows using the table may route on the variable it wrote. So is changing the hit policy, which turns the outputs into lists, or back. Saving then warns, naming the workflows whose flows to check.
Trying a table¶
Saving checks how a table is written, not what it decides. The Try it panel, below the rules, shows what it decides, before any workflow runs it.
It has one field per variable the table reads: each input column's, labeled
with the column, and any other a cell names, such as budget in > budget,
or OK in an output cell written without quotes. Give such a field the value
the process would hold, to see what the rule writes with it. A
sample value is written like a cell: 6000, "sales" with its quotes, or
false. A field left empty is read as not set. A sample without its quotes,
such as sales, would read a variable called sales, so it is refused.
With the expense example above, the samples 6000, "sales" and false, then
Test:
- Rule 1 answers. The rule is highlighted in the grid.
- What the table writes:
approver,"director".
With 2000 instead of 6000, only the last rule matches, so it answers
"manager". Without that last rule, the panel says No rule matches: the table
writes nothing. A collecting table names every rule that answers, and writes
lists. A cell that fails on the samples, such as amount / 0, is named with its
rule and its column, in the words of the incident a step would raise.
The panel decides with the table as it stands in the form, saved or not, through the code a step runs: what it shows is what a workflow would decide. Nothing is saved, and no workflow runs. Adding or removing a column or a rule clears the answer, which was for the grid as it was. After changing a cell, test again.
Using a table in a workflow¶
Add the Decision table provider to a step's Computed variables, and pick the table. The editor shows what it writes: Writes: approver. Like every computed variable, the outputs are there before the step's task runs, and for its outgoing flows. See Variables.
To decide on what the step's task produced, such as a person's answer, set the row's When to Step ends: the table then decides after the task, before the flows, and no step after it is needed to decide:
n_review:
type: user
config:
outcomes: 'approved,rejected'
payload_variable: decision
features:
variables:
-
plugin: decision_table
when: end
settings:
table: example_review_next
scope: instance
The table has an input column on decision, so a rule testing "approved"
applies only when the person approved. It decides whatever the outcome, the
failure outcome included. A step that names a
failure variable gives the table another input column to test, and a rule
testing "failed" under it handles the failure. When no rule matches, the
table writes nothing.
In the BPMN.io editor, the row is written in the step's JSON field:
[{"plugin": "decision_table", "scope": "instance", "settings": {"table": "example_expense_approver"}}]
A row holding "when": "end" decides as the step ends, and "when": "both" at
both moments. Without it, the row decides as the step starts.
Which tables a workflow depends on¶
A workflow depends on the tables its steps decide with. So a table is imported before the workflows using it, and it cannot be deleted while a workflow uses it: the delete page says how many workflows use it, and the list of tables shows it too. An instance still running on an older version of a workflow can name a table deleted since: its step then fails, saying the table does not exist.
A table is not versioned with the workflows using it. A workflow's definition is kept as it was when each instance started, but a table is shared configuration: a changed rule applies at once, to the next step deciding with it, in every workflow and every running instance. That is the point of keeping the rules in one place, and it is why changing a table is a permission of its own.
When a table fails¶
A cell that cannot be evaluated, such as lower(tags) given a list, fails the
step, and its failure setting decides what happens next. The message names the
table, the rule and the column: its label, or its expression when it has none.
See Incidents.
When to use a table¶
- A single test, such as
amount > budget, is simpler as an expression condition on the flow. - Several inputs combining into several outcomes read better as a table: the rules sit in one grid instead of across the conditions of the flows, and changing one is changing a cell.
- The same rules in several workflows: one table, used by all of them, is changed in one place.