Skip to content

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:

VariableWhere it worksMeaning
$ARGUMENTSCommands, promptsAll arguments passed to the command as a single string
$1, $2, $3, ...Commands, promptsPositional arguments by index
$ARTIFACTS_DIRCommands, promptsAbsolute path to the run's artifact directory
$WORKFLOW_IDCommands, promptsThe current run's id
$BASE_BRANCHCommands, promptsThe project's primary git branch (auto-detected, or set via worktree.baseBranch)
$DOCS_DIRCommands, promptsDocumentation directory (default: docs/)
$<node-id>.outputDAG when: conditions, downstream prompt: fieldsThe 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:

yaml
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:

bash
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:

yaml
  - id: draft
    type: prompt
    prompt: "Write something."

  - id: review
    type: prompt
    depends_on: [draft]
    prompt: |
      Review this draft:

      $draft.output

For 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:

yaml
  - 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:

yaml
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_DIR falls back to docs/ 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 with variable_missing: TOPIC.
  • $BASE_BRANCH is 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.

AI that follows a recipe, not a conversation.