Skip to main content

Workflow YAML syntax reference (v1)

Loom workflows are YAML files validated as schema v1. This document lists the configuration options for the Loom .loom/workflow.yml file — the file where you define the jobs that make up your workflow.

  • Default location: .loom/workflow.yml
  • Validate: loom check
  • Compile (resolve includes + templates): loom compile --workflow .loom/workflow.yml
  • Run (local): loom run --local --workflow .loom/workflow.yml

When you are editing your workflow file, validate it with loom check.

Loom workflow configuration uses YAML formatting, so the order of keys is not important unless otherwise specified. Explicit YAML tags are rejected.

YAML composition

Anchors, aliases, and mapping merge keys can remove repetition within one workflow file. Loom expands them into independent concrete values before it resolves include, extends, or default.

.linux: &linux
target: linux
script: &checks
- go test ./...

unit:
<<: *linux
stage: test

race:
<<: *linux
stage: test

lint:
stage: test
target: linux
script: *checks

Mapping merge precedence is deterministic:

  1. An explicit key in the destination mapping overrides every merged value.
  2. For <<: [*first, *second], the first source wins when both sources define the same key.

Anchors are file-local. An included file can declare and use its own anchors, but an alias cannot refer to an anchor in another file. Duplicate explicit keys, alias cycles, and merge sources that are not mappings are rejected. Loom also limits alias nesting to 32 levels and materialization to 100,000 expanded nodes across the root workflow and all included files.


Keywords

A Loom workflow configuration includes:

  • Global keywords that configure workflow structure and behavior:

    KeywordDescription
    versionSchema version. Must be v1.
    stagesThe names and order of the workflow stages.
    includeImport configuration from local template YAML files.
    workflowOptional workflow gate; non-empty rules are Experimental.
    defaultDefault values for job keywords.
    variablesDefine default variables for all jobs in the workflow.
  • Jobs configured with job keywords:

    KeywordDescription
    stageThe stage a job belongs to.
    targetExecution target platform.
    before_scriptSetup commands to run before the main script.
    scriptList of shell commands to execute.
    after_scriptFinalization commands for the job.
    extendsInherit configuration from a template job.
    rulesRun or skip this job from declared inputs.
    needsDeclare DAG-style dependencies between jobs.
    imageRun the job inside a Docker container.
    runner_poolSelect a runner pool for remote execution.
    variablesJob-scoped variables that override defaults.
    secretsDeclare secrets resolved at runtime.
    servicesSidecar containers that run alongside the job.
    invariantPolicy configuration for job-level checkpoints.
    cacheCache directories between runs for performance.
    artifactsExtract files from the workspace after execution.

Global keywords

Some keywords are not defined inside a job. These keywords control workflow structure or import additional configuration.


version

Use version to declare which schema version this workflow file conforms to. The validator uses this value to select the correct set of validation rules.

Keyword type: Global keyword.

Supported values:

  • Must be exactly the string v1.

Example of version:

version: v1

Additional details:

  • version is required. Omitting it produces a schema error.
  • Future schema versions will ship with explicit migration notes and will use a different version string (e.g. v2). Within v1, changes are intended to be additive and backwards-compatible.

stages

Use stages to define default dependency barriers. An executable job that omits needs waits for every executable job in every earlier declared stage. The planner lowers these barriers into ordinary dependency edges before execution.

Keyword type: Global keyword.

Supported values:

  • A non-empty YAML sequence of stage name strings.
  • Each stage name must match: ^[a-z][a-z0-9_-]{0,31}$ (lowercase, starts with a letter, up to 32 characters, using only letters, digits, hyphens, and underscores).
  • Stage names must be unique — duplicates are rejected.

Example of stages:

stages:
- deps
- ci

When jobs omit needs:

  1. Jobs in deps become ready without stage prerequisites.
  2. Jobs in ci become ready only after every executable deps job succeeds.
  3. Dependency-ready jobs may run concurrently within the configured limit.

If a job fails, jobs that require it remain blocked while independent jobs can continue.

Additional details:

  • stages is required. Omitting it produces a schema error.
  • An explicit needs list replaces the job's stage prerequisites. An explicit empty list permits immediate readiness, subject to additional artifact prerequisites.
  • A dependency that conflicts with another explicit dependency or an omitted-needs stage barrier fails compilation as a cycle.
  • If a stage is defined but no jobs reference it, the stage is silently ignored.
  • Stage names are validated against the regex pattern at schema time. Invalid names produce an error like: rename stage to match ^[a-z][a-z0-9_-]{0,31}$.

Related topics:

  • stage (job keyword) to assign a job to a stage.

include

Use include to import template YAML files into your workflow configuration. You can split a large workflow into multiple files to increase readability or reduce duplication across jobs.

Included files are merged with the main workflow file. Includes are resolved before validation and before template extension (extends).

Keyword type: Global keyword.

Supported values: A YAML sequence of include entries. Each entry supports only the include:local subkey.

Example of include:

include:
- local: .loom/templates/common.yml
- local: .loom/templates/languages/node.yml

Additional details:

  • Include files can themselves contain include entries (nested includes). Cycles are detected and rejected with a descriptive error showing the include chain.
  • Included files are merged in order: earlier includes are applied first, then later includes, then the main workflow file. If the same key appears in multiple files, the last definition wins (for root-level scalars and sequences) or keys are merged recursively (for mappings).
  • Loom resolves includes, then applies default to all jobs, then resolves extends chains.

Related topics:


include:local

Use include:local to include a template file from the same repository.

Keyword type: Global keyword.

Supported values:

  • A file path string that:
    • Starts with .loom/templates/
    • Ends with .yml or .yaml
    • Does not contain .. (path traversal is rejected)

Example of include:local:

include:
- local: .loom/templates/common.yml

Multiple includes:

include:
- local: .loom/templates/common.yml
- local: .loom/templates/languages/node.yml
- local: .loom/templates/jobs/lint.yml

Additional details:

  • The path restriction (.loom/templates/ prefix, no ..) exists for security and reviewability: all included files are checked into the repo under a known directory, making them easy to audit and harder to smuggle from unrelated paths.
  • If you want to use a template from elsewhere, vendor it into .loom/templates/ and review it like any other code change.
  • Each include entry must be a mapping with only the key local. Other keys (such as remote) are not yet supported and will produce an error.

Related topics:


workflow

Use workflow for workflow-wide conditional execution. Loom evaluates rules once before provider setup, secret resolution, or execution. Non-empty rules and their decision evidence are Experimental; see Rules.

Keyword type: Global keyword.

Supported values:

  • A mapping with the optional key rules. No other keys are allowed under workflow.

Example of workflow:

workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Related topics:

  • Rules for details on rule expression syntax and behavior.

workflow:rules

workflow:rules contains ordered conditions that determine whether the workflow runs.

Keyword type: Global keyword.

Supported values:

  • A YAML sequence. It may be empty; each non-empty entry must be a mapping with exactly one non-empty string key, if.

Accepted example:

workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Additional details:

  • Rule-contract v1 accepts $VARIABLE, $VARIABLE == "literal", and $VARIABLE != "literal". V2 additionally accepts RE2 matching with =~, &&, ||, parentheses, and variable-to-variable equality or inequality. The planner emits the lowest capable contract version, so existing narrow workflows retain v1.
  • Rules are evaluated in order after all expressions pass syntax validation. The first match allows execution; no match skips the workflow.
  • In v2, && binds tighter than ||; both associate left and short-circuit from left to right. Only spaces and tabs separate expression tokens. String and regex operands are JSON double-quoted strings after YAML decoding.
  • Expressions requiring V2 syntax are limited to 4,096 source bytes, 32 group levels, 256 parsed nodes, 1,024 decoded regex bytes, and 65,536 bytes per accessed regex input. Complete V1 expressions retain V1 parsing inside V2 lists. See Rules for the exact grammar and behavior.
  • Missing or non-string inputs and invalid expressions fail closed before any provider-capable work.
  • Only predefined variables marked Pipeline are available. Job, step, workflow-defined, and ambient environment variables are not rule inputs.
  • Unary negation, !~, and slash-delimited regex are unsupported.

default

Use default to set default values for job keywords. Each default is applied to every job that doesn't already define that keyword. This lets you avoid repeating the same configuration across many jobs.

Default configuration is merged into each job mapping before validation. Job values override defaults. For nested mappings (like variables and cache), keys are merged recursively — the job provides overrides and the default provides fallbacks.

Keyword type: Global keyword.

Supported values: A mapping. The following keys are allowed under default:

KeyDescription
targetDefault execution target for all jobs.
before_scriptDefault setup commands for all jobs.
after_scriptDefault finalization commands for all jobs.
imageDefault Docker image for all jobs.
runner_poolDefault runner pool for all jobs.
variablesDefault variables merged into all jobs.
rulesDefault ordered rules for all jobs.
cacheDefault cache configuration for all jobs.
servicesDefault sidecar services for all jobs.
invariantDefault invariant policy for all jobs.

Example of default:

default:
target: linux
image: alpine:3.20
before_script:
- echo "prepare"
variables:
PNPM_STORE_DIR: .pnpm-store
cache:
paths: [.pnpm-store, .nx/cache]
policy: pull-push
when: always

In this example:

  • All jobs default to target: linux, so individual jobs can omit target.
  • All jobs default to image: alpine:3.20 unless they set their own image.
  • All jobs inherit the setup command unless they replace or clear before_script.
  • All jobs inherit PNPM_STORE_DIR unless they override it in their own variables.
  • All jobs inherit rules unless they replace the sequence or set rules: [].
  • All jobs inherit the cache configuration unless they override cache.

Additional details:

  • default is optional. If omitted, jobs must specify all required keys themselves.
  • default.target can satisfy the required target key for non-template jobs. If default.target is set to linux, jobs may omit target and inherit the default.
  • Default and job configuration do not concatenate — they merge. If the job already has a keyword defined, the job value takes precedence over the default for scalars and sequences. For mappings, keys merge recursively.
  • Unknown keys under default produce an error: remove unknown default key; allowed keys are target, before_script, after_script, image, runner_pool, variables, invariant, cache, services, rules.

Related topics:

  • variables for variable merge behavior.
  • cache for cache merge behavior.

variables

Use variables to define default variables available to all jobs. Variables are key/value string pairs that are injected into the job environment as shell environment variables.

Variables defined at the top level act as defaults. Each default variable is available in every job, except when the job already has a variable defined with the same name — the job variable takes precedence.

Keyword type: Global keyword.

Supported values: A YAML mapping of variable name/value pairs:

  • Variable names must match ^[A-Z_][A-Z0-9_]*$ (uppercase letters, digits, and underscores; must start with a letter or underscore).
  • Values must be YAML scalars. Loom stores their text as strings, including unquoted numbers and booleans. Quote values to make the intended text clear.

Example of variables:

variables:
PNPM_STORE_DIR: .pnpm-store
NODE_ENV: production

Additional details:

  • Top-level variables and default.variables both provide default values to jobs. The merge happens as part of the default merge process — see default.
  • Lists, mappings, and other non-scalar variable values produce a schema error. The validator checks scalar shape, not the scalar's implicit YAML type.
  • YAML variable values are literal strings. Loom does not expand $VARIABLE references inside those values. To derive a value from another variable, use shell expansion in a script: command.

Related topics:


Jobs

Any top-level key that is not a global keyword (version, stages, include, workflow, variables, default) is treated as a job definition. The key must be a valid job name (see Job naming).

A Loom workflow must contain at least one job. Jobs define the actual work — the commands to run, the environment to run them in, and how they relate to other jobs.

Job naming

Job names must match ^\.?[a-z][a-z0-9_-]{0,63}$:

  • Lowercase letters, digits, hyphens, and underscores only.
  • Must start with a lowercase letter (or . for template jobs).
  • Maximum 64 characters.

Names starting with . are template jobs. Template jobs are not executed directly — they exist to provide reusable configuration via extends.

Example of job naming:

# Regular jobs (executed)
build-image:
stage: deps
target: linux
script:
- docker build -t myapp .

# Template job (not executed, used via extends)
.node-base:
image: node:20
variables:
NODE_ENV: production

Non-template job requirements

Non-template jobs (names that do not start with .) must include three required keys:

Required keyDescription
stageMust reference a stage declared in stages.
targetMust be linux (the only supported target in the current release).
scriptA non-empty list of shell command strings.

If default.target is set, jobs may omit target and inherit the default.

Example of a non-template job:

check:
stage: ci
target: linux
script:
- echo "running checks"

Template job requirements

Template jobs (names starting with .) have relaxed requirements. They must include at least one of:

  • before_script — so they can provide setup commands to inheriting jobs.
  • script — so they can provide main commands to inheriting jobs.
  • after_script — so they can provide finalization commands to inheriting jobs.
  • extends — so they can chain to another template.

Example of a template job:

.base:
image: alpine:3.20
variables:
MODE: default

This template provides image and variables but has no command section or extends. This produces a schema error asking for before_script, script, after_script, or extends.

To fix it, add a script:

.base:
image: alpine:3.20
variables:
MODE: default
script:
- echo "base"

Job keywords

The keywords below are valid inside a job mapping. Each section describes what the schema validator enforces and how the keyword affects runtime behavior.


stage

Use stage to define which stage a job runs in. When needs is omitted, the planner adds dependencies on all executable jobs in earlier declared stages. The local executor can run dependency-ready jobs concurrently.

Keyword type: Job keyword. You can use it only as part of a job.

Supported values:

  • A string that must match one of the stage names declared in stages.

Example of stage:

stages:
- build
- test

compile:
stage: build
target: linux
script:
- make build

unit-tests:
stage: test
target: linux
script:
- make test

In this example, compile runs first (in the build stage). After it succeeds, unit-tests runs (in the test stage).

Additional details:

  • stage is required for non-template jobs. Omitting it produces: add required key stage for this job.
  • If the value doesn't match a declared stage, the error is: set stage to a declared stage from /stages; "xyz" is not declared.
  • Template jobs may omit stage. The inheriting job must provide it.

target

Use target to specify the execution platform for a job.

Keyword type: Job keyword. You can use it only as part of a job or in the default section.

Supported values:

  • Must be exactly linux. This is the only supported target in the current release.

Example of target:

check:
stage: ci
target: linux
script:
- echo "running on linux"

Additional details:

  • target is required for non-template jobs, unless default.target is set.
  • If you're on macOS or Windows, use loom run --local inside a Linux environment (VM, container, or CI runner) since the local executor targets Linux.
  • Any value other than linux produces: set target to "linux" (MVP currently supports linux only).
  • Additional target platforms are planned for future releases.

before_script

Use before_script for optional setup commands that Loom compiles into the graph node's setup phase.

Keyword type: Job, template, or default keyword.

Supported values:

  • A YAML sequence of non-empty scalar commands.
  • An empty sequence ([]) to clear inherited setup commands.

Commands retain their order and scalar content. Literal (|) and folded (>) block scalars are supported.


script

Use script to specify the shell commands the runner executes for a job.

Keyword type: Job keyword. You can use it only as part of a job or in a template that provides it via extends.

Supported values:

  • A non-empty YAML sequence of non-empty strings.
  • Each entry is a non-empty scalar command. Literal and folded block scalars are supported and retain their YAML-decoded content.

Example of script:

check:
stage: ci
target: linux
script:
- pnpm install --frozen-lockfile
- pnpm nx run-many -t lint,test
- echo "all checks passed"

Additional details:

  • script is required for non-template jobs. Omitting it produces: add required key script as a non-empty string sequence.
  • Each command must be a non-empty YAML scalar. Literal block scalars preserve embedded newlines; folded block scalars preserve their YAML-decoded folded content.
  • Empty strings and whitespace-only strings are rejected: replace empty script command with a non-empty string.
  • Commands execute sequentially. If any command exits with a non-zero code, the job fails and remaining commands do not run.

after_script

Use after_script for optional finalization commands that Loom compiles into the graph node's finalization phase.

Keyword type: Job, template, or default keyword.

Supported values:

  • A YAML sequence of non-empty scalar commands.
  • An empty sequence ([]) to clear inherited finalization commands.

Commands retain their order and scalar content. Literal (|) and folded (>) block scalars are supported.


extends

Use extends to reuse configuration from a template job. The template's keys are merged into the inheriting job, with the inheriting job's values taking precedence.

Keyword type: Job keyword. You can use it only as part of a job.

Supported values:

  • A single string naming a template job. The template name must start with . and must exist in the workflow (either directly or via include).

Example of extends:

.node-base:
image: node:20
variables:
NODE_ENV: production

lint:
extends: .node-base
stage: ci
target: linux
script:
- npm run lint

test:
extends: .node-base
stage: ci
target: linux
script:
- npm test

In this example, both lint and test inherit image: node:20 and the NODE_ENV variable from .node-base. Each job defines its own script.

Additional details:

  • Merge behavior:
    • Scalars and sequences (strings, numbers, lists): the child value replaces the parent value entirely. For example, if the parent has script: ["echo base"] and the child has script: ["echo child"], the result is ["echo child"].
    • Mappings (like variables, cache in mapping form): keys merge recursively. The parent provides defaults; the child overrides specific keys.
  • Only single inheritance is supported. extends must be a single string, not a list. Multi-template composition is planned.
  • Cycles are detected and rejected. If .a extends .b and .b extends .a, the validator reports an error.
  • Templates can extend other templates (chaining). The chain is resolved recursively.
  • To verify the resolved configuration after template merging, run: loom compile --workflow .loom/workflow.yml

Related topics:


rules

Use rules to decide whether an individual job runs. Loom evaluates job rules before provider setup, secret resolution, services, or commands for that job.

Keyword type: Job keyword.

Supported values:

  • A YAML sequence. It may be empty; each entry must be a mapping with exactly one non-empty string field, if.

Example of rules:

default:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

test:
stage: ci
target: linux
script: [task test]
variables:
RUN_TESTS: "true"
rules:
- if: '$RUN_TESTS == "true"'

Additional details:

  • Rules are evaluated in declaration order. The first match allows the job; no match skips it. An omitted or empty sequence allows the job.
  • The complete v2 grammar supports truthiness, ==, !=, =~, &&, ||, grouping, and variable-to-variable string or boolean comparison.
  • Inputs use job variables over default variables over workflow variables. Loom also supplies reserved LOOM_* job metadata; declarations cannot replace those reserved values. Job rules never read the ambient environment.
  • A job sequence replaces inherited or default rules as a whole. Use rules: [] to clear inherited rules.
  • Syntax, regex, and expression resource errors are reported by loom check at the rule's /rules/{index}/if path. Missing and wrong-type declared inputs fail that job closed at runtime.
  • A denied or invalid job blocks jobs that require it. Independent branches continue. Receipts record each declared decision without expressions or values.

See Rules for the exact grammar, context, and receipt behavior.


needs

Use needs to replace a job's default stage prerequisites with explicit job dependencies. The execution planner lowers both explicit dependencies and default stage barriers into graph edges that the executor uses for readiness.

Keyword type: Job keyword. You can use it only as part of a job.

Supported values:

  • A YAML sequence of job name strings.

Example of needs:

stages:
- deps
- ci

install:
stage: deps
target: linux
script:
- pnpm install

lint:
stage: ci
target: linux
needs:
- install
script:
- pnpm run lint

test:
stage: ci
target: linux
needs:
- install
script:
- pnpm test

In this example, install runs first. Then lint and test are both ready and may run concurrently within the configured limit.

Additional details:

  • Omitting needs adds prerequisites from every executable job in every earlier declared stage.
  • An explicit list replaces those stage prerequisites. needs: [] adds no dependency edges and can make the job immediately ready.
  • loom compile rejects missing targets, self-dependencies, and dependency cycles at the offending needs entry.
  • needs orders jobs but does not transfer files. Use artifact_inputs when the consumer needs a producer's declared artifacts. Artifact inputs remain additional prerequisites even when needs is empty.

artifact_inputs

Use artifact_inputs to restore artifacts from named producer jobs before the consumer's first command. Producers must declare artifacts.

test:
stage: test
target: linux
artifact_inputs:
- build
script:
- test -f dist/app.js

The value is a sequence of producer job IDs. Omission and artifact_inputs: [] transfer no files. An extends override replaces the inherited sequence as a whole, and an explicit empty sequence clears it. Missing producers, self references, duplicates, producers without artifacts, and dependency cycles are compile errors reported at the indexed input path.


image

Use image to run a job inside a Docker container. When image is set, Loom uses the Docker provider instead of the Host provider, executing the job's script commands inside the specified container image.

Keyword type: Job keyword. You can use it only as part of a job or in the default section.

Supported values:

  • A non-empty string specifying the Docker image name, including optional registry path and tag:
    • <image-name> (uses latest tag)
    • <image-name>:<tag>
  • A mapping that names the image and optionally describes how to build it locally before execution.

The following subkeys are available in the image mapping:

SubkeyDescription
image:nameRequired in mapping form. Docker image reference to run for the job.
image:pull_policyAcquisition policy for a named prebuilt image.
image:userUsername, UID, or UID:GID used to run commands in the container.
image:buildOptional local build configuration to produce the image before job execution.

Example of scalar image:

stages:
- ci

check:
stage: ci
target: linux
image: node:20-alpine
script:
- node --version
- npm test

This example runs the check job inside the node:20-alpine container.

Example without image (Host provider):

check:
stage: ci
target: linux
script:
- echo "runs directly on the host"

Mapping form:

check:
stage: ci
target: linux
image:
name: loom/nix-local
user: 1000
build:
context: .
dockerfile: Dockerfile.ci
output: type=docker,dest=loom.tar
script:
- echo "building and running in loom/nix-local"
  • name: non-empty Docker image reference (same semantics as the scalar form).
  • pull_policy (optional): controls acquisition of a named prebuilt image. It defaults to if-not-present.
  • user (optional): nonblank username, UID string, UID:GID string, or nonnegative integer UID used for processes in the job container.
  • build (optional): describes a local build to run before the job.
    • context and dockerfile must be non-empty strings. They are interpreted relative to the workflow file and mirror docker build's context path and Dockerfile flag.
    • output is optional; when present it is passed through to docker build --output (e.g. type=docker,dest=/tmp/my-image.tar). The validator enforces the mapping shape; the Docker provider performs the build at runtime.

Additional details:

  • When image is set, the Docker provider mounts the workspace into the container and executes commands there. When image is not set, the Host provider runs commands directly on the machine.
  • The named image must be available locally or retrievable by the Docker daemon on the machine running loom run, according to its pull policy. For local images (not on a registry), build them first — see the Docker provider for details.
  • Named prebuilt images use if-not-present when pull_policy is omitted. Set always to acquire before use or never to require a locally present image.
  • String user values are preserved exactly. Integer UIDs are normalized to unsigned base-10 text before they reach the provider.
  • An empty string for image in default produces: set default image to a non-empty string.
  • image.build is supported in the Docker provider. The image is built locally before the job container starts.
  • The mapping shape is executor-neutral; future remote or Kubernetes executors can adopt the same name/build spec when they start building container images.

Related topics:


image:name

Use image:name to specify the Docker image reference when image is written as a mapping.

Keyword type: Job keyword (image subkey).

Supported values:

  • A non-empty string using the same image reference rules as scalar image.

Example of image:name:

image:
name: ghcr.io/acme/build:latest

Additional details:

  • name is required when image uses the mapping form. Omitting it produces: add required key name for image mapping.
  • Use the scalar form when you only need an image reference and do not need image:pull_policy, image:user, or image:build.

image:user

Use image:user to select the identity for commands in the job container.

Keyword type: Job keyword (image subkey).

Supported values:

  • A nonblank string such as runner, 1000, or 1000:1000. Loom preserves the string exactly.
  • A nonnegative YAML integer UID. Loom normalizes it to unsigned base-10 text.

Example of image:user:

image:
name: ghcr.io/acme/build:latest
user: "1000:1000"

Additional details:

  • user is available only in the image mapping form; it is not a job-level key.
  • Omission leaves the image's configured runtime user unchanged.
  • Blank strings, booleans, nulls, floats, sequences, mappings, and negative integers are rejected at the job's image/user path.

image:pull_policy

Use image:pull_policy to control acquisition of a named prebuilt image before the job container starts.

Keyword type: Job keyword (image subkey).

Supported values:

  • always: acquire the named image before use.
  • if-not-present: use a local image when present and acquire it when absent.
  • never: require the named image to be present locally.

Example of image:pull_policy:

image:
name: ghcr.io/acme/build:latest
pull_policy: always

Additional details:

  • Omission defaults to if-not-present, including scalar image shorthand.
  • pull_policy cannot be combined with image:build, because a built image is produced locally rather than acquired as a named prebuilt image.
  • Invalid values produce an error at the image/pull_policy path listing all three supported values.

image:build

Use image:build to describe a local image build that runs before the job container starts.

Keyword type: Job keyword (image subkey).

Supported values:

  • A mapping with the following subkeys:
SubkeyDescription
image:build:contextRequired. Build context path passed to the Docker build.
image:build:dockerfileRequired. Dockerfile path to use for the build.
image:build:outputOptional build output configuration forwarded to the backend.

Example of image:build:

image:
name: ghcr.io/acme/build:latest
build:
context: .
dockerfile: Dockerfile.ci
output:
type: registry
name: ghcr.io/acme/build:latest

Additional details:

  • build must be a mapping. Non-mapping values produce: set image build to a mapping with required keys context and dockerfile.
  • Unknown keys produce: remove unknown image build key; allowed keys are context, dockerfile, output.

image:build:context

Use image:build:context to set the build context path for the local image build.

Keyword type: Job keyword (image build subkey).

Supported values:

  • A non-empty string.

Example of image:build:context:

image:
name: loom/nix-local
build:
context: .
dockerfile: Dockerfile.ci

Additional details:

  • context is required when image:build is present. Omitting it produces: add required key context for image build.
  • The path is interpreted relative to the workflow file location.

image:build:dockerfile

Use image:build:dockerfile to choose which Dockerfile the local image build should use.

Keyword type: Job keyword (image build subkey).

Supported values:

  • A non-empty string.

Example of image:build:dockerfile:

image:
name: loom/nix-local
build:
context: .
dockerfile: Dockerfile.ci

Additional details:

  • dockerfile is required when image:build is present. Omitting it produces: add required key dockerfile for image build.
  • The path is interpreted relative to the workflow file location.

image:build:output

Use image:build:output to pass build output configuration through to the image build backend.

Keyword type: Job keyword (image build subkey).

Supported values:

  • A non-empty string, or
  • A YAML mapping.

Example of image:build:output:

image:
name: loom/nix-local
build:
context: .
dockerfile: Dockerfile.ci
output: type=docker,dest=loom.tar

Mapping form:

image:
name: ghcr.io/acme/build:latest
build:
context: .
dockerfile: Dockerfile.ci
output:
type: registry
name: ghcr.io/acme/build:latest

Additional details:

  • Invalid values produce: set image build output to either a non-empty string or mapping.
  • Loom validates only the top-level output shape. If you use the mapping form, nested output keys are backend-defined and are passed through as-is.

runner_pool

runner_pool records a pool name for planned remote execution. The schema accepts the field, but the local executor does not route jobs to named pools or enforce pool capabilities.

Keyword type: Job keyword. You can use it only as part of a job or in the default section.

Supported values:

  • A non-empty string identifying the runner pool.

Example of runner_pool:

check:
stage: ci
target: linux
runner_pool: default
script:
- echo "running on default pool"

Additional details:

  • The schema accepts runner_pool as a valid job key. Runtime routing to named pools and pool capability/constraint enforcement are planned for remote execution support.
  • An empty string for runner_pool in default produces: set default runner_pool to a non-empty string.

Job variables

Use job-level variables to define or override variables for a specific job. Job variables take precedence over workflow-level variables and default.variables for that job.

Keyword type: Job keyword. You can use it only as part of a job or in the default section.

Supported values: A YAML mapping of variable name/value pairs:

  • Variable names must match ^[A-Z_][A-Z0-9_]*$.
  • Values must be YAML scalars. Loom stores their text as strings, including unquoted numbers and booleans. Quote values to make the intended text clear.

Example of job variables:

version: v1
stages: [ci]

default:
target: linux
variables:
MODE: default

print-default:
stage: ci
script:
- printf '%s\n' "$MODE"

print-override:
stage: ci
variables:
MODE: job-override
script:
- printf '%s\n' "$MODE"

In this example:

  • print-default inherits MODE=default from default.variables.
  • print-override overrides MODE to job-override via its own variables.

Additional details:

  • Job variables and default variables merge — the job provides overrides, the default provides fallbacks. If both the default and the job define MODE, the job value wins. If only the default defines FOO, the job inherits FOO.
  • Variable names that don't match the pattern produce: rename variable key to match ^[A-Z_][A-Z0-9_]*$.
  • Non-scalar values produce: set variable value to a string. Numeric and boolean scalars are accepted and stored as their text.

Related topics:


secrets

Use secrets to declare sensitive values (passwords, tokens, private keys) that are resolved at runtime by a secrets provider. Unlike variables, secrets store references only — the actual value is never written to the workflow file.

Keyword type: Job keyword. You can use it only as part of a job. Secrets are not allowed in the default section — they must be declared per job.

Supported values: A YAML mapping of secret name/spec pairs:

  • Secret names must match ^[A-Z_][A-Z0-9_]*$ (same pattern as variable names).
  • Each secret spec is a mapping with the following subkeys:
SubkeyDescription
secrets:refRequired. Non-empty reference string identifying the secret in the provider.
secrets:fileInject via file path (true, default) or environment variable (false).
secrets:requiredFail the job if the secret cannot be resolved (true, default).

Example of secrets:

check:
stage: ci
target: linux
secrets:
DB_PASSWORD:
ref: op://vault/db/password
API_TOKEN:
ref: op://vault/api/token
file: false
required: true
script:
- echo "secrets available"

Additional details:

  • A job cannot define the same key in both variables and secrets. Collisions produce: remove key collision with variables; a key cannot exist in both variables and secrets.
  • When file is true (default), the secret value is written to a temporary file and the environment variable is set to the file path. When file is false, the secret value is injected directly as an environment variable.
  • When required is true (default), a missing or unresolvable secret fails the job. Set required: false for optional secrets that may not exist in all environments.
  • Configured exact secret values are redacted at verified textual runtime sinks (stdout, stderr, and structured events). Transformed values and raw declared artifacts are not covered; inspect and sanitize evidence before sharing it.
  • secrets is not allowed under default. Attempting to set default.secrets produces: remove default.secrets; secrets are job-scoped and must be declared per job.

Related topics:


secrets:ref

Use secrets:ref to identify which provider-backed secret Loom should resolve for a given environment variable name.

Keyword type: Job keyword (secret subkey).

Supported values:

  • A non-empty string secret reference.

Example of secrets:ref:

secrets:
API_TOKEN:
ref: op://vault/api/token

Additional details:

  • ref is required for every secret entry. Omitting it produces: add required key ref with a non-empty string secret reference.
  • An empty or non-string value produces: set ref to a non-empty string secret reference.

secrets:file

Use secrets:file to choose whether Loom injects the secret as a temporary file path or directly as an environment variable value.

Keyword type: Job keyword (secret subkey).

Supported values:

  • true or false.

Example of secrets:file:

secrets:
API_TOKEN:
ref: op://vault/api/token
file: false

Additional details:

  • file defaults to true.
  • When file: true, Loom writes the secret value to a temporary file and sets the environment variable to that file path.
  • When file: false, Loom injects the secret value directly into the environment variable.

secrets:required

Use secrets:required to control whether a missing or unresolvable secret should fail the job.

Keyword type: Job keyword (secret subkey).

Supported values:

  • true or false.

Example of secrets:required:

secrets:
OPTIONAL_LICENSE:
ref: env://OPTIONAL_LICENSE
required: false

Additional details:

  • required defaults to true.
  • When required: false, Loom allows the job to proceed even if the secret cannot be resolved.

services

Use services to run sidecar containers alongside a job. Services are started before the job's script executes and are torn down after the job finishes. Common uses include databases, caches, and other network-accessible dependencies that the job needs during execution.

Keyword type: Job keyword. You can use it as part of a job or in the default section.

Supported values:

  • A YAML sequence of service entries. Each entry is either:
    • A non-empty image string (shorthand — the image name is used as the service name).
    • A mapping with service subkeys (see below).

The following subkeys are available in a service mapping:

SubkeyDescription
services:nameRequired. Docker image to run as the service container.
services:pull_policyAcquisition policy for the service image.
services:userUsername, UID, or UID:GID for the service process.
services:aliasNetwork alias(es) for the service on the job network.
services:entrypointOverride the container entrypoint.
services:commandOverride the container command.
services:variablesEnvironment variables passed into the service container.

Example of services (shorthand image strings):

test:
stage: ci
target: linux
image: node:20-alpine
services:
- postgres:16
- redis:7
script:
- npm test

Example of services (mapping form):

integration:
stage: ci
target: linux
image: node:20-alpine
services:
- name: postgres:16
pull_policy: always
user: postgres
alias: db
variables:
POSTGRES_DB: testdb
POSTGRES_USER: runner
POSTGRES_PASSWORD: secret
- name: redis:7
alias: cache
script:
- npm run test:integration

Additional details:

  • services is optional. If omitted, the job runs without sidecar containers.
  • Each service defaults to if-not-present when pull_policy is omitted, including shorthand image strings.
  • A service mapping can set its own user; string values are preserved exactly, and nonnegative integer UIDs are normalized to unsigned base-10 text.
  • Services are only meaningful for Docker jobs (those with image set). The Docker SDK backend creates a shared network, starts each service container on it, waits for a brief grace period, then runs the main job container on the same network. The job can reach each service by its image name or by any configured services:alias.
  • Default and job precedence: if default.services is set and a job also defines services, the job's list replaces the default entirely (no merge). If only the default defines services, every job inherits them. To opt a job out of default services, set services: [] on that job.
  • The same replacement (no-merge) behavior applies to extends: if a child job defines services, the parent's services are replaced, not merged.
  • Runtime support: services execution is supported in Docker jobs (those with image: set). See Docker provider for details.

Unsupported / deferred subkeys:

The following subkeys are recognized by the validator but not yet supported. Using them produces a schema error explaining they are deferred:

SubkeyValidator error message
dockerservice docker is not supported yet
kubernetesservice kubernetes is not supported yet

Related topics:


services:name

Use services:name to specify the Docker image to run as a service container. When using the mapping form, name is the image reference (e.g. postgres:16).

Keyword type: Job keyword (service subkey).

Supported values:

  • A non-empty string specifying the Docker image.

Example of services:name:

services:
- name: postgres:16

Additional details:

  • name is required when the service entry is a mapping. Omitting it produces: add required key name for each service definition.
  • When the shorthand (scalar) form is used, the image string is treated as the name.

services:user

Use services:user to select the identity for one service container.

Keyword type: Job keyword (service subkey).

Supported values:

  • A nonblank string such as postgres, 1000, or 1000:1000. Loom preserves the string exactly.
  • A nonnegative YAML integer UID. Loom normalizes it to unsigned base-10 text.

Example of services:user:

services:
- name: postgres:16
user: postgres

Additional details:

  • The setting applies only to the service entry that declares it.
  • Omission leaves that service image's configured runtime user unchanged.
  • Blank strings, booleans, nulls, floats, sequences, mappings, and negative integers are rejected at that service entry's user path.

services:pull_policy

Use services:pull_policy to control acquisition of one service image before its container starts.

Keyword type: Job keyword (service subkey).

Supported values:

  • always: acquire the service image before use.
  • if-not-present: use a local image when present and acquire it when absent.
  • never: require the service image to be present locally.

Example of services:pull_policy:

services:
- name: postgres:16
pull_policy: never

Additional details:

  • Omission defaults to if-not-present, including scalar service shorthand.
  • The policy applies only to the service entry that declares it.
  • Invalid values produce an error at that service's pull_policy path listing all three supported values.

services:alias

Use services:alias to assign one or more network aliases to a service container. Aliases let the job container reach the service using a friendly hostname instead of the image name.

Keyword type: Job keyword (service subkey).

Supported values:

  • A non-empty string. Multiple aliases can be specified as a comma-separated or space-separated list within the string (e.g. "db, database"). Duplicates are deduplicated automatically.

Example of services:alias:

services:
- name: postgres:16
alias: db

Additional details:

  • If alias is omitted, the service is reachable by its image name on the job network.

services:entrypoint

Use services:entrypoint to override the default entrypoint of the service container image.

Keyword type: Job keyword (service subkey).

Supported values:

  • A non-empty YAML sequence of non-empty strings.

Example of services:entrypoint:

services:
- name: postgres:16
entrypoint:
- docker-entrypoint.sh
- postgres

services:command

Use services:command to override the default command of the service container image.

Keyword type: Job keyword (service subkey).

Supported values:

  • A non-empty YAML sequence of non-empty strings.

Example of services:command:

services:
- name: postgres:16
command:
- "-c"
- "max_connections=200"

services:variables

Use services:variables to pass environment variables into a service container.

Keyword type: Job keyword (service subkey).

Supported values: A YAML mapping of variable name/value pairs following the same rules as job variables.

Example of services:variables:

services:
- name: postgres:16
variables:
POSTGRES_DB: testdb
POSTGRES_USER: runner
POSTGRES_PASSWORD: secret

invariant

Use invariant to attach policy configuration to a job. Invariants enable job-level policy checkpoints where decisions can be captured in receipts and runtime events.

Keyword type: Job keyword. You can use it only as part of a job or in the default section.

Supported values:

  • A YAML mapping. Use {} for an empty invariant.

Example of invariant:

check:
stage: ci
target: linux
invariant: {}
script:
- echo "policy-gated"

Additional details:

  • The schema accepts invariant as a valid job key. Policy checkpoint semantics and decision capture in receipts/events are planned.
  • In default, invariant must be a mapping. Non-mapping values produce: set default invariant to a mapping (use {} for empty).

cache

Use cache to specify files and directories to cache between workflow runs. Caching expensive-to-recompute directories (package manager stores, build caches) can significantly speed up subsequent runs.

Cache can be specified in two forms:

  • Mapping form (single cache): a single cache configuration.
  • Sequence form (multiple caches): a list of named cache configurations, each operating independently.

To explicitly disable cache for a job (overriding a default), set cache to null or [].

Keyword type: Job keyword. You can use it only as part of a job or in the default section.

Supported values:

  • A mapping (single cache), or
  • A YAML sequence of cache mappings (multiple caches), or
  • null or [] to explicitly disable caching for a job.

The following subkeys are available in a cache mapping:

SubkeyDescription
cache:pathsRequired. Directories/files to cache.
cache:keyCache key (string or structured mapping).
cache:fallback_keysFallback keys if primary key misses.
cache:policyWhen to restore/save (pull, push, pull-push).
cache:whenSave cache based on job status.
cache:nameRequired name when using sequence form.
cache:disabledDisable a named cache entry.

Example of cache (single cache, mapping form):

check:
stage: ci
target: linux
cache:
key:
prefix: loom-cache
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
- .nx/cache
policy: pull-push
when: always
script:
- pnpm install --frozen-lockfile
- pnpm nx run-many -t check

Example of cache (multiple caches, sequence form):

check:
stage: ci
target: linux
cache:
- name: pnpm
key:
prefix: loom-cache-pnpm
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
policy: pull-push
when: always
- name: go
key:
prefix: loom-cache-go
files:
- go.work
- "**/go.sum"
paths:
- .go
policy: pull-push
when: always
script:
- pnpm install --frozen-lockfile

Example of cache (disable cache for a specific job):

default:
cache:
paths: [.pnpm-store]
policy: pull-push
when: always

build-image:
stage: deps
target: linux
cache: []
script:
- docker build -t myapp .

In this example, build-image explicitly disables cache with cache: [], overriding the default cache configuration.

Additional details:

  • When cache is a sequence, each entry must include a unique name. Duplicate names are rejected.
  • Unknown keys produce: remove unknown cache key; allowed keys are name, disabled, paths, key, fallback_keys, policy, when.
  • At runtime, cache behavior is recorded in runtime logs as system sections (cache_restore and cache_save). Follow the Diagnostics ladder to find cache events.

Related topics:


cache:paths

Use cache:paths to specify which directories or files to cache.

Keyword type: Job keyword (cache subkey).

Supported values:

  • A non-empty YAML sequence of non-empty strings. Each string is a path relative to the project directory.

Example of cache:paths:

cache:
paths:
- .pnpm-store
- .nx/cache
- node_modules

Additional details:

  • paths is required when cache is a mapping (unless disabled: true is set in sequence form). Omitting it produces: add required key paths with at least one entry.
  • Paths are relative to the project workspace root.
  • For Docker jobs (image set), paths refer to directories inside the container's workspace mount. Mismatches between host and container paths are a common source of "cache did nothing" issues.

cache:key

Use cache:key to give each cache a unique identifying key. All runs that produce the same cache key share the same cached data.

Keyword type: Job keyword (cache subkey).

Supported values:

  • A non-empty string (supports template variable placeholders), or
  • A mapping with prefix and files subkeys.

Example of cache:key (string form):

cache:
key: "pnpm-${job_name}-${head_sha}"
paths:
- .pnpm-store

Example of cache:key (mapping form):

cache:
key:
prefix: loom-cache
files:
- pnpm-lock.yaml
paths:
- .pnpm-store

Additional details:

  • If key is omitted, the runtime uses a default key computation.
  • An empty string produces: replace empty cache key string with a non-empty value.
  • When using the mapping form, the files subkey is required.

Template variable placeholders (expanded at runtime when key is a string or when used in prefix):

PlaceholderDescription
${job_name}Job name (graph node id).
${job_id}Job id (same as job name today).
${run_id}Executor run id.
${pipeline_id}Executor pipeline id.
${head_sha}Snapshot HEAD commit SHA.

cache:key:prefix

Use cache:key:prefix to add a prefix to the cache key computed from cache:key:files. This lets you namespace cache keys for different purposes while still keying off file content.

Keyword type: Job keyword (cache subkey).

Supported values:

  • A non-empty string. Supports template variable placeholders.

Example of cache:key:prefix:

cache:
key:
prefix: loom-cache-pnpm
files:
- pnpm-lock.yaml
paths:
- .pnpm-store

Additional details:

  • prefix is optional. If omitted, the key is computed from files alone.
  • An empty string produces: replace empty cache key prefix string with a non-empty value.

cache:key:files

Use cache:key:files to compute a cache key based on the content of specific files. When any of these files change, a new cache key is generated and a new cache is created.

Keyword type: Job keyword (cache subkey).

Supported values:

  • A non-empty YAML sequence of non-empty strings. Each entry can be:
    • A literal file path (e.g. pnpm-lock.yaml)
    • A glob pattern using *, ?, or ** (doublestar recursive semantics)

Example of cache:key:files:

cache:
key:
prefix: loom-cache
files:
- pnpm-lock.yaml
- package.json
paths:
- .pnpm-store

Example with glob patterns:

cache:
key:
prefix: loom-cache-go
files:
- go.work
- go.work.sum
- "**/go.sum"
paths:
- .go

Additional details:

  • files is required when cache:key is a mapping. Omitting it produces: add required key files with at least one entry to cache key mapping.
  • Loom supports *, ?, and ** glob patterns in cache key files.
  • Entries like "**/go.sum" match go.sum files in any subdirectory.

cache:fallback_keys

Use cache:fallback_keys to specify alternative keys to try if the primary cache:key doesn't find a cached archive. Keys are tried in order.

Keyword type: Job keyword (cache subkey).

Supported values:

  • A non-empty YAML sequence of non-empty strings. Supports template variable placeholders.

Example of cache:fallback_keys:

cache:
key: "pnpm-${head_sha}"
fallback_keys:
- "pnpm-main"
- "pnpm-default"
paths:
- .pnpm-store

In this example, if no cache exists for the current commit SHA, the runtime tries pnpm-main, then pnpm-default.


cache:policy

Use cache:policy to control when the cache is restored and saved.

Keyword type: Job keyword (cache subkey).

Supported values:

  • pull: Only restore the cache at job start. Never save after the job finishes. Use when multiple jobs share the same cache and you want to avoid redundant saves.
  • push: Only save the cache after the job finishes. Never restore at job start. Use for jobs that build/populate the cache.
  • pull-push (default behavior): Restore the cache at job start and save it after the job finishes.

Example of cache:policy:

install:
stage: deps
target: linux
cache:
paths: [.pnpm-store]
policy: pull-push
script:
- pnpm install

lint:
stage: ci
target: linux
cache:
paths: [.pnpm-store]
policy: pull
script:
- pnpm run lint

In this example, install both restores and saves the cache (pull-push). lint only restores it (pull), avoiding a redundant save since the cache contents didn't change.

Additional details:

  • Invalid values produce: set cache policy to one of: pull, push, pull-push.

cache:when

Use cache:when to control when the cache is saved, based on the job's exit status.

Keyword type: Job keyword (cache subkey).

Supported values:

  • on_success (default): Save the cache only when the job succeeds.
  • on_failure: Save the cache only when the job fails.
  • always: Save the cache regardless of the job's exit status.

Example of cache:when:

check:
stage: ci
target: linux
cache:
paths: [.pnpm-store]
when: always
script:
- pnpm install
- pnpm test

In this example, the cache is saved whether the tests pass or fail, so subsequent runs benefit from the installed dependencies regardless.

Additional details:

  • Invalid values produce: set cache when to one of: on_success, on_failure, always.

cache:name

Use cache:name to give a unique name to a cache entry when using the sequence (multi-cache) form. The name identifies the cache across the workflow and allows different jobs to reference the same named cache.

Keyword type: Job keyword (cache subkey, sequence form only).

Supported values:

  • A non-empty string.

Example of cache:name:

default:
cache:
- name: pnpm
key:
prefix: loom-cache
files: [pnpm-lock.yaml]
paths: [.pnpm-store]
policy: pull-push
when: always
- name: go
key:
prefix: loom-cache
files: [go.work, "**/go.sum"]
paths: [.go]
policy: pull-push
when: always

Additional details:

  • name is required when cache is a sequence. Omitting it produces: add required key name with a non-empty string value when cache is a sequence.
  • Names must be unique within a cache sequence. Duplicate names produce: use unique cache names; "xyz" is duplicated.
  • When a job overrides a specific named cache from the default, it can reference just the caches it wants to change:
lint:
stage: ci
target: linux
cache:
- name: pnpm
policy: pull
- name: go
policy: pull
script:
- pnpm run lint

cache:disabled

Use cache:disabled in a sequence (multi-cache) entry to disable that specific named cache for a job, without removing it from the configuration.

Keyword type: Job keyword (cache subkey, sequence form only).

Supported values:

  • true or false.

Example of cache:disabled:

check:
stage: ci
target: linux
cache:
- name: pnpm
disabled: true
- name: go
paths: [.go]
script:
- go test ./...

In this example, the pnpm cache is disabled for this job, but the go cache is active.

Additional details:

  • When disabled: true is set, the paths key is not required for that entry.

artifacts

Use artifacts to publish files from the workspace that executed the job. Published files are copied to the live checkout's structured log directory at .loom/.runtime/logs/<run_id>/jobs/<job_id>/artifacts/, preserving their relative directory structure within the workspace.

Keyword type: Job keyword. You can use it only as part of a job.

Supported values: A YAML mapping with the following subkeys:

SubkeyDescription
artifacts:pathsRequired. Glob patterns matching files or directories to publish.
artifacts:excludeGlob patterns for files to exclude from publication.
artifacts:nameHuman-readable name for the artifact set.
artifacts:whenWhen to publish: on_success (default), on_failure, or always.
artifacts:requiredtrue fails a due publication when no configured path matches. Default: false.

Example of artifacts:

build:
stage: ci
target: linux
image: node:20-alpine
script:
- npm run build
- npm test -- --coverage
artifacts:
paths:
- dist/
- coverage/
exclude:
- dist/**/*.map
name: build-output
when: on_success
required: true

Example with when: on_failure (capture test reports on failure):

test:
stage: ci
target: linux
image: node:20-alpine
script:
- npm test -- --reporter=junit --outputFile=reports/junit.xml
artifacts:
paths:
- reports/
when: on_failure

Additional details:

  • artifacts is optional. If omitted, no files are extracted after the job completes.
  • paths is required when artifacts is present. Omitting it produces: add required key paths for artifacts.
  • when controls extraction timing based on job outcome:
    • on_success (default): publish only when command/provider execution succeeds.
    • on_failure: publish only when command/provider execution fails.
    • always: publish regardless of command/provider outcome.
  • Invalid when values produce: set artifacts when to one of: on_success, on_failure, always.
  • required defaults to false. Optional no-match publishes zero files and no archive; a due required: true declaration fails when no path matches after exclusions.
  • A declaration skipped by when does not fail because it is required; provider export failures are evaluated before when and still fail publication.
  • For a due publication, file-secret cleanup, copy, permission, and archive failures fail publication. When publication is not due, it remains skipped; a file-secret cleanup error still fails the job's cleanup independently.
  • Unknown keys produce: remove unknown artifacts key; allowed keys are paths, exclude, name, when, required.
  • Secret cleanup finishes before artifact publication. Publication retains the raw system section name artifact_extract; under runtime-logs v2, its events appear in jobs/<job_id>/system/artifact_extract/events.jsonl with the compatibility phase code job.artifact_restore. Use the receipt's ArtifactPublication outcome and the section metrics for publication status.
  • When at least one file is published, an archive is also produced at jobs/<job_id>/artifacts/artifacts.tar.gz. The job manifest and pipeline manifest include metadata fields pointing to this archive (see Runtime logs contract).
  • Paths are relative to the execution workspace, not the live checkout. For bind-mount Docker jobs they reference /workspace; ephemeral-volume jobs export from that execution volume.
  • Literal directory paths may end with / and include descendants. The root name artifacts.tar.gz is reserved for Loom's generated archive; that basename remains valid below a subdirectory.
  • Ephemeral-volume exports use private staging. Includes and excludes are reapplied before only selected files reach the final directory, and staging is removed for success, skip, or failure.

Related topics:


artifacts:paths

Use artifacts:paths to declare which files or directories Loom should extract from the job workspace after execution.

Keyword type: Job keyword (artifacts subkey).

Supported values:

  • A non-empty YAML sequence of non-empty strings.

Example of artifacts:paths:

artifacts:
paths:
- dist/
- coverage/

Additional details:

  • paths is required when artifacts is present. Omitting it produces: add required key paths for artifacts.
  • Paths are matched relative to the job workspace root.

artifacts:exclude

Use artifacts:exclude to omit matching files from the extracted artifact set.

Keyword type: Job keyword (artifacts subkey).

Supported values:

  • A YAML sequence of non-empty strings.

Example of artifacts:exclude:

artifacts:
paths:
- dist/
exclude:
- dist/**/*.map

Additional details:

  • exclude is optional.
  • Empty entries produce: replace empty artifact exclude entry with a non-empty string.

artifacts:name

Use artifacts:name to attach a human-readable label to the artifact set.

Keyword type: Job keyword (artifacts subkey).

Supported values:

  • A non-empty string.

Example of artifacts:name:

artifacts:
paths:
- dist/
name: build-output

Additional details:

  • name is optional.

artifacts:when

Use artifacts:when to control when Loom extracts artifacts based on the job result.

Keyword type: Job keyword (artifacts subkey).

Supported values:

  • on_success (default)
  • on_failure
  • always

Example of artifacts:when:

artifacts:
paths:
- reports/
when: on_failure

Additional details:

  • Invalid values produce: set artifacts when to one of: on_success, on_failure, always.

Common validator failures (and quick fixes)

When you run loom check and encounter errors, use this section as a quick reference. Errors follow the format WF_SCHEMA_V1 /path: message.

SymptomFix
Missing versionAdd version: v1 at the root of your workflow.
Missing stagesAdd stages: with at least one stage name.
Missing required job keysNon-template jobs need stage, target, and script. Add the missing key, or set default.target so jobs can inherit it.
Job references an undeclared stageEnsure the job's stage: value matches one of the names in stages:.
Invalid stage nameRename to match ^[a-z][a-z0-9_-]{0,31}$ — lowercase, starts with a letter, max 32 characters.
Invalid job nameRename to match ^\.?[a-z][a-z0-9_-]{0,63}$ — lowercase, starts with a letter (or . for templates), max 64 characters.
Invalid variable nameRename to match ^[A-Z_][A-Z0-9_]*$ — uppercase with underscores.
Command section shape errorsbefore_script, script, and after_script entries must be non-empty YAML scalars. Literal and folded block scalars are supported; script must be non-empty, while optional phases may use [] to clear inherited commands.
Invalid include.local pathPath must start with .loom/templates/, end with .yml/.yaml, and must not contain ...
Unknown job keyRemove the unrecognized key. Allowed job keys: stage, target, before_script, script, after_script, extends, needs, image, runner_pool, variables, secrets, invariant, cache, services, artifacts, artifact_inputs, allow_failure, rules.
Unknown default keyRemove the unrecognized key. Allowed default keys: target, before_script, after_script, image, runner_pool, variables, invariant, cache, services, rules.
secrets in defaultSecrets are job-scoped and cannot be set in default. Move each secret declaration into the jobs that need it.
Variable/secret key collisionA key cannot exist in both variables and secrets within the same job. Rename or remove the duplicate.
YAML alias cannot be resolvedDefine the anchor earlier in the same file. Anchors cannot cross an include boundary.
YAML composition limit exceededReduce nested or repeatedly expanded aliases. Alias depth is limited to 32 and one compilation can materialize at most 100,000 nodes.

Minimal example

The smallest workflow that passes loom check:

version: v1
stages: [ci]

check:
stage: ci
target: linux
script:
- echo "hello"

A real-world workflow using most v1 features:

version: v1

stages:
- deps
- ci

default:
target: linux
cache:
- name: pnpm
key:
prefix: loom-cache
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
policy: pull-push
when: always
- name: go
key:
prefix: loom-cache
files:
- go.work
- go.work.sum
- apps/**/go.sum
- libs/**/go.sum
paths:
- .go
policy: pull-push
when: always
variables:
PNPM_STORE_DIR: .pnpm-store
GOPATH: .go

build-nix-image:
stage: deps
target: linux
cache: []
script:
- docker build -t loom:nix-local -f nix/Dockerfile .

install-deps:
stage: ci
image: loom:nix-local
variables:
PNPM_STORE_DIR: .pnpm-store
script:
- pnpm i --frozen-lockfile
- pnpm nx run-many -t go-deps-verify

check:
stage: ci
image: loom:nix-local
needs:
- install-deps
cache:
- name: pnpm
policy: pull
- name: go
policy: pull
script:
- pnpm i --frozen-lockfile
- task check

Next steps

Pick the path that matches what you're trying to do: