JavaScript Expressions
The JS-first guide - where JavaScript works, what it can see, its restrictions, and when to fall back to Go templates
OrchStep evaluates expressions in two languages: JavaScript (goja,
ES5+) and Go templates (Sprig). Our recommendation is JS-first:
vars.count > 5 && vars.mode === "fast" reads the way you think, while
the Go-template spelling ({{ and (gt vars.count 5) (eq vars.mode "fast") }})
exists for interpolation and as the fallback. Everything below was
verified against v0.7.1.
How the engine decides
If the expression is wrapped in {{ }} it is a Go template; otherwise it
is evaluated as JavaScript:
if: 'vars.score > 80' # JavaScript
if: '{{ gt vars.score 80 }}' # Go templateWhere JavaScript works
| Location | Example |
|---|---|
if: / elif: conditions | if: 'vars.env === "prod" && steps.scan.hits === 0' |
assert conditions | condition: 'steps.calc.total === 15' |
loop.until | until: 'loop.iteration === 3' |
retry.when | when: 'result.exit_code == 1' |
transform function bodies | full JS programs with return |
Not JavaScript: do: for shell (that is shell), outputs: values and
switch.value (Go templates), and anything you want interpolated into
text - producing strings inside YAML is template territory.
What JavaScript can see (verified)
| Context object | Available in conditions | Available in transform |
|---|---|---|
vars.* (defaults/task/step/env layers) | yes | yes |
vars.* from CLI --var | yes | yes (since v0.8.0; invisible before) |
steps.<name>.<output> | yes | yes |
env.<NAME> (OS environment) | yes | yes |
loop.* (inside loops) | yes (until) | - |
result.* (just-ran attempt) | yes (retry.when) | - |
Before v0.8.0, transform JS did not see runtime --var overrides
(conditions did). Since v0.8.0 every JavaScript context uses the same
merged vars namespace as templates. On older engines, stage such values
through a previous step's outputs:
- name: stage_inputs
func: shell
do: 'echo "slo={{ vars.slo }}"' # templates see --var
outputs:
slo: '{{ result.output | regexFind "slo=([0-9.]+)" }}'
- name: math
func: transform
do: |
const slo = Number(steps.stage_inputs.slo); # JS sees steps.*
return { breach: availability < slo };Numbers: the one rule that matters
Values that traveled through outputs, with: module parameters, or env
files arrive as strings. Compare numerically with Number():
condition: 'Number(steps.tally.errors) === 0' # right
condition: 'Number(vars.orders) >= Number(vars.min)' # right (module with:)
condition: 'steps.tally.errors === 0' # wrong: "0" !== 0(Go templates have the mirror-image problem: gt errors out on mixed
string/number types.)
transform: full JavaScript programs
- name: aggregate
func: transform
do: |
const rows = steps.fetch.outputs; // loop outputs = array
const bad = rows.filter(r => r.status !== "ok");
return {
total: rows.length,
failures: bad.length,
names: bad.map(r => r.name).join(", "),
};Rules (verified):
returnan object: its fields becomesteps.<step>.<field>.returna primitive: it lands atsteps.<step>.result.- Built-in helpers:
utils.parseJSON,utils.stringifyJSON,utils.unique(arr),utils.sum(arr). - Transform code is not template-rendered - a literal
{{ }}inside it stays literal text. Bridge values via step outputs (above).
Restrictions & limitations
- ES5+ via goja: no
require/import, no filesystem or network, nosetTimeout- expressions and pure computation only. I/O belongs inshell/httpsteps. - No mutation across steps: returning values is the only way out of a
transform; assigning to
varsdoes not persist. - Conditions must evaluate to a truthy/falsy value; thrown errors fail the step.
When to prefer Go templates
- Interpolation - building strings:
do: 'echo "{{ vars.app }}"',outputs:values, rendered config content. - Pipes on outputs -
{{ result.output | regexFind "v([0-9.]+)" }}. - One-liner equality where either reads fine - house style decides.
Quick translation table:
| Intent | JavaScript | Go template |
|---|---|---|
| equality | vars.env === "prod" | {{ eq vars.env "prod" }} |
| numeric compare | Number(vars.n) > 5 | {{ gt vars.n 5 }} |
| and / or / not | a && b, !a | {{ and a b }}, {{ not a }} |
| substring | vars.msg.includes("ok") | {{ contains "ok" vars.msg }} (needle first) |
| array length | steps.x.outputs.length | {{ len steps.x.outputs }} |
| array index | steps.x.outputs[1].host | {{ (index steps.x.outputs 1).host }} |
| default | vars.x || "fallback" | {{ vars.x | default "fallback" }} |
Try expressions instantly
orchstep eval evaluates any expression on this page against a real
workflow context - same engine code paths, no steps executed. See the
Expression Playground:
orchstep eval --env production 'vars.db_host'
orchstep eval '{{ gt vars.replicas 4 }}'
orchstep eval # interactive REPLReference
- Templates & Expressions - the Go-template side
- transform - function reference
- assert - dual-syntax assertions
- goja implements ECMAScript 5.1+ (github.com/dop251/goja)