Skip to main content

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:

CategorySourceScopeExample
Workflow variablesvariables: block at workflow rootAll jobsAPP_ENV: "staging"
Default variablesdefault.variables: blockAll jobs (overridden by job variables)LANG: "en_US.UTF-8"
Job variablesvariables: block inside a jobSingle jobVERBOSE: "true"
Predefined runtime variablesProvided by Loom at execution timePer-pipeline or per-jobLOOM_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:

PriorityLayerWhere defined
1 (lowest)Workflow variablesRoot-level variables: block
2Default variablesdefault.variables: block
3 (highest)Job variablesJob-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 ... overrides KEY for 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:

VariableWorkflow valueDefault valueJob valueResolved 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:

VariableScopeDescription
LOOM_RUN_IDPipelineRun identifier — maps to .loom/.runtime/logs/<run_id>/
LOOM_PROVIDERJobExecution provider: host or docker
LOOM_JOB_NAMEJobCurrent job name (graph node ID)
CI_PROJECT_DIRJobWorkspace root directory where the job runs
CI_COMMIT_SHAPipelineSnapshot commit SHA for the current run

The authoritative list with scopes, availability, and stability metadata is at Predefined CI/CD variables (Loom).

caution

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:

  1. Validate first: run loom check to catch schema-level mistakes (wrong keys, non-strings, typos in variable names).

  2. 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)
  3. Print what the job actually sees: add a temporary script line:

    env | grep -E '^CI_|^LOOM_|^(FOO|BAR)=' | sort
  4. Inspect the compiled graph: run loom compile to see the resolved variables for each job in the Graph IR JSON output.

  5. If the run fails, follow pointers instead of guessing:

  6. 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.

VariablesSecrets
Values visible in workflow YAMLYesNo (only references)
Values visible in Graph IR, receipts, logsYesNo (redacted)
Automatic redactionNoYes, at all output boundaries
Can coexist on same key in one jobNo — schema rejects this

To migrate existing sensitive variables, see Migrating from variables to secrets.

caution

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.