Debugging Expressions
When a JSONata expression doesn't resolve as expected, the expression activity in your Temporal workflow timeline shows you exactly what was evaluated and what it resolved to.
Template expressions ({{ }}) are resolved at runtime just before each node executes. When something goes wrong β a typo in a path, a missing upstream result, or an unexpected data shape β the raw expression string gets sent as-is in the HTTP request body. Without visibility into the evaluation context, figuring out why it failed is difficult.
The expression activity solves this by recording both the input (the full evaluation context) and the output (the resolved values) as a dedicated event in the Temporal timeline.
How expression resolution works
Every node that contains {{ }} expressions in its URL, headers, or body goes through a resolution step before the HTTP call is made. The system:
- Detects expressions β scans the node's URL, headers, and body for
{{ }}patterns - Builds the evaluation context β collects all available data sources (
context,results,row,rows,workflowId) - Evaluates each expression β runs JSONata against the context to produce concrete values
- Passes resolved values to the HTTP activity for the actual API call
When a node has no template expressions, this step is skipped entirely β static nodes go straight to the HTTP call.
The expression activity
For nodes that contain expressions, Graph Compose executes a dedicated Temporal activity called expression before the HTTP call. This activity appears in your workflow timeline as its own event with full input and output visibility.
Input β what the expression was evaluated against
The activity input contains everything you need to understand why an expression resolved (or failed to resolve). It's split into two clear sections β the {{ }} expressions to be evaluated (jsonata) and the data they're evaluated against (evaluationContext):
expression activity input
{
"nodeId": "create_order",
"jsonata": {
"url": "https://api.example.com/orders",
"headers": {
"Authorization": "Bearer {{ $secret('api_token') }}"
},
"body": {
"userId": "{{ results.get_user.data.id }}",
"total": "{{ results.get_cart.data.total * 1.08 }}"
}
},
"evaluationContext": {
"context": { "region": "us-east" },
"results": {
"get_user": {
"data": { "id": 42, "name": "Alice" },
"statusCode": 200
},
"get_cart": {
"data": { "items": ["..."], "total": 89.99 },
"statusCode": 200
}
},
"workflowId": "wf-abc-123"
}
}
| Field | Description |
|---|---|
nodeId | The ID of the node being resolved |
jsonata | The raw URL, headers, and body containing {{ }} expressions to be evaluated |
evaluationContext | The full JSON object that the expressions are evaluated against |
Output β what the expressions resolved to
The activity output shows the final resolved values and any warnings:
expression activity output
{
"nodeId": "create_order",
"resolved": {
"url": "https://api.example.com/orders",
"headers": {},
"body": {
"userId": 42,
"total": 97.19
}
},
"warnings": []
}
| Field | Description |
|---|---|
resolved.url | The fully resolved URL |
resolved.headers | The fully resolved headers |
resolved.body | The fully resolved body with all expressions replaced by their values |
warnings | Any resolution warnings (e.g. expressions that returned undefined) |
Reading the activity in Temporal
When viewing a workflow execution in Temporal, look for the expression activity in the timeline. It appears right before the corresponding httpCall activity for any node that uses template expressions.
Step-by-step
- Open the workflow execution in the Temporal UI
- Find the
expressionactivity in the event timeline β it will appear before thehttpCallfor the node you're debugging - Click on the activity to expand it
- Check the Input tab to see:
jsonataβ the raw url, headers, and body with their original{{ }}expressionsevaluationContextβ the full JSON the expressions were evaluated against
- Check the Output tab to see:
- The
resolvedvalues β what each expression actually resolved to - Any
warningsfrom the resolution process
- The
What to look for
Compare jsonata (input) with resolved (output). If an expression from the input still appears as a raw string in the resolved output, the JSONata evaluation failed silently. Cross-reference the expression path with the evaluationContext to see if the data exists where you expect it.
The expression activity only appears for nodes that contain {{ }} expressions. Static nodes skip this activity entirely and go straight to httpCall.
Common resolution failures
Missing upstream result
The most common failure: referencing a node result that doesn't exist yet or was spelled differently.
Expression references a node that does not exist in results
// Expression in the node
"{{ results.get_users.data.id }}"
// But the evaluationContext shows:
{
"results": {
"get_user": { "data": { "id": 42 } }
}
}
// get_users vs get_user β the node ID is wrong
Fix: Check the evaluationContext.results object for the correct node ID. Node IDs are case-sensitive.
Accessing a nested path that doesn't exist
The upstream node returned a different shape than expected.
Path does not match the response shape
// Expression
"{{ results.get_user.data.address.city }}"
// evaluationContext shows the response has no address field:
{
"results": {
"get_user": {
"data": { "id": 42, "name": "Alice" }
}
}
}
Fix: Inspect the evaluationContext to see the actual shape of the upstream response. You may need to adjust your expression or add the missing field to the upstream API call.
Row context missing in forEach children
Inside a forEach child workflow, row is available but only while the child is running. If you reference row.data.someField and it's not resolving:
Verify row data in the evaluationContext
// Check that row is present in the evaluationContext
{
"evaluationContext": {
"row": {
"data": { "url": "https://example.com/file.mp3", "language": "es" },
"index": 0
},
"rows": [...]
}
}
Fix: Verify the row object is present in the evaluationContext and that your expression path matches the actual row data shape.
Debugging checklist
When an expression isn't resolving correctly:
- Find the
expressionactivity for the failing node in the Temporal timeline - Open the Input tab and look at
evaluationContext - Verify the data path β does
results.nodeId.data.fieldactually exist in the context? - Check for typos β node IDs are case-sensitive and must match exactly
- Check the Output tab β look at
resolvedto see what the expression actually produced - Review warnings β the
warningsarray may contain specific error messages - Test the expression in the JSONata Playground using the
evaluationContextJSON as input
You can copy the evaluationContext JSON directly from the Temporal UI Input tab and paste it into the JSONata Playground to test your expressions interactively.
Fallback behavior
The expression activity is a best-effort diagnostic tool. If the activity itself fails β for example, because the evaluation context exceeds Temporal's payload size limit (~2 MB) β the workflow does not break. Instead:
- A warning is logged:
[expression-resolution] Activity failed for node "nodeId": ... Falling back to local resolution. - Expression resolution falls back to happening locally inside the workflow
- The node continues executing normally
The only difference is that you won't see the expression activity in the timeline for that particular node. The HTTP call still happens with the same resolved values.