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
| Root | Meaning |
|---|---|
| inputs.<key> | the run's validated inputs |
| steps.<id>.output… | outputs of steps that already succeeded |
| run.id · run.tenant_id · run.started_at | run metadata |
| item · item_index | the 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.