Variables & Outputs
defaults, task and step vars, runtime overrides, and passing data between steps
Two flows of data run through every real workflow: variables flow in (configuration), outputs flow forward (results of steps feeding later steps).
Declaring variables
File-level variables live under defaults: (note: not vars: at the top
level). Tasks and steps declare vars: blocks that override them:
name: vars-demo
defaults:
environment: "dev"
replicas: "1"
tasks:
show:
vars:
environment: "task-env" # overrides defaults
steps:
- name: print
vars:
environment: "step-env" # overrides the task
func: shell
do: echo "env={{ vars.environment }} replicas={{ vars.replicas }}"Everything is referenced the same way: {{ vars.name }}. The engine
resolves the nearest definition. Verified precedence, lowest to highest:
defaults < env group < environment < task vars < step vars < --vars-file < --varDebugging precedence
Seven layers is a lot of power, and eventually a value will surprise you — "why is
region set to that?" eval tells you what a value is; --explain tells you
why — which layer won, and which it shadowed. (Think git config --show-origin.)
Take this workflow:
name: deploy
defaults:
region: us-east-1
replicas: 2
environments:
production:
vars:
region: eu-west-1Ask "what does each variable resolve to, and from where?":
$ orchstep eval --explain --env production --var replicas=9
VARIABLE SOURCE VALUE
region environment eu-west-1 (over defaults)
replicas runtime 9 (over defaults)region came from the production environment (shadowing the default);
replicas came from your --var (shadowing the default). To see the full story
for one key, name it — * marks the winner:
$ orchstep eval --explain --env production region
region = eu-west-1 (from environment)
precedence (low -> high; last wins):
defaults us-east-1
* environment eu-west-1The same flags a run would use apply (-f, --env, --var, --vars-file), so
you're inspecting the exact context a run would see — without executing anything.
Environment variables too
--explain also reports the OS environment your workflow's top-level env: sets
(values masked; a {{ secrets.X }} shows as a placeholder and is never resolved),
flagging when it overrides an inherited OS variable:
# OS environment set by the workflow's env: (masked; task/step env: not shown)
ENV VAR SOURCE VALUE
API_TOKEN workflow env: ***
AWS_REGION workflow env: us-east-1
HOME workflow env: /custom/home (over inherited (os))See the CLI reference for the
full flag. --explain inspects the context before a run; to inspect live
state partway through a run, set a breakpoint
on a step (flags: [breakpoint], or --break-before <step> with no file edit).
Runtime overrides
orchstep run show --var environment=qa --var replicas=3
orchstep run show --vars-file overrides.yml # YAML map of key: value--var beats everything, including --vars-file.
Value formats: strings vs structured data
How a variable is typed depends on where it comes from:
| Source | Multiline text | JSON / object value |
|---|---|---|
defaults: / task & step vars: | yes — YAML block scalars (|) | yes — nested maps/lists, kept as objects |
--vars-file file.yml | yes (YAML block scalars) | yes (native, nested) |
stdin (… | orchstep run --stdin-var X) | yes (raw text) | yes — auto-detected (JSON, then YAML) |
--var key=value | awkward (the shell must pass a literal newline) | no — always a plain string |
The one that surprises people: --var values are always strings.
--var cfg='{"port":8080}' gives you the literal string {"port":8080}, not an
object — {{ vars.cfg.port }} won't work until you parse it (see below).
Multiline and structured values
Declare them in YAML — block scalars for text, nesting for objects:
defaults:
banner: | # multiline string (newlines preserved)
Deploying service
Please wait...
db: # nested object
host: db.internal
port: 5432
replicas: [a, b, c]Read them as {{ vars.banner }}, {{ vars.db.host }}, {{ index vars.db.replicas 0 }}.
For data coming from another program, pipe JSON to stdin — it's parsed
automatically (no flag needed beyond --stdin-var):
echo '{"user":"ada","roles":["admin"]}' | orchstep run grant --stdin-var input
# then: {{ vars.input.user }} -> ada (also available as {{ stdin.user }})Converting between text and objects
| You have | You want | Template |
|---|---|---|
| object | JSON string | {{ vars.obj | toJson }} (or toPrettyJson) |
| object | YAML string | {{ vars.obj | toYaml }} |
| JSON/YAML string | object | {{ toObj vars.s }} or {{ vars.s | fromJson }} |
Two rules of thumb:
- Pick one type per variable (string or object) and convert only at the
boundary: parse a string into an object once with
toObj, and serialize an object back to a string only when something needs text (toJson). So if you're handed a JSON string via--var, normalize it early — e.g. a step varcfg: "{{ toObj vars.raw }}"— then use{{ vars.cfg.field }}everywhere after. - Shells only receive strings. To pass an object to an external CLI, serialize
it —
do: "mytool --config '{{ vars.db | toJson }}'"— or pass individual fields like{{ vars.db.host }}. A subprocess can't see a nested object directly.
Step outputs
A step exposes results with an outputs: block. Later steps read them as
steps.<step_name>.<output_name>:
tasks:
main:
steps:
- name: build_image
func: shell
do: echo "Built image myapp:v1.2.3"
outputs:
tag: '{{ result.output | regexFind "myapp:([a-z0-9.]+)" }}'
- name: deploy
func: shell
do: echo "deploying {{ steps.build_image.tag }}"result is the just-finished execution: result.output (stdout),
result.exit_code, and - when the output parses as JSON or YAML -
result.data_object for direct field access:
- name: emit_json
func: shell
do: |
echo '{"orders": 124, "region": "apac"}'
outputs:
orders: "{{ result.data_object.orders }}"regexFind
The workhorse extractor. With a capture group it returns the group; without one it returns the whole match:
outputs:
version: '{{ result.output | regexFind "myapp:([a-z0-9.]+)" }}' # -> v1.2.3
full: '{{ result.output | regexFind "myapp:[a-z0-9.]+" }}' # -> myapp:v1.2.3Use explicit character classes like [a-z0-9.]+ in patterns -
backslash-escapes such as \w are invalid inside Go template strings.
Where to go next
Branching - make steps conditional on those variables and outputs.