Variables
Variables show up in prompt bodies, command strings, and template bodies. Z.E.N. resolves them just before each node fires, replacing the $NAME reference with the actual value. Write a workflow once, run it with different inputs.
Built-in variables
These are always available without setup:
| Variable | Where it works | Meaning |
|---|---|---|
$ARGUMENTS | Commands, prompts | All arguments passed to the command as a single string |
$1, $2, $3, ... | Commands, prompts | Positional arguments by index |
$ARTIFACTS_DIR | Commands, prompts | Absolute path to the run's artifact directory |
$WORKFLOW_ID | Commands, prompts | The current run's id |
$BASE_BRANCH | Commands, prompts | The project's primary git branch (auto-detected, or set via worktree.baseBranch) |
$DOCS_DIR | Commands, prompts | Documentation directory (default: docs/) |
$<node-id>.output | DAG when: conditions, downstream prompt: fields | The text output of a completed node |
$ARGUMENTS is the most common. When a workflow is fired with zen workflow run morning-brief "summarize Slack", the summarize Slack text becomes $ARGUMENTS. Inside a workflow node that's a command, $ARGUMENTS carries whatever the caller passed.
Variables from the workflow itself
Top-level vars: declarations interpolate everywhere downstream:
name: research-sprint
vars:
TOPIC: "Q3 strategy"
AUDIENCE: "leadership"
nodes:
- id: search
type: prompt
prompt: |
Research $TOPIC for $AUDIENCE.Override per-fire on the command line:
zen workflow run research-sprint --vars TOPIC="board agenda" AUDIENCE="board members"Variables from node outputs
Every node has an output, accessible to downstream nodes as $<node-id>.output:
- id: draft
type: prompt
prompt: "Write something."
- id: review
type: prompt
depends_on: [draft]
prompt: |
Review this draft:
$draft.outputFor nodes that return structured output (JSON, multiple fields), drill in with dot notation: $draft.fields.headline. For nodes that return plain text, $draft.output is the whole text.
Variables from environment
Environment variables are accessible directly by name in bash nodes:
- id: post
type: bash
command: curl -X POST -H "Authorization: Bearer $SLACK_TOKEN" ...Z.E.N. filters which env vars are exposed to subprocesses via SUBPROCESS_ENV_ALLOWLIST. By default the allowlist covers system essentials, provider tokens, git identity, and GitHub tokens; arbitrary keys from your codebase are blocked. See Security for the full set.
Variables from the codebase config
.zen/config.yaml can declare env: and codebase_env_vars: blocks that get merged on top of the filtered base. Per-codebase env vars are scoped to runs against that codebase.
Conditional rendering
Mustache-style conditionals work in prompt bodies and templates:
prompt: |
Draft the brief.
{{#REJECTION_REASON}}
Previous draft was rejected. Reason: $REJECTION_REASON
Address it.
{{/REJECTION_REASON}}If $REJECTION_REASON is set, the block renders. If not, it's silently omitted.
What happens when a variable is missing
Z.E.N. handles missing variables differently depending on the variable:
- Always-available with default.
$DOCS_DIRfalls back todocs/if not configured. Never errors. - Required but missing. If a workflow declares
vars: { TOPIC: ... }and you don't pass a value, the run fails at render time withvariable_missing: TOPIC. $BASE_BRANCHis strict. If a workflow references it and Z.E.N. can't auto-detect a base branch, the workflow fails before any node fires. Better to fail loudly than push to the wrong branch.
Render-time vs load-time
"Render time" means the moment just before a node fires. Variables are resolved then, not when the workflow YAML is loaded. The difference matters when a variable's value depends on a previous node's output: that output doesn't exist at load time, only at render time.
Naming
By convention, variables are UPPER_SNAKE_CASE. Node ids are kebab-case. The renderer is case-sensitive on both.