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

  1. The run_expression operation — a dedicated flow step that evaluates one expression and outputs the result.
  2. 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

  1. String concatenation uses &, not +. Using + on strings is an error.
  2. Equality is =, not ===.
  3. No statements or semicolons — an expression is a single value. Use (expr1; expr2; result) for sequencing.
  4. Array filtering uses [predicate]items[price > 10] filters, items[0] indexes.
  5. Functions use function($param) { ... }, not arrow syntax.

#Limits

  • Timeout: 5 seconds by default; the run_expression operation accepts up to 10 seconds.
  • No side effects: expressions cannot make network requests or write data. Pair them with operations like http_request and crud_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.