Workflow schema v1
This is the definitive reference for Loom's schema v1 — every key, type, constraint, and validation rule enforced by loom check.
Workflow files are authored in YAML and validated against schema v1.
- Validate:
loom check - Run (local):
loom run --local
If you're trying to author a workflow, start with Hello Loom and Syntax (v1). Come back here when you need the exact rule for a specific key.
Root structure
The root must be a YAML mapping. Two keys are required; four are optional. Any other key is treated as a job definition.
| Key | Required | Type | Description |
|---|---|---|---|
version | yes | string | Must be the exact string "v1". |
stages | yes | string[] | Non-empty sequence of stage names. Defines execution order. |
include | no | sequence | Sequence of {local: <path>} entries for template includes. |
workflow | no | mapping | Workflow-wide rules; non-empty v1/v2 if conditions are Experimental. |
variables | no | mapping | Workflow-level variables. Keys: UPPER_SNAKE_CASE → string values. |
default | no | mapping | Default job keyword values merged into every job. |
<job_name> | no | mapping | Any key matching the job naming pattern is treated as a job definition. |
Unrecognized root keys that don't match the job naming pattern produce a schema error.
Naming patterns
| Entity | Pattern | Max length | Notes |
|---|---|---|---|
| Stage name | ^[a-z][a-z0-9_-]{0,31}$ | 32 chars | Lowercase, starts with letter. |
| Job name | ^\.?[a-z][a-z0-9_-]{0,63}$ | 64 chars | Dot prefix (.) marks a template job. |
| Variable key | ^[A-Z_][A-Z0-9_]*$ | — | UPPER_SNAKE_CASE. Applies to both variables and secrets. |
version
Must be the exact scalar string "v1". Any other value fails validation.
stages
A non-empty YAML sequence of unique stage names. Each stage name must match ^[a-z][a-z0-9_-]{0,31}$. Duplicate stage names are rejected.
stages: [ci, build, deploy]
include
A YAML sequence of include entries. Each entry must be a mapping with a local key.
include[].local constraints
- Must start with
.loom/templates/ - Must end with
.ymlor.yaml - Must not contain
..
include:
- local: .loom/templates/go-jobs.yml
See Includes & templates for how included templates interact with extends.
workflow
A mapping with optional key rules. No other key is allowed. rules must be a
YAML sequence; it may be empty. Each rule entry must be a mapping with exactly
one non-empty scalar string field, if.
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Schema validation enforces the shape. Non-empty v1 and v2 rules are Experimental. The executor evaluates them against resolved pipeline variables and fails closed on invalid syntax, missing inputs, or type errors before provider setup or execution.
See Workflows → Rules for the rules syntax.
variables
A mapping of UPPER_SNAKE_CASE keys to string values. Applies at the workflow level (available to all jobs).
variables:
GO_VERSION: "1.22"
NODE_ENV: production
Variable keys must match ^[A-Z_][A-Z0-9_]*$. Values must be YAML scalars.
Loom stores each scalar's text as a string, including unquoted numbers and
booleans. Lists, mappings, and other non-scalar values are rejected. Quote values
to make the intended text clear; quoting is a recommendation, not a requirement.
default
A mapping of job keyword defaults that are merged into every job definition. Supports these keys:
| Key | Type | Description |
|---|---|---|
target | string | Default execution target. Must be "linux". |
before_script | string[] | Default setup command sequence. |
after_script | string[] | Default finalization command sequence. |
image | string or mapping | Default container image. See image. |
runner_pool | string | Default runner pool identifier. |
variables | mapping | Default variables merged into each job. |
invariant | mapping | Default invariant configuration. |
cache | mapping or sequence | Default cache configuration. See cache. |
services | sequence | Default sidecar services. See services. |
default.secrets is explicitly invalid and fails validation. Secrets must be declared per-job.
Jobs
Any root key matching the job naming pattern (^\.?[a-z][a-z0-9_-]{0,63}$) is treated as a job definition. Jobs whose name starts with . are template jobs.
Job-level keys
| Key | Type | Required | Description |
|---|---|---|---|
stage | string | yes (non-template) | Must reference a name declared in stages. |
target | string | yes (non-template) | Execution target. Must be "linux" (MVP). |
before_script | string[] | no | Optional setup command sequence. |
script | string[] | yes (non-template) | Non-empty sequence of command strings. |
after_script | string[] | no | Optional finalization command sequence. |
extends | string or string[] | no | Template job(s) to inherit from. |
needs | — | no | Accepted by schema. Used for dependency declarations. |
image | string or mapping | no | Container image. See image. |
runner_pool | string | no | Runner pool identifier. |
variables | mapping | no | Job-scoped variables. |
secrets | mapping | no | Job-scoped secrets. See secrets. |
invariant | mapping | no | Invariant configuration. |
cache | mapping, sequence, or null | no | Cache configuration. See cache. |
services | sequence | no | Sidecar services. See services. |
artifacts | mapping | no | Artifact publication. See artifacts. |
artifact_inputs | string[] | no | Producer artifacts to restore before this job runs. |
allow_failure | boolean | no | Allow main-script failure. Default false. |
Unknown job keys are rejected. The complete allowed set is: stage, target, before_script, script, after_script, extends, needs, image, runner_pool, variables, secrets, invariant, cache, services, artifacts, artifact_inputs, allow_failure.
Non-template job requirements
Non-template jobs must have all three: stage, target, and script.
Template job requirements
Template jobs (dot-prefixed names like .go-base) must have at least one of
before_script, script, after_script, or extends.
Command phases
before_script, script, and after_script are YAML sequences of non-empty
scalar commands. Commands retain their order and YAML-decoded scalar content,
including literal and folded block scalars.
before_scriptis optional and compiles to graphsetup.scriptis required and non-empty on every resolved executable job.after_scriptis optional and compiles to graphfinalization.before_script: []andafter_script: []clear inherited commands.script: []is invalid.- Default, template, and child sequences replace one another as complete values; they are not concatenated.
script
A non-empty YAML sequence of non-empty scalar commands.
- Empty strings are rejected.
- Literal block scalars preserve embedded newlines. Folded block scalars preserve their YAML-decoded folded content.
script:
- echo "step one"
- make build
- ./run-tests.sh
allow_failure
A strict boolean accepted on jobs and template jobs. Omission means false. A job
inherits the value from its template, and an explicit child value overrides it.
When true, an ordinary nonzero exit from the main script may satisfy dependency
ordering. Loom retains the command's failed status, exit code, and reason. Setup,
cancellation, timeout, provider, artifact publication, finalization, and cleanup
failures remain fatal.
image
Controls whether a job runs on the Host or Docker provider and which container image is used. Accepted at both default and job level.
Scalar form
A non-empty string naming a Docker image (pullable by the Docker daemon):
image: node:20-slim
Mapping form
| Key | Type | Required | Description |
|---|---|---|---|
name | string | yes | Non-empty image reference. |
pull_policy | string | no | always, if-not-present (default), or never. |
user | string or integer | no | Nonblank username, UID, or UID:GID. |
build | mapping | no | Build configuration for the image. |
Unknown keys under image are rejected.
pull_policy controls acquisition of named prebuilt images. It cannot be used
with image.build; built images omit acquisition policy in compiled Graph IR.
Scalar image shorthand defaults to if-not-present.
user selects the identity for job-container processes. Nonblank strings are
preserved exactly. Nonnegative YAML integer UIDs are normalized to unsigned
base-10 text. Blank strings, booleans, nulls, floats, sequences, mappings, and
negative integers are rejected. Omission leaves the image's configured runtime
user unchanged. user is an image subkey and is not valid at job level.
image.build
| Key | Type | Required | Description |
|---|---|---|---|
context | string | yes | Build context directory. Maps to docker build context. |
dockerfile | string | yes | Dockerfile path. Maps to docker build --file. |
output | string or mapping | no | Forwarded to docker build --output. |
Unknown keys under build are rejected.
image:
name: my-app:latest
build:
context: .
dockerfile: Dockerfile
Runtime notes
- When no
imageis specified, the job runs on the Host provider.
secrets
Declares sensitive values resolved at runtime from external providers and injected into the job environment. Exact matches to resolved secret values are redacted in supported console, log, and receipt output; transformed values and raw declared artifacts are not covered.
Placement rules
- Allowed: per-job blocks only.
- Disallowed:
default.secretsis explicitly invalid and fails schema validation. This minimizes accidental fan-out of secrets across jobs.
Secret spec shape
<job_name>:
secrets:
<SECRET_NAME>:
ref: <provider-uri>
file: true
required: true
Fields
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
ref | string | yes | — | Provider reference URI (env://, keepass://, op://). Must be non-empty. |
file | boolean | no | true | true: inject temp-file path. false: inject raw value. |
required | boolean | no | true | true: fail on unresolved. false: silently omit. |
Validation rules
- Secret names must match
^[A-Z_][A-Z0-9_]*$(same as variable keys). refmust be non-empty. Scheme/shape validity is enforced during secrets resolution.fileandrequiredmust be booleans.- A key cannot appear in both
variablesandsecretsfor the same effective job after merge — this fails with a schema/planner error.
Supported provider URI schemes
| Scheme | Format | Status |
|---|---|---|
env:// | env://<ENV_VAR_NAME> | Implemented |
keepass:// | keepass://<db-alias>#<entry-path>:<field> | Implemented |
op:// | op://<vault>/<item>/<field> | Implemented (service account token; no op CLI required) |
Injection behavior
file: true(default): value written to a node-scoped temp file (0600permissions). The variable is set to the file path. Docker providers bind-mount and rewrite to container-local paths.file: false: variable set directly to the secret value. Blocked whenCI_DEBUG_TRACE=true.
Redaction
Configured exact values are replaced before Loom persists verified textual sinks (events.jsonl, receipt stdout/stderr, output-bearing error fields, and provider lifecycle messages). Redaction replaces matches with [REDACTED:SECRET_<NAME>]. Inspect and sanitize runtime evidence and declared artifacts before sharing them.
Error codes
Resolution failures produce structured SECRETS_* error codes. See Secrets error codes.
Example
deploy:
stage: ci
target: linux
image: loom:nix-local
secrets:
DATABASE_PASSWORD:
ref: env://DEPLOY_DB_PASSWORD
file: true
API_TOKEN:
ref: env://DEPLOY_API_TOKEN
file: false
script:
- ./scripts/deploy.sh
Related pages
- Secrets concept — mental model, file-vs-env injection, redaction
- Secrets workflow authoring — YAML syntax, provider URIs, worked examples
- Secrets error codes — all
SECRETS_*error codes
artifacts
Declares files to publish from the workspace that executed the job. Host and Docker bind-mount jobs match files in the isolated execution snapshot. Ephemeral-volume jobs export from the execution volume. Published artifacts are written to the live checkout's .loom/.runtime/logs/<run_id>/jobs/<job_id>/artifacts/, preserving relative paths.
Artifact mapping keys
| Key | Type | Required | Description |
|---|---|---|---|
paths | string[] | yes | Non-empty sequence of glob patterns matching files/directories to publish. |
exclude | string[] | no | Glob patterns for files to exclude from publication. |
name | string | no | Human-readable name for the artifact set. |
when | string | no | When to publish: on_success (default), on_failure, or always. |
required | boolean | no | Default false. If true, a due declaration fails when no configured path matches. |
Unknown keys under artifacts are rejected.
build:
stage: ci
target: linux
image: node:20-alpine
script:
- npm run build
artifacts:
paths:
- dist/
exclude:
- dist/**/*.map
name: build-output
when: on_success
required: true
artifacts:when values
| Value | Behavior |
|---|---|
on_success (default) | Publish only when the command/provider execution succeeds. |
on_failure | Publish only when command/provider execution fails. Useful for test reports or core dumps. |
always | Publish regardless of command/provider outcome. |
Runtime notes
- Secret cleanup finishes before artifact publication. Publication then runs as the compatibility-named system section
artifact_extract; under runtime-logs v2, its normalized phase code remainsjob.artifact_restore. Useexecutor_receipt.Nodes[].ArtifactPublicationand the section metrics for the semantic publication outcome. - Publication events appear in
jobs/<job_id>/system/artifact_extract/events.jsonl. - Published files remain under
jobs/<job_id>/artifacts/. When at least one file is published, an archive is also produced atjobs/<job_id>/artifacts/artifacts.tar.gz. - The job manifest includes
artifacts.archive_path,artifacts.archive_format, andartifacts.archive_size_byteswhen an archive exists. The pipeline manifest includesartifacts_archive_pathper job. - If no files match after exclusions, publication succeeds with zero files and no archive by default. With
required: true, the due publication fails. - A
whencondition that is not due is skipped even whenrequired: true; provider export failures are evaluated beforewhenand still fail publication. - File-secret cleanup, copy, permission, and archive failures fail a due publication. When publication is not due, it remains skipped; a file-secret cleanup error still fails the job's cleanup independently.
required: falseonly makes the no-match case optional. - Paths are relative to the execution workspace, never the live checkout. For ephemeral-volume jobs, the provider exports candidate roots from the execution volume and Loom applies the same matcher in staging.
- Literal directory paths may end with
/(for example,dist/) and include their descendants. - The root output name
artifacts.tar.gzis reserved for Loom's generated archive. A declaration that selects that exact root path fails publication; the same basename is valid in subdirectories. - Ephemeral-volume candidates are exported to user-owned staging. Loom applies includes and excludes there, copies only selected files to the final directory, and removes staging on success, skip, or failure.
artifact_inputs
Lists producer job IDs whose declared artifacts Loom restores into this job's workspace before its first command. The value is always a sequence; omission and [] both request no artifact files.
build:
stage: build
target: linux
script: [npm run build]
artifacts:
paths: [dist/]
test:
stage: test
target: linux
artifact_inputs: [build]
script: [npm test]
Each producer must be an executable job with an artifacts declaration. Missing producers, self references, duplicate entries, and dependency cycles fail compilation at the indexed artifact_inputs source path. needs only orders jobs and never transfers files; declaring both for the same producer retains artifact transfer while counting as one ordering prerequisite.
When a job inherits artifact_inputs through extends, its own sequence replaces the inherited sequence as a whole. Set artifact_inputs: [] to clear inherited inputs.
services
Declares sidecar containers that run alongside the job. Accepted at both default and job level.
Must be a YAML sequence. Each entry is either a scalar image string or a mapping.
Scalar form
A non-empty string naming the service image:
services:
- postgres:15
- redis:7
Mapping form
| Key | Type | Required | Description |
|---|---|---|---|
name | string | yes | Non-empty image reference for the service. |
pull_policy | string | no | always, if-not-present (default), or never. |
user | string or integer | no | Nonblank username, UID, or UID:GID for this service. |
alias | string | no | Network alias for the service container. |
entrypoint | string[] | no | Override container entrypoint. Non-empty sequence of strings. |
command | string[] | no | Override container command. Non-empty sequence of strings. |
variables | mapping | no | Service-specific environment variables. |
Unknown keys are rejected. The following keys produce explicit "not supported yet" errors: docker, kubernetes.
services:
- name: postgres:15
pull_policy: never
user: postgres
alias: db
variables:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
Scalar service shorthand and mapping entries without pull_policy default to
if-not-present. Each service policy is compiled independently.
Each service mapping can set its own user. Nonblank strings are preserved
exactly, while nonnegative YAML integer UIDs are normalized to unsigned base-10
text. Invalid types, blank strings, and negative integers are rejected at that
service entry's user path. Omission leaves the service image's configured
runtime user unchanged.
Runtime notes
Services are supported in Docker jobs (those with image: set).
cache
Configures job-level caching. Accepted at both default and job level.
Accepted forms
| Form | Meaning |
|---|---|
cache: null | Explicitly disables cache for this job. |
cache: [] | Explicitly disables cache for this job. |
cache: {paths: [...], ...} | Single cache mapping. |
cache: [{name: ..., paths: [...]}, ...] | Sequence of named cache entries. |
Cache mapping keys
| Key | Type | Required | Description |
|---|---|---|---|
name | string | yes (sequence) | Unique name for the cache entry. Required when cache is a sequence. |
disabled | boolean | no | Disables this cache entry when true. |
paths | string[] | yes (unless disabled) | Non-empty sequence of paths to cache. |
key | mapping | no | Cache key specification. Allowed sub-keys: prefix and files. |
fallback_keys | string[] | no | Fallback cache keys tried if primary key misses. |
policy | string | no | One of: pull, push, pull-push. |
when | string | no | One of: on_success, on_failure, always. |
Unknown cache keys are rejected.
Example
cache:
paths:
- node_modules/
key:
prefix: deps
files:
- pnpm-lock.yaml
policy: pull-push
when: on_success
See Workflows → Cache for key placeholder syntax and strategies.
Minimal valid example
The smallest workflow that passes schema v1 validation:
version: v1
stages: [ci]
check:
stage: ci
target: linux
script:
- echo "hello"
Validator vs runtime semantics
Schema v1 has two layers:
- Validator contract — what
loom checkenforces (YAML shape + constraints). Everything on this page. - Runtime semantics — what
loom rundoes when executing jobs (provider routing, service lifecycle, caching behavior, artifact writes).
Some keywords are accepted by the schema for forward compatibility, but their runtime effect may be partial or planned. When in doubt, treat what's documented on Syntax (v1) as the contract.
Runtime-only behavior (not validated by loom check)
- Provider routing — host vs Docker based on
imagepresence. See Providers. - Services lifecycle — sidecar containers via Docker provider. See Docker provider.
- Artifact publication — files from the execution workspace or provider export are copied to
.loom/.runtime/logs/<run_id>/jobs/<job_id>/artifacts/, with an archive atartifacts/artifacts.tar.gzwhen files exist. See Runtime logs contract.
Versioning policy
Schema v1 is expected to evolve:
- Additive changes remain compatible within v1 when possible.
- Breaking changes ship as a new schema version with migration notes.
See the CLI overview for the current command surface.
Cross-references
- Syntax (v1) — narrative keyword guide with worked examples
- Troubleshooting → Common failures — error signatures and first actions
- Includes & templates — how
includeandextendscompose