JSONata expressions
Query, filter, and transform data in flows with JSONata, a safe expression language that is always available.
#JSONata expressions
JSONata is a lightweight expression language for querying and transforming JSON. It's EmuView's primary way to work with data inside flows: expressions cannot make HTTP requests, write to the database, or touch files, so they're safe to run anywhere — no sandbox setup required.
For most data transformation, an expression replaces a custom script. Reach for scripts only when you need side effects or multi-step logic.
#Where you can use expressions
- The
run_expressionoperation — a dedicated flow step that evaluates one expression and outputs the result. - Template expressions — inline in any operation config field that supports templates.
/* Filter active items from the trigger payload */
$trigger.body.items[status = "active"]
/* Sum all prices */
$sum($trigger.body.items.price)
/* Build a greeting string */
"Hello, " & $trigger.body.name & "!"
#Template expressions
Operation config fields support two template forms:
{{$variable}}— direct dot-path access, resolved without JSONata. Fast and covers most cases.{{= expression }}— a full JSONata expression, marked by the=.
{
"to": "{{$trigger.body.email}}",
"subject": "Order #{{$trigger.body.order_id}} - {{= $trigger.body.total > 100 ? 'Premium' : 'Standard' }}",
"body": "Hi {{$trigger.body.name}}, your total is ${{= $round($trigger.body.total, 2) }}"
}
When a field contains a single template with no surrounding text — "{{$steps.fetch.records}}" — the resolved value keeps its original type. An array stays an array; it isn't converted to a string.
#Available variables
The flow context is available in every expression:
| Variable | Description | Example |
|---|---|---|
$trigger |
Full trigger payload | $trigger.body.email |
$data |
Shorthand for $trigger.body |
$data.items |
$steps |
Previous step outputs, keyed by step key | $steps.fetch_users.records |
$user |
The user the flow runs as | $user.email, $user.role |
$flow |
Flow metadata | $flow.name, $flow.id |
$run |
Current run | $run.id |
$now |
Current ISO timestamp | $now |
$selection |
Selected records (manual triggers) | $selection[0].id |
#Common patterns
#Accessing data
/* A field, a nested field, a step output, an array element */
$trigger.body.name
$trigger.body.address.city
$steps.read_records.records
$trigger.body.items[0]
#Filtering arrays
/* By value, by comparison, combined, negated */
$trigger.body.items[status = "active"]
$trigger.body.items[price > 50]
$trigger.body.items[status = "active" and price > 10]
$trigger.body.items[status != "archived"]
#Transforming data
/* Pluck one field from every item */
$trigger.body.items.name
/* → ["Widget", "Gadget", "Doohickey"] */
/* Reshape objects */
$trigger.body.items.{
"id": id,
"label": name & " ($" & $string(price) & ")",
"active": status = "active"
}
/* Computed fields */
$trigger.body.items.{
"name": name,
"total": price * quantity,
"tax": price * quantity * 0.1
}
#Strings
$trigger.body.first_name & " " & $trigger.body.last_name
$uppercase($trigger.body.name)
$lowercase($trigger.body.email)
$substring($trigger.body.description, 0, 100) & "..."
$replace($trigger.body.text, "old", "new")
$substringBefore($trigger.body.email, "@") /* "alex" from "alex@example.com" */
$trim($trigger.body.input)
$pad($string($trigger.body.order_number), 8, "0") /* "00000042" */
#Maths and aggregation
$sum($trigger.body.items.price)
$average($trigger.body.items.score)
$count($trigger.body.items)
$min($trigger.body.items.price)
$max($trigger.body.items.price)
$round(total * 1.1, 2)
($trigger.body.subtotal + $trigger.body.shipping) * 1.1
#Conditional logic
/* Ternary */
$trigger.body.amount > 100 ? "premium" : "standard"
/* Nested ternary */
$trigger.body.score >= 90 ? "A" :
$trigger.body.score >= 80 ? "B" :
$trigger.body.score >= 70 ? "C" : "F"
/* Defaults for missing values */
$exists($trigger.body.discount) ? $trigger.body.discount : 0
#Sorting and grouping
/* Sort ascending / descending with the ^ operator */
$trigger.body.items^(price)
$trigger.body.items^(>price)
/* Group totals per category */
$trigger.body.items{ category: $sum($.price) }
/* Distinct values */
$distinct($trigger.body.items.category)
#Dates
$now() /* current ISO timestamp */
$toMillis("2026-01-15T10:30:00Z") /* date string → milliseconds */
$fromMillis(1737024600000) /* milliseconds → ISO string */
$toMillis($trigger.body.expires_at) > $toMillis($now) /* compare */
#EmuView functions
EmuView registers these functions on top of the standard JSONata library:
| Function | Signature | Description |
|---|---|---|
$uuid() |
→ string |
Generate a UUID v4 |
$slug(str) |
string → string |
Convert to a URL-safe slug |
$base64encode(str) |
string → string |
Base64 encode |
$base64decode(str) |
string → string |
Base64 decode |
$truncate(str, n) |
string, number → string |
Truncate with an ellipsis |
$hash(str) |
string → string |
Non-cryptographic hash (djb2) |
$coalesce(a, b) |
any, any → any |
First non-null value |
$keys(obj) |
object → string[] |
Object keys |
$values(obj) |
object → any[] |
Object values |
$entries(obj) |
object → {key, value}[] |
Object entries |
$slug($trigger.body.title) /* "My Blog Post!" → "my-blog-post" */
$coalesce($trigger.body.nickname, $trigger.body.name) /* fallback default */
#Coming from JavaScript
| JavaScript | JSONata | Notes |
|---|---|---|
arr.filter(x => x.active) |
arr[active = true] |
Predicate filters |
arr.map(x => x.name) |
arr.name |
Path projection |
arr.length |
$count(arr) |
Count function |
arr.reduce((s, x) => s + x.price, 0) |
$sum(arr.price) |
Built-in aggregation |
str.toUpperCase() |
$uppercase(str) |
Function call syntax |
a + " " + b |
a & " " & b |
Concatenation uses & |
obj.hasOwnProperty('key') |
$exists(obj.key) |
Existence check |
JSON.stringify(obj) |
$string(obj) |
Stringify |
parseInt(str) |
$number(str) |
Parse number |
if/else |
condition ? a : b |
Ternary only |
arr.sort((a, b) => ...) |
$sort(arr, fn) |
Sort function |
#Gotchas
- String concatenation uses
&, not+. Using+on strings is an error. - Equality is
=, not===. - No statements or semicolons — an expression is a single value. Use
(expr1; expr2; result)for sequencing. - Array filtering uses
[predicate]—items[price > 10]filters,items[0]indexes. - Functions use
function($param) { ... }, not arrow syntax.
#Limits
- Timeout: 5 seconds by default; the
run_expressionoperation accepts up to 10 seconds. - No side effects: expressions cannot make network requests or write data. Pair them with operations like
http_requestandcrud_update— the expression shapes the data, the operation acts on it.
#Further reading
The complete language specification lives in the official JSONata documentation. Useful reference pages: path operators, predicate queries, string functions, numeric functions, array functions, and date/time functions.