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:

  1. Detects expressions β€” scans the node's URL, headers, and body for {{ }} patterns
  2. Builds the evaluation context β€” collects all available data sources (context, results, row, rows, workflowId)
  3. Evaluates each expression β€” runs JSONata against the context to produce concrete values
  4. 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"
  }
}
FieldDescription
nodeIdThe ID of the node being resolved
jsonataThe raw URL, headers, and body containing {{ }} expressions to be evaluated
evaluationContextThe 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": []
}
FieldDescription
resolved.urlThe fully resolved URL
resolved.headersThe fully resolved headers
resolved.bodyThe fully resolved body with all expressions replaced by their values
warningsAny 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

  1. Open the workflow execution in the Temporal UI
  2. Find the expression activity in the event timeline β€” it will appear before the httpCall for the node you're debugging
  3. Click on the activity to expand it
  4. Check the Input tab to see:
    • jsonata β€” the raw url, headers, and body with their original {{ }} expressions
    • evaluationContext β€” the full JSON the expressions were evaluated against
  5. Check the Output tab to see:
    • The resolved values β€” what each expression actually resolved to
    • Any warnings from the resolution process

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.

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:

  1. Find the expression activity for the failing node in the Temporal timeline
  2. Open the Input tab and look at evaluationContext
  3. Verify the data path β€” does results.nodeId.data.field actually exist in the context?
  4. Check for typos β€” node IDs are case-sensitive and must match exactly
  5. Check the Output tab β€” look at resolved to see what the expression actually produced
  6. Review warnings β€” the warnings array may contain specific error messages
  7. Test the expression in the JSONata Playground using the evaluationContext JSON as input

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:

  1. A warning is logged: [expression-resolution] Activity failed for node "nodeId": ... Falling back to local resolution.
  2. Expression resolution falls back to happening locally inside the workflow
  3. 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.

Next steps