DOC_INDEX
THEME
DOCS/Specification/JavaScript Expressions

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 template

Where JavaScript works

LocationExample
if: / elif: conditionsif: 'vars.env === "prod" && steps.scan.hits === 0'
assert conditionscondition: 'steps.calc.total === 15'
loop.untiluntil: 'loop.iteration === 3'
retry.whenwhen: 'result.exit_code == 1'
transform function bodiesfull 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 objectAvailable in conditionsAvailable in transform
vars.* (defaults/task/step/env layers)yesyes
vars.* from CLI --varyesyes (since v0.8.0; invisible before)
steps.<name>.<output>yesyes
env.<NAME> (OS environment)yesyes
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):

  • return an object: its fields become steps.<step>.<field>.
  • return a primitive: it lands at steps.<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, no setTimeout - expressions and pure computation only. I/O belongs in shell/http steps.
  • No mutation across steps: returning values is the only way out of a transform; assigning to vars does not persist.
  • Conditions must evaluate to a truthy/falsy value; thrown errors fail the step.

When to prefer Go templates

  1. Interpolation - building strings: do: 'echo "{{ vars.app }}"', outputs: values, rendered config content.
  2. Pipes on outputs - {{ result.output | regexFind "v([0-9.]+)" }}.
  3. One-liner equality where either reads fine - house style decides.

Quick translation table:

IntentJavaScriptGo template
equalityvars.env === "prod"{{ eq vars.env "prod" }}
numeric compareNumber(vars.n) > 5{{ gt vars.n 5 }}
and / or / nota && b, !a{{ and a b }}, {{ not a }}
substringvars.msg.includes("ok"){{ contains "ok" vars.msg }} (needle first)
array lengthsteps.x.outputs.length{{ len steps.x.outputs }}
array indexsteps.x.outputs[1].host{{ (index steps.x.outputs 1).host }}
defaultvars.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 REPL

Reference