Expressions
Zorb uses ${{ }} as a template syntax for injecting dynamic values into workflow configuration. Expressions are resolved before a step executes.
Where expressions work
Expressions are resolved in env: values at every scope (workflow, task, and step level). They also apply to with: inputs on uses: steps.
tasks:
deploy:
inputs:
environment:
type: string
required: true
env:
TARGET: ${{ inputs.environment }} # ✓ resolved before the step runs
steps:
- run: echo "Deploying to $TARGET" # ✓ $TARGET — native shell varrun: strings are never interpolated. They are passed to the shell verbatim. To use an expression result inside a shell command, map it to an env var first and read it natively:
env:
MODE: ${{ inputs.dry-run ? 'dry-run' : 'apply' }}
steps:
- run: echo "Running in $MODE mode" # reads the env var, not an expressionThis avoids two layers of substitution and keeps shell steps readable as plain scripts.
Variables
inputs.<name>
Refers to a task input resolved from --with flags or its declared default.
tasks:
deploy:
inputs:
environment:
type: string
required: true
dry-run:
type: boolean
default: false
env:
TARGET: ${{ inputs.environment }}
DRY: ${{ inputs.dry-run }}Input names can contain hyphens (inputs.dry-run). Referencing an input that doesn't exist is an error.
env.<name>
Refers to an environment variable in scope at the point of evaluation. The scope builds up in layers — process environment, then workflow-level env:, then task-level env: — so earlier layers are visible to later ones.
env:
BASE_URL: https://example.com
tasks:
ping:
env:
HEALTH_URL: ${{ env.BASE_URL }}/health # BASE_URL is in scope here
steps:
- run: curl $HEALTH_URLsecrets.<name>
Refers to a value registered into the run-scoped secret table by a previous action (typically a @zorb/secrets/* loader). Secrets are resolved in with: and env: the same as other variables, and any registered value is replaced with *** in step stdout/stderr.
secrets:
- uses: '@zorb/secrets/load-1password'
with:
vault: Production
items: [DATABASE_URL]
tasks:
deploy:
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- run: ./bin/migrateReferencing a secret that hasn't been registered is an error.
steps.<id>.outputs.<key>
Refers to an output produced by an earlier step in the same task. Steps opt in to outputs by giving themselves an id:.
tasks:
release:
steps:
- id: version
uses: ./scripts/version.action
- name: Tag
env:
TAG: ${{ steps.version.outputs.tag }}
run: git tag "$TAG"Code actions produce outputs from the object returned by action. Shell steps write key=value lines to the file path in $ZORB_OUTPUT. Multi-line values use heredoc syntax:
echo "tag=v1.2.3" >> "$ZORB_OUTPUT"
{
echo 'notes<<EOF'
cat CHANGELOG.md
echo 'EOF'
} >> "$ZORB_OUTPUT"Referencing an unknown step id or output key is an error.
Operators
Equality: ==, !=
Both sides are coerced to strings before comparison, so true == 'true' and 3 == '3' both hold.
MODE: "${{ inputs.environment == 'prod' ? 'production' : 'staging' }}"Logical: &&, ||, !
Short-circuit evaluation, same semantics as JavaScript: && returns the first falsy value or the last value; || returns the first truthy value or the last value; ! always returns a boolean.
env:
SKIP: ${{ inputs.dry-run || inputs.no-deploy }}
RUN: ${{ !inputs.dry-run }}Falsy values: false, 0, empty string ''. Everything else is truthy.
Ternary
${{ condition ? value_if_true : value_if_false }}env:
TAG: "${{ inputs.env == 'prod' ? 'latest' : inputs.env }}"
MODE: "${{ inputs.dry-run ? 'dry-run' : 'apply' }}"The condition can be any expression. Both branches are valid expressions too, including nested ternaries (though deeply nested ternaries are hard to read — prefer mapping inputs to env vars and using shell conditionals instead).
Functions
Call a function directly or chain it as a filter with | (see Filter syntax below).
| Function | Signature | Description |
|---|---|---|
upper | upper(s) | Uppercase string |
lower | lower(s) | Lowercase string |
trim | trim(s) | Strip leading and trailing whitespace |
replace | replace(s, from, to) | Replace all occurrences of from with to |
contains | contains(s, needle) | true if s contains needle |
startsWith | startsWith(s, prefix) | true if s starts with prefix |
endsWith | endsWith(s, suffix) | true if s ends with suffix |
length | length(s) | Character count of s |
string | string(v) | Convert any value to its string representation |
number | number(v) | Parse a string to a number; errors if not numeric |
boolean | boolean(v) | Convert to boolean; accepts true/false, 1/0, yes/no |
default | default(v, fallback) | Return v if non-empty, otherwise fallback |
env:
UPPER_ENV: ${{ upper(inputs.environment) }}
SAFE_NAME: ${{ replace(inputs.name, '/', '-') }}
HAS_PREFIX: ${{ startsWith(inputs.tag, 'v') }}
COUNT: ${{ number(inputs.replicas) }}Filter syntax
Functions can be applied as filters using |. The value on the left becomes the first argument:
${{ value | fn }} → fn(value)
${{ value | fn(arg) }} → fn(value, arg)Filters compose left-to-right:
${{ value | trim | lower }} → lower(trim(value))This is particularly readable for transformation chains:
env:
TAG: ${{ inputs.version | trim | lower | replace('.', '-') }}
NAME: ${{ inputs.name | default('anonymous') | upper }}Error behaviour
Referencing an undefined variable is always an error — there is no silent empty-string fallback. This catches typos early.
env:
TARGET: ${{ inputs.environemnt }} # error: undefined variable: inputs.environemntCalling an unknown function, referencing an undefined secret or step output, or naming a namespace other than inputs, env, secrets, or steps is also an error.
Quoting in YAML
YAML parses : and { as structure characters in certain positions. Wrap any ${{ }} value that might trigger this in double quotes:
env:
OK: ${{ inputs.env }} # fine — no special chars
SAFE: "${{ inputs.env == 'prod' ? 'a' : 'b' }}" # quote when : appears