JSONata Deep Dive
Advanced patterns for using JSONata expressions inside {'{{'} {'}}'} template delimiters across your workflow nodes.
JSONata is a lightweight query and transformation language for JSON data. Graph Compose uses it as the expression language inside template delimiters. You can test expressions interactively in the JSONata Playground.
This page assumes you are familiar with the basics of template expressions. If you are new to templates, start with Template Syntax, which covers expression delimiters, data sources, and the full list of supported functions.
All examples on this page show the JSONata expression as it would appear inside a template value (URL, header, or body field). The expression is always wrapped in {'{{'} {'}}'} delimiters.
String operations
Concatenate strings with the & operator. Do not use $concat() as it does not exist in JSONata.
String concatenation
// Join first and last name
'{{ results.get_user.data.firstName & " " & results.get_user.data.lastName }}'
// Build a greeting
'{{ "Hello, " & results.get_user.data.name }}'
Case conversion and trimming
// Uppercase
'{{ $uppercase(results.get_user.data.email) }}'
// Lowercase
'{{ $lowercase(results.get_user.data.role) }}'
// Trim whitespace
'{{ $trim(results.get_input.data.query) }}'
Substrings and splitting
// Extract domain from email
'{{ $substringAfter(results.get_user.data.email, "@") }}'
// Get first 10 characters
'{{ $substring(results.get_user.data.description, 0, 10) }}'
// Split a comma-separated string into an array
'{{ $split(results.get_tags.data.tags, ",") }}'
// Join an array into a string
'{{ $join(results.get_items.data.names, ", ") }}'
Search and replace
// Check if a string contains a substring
'{{ $contains(results.get_user.data.bio, "developer") }}'
// Replace text
'{{ $replace(results.get_template.data.body, "{name}", results.get_user.data.name) }}'
Array operations
JSONata provides powerful array operations. These work on arrays nested inside node results.
Filtering
// Get active users
'{{ results.get_users.data[status = "active"] }}'
// Get items above a price threshold
'{{ results.get_items.data[price > 50] }}'
// Filter by multiple conditions
'{{ results.get_orders.data[status = "completed" and total > 100] }}'
// Negative filter
'{{ results.get_users.data[role != "admin"] }}'
Extracting fields
// Get all names from an array of objects
'{{ results.get_users.data.name }}'
// Get all email addresses from active users
'{{ results.get_users.data[status = "active"].email }}'
Aggregation
// Sum prices
'{{ $sum(results.get_orders.data.total) }}'
// Count items
'{{ $count(results.get_orders.data) }}'
// Average rating
'{{ $average(results.get_reviews.data.rating) }}'
// Min and max
'{{ $min(results.get_bids.data.amount) }}'
'{{ $max(results.get_bids.data.amount) }}'
Sorting and deduplication
// Sort by price ascending
'{{ $sort(results.get_items.data, function($a, $b) { $a.price > $b.price }) }}'
// Reverse an array
'{{ $reverse(results.get_items.data) }}'
// Remove duplicates
'{{ $distinct(results.get_tags.data) }}'
// Get first and last elements
'{{ $first(results.get_items.data) }}'
'{{ $last(results.get_items.data) }}'
Map, filter, and reduce
// Map: transform each item
'{{ $map(results.get_users.data, function($user) { $user.firstName & " " & $user.lastName }) }}'
// Filter: keep items matching a predicate
'{{ $filter(results.get_items.data, function($item) { $item.inStock = true }) }}'
// Reduce: accumulate a value
'{{ $reduce(results.get_cart.data.items, function($acc, $item) { $acc + $item.price * $item.quantity }, 0) }}'
Array wrapping gotcha: JSONata's $map, $filter, and similar functions unwrap single-element results to a scalar value instead of returning a one-element array. When the downstream consumer expects an array (e.g., an API that validates the field as array), wrap the call in the JSONata array constructor [...]:
// BAD — returns a bare string when there is only 1 item
{'{{'} $map(items, function($i) {'{'} $i.url {'}'}) {'}}'}
// GOOD — always returns an array
{'{{'} [$map(items, function($i) {'{'} $i.url {'}'})] {'}}'}
This also applies to $filter, $sort, and any expression that may produce a single-element sequence. You can verify the behavior in the JSONata Playground.
Numeric operations
Arithmetic
// Calculate total with tax
'{{ results.get_order.data.subtotal * 1.08 }}'
// Percentage
'{{ results.get_stats.data.completed / results.get_stats.data.total * 100 }}'
// Rounding
'{{ $round(results.get_order.data.total, 2) }}'
// Floor and ceiling
'{{ $floor(results.get_calc.data.value) }}'
'{{ $ceil(results.get_calc.data.value) }}'
Math functions
// Absolute value
'{{ $abs(results.get_balance.data.amount) }}'
// Power and square root
'{{ $power(results.get_data.data.base, 2) }}'
'{{ $sqrt(results.get_data.data.variance) }}'
Conditional logic
Use the ternary operator (? :) for conditional values.
Ternary expressions
// Simple condition
'{{ results.get_user.data.age >= 18 ? "adult" : "minor" }}'
// Discount based on order total
'{{ results.get_order.data.total > 100 ? results.get_order.data.total * 0.9 : results.get_order.data.total }}'
// Choose an endpoint based on environment
'{{ context.env = "production" ? "https://api.example.com" : "https://staging.example.com" }}'
Boolean checks
// Check if a field exists
'{{ $exists(results.get_user.data.premium) }}'
// Negate a boolean
'{{ $not(results.get_user.data.blocked) }}'
// Equality check (single = in JSONata, not ==)
'{{ results.get_order.data.status = "paid" }}'
JSONata uses a single = for equality comparison, not ==. This is different from JavaScript.
Object transformation
Reshape objects using JSONata's object projection syntax.
Object projection
// Transform each item in an array into a new shape
'{{ results.get_cart.data.items.{ "productId": id, "total": price * quantity } }}'
// Build a summary object
'{{ { "name": results.get_user.data.name, "orderCount": $count(results.get_orders.data), "totalSpent": $sum(results.get_orders.data.total) } }}'
Object utilities
// Get keys of an object
'{{ $keys(results.get_config.data) }}'
// Merge two objects
'{{ $merge([results.get_defaults.data, results.get_overrides.data]) }}'
// Spread object into key-value pairs
'{{ $spread(results.get_config.data) }}'
// Check the type of a value
'{{ $type(results.get_data.data.value) }}'
Combining data from multiple nodes
When a node depends on multiple upstream nodes, you can reference results from all of them in a single expression.
Cross-node references
// Build a request body from two upstream nodes
.withBody({
userId: '{{ results.get_user.data.id }}',
orderId: '{{ results.get_order.data.id }}',
total: '{{ results.get_order.data.subtotal + results.get_shipping.data.cost }}',
address: '{{ results.get_user.data.address }}'
})
Conditional routing based on upstream data
// Use in a continueTo condition
.withConditions({
continueTo: [
{
to: 'process_premium',
when: '{{ results.get_user.data.tier = "premium" and results.get_order.data.total > 50 }}'
},
{
to: 'process_standard',
when: '{{ results.get_user.data.tier != "premium" }}'
}
]
})
Date and time
Timestamps
// Current time as ISO string
'{{ $now() }}'
// Current time as milliseconds since epoch
'{{ $millis() }}'
// Convert milliseconds to formatted string
'{{ $fromMillis($millis(), "[Y]-[M01]-[D01]T[H01]:[m01]:[s01]Z") }}'
// Convert ISO string to milliseconds
'{{ $toMillis(results.get_event.data.createdAt) }}'
Encoding
URL encoding
// Encode a query parameter value
'{{ $encodeUrlComponent(results.get_input.data.query) }}'
// Decode a URL-encoded value
'{{ $decodeUrlComponent(results.get_data.data.encoded) }}'
Base64
// Encode credentials for Basic auth
'{{ $base64encode(context.username & ":" & context.password) }}'
// Decode a base64 string
'{{ $base64decode(results.get_data.data.encoded) }}'
Common recipes
Build a dynamic URL from upstream data
Dynamic URL
graph
.node('get_user')
.get('https://api.example.com/users/{{ context.userId }}')
.end()
.node('get_orders')
.get('https://api.example.com/users/{{ results.get_user.data.id }}/orders')
.withDependencies(['get_user'])
.end()
Construct an authorization header from a secret
Basic auth header
graph
.node('call_api')
.post('https://api.example.com/data')
.withHeaders({
'Authorization': 'Basic {{ $base64encode($secret("api_user") & ":" & $secret("api_pass")) }}'
})
.end()
Sum items from an upstream response
Aggregate and forward
graph
.node('get_cart')
.get('https://api.example.com/cart/{{ context.cartId }}')
.end()
.node('create_invoice')
.post('https://api.example.com/invoices')
.withBody({
cartId: '{{ context.cartId }}',
lineItems: '{{ $count(results.get_cart.data.items) }}',
subtotal: '{{ $sum(results.get_cart.data.items.(price * quantity)) }}',
currency: '{{ results.get_cart.data.currency }}'
})
.withDependencies(['get_cart'])
.end()
Filter and reshape an array before sending
Filter and transform
graph
.node('get_products')
.get('https://api.example.com/products')
.end()
.node('submit_report')
.post('https://api.example.com/reports')
.withBody({
inStockProducts: '{{ results.get_products.data[inStock = true].{ "sku": sku, "name": name, "price": price } }}'
})
.withDependencies(['get_products'])
.end()