The expression language: advantages and limits

Everywhere an agent needs a dynamic value — a prompt, a URL, a request body, a condition — you write a small expression inside {{ … }}. It is deliberately not JavaScript: it is a closed, fully specified language that can be audited and can never execute injected text.

Two evaluation modes

  • A string that is exactly one {{ … }} yields the value with its native type — an array stays an array.
  • A string mixing text and {{ … }} substitutes each value as text into the template.
"{{ steps.extract.output.rows }}"        → the array itself
"Found {{ steps.extract.output.rows | length }} rows"  → "Found 3 rows"

What you can reference

RootMeaning
inputs.<key>the run's validated inputs
steps.<id>.output…outputs of steps that already succeeded
run.id · run.tenant_id · run.started_atrun metadata
item · item_indexthe current element inside a map step
vars.<key>tenant variables (secret values never appear in logs)

A missing path is never an error — it resolves to null, and | default('…') supplies a fallback. Referencing something out of scope (like item outside a map) is caught when you validate, not at runtime.

Functions — a closed set

Values pass through functions with |, applied left to right. The whole set: default, prefix, json, from_json, upper, lower, trim, length, join, split, first, last, number, string, b64encode, b64decode, slice. There are no others, and the validator rejects unknown names.

{{ steps.classify.output.rows | length }}
{{ inputs.title | trim | upper }}
{{ inputs.language | default('English') }}

Conditions

Branch conditions compare values with == != > >= < <= contains, combined with and / or / not. That is the whole grammar.

"{{ steps.classify.output.rows | length }} > 0"

What you can't do — and why

  • No arithmetic (+ - * /). No method calls. No ternaries. No user-defined functions. No access to anything outside the run's own data.
  • Why: a spec stays reviewable by a human and by the validator; evaluation is deterministic; and because expressions are data lookups — never code — injection is impossible by construction.

The escape hatch: the code step

When a transformation outgrows expressions — arithmetic, reshaping, parsing — write a code step. It is plain code with one convention:

export default ({ inputs, steps }) => {
  const rows = steps.extract.output.rows ?? [];
  return { total: rows.length, sum: rows.reduce((a, r) => a + r.amount, 0) };
};
  • Runs out-of-process in a permissionless Deno sandbox: no network, no filesystem, no environment variables.
  • Bounded: CPU-time limit and a capped output size — a runaway script fails its step, not the platform.