Expression Language
Template expressions, JSONata transforms, built-in functions, and the full variable context available inside workflow nodes.
Overview
FlowOS uses a dual expression system: Mustache-style templates ({{...}}) for simple value interpolation in node config strings, and JSONata for complex data transformation in Transform nodes. Both have access to the same runtime context.
Template Syntax
Wrap any JSONata path expression in double curly braces to interpolate its value into a string field:
// Simple value
"to": "{{trigger.record.email}}"
// Nested path
"body": "Incident {{trigger.record.number}} assigned to {{trigger.record.assignedTo.name}}"
// Arithmetic
"ttl": "{{nodes.computeTtl.output.hours * 3600}}"
// Conditional (ternary)
"priority": "{{trigger.record.severity = 'P1' ? 'urgent' : 'normal'}}"
// Array index
"firstOwner": "{{trigger.record.cmdbItems[0].ownerEmail}}"
// String concatenation
"subject": "{{'[' & trigger.record.severity & '] ' & trigger.record.title}}"Runtime Variable Context
The following variables are available in all template expressions and JSONata transforms:
trigger.*
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| trigger.record | object | optional | — | For record_event triggers: the full record (after change for updates). |
| trigger.before | object | optional | — | For record_updated: field values before the change. |
| trigger.changes | object[] | optional | — | For record_updated: [{ field, from, to }] array of changed fields. |
| trigger.body | object | optional | — | For webhook triggers: parsed request body. |
| trigger.headers | object | optional | — | For webhook triggers: request headers (lowercased keys). |
| trigger.event | object | optional | — | For itsm_event and event_bus triggers: the full event payload. |
| trigger.scheduledAt | timestamp | optional | — | For schedule triggers: intended fire timestamp. |
| trigger.input | object | optional | — | For manual triggers: the input object passed to the trigger call. |
nodes.*
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| nodes.NODE_ID.output | object | optional | — | The output of a completed node. Only accessible from nodes that run after the referenced node. |
| nodes.NODE_ID.status | string | optional | — | completed · failed · skipped. Useful in condition expressions. |
| nodes.NODE_ID.error | object | optional | — | Error details if the node failed: { code, message }. |
| nodes.NODE_ID.durationMs | integer | optional | — | How long the node took to execute. |
vars.*
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| vars.KEY | any | optional | — | Variables set by set_variable nodes. Mutable across the run. |
run.*
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| run.id | string | optional | — | Current run ID (run_...). |
| run.workflowId | string | optional | — | Workflow ID that owns this run. |
| run.startedAt | timestamp | optional | — | When this run started executing. |
| run.attempt | integer | optional | — | Attempt number (1 = first run, 2+ = retries). |
workspace.*
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| workspace.id | string | optional | — | Workspace ID. |
| workspace.slug | string | optional | — | Workspace slug. |
| workspace.name | string | optional | — | Workspace display name. |
| workspace.config.KEY | any | optional | — | Workspace configuration values set under Settings → Config. |
secrets.*
Secrets are write-once, read-in-node. They are resolved at runtime and never appear in logs or traces.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| secrets.SECRET_NAME | string | optional | — | Value of a secret stored in the vault. Case-sensitive. Never logged. |
JSONata Reference
JSONata is used in Transform nodes and condition expressions. The full JSONata specification is at jsonata.org. Key patterns for FlowOS:
Path navigation
trigger.record.title // string property
trigger.record.tags[0] // first array element
trigger.record.cmdbItems.ownerEmail // extract field from each array element
trigger.record.cmdbItems[class='server'] // filter arrayConstructing objects and arrays
// Build a new object
{
"summary": trigger.record.title,
"severity": trigger.record.severity,
"owners": trigger.record.cmdbItems.ownerEmail[], // force array
"isP1": trigger.record.severity = 'P1'
}
// Map array to new shape
trigger.record.cmdbItems.{
"name": name,
"owner": ownerEmail,
"class": class
}String functions
$string(value) // cast to string
$uppercase(str) // "hello" → "HELLO"
$lowercase(str) // "HELLO" → "hello"
$trim(str) // strip whitespace
$contains(str, pattern) // true/false, pattern can be regex
$replace(str, from, to) // replace occurrences
$split(str, separator) // split to array
$join(array, separator) // join array to string
$length(str) // string length
$substring(str, start, len) // substring extractionNumeric functions
$sum(array) // sum numeric array
$min(array) // minimum value
$max(array) // maximum value
$average(array) // arithmetic mean
$round(n, dp) // round to decimal places
$floor(n) // round down
$ceil(n) // round up
$abs(n) // absolute value
$formatNumber(n, picture) // e.g. $formatNumber(1234.5, '#,###.00') → "1,234.50"Array functions
$count(array) // number of elements
$append(a1, a2) // concatenate arrays
$sort(array, fn) // sort; optional comparator
$reverse(array) // reverse order
$distinct(array) // remove duplicates
$zip(a1, a2) // merge two arrays by index
$filter(array, fn) // filter: $filter(items, function($v){ $v.active })
$map(array, fn) // transform: $map(items, function($v){ $v.name })
$reduce(array, fn, init)// accumulateDate/time functions
$now() // current UTC timestamp ISO 8601
$millis() // current time in milliseconds
$fromMillis(ms) // ms → ISO 8601 string
$toMillis(timestamp) // ISO 8601 → ms
$dateTime(timestamp, picture) // format: $dateTime($now(), '[M01]/[D01]/[Y0001]')Conditional and logic
// Ternary
condition ? trueValue : falseValue
// Null coalescing
value ~> $default('fallback')
// Boolean operators
a and b
a or b
not condition
// Comparison
= (equals), != (not equals)
< > <= >=
in (array membership): "P1" in ["P1","P2"]Real-World Examples
Route based on SLA breach percentage
{
"type": "condition",
"config": {
"expression": "(($toMillis($now()) - $toMillis(trigger.record.createdAt)) / (trigger.record.sla.resolutionTarget * 1000)) * 100 >= 75"
}
}Build a Slack message with incident details
{
"action": "slack.postMessage",
"config": {
"input": {
"channel": "#incidents",
"text": "*[{{trigger.record.severity}}] {{trigger.record.title}}*
Assigned: {{trigger.record.assignedTeam}}
SLA due: {{trigger.record.sla.resolutionDue}}
<https://acme.flowos.io/itsm/{{trigger.record.id}}|View Incident>"
}
}
}Filter and reshape CMDB items for downstream use
{
"type": "transform",
"config": {
"expression": "trigger.record.cmdbItems[class='server'].{ 'host': hostname, 'ip': ipAddresses[0], 'owner': ownerEmail }",
"outputAs": "affectedServers"
}
}
// Access as: {{nodes.TRANSFORM_NODE_ID.output.affectedServers}}