Appearance
Workflow expressions
Node settings that depend on data use expressions, written in a small subset of the Jinja expression syntax. There is one expression language for all nodes: the same rules apply to conditions, messages, mappings, URLs and query variables.
Two kinds of fields
Predicate fields hold a bare expression, with no braces. These are condition's expression, each switch case's expression, assert's condition and logic's operands:
yaml
expression: nodes.fetch_job.output.data.getJob.quantity > 1000Value and text fields are everything else that accepts expressions, such as messages, mappings, variables, URLs and bodies. There you wrap the expression in double curly braces:
yaml
# A single expression and nothing else: the typed value (number, list, object...)
itemCount: "{{ input.items | length }}"
# Text with expressions in it: always a string
message: "Order {{ input.orderId }} has {{ input.items | length }} items"
# No braces: the literal text
label: static textA field that contains exactly one braced expression and nothing else keeps the value's type. Any surrounding text, or more than one expression, turns the result into a string. Dates are rendered as ISO 8601 text.
In YAML, quote any value that starts with a brace, as in the examples.
References
| Reference | Value |
|---|---|
input.<field> | A field of the workflow input |
nodes.<id>.output.<field> | A field of an earlier node's output |
variables.<name> | A workflow variable (declared, or set by set_variable) |
.outputis always written out:nodes.fetch.output.status, notnodes.fetch.status.nodes.<x>finds the node by id first, then by name. Prefer ids, because names can contain spaces.- Use brackets for indexes and unusual keys:
input.items[0],input["order-id"]. - A path that does not exist gives
nullinstead of an error. Usedefaultto supply a fallback. - Inside a
for_eachbody, the current item is available under the loop'sasname (itemby default), and its position asindex.
What a node's output contains depends on the node type. See the node reference.
Operators
| Kind | Operators |
|---|---|
| Comparison | == != < <= > >= |
| Logic | and or not |
| Membership | in not in |
| Arithmetic | + - * / // (whole-number division) % |
| Text concatenation | ~ |
| Conditional | a if condition else b |
| Grouping | ( … ) |
Literals: numbers, strings in single or double quotes, true, false, null (or none), lists [1, 2, 3] and objects written as JSON-style key/value pairs.
Comparing 1 == 1.0 is true: numbers compare by value. Dates compare in time order. The ordered comparisons (<, <=, >, >=) need two numbers, two strings or two dates. Comparing null with a number fails the node, so give values that may be missing a fallback: (nodes.fetch.output.data.getJob.quantity | default(0)) > 1000.
There are no statements or blocks ({% %}) and no comments. For branching and loops, use the condition, switch and for_each nodes.
Filters
Filters transform a value: value | filter or value | filter(argument). They can be chained: input.name | trim | upper. Only these filters exist:
| Filter | Result |
|---|---|
default(fallback) | The value, or fallback if the value is null |
length | Length of a string, list or object |
min, max | Smallest or largest item of a non-empty list |
lower, upper, trim | Changed text |
join, join(separator) | List items joined into a string (no separator by default) |
abs | Absolute value of a number |
round, round(digits) | Rounded number |
merge(other) | Object with the keys of other added; keys in other win |
pick('a', 'b', …) | Object with only the named keys |
omit('a', 'b', …) | Object without the named keys |
concat(other) | Two lists (or strings) joined |
Filters bind more tightly than every operator, so input.items | length > 10 compares the length. Parentheses, as in (input.items | length) > 10, make that easier to read.
There are no regular expressions in expressions; use the regex node for that.
Truthiness
Predicates and and / or / not treat these values as false: null, false, 0, 0.0, "", [] and an empty object. Everything else is true, including every date.
Examples
| Goal | Expression |
|---|---|
| Quantity over a threshold | nodes.fetch.output.data.getJob.quantity > 1000 |
| Two conditions | input.amount > 1000 and input.status == "approved" |
| Value from a list | input.priority in ["high", "urgent"] |
| Fallback | input.customer | default("unknown") |
| Count items | input.items | length |
| Build a label | "Job " ~ input.jobNumber ~ " (" ~ input.status ~ ")" |
| Pick a tier | "large" if input.total >= 1000 else "standard" |
This workflow uses most of these features. It passes validation as shown:
yaml
nodes:
- id: defaults
name: Set defaults
type: set_variable
config:
variables:
threshold: "{{ input.threshold | default(1000) }}"
label: "{{ input.customer | default('unknown') | upper }}"
- id: fan_out
name: Fan out
type: split
- id: summary
name: Build summary
type: transform
config:
mapping:
title: "Order {{ input.orderId }} for {{ variables.label }}"
itemCount: "{{ input.items | length }}"
isBig: "{{ (input.items | length) > 10 or input.total >= variables.threshold }}"
tier: "{{ 'large' if input.total >= variables.threshold else 'standard' }}"
firstItem: "{{ input.items[0] | pick('sku', 'qty') }}"
- id: each_item
name: Log each item
type: for_each
config:
items: "{{ input.items }}"
as: item
maxConcurrency: 4
body:
type: log
config:
message: "Item {{ index }}: {{ item.sku ~ ' x ' ~ item.qty }}"
- id: both
name: Wait for both
type: join
config:
strategy: keyed
keys:
summary: summary
each_item: items
onPartialFailure: fail
- id: done
name: Done
type: log
config:
message: "{{ nodes.summary.output.title }}: {{ nodes.each_item.output.count }} items logged"
edges:
- id: e1
fromNodeId: defaults
toNodeId: fan_out
- id: e2
fromNodeId: fan_out
toNodeId: summary
- id: e3
fromNodeId: fan_out
toNodeId: each_item
- id: e4
fromNodeId: summary
toNodeId: both
- id: e5
fromNodeId: each_item
toNodeId: both
- id: e6
fromNodeId: both
toNodeId: done