Template Syntax
Template expressions let you pass data between nodes, read workflow context, and transform values at runtime. Write JSONata expressions inside double curly brace delimiters.
Expression not resolving as expected? See Debugging Expressions for how to inspect the full evaluation context and resolved values directly in the Temporal workflow timeline.
Expression syntax
All template expressions use double curly braces with a JSONata expression inside:
{{ expression }}
Expressions are resolved at runtime, just before each node executes. Any string value in a node's URL, headers, or body can contain template expressions.
Basic expressions
'{{ context.userId }}' // read from workflow context
'{{ results.get_user.data.name }}' // read from a completed node
'{{ $secret("api_key") }}' // read a secret
'{{ results.get_order.data.total * 1.1 }}' // compute a value
Data sources
Four data sources are available inside template expressions:
| Source | Syntax | Description |
|---|---|---|
| Node results | results.nodeId.data.field | HTTP response body from a completed upstream node |
| Context | context.key | Global data passed when executing the workflow |
| Row | row.data.column / row.index | Current row in iterator child workflows |
| Secrets | $secret('name') | Credential resolved at runtime from your secrets vault |
$secret() is not a JSONata function. It uses a separate resolution path and is resolved after JSONata evaluation. See Secrets for setup and usage.
Accessing node results
When a node completes, its result is stored with three fields:
| Field | Type | Description |
|---|---|---|
data | any | The HTTP response body |
statusCode | number | The HTTP status code (e.g. 200, 404) |
headers | object | The response headers |
If a node called get_user returns this response body:
Response from get_user
{
"id": 123,
"name": "Alice",
"email": "alice@example.com"
}
Access its fields like this:
Accessing result fields
'{{ results.get_user.data.id }}' // 123
'{{ results.get_user.data.name }}' // "Alice"
'{{ results.get_user.data.email }}' // "alice@example.com"
'{{ results.get_user.statusCode }}' // 200
'{{ results.get_user.headers.content-type }}' // "application/json"
The .data. level is required when accessing the response body. results.get_user.data.name accesses a field in the response body. results.get_user.statusCode accesses the HTTP metadata directly.
Using templates in nodes
Templates work in URLs, headers, and request bodies. Here is a node that uses all three:
import { GraphCompose } from '@graph-compose/client'
const graph = new GraphCompose({
token: process.env.GRAPH_COMPOSE_TOKEN
})
graph
.node('get_user')
.get('https://api.example.com/users/{{ context.userId }}')
.end()
.node('create_order')
.post('https://api.example.com/orders')
.withHeaders({
'Authorization': 'Bearer {{ $secret("api_token") }}'
})
.withBody({
userId: '{{ results.get_user.data.id }}',
name: '{{ results.get_user.data.name }}',
discount: '{{ results.get_user.data.loyaltyPoints > 1000 ? 0.1 : 0 }}'
})
.withDependencies(['get_user'])
.end()
Embedded expressions
You can mix literal text with template expressions in a single string. This is common in URLs and header values.
Embedded expressions
// URL with path parameter
'https://api.example.com/users/{{ context.userId }}/orders'
// Authorization header
'Bearer {{ results.auth.data.token }}'
// Multiple expressions in one string
'https://api.example.com/{{ context.version }}/users/{{ results.get_user.data.id }}'
Embedded expressions in URLs are URL-encoded automatically. Expressions in request bodies are not URL-encoded.
Transforming data with JSONata
The expression language inside template delimiters is JSONata, a query and transformation language for JSON. Beyond simple field access, you can filter arrays, aggregate values, and reshape objects.
Filtering
// Get only active orders
'{{ results.get_orders.data[status = "active"] }}'
// Get names of items over $50
'{{ results.get_items.data[price > 50].name }}'
Aggregation
// Sum all item prices
'{{ $sum(results.get_orders.data.items.price) }}'
// Count completed orders
'{{ $count(results.get_orders.data[status = "completed"]) }}'
// Get the most expensive item
'{{ $max(results.get_items.data.price) }}'
Conditionals
// Ternary expression
'{{ results.check.data.amount > 100 ? "large" : "small" }}'
// Boolean check
'{{ results.get_user.data.verified = true }}'
Object projection
// Reshape array items into a new structure
'{{ results.get_cart.data.items.{ "productId": id, "total": price * quantity } }}'
Supported functions
Graph Compose supports the following JSONata built-in functions. Functions not listed here are blocked during validation.
| Category | Functions |
|---|---|
| String | $string, $length, $substring, $substringBefore, $substringAfter, $uppercase, $lowercase, $trim, $pad, $contains, $split, $join, $match, $replace |
| Encoding | $base64encode, $base64decode, $encodeUrlComponent, $encodeUrl, $decodeUrlComponent, $decodeUrl |
| Numeric | $number, $sum, $max, $min, $average, $round, $power, $sqrt, $abs, $floor, $ceil |
| Date/Time | $now, $millis, $fromMillis, $toMillis |
| Boolean | $boolean, $not, $exists |
| Array | $count, $append, $sort, $reverse, $shuffle, $distinct, $zip, $map, $filter, $reduce, $first, $last, $single |
| Object | $keys, $lookup, $spread, $sift, $each, $merge, $type |
$secret() is not a JSONata function and does not appear in this list. It is handled by a separate resolution step. See Secrets for details.
Array wrapping gotcha: $map, $filter, and similar functions unwrap single-element results to a scalar instead of a one-element array. When the result must be an array, wrap the call in [...] — e.g., {'{{'} [$map(items, function($i) {'{'} $i.url {'}'})] {'}}'}. See the JSONata Deep Dive for details.