Variables
Variables let you parameterize Loom workflows and jobs without duplicating YAML. They are injected as environment variables during job execution and are also available in places that accept variable templates (such as cache key templates).
This page covers the three things that most often cause confusion: string-only values, which layer wins when keys collide, and how to debug precedence quickly.
Why variables matter
Without variables, every job that needs a shared value (a version string, an environment name, a build flag) would hard-code it. Variables let you define a value once and reference it everywhere — and override it at narrower scopes when you need to.
Variables affect two runtime artifacts:
- Job environment: every variable is available as an environment variable in
script:commands. - Cache key templates: variables can be interpolated into cache key expressions.
Types of variables
Loom has four categories of variables, each from a different source:
| Category | Source | Scope | Example |
|---|---|---|---|
| Workflow variables | variables: block at workflow root | All jobs | APP_ENV: "staging" |
| Default variables | default.variables: block | All jobs (overridden by job variables) | LANG: "en_US.UTF-8" |
| Job variables | variables: block inside a job | Single job | VERBOSE: "true" |
| Predefined runtime variables | Provided by Loom at execution time | Per-pipeline or per-job | LOOM_RUN_ID, LOOM_PROVIDER |
A fifth category — command-local overrides — comes from standard shell syntax (KEY=value command) inside a job's script: and affects only that single command.
The layering model
When the same variable key is defined in multiple places, Loom merges layers so that more specific scopes override more general scopes. The merge order from lowest to highest priority is:
Lowest priority Highest priority
┌──────────────┐ ┌───────────────┐ ┌──────────────┐ ┌────────────────┐
│ Predefined │ → │ Workflow │ → │ default + │ → │ Provider │
│ (registry) │ │ variables │ │ job vars │ │ overrides │
└──────────────┘ └───────────────┘ └──────────────┘ └────────────────┘
In practice, for the YAML layers you write, the relevant merge order is:
| Priority | Layer | Where defined |
|---|---|---|
| 1 (lowest) | Workflow variables | Root-level variables: block |
| 2 | Default variables | default.variables: block |
| 3 (highest) | Job variables | Job-level variables: block |
Default and job variables are merged via deep merge at the schema level — the default block's values are overlaid with the job's values before execution. Workflow-level variables provide the base, and job-level variables override keys from both default and workflow scope.
At runtime, Loom also injects predefined variables (like LOOM_RUN_ID and LOOM_PROVIDER). If your workflow defines a key that collides with a predefined variable name, the workflow value wins for that key.
Provider overrides (internal to Loom's execution engine) have the highest priority and cannot be overridden by workflow YAML.
Command-local overrides in script:
Inside a job's script:, you can use standard shell syntax to override a variable for a single command:
KEY=value command ...overridesKEYfor that one command only.- The job's environment is unchanged for subsequent commands.
Precedence example
This example demonstrates how all three YAML layers interact:
version: v1
stages: [ci]
variables:
FOO: "workflow"
BAR: "workflow"
default:
target: linux
variables:
BAR: "default"
show-vars:
stage: ci
variables:
FOO: "job"
script:
- echo "FOO=$FOO BAR=$BAR"
- FOO="step" echo "FOO=$FOO (command-local override)"
- echo "FOO=$FOO (back to job value)"
Expected output:
FOO=job BAR=default
FOO=step (command-local override)
FOO=job (back to job value)
What happened:
| Variable | Workflow value | Default value | Job value | Resolved value |
|---|---|---|---|---|
FOO | "workflow" | — | "job" | "job" (job overrides workflow) |
BAR | "workflow" | "default" | — | "default" (default overrides workflow) |
Predefined runtime variables
Loom provides runtime variables (prefixed LOOM_* and CI_*) that jobs can read. These are resolved at execution time based on the current run, job, and environment context.
Common examples:
| Variable | Scope | Description |
|---|---|---|
LOOM_RUN_ID | Pipeline | Run identifier — maps to .loom/.runtime/logs/<run_id>/ |
LOOM_PROVIDER | Job | Execution provider: host or docker |
LOOM_JOB_NAME | Job | Current job name (graph node ID) |
CI_PROJECT_DIR | Job | Workspace root directory where the job runs |
CI_COMMIT_SHA | Pipeline | Snapshot commit SHA for the current run |
The authoritative list with scopes, availability, and stability metadata is at Predefined CI/CD variables (Loom).
Avoid using CI_* or LOOM_* prefixes for your own variables. If you accidentally collide with a predefined variable name, the workflow value wins — but debugging becomes harder because the expected runtime value won't match.
String-only rule
All workflow and job variable values must be strings. The schema validator rejects numbers, booleans, and other YAML types.
Incorrect (non-string values):
variables:
RETRIES: 3
DEBUG: true
Correct (quoted strings):
variables:
RETRIES: "3"
DEBUG: "true"
If you run loom check on a workflow with non-string values, validation fails with an error pointing at the offending key:
WF_SCHEMA_V1 /show-vars/variables/RETRIES: set variable value to a string
Variable keys must also follow a strict pattern: ^[A-Z_][A-Z0-9_]*$ (uppercase letters, digits, and underscores only).
How to debug precedence
When "the value I expected isn't the value I got," follow this checklist:
-
Validate first: run
loom checkto catch schema-level mistakes (wrong keys, non-strings, typos in variable names). -
Confirm the layers: identify which layers define the key:
- Workflow
variables:(global defaults) default.variables:(job-default overrides)- Job
variables:(job-specific overrides) - Command-local overrides in
script:lines (KEY=value cmd)
- Workflow
-
Print what the job actually sees: add a temporary script line:
env | grep -E '^CI_|^LOOM_|^(FOO|BAR)=' | sort -
Inspect the compiled graph: run
loom compileto see the resolved variables for each job in the Graph IR JSON output. -
If the run fails, follow pointers instead of guessing:
- Start from the printed receipt path → Receipts contract
- Jump into the run logs layout → Runtime logs contract
- Use the Diagnostics ladder to reach the failing step's event stream
-
When sharing for help: use What to share and redact sensitive values.
Sensitive values belong in secrets, not variables
If a value is a password, token, certificate, or private key, use Secrets instead of variables. Secrets store references (not plaintext values) in workflow YAML and are automatically redacted in all runtime output.
| Variables | Secrets | |
|---|---|---|
| Values visible in workflow YAML | Yes | No (only references) |
| Values visible in Graph IR, receipts, logs | Yes | No (redacted) |
| Automatic redaction | No | Yes, at all output boundaries |
| Can coexist on same key in one job | No — schema rejects this | — |
To migrate existing sensitive variables, see Migrating from variables to secrets.
Be careful when printing environments for debugging. Paths can be sensitive — variables like CI_PROJECT_DIR or LOOM_PROJECT_DIR can expose usernames or internal mount points. Share the receipt path + run ID first and redact any sensitive values per What to share.
What to read next
- Variable syntax details: Syntax v1 — variables, Syntax v1 — job variables
- Deeper variable notes: Workflows → Variables
- Predefined variable reference: Predefined CI/CD variables (Loom)
- Secrets: Concepts → Secrets, Workflows → Secrets
- Debugging runs: CLI run, Diagnostics ladder
- Sharing for help: What to share