Stages, jobs, and steps
Loom workflows are organized into three levels: stages, jobs, and steps. Together, they control execution order, environment isolation, and the structure of runtime diagnostics — so when something fails, you can pinpoint the exact command without scanning aggregated logs.
Why this hierarchy matters
Each level has a different role:
- Stages define execution phases — build before test, test before deploy.
- Jobs are the unit of scheduling, isolation, and provider selection.
- Steps are individual commands with their own event streams, so you can jump straight to the failing command's output.
Runtime state and event records carry IDs for these execution scopes so you can identify the failed operation and its output.
The three levels
| Level | What it is | YAML location | Required |
|---|---|---|---|
| Stage | An execution phase that groups jobs | stages: [build, test, deploy] at root | Yes (at least one) |
| Job | A scheduled unit of work with its own environment | Top-level key matching ^[a-z][a-z0-9_-]{0,63}$ | Yes (at least one) |
| Step | A single command inside a job's script: | Each entry in a job's script: list | Yes (at least one per job) |
How they nest
workflow.yml
├── stages: [build, test] ← execution phases
│
├── compile: ← job (build stage)
│ ├── stage: build
│ ├── target: linux
│ └── script:
│ ├── step 1: make deps ← step
│ └── step 2: make build ← step
│
├── unit: ← job (test stage)
│ ├── stage: test
│ ├── target: linux
│ └── script:
│ └── step 1: go test ./... ← step
│
└── lint: ← job (test stage)
├── stage: test
├── target: linux
└── script:
└── step 1: golangci-lint run ← step
Corresponding YAML
version: v1
stages: [build, test]
compile:
stage: build
target: linux
script:
- make deps
- make build
unit:
stage: test
target: linux
script:
- go test ./...
lint:
stage: test
target: linux
script:
- golangci-lint run
Execution ordering
Loom compiles stage barriers, explicit needs, and artifact inputs into dependency
edges. The local executor starts dependency-ready jobs concurrently within its
configured limit. A failed job blocks its dependents while independent jobs continue.
Stage ordering
The stages: list supplies default barriers. A job that omits needs waits for every
executable job in every earlier declared stage.
In the example above: compile (build stage) finishes before both unit and lint (test stage) start.
Job ordering within a stage
Jobs in the same stage have no implicit dependencies. Dependency-ready jobs may run concurrently; Loom uses job-name order when selecting among ready jobs.
In the example above, lint and unit can run concurrently after compile finishes.
Explicit dependencies with needs
Use needs to declare that a job depends on specific other jobs:
integration:
stage: test
needs: [compile]
target: linux
script:
- go test -tags=integration ./...
An explicit list replaces the job's stage barriers, so integration waits for only
compile. An explicit empty list (needs: []) adds no stage dependency and permits
immediate readiness. Artifact inputs remain additional prerequisites in both cases.
This is useful for:
- Dependencies between jobs in the same stage.
- Selecting only the earlier jobs that a later-stage job requires.
- Bypassing the default stage barrier with
needs: [].
Compilation rejects missing targets, self-dependencies, and cycles at the offending
needs entry.
Step ordering
Steps within a job run sequentially in the order listed. If any step exits with a non-zero code, the job fails immediately and remaining steps are skipped.
Each step must be a non-empty string. Multi-line commands within a single step entry are not allowed — use one command per script: entry.
Ordering summary
| Rule | Guarantee |
|---|---|
| Stages | Omitted needs waits for all executable jobs in all earlier declared stages |
| Jobs within a stage | No implicit dependencies; ready jobs may run concurrently |
Jobs with needs | The explicit list replaces stage barriers; missing, self, and cyclic dependencies fail compilation |
| Artifact inputs | Add prerequisites independently of needs |
| Steps within a job | Sequential, in list order; fail-fast on non-zero exit |
Required job keys
Every executable (non-template) job must have these keys:
| Key | Type | Description |
|---|---|---|
stage | string | Must match a name declared in root stages: |
target | string | "linux" or "darwin"; must match the selected provider |
script | sequence of strings | At least one non-empty command string |
Template jobs (names starting with .) require at least one of script or extends.
Optional job keys
| Key | Purpose |
|---|---|
image | Container image — triggers Docker provider (see Providers) |
variables | Job-scoped environment variables (see Variables) |
secrets | Job-scoped secret references (see Secrets) |
needs | Explicit dependencies that replace the job's stage barriers |
artifact_inputs | Artifact producers that add prerequisites and restore declared files |
extends | Inherit configuration from a template job |
services | Sidecar containers (requires image to be set) |
cache | Cache configuration for the job |
runner_pool | Runner pool assignment |
invariant | Invariant metadata |
The default block
The default block sets inherited defaults for all jobs. Job-level values override these defaults through a deep merge — nested mappings (like variables) are merged key-by-key, not replaced wholesale.
default:
target: linux
variables:
LANG: "en_US.UTF-8"
Allowed keys in default:
| Key | Purpose |
|---|---|
target | Default target for all jobs |
image | Default container image |
runner_pool | Default runner pool |
variables | Default variables (merged with job variables) |
invariant | Default invariant metadata |
cache | Default cache configuration |
services | Default sidecar services |
secrets is not allowed in default — secrets must be declared per job because they are job-scoped.
Diagnostics: from failure to root cause
The run records every scope in two files:
.loom/.runtime/runs/<run_id>/
state.jsonl
events.jsonl
Read loom logs <run_id> --state to find a failed workflow, job, step or command.
Match its job_id, step_id and command_id in loom logs <run_id> --json to
read contextual output and diagnostics. MCP provides loom_read_run for state
and loom_read_events with job_id or stream filters for messages.
Steps group commands into setup, script and finalization. A step ID
includes the job and phase, such as build:script; commands have phase-local
IDs such as script-01. Image-build work has its own image_build step.
See Runtime logs for the record fields.
Common pitfalls
Missing required keys
- Symptom:
loom checkreports errors likeadd required key stage for this joboradd required key target with value "linux". - Fix: Ensure every executable job has
stage,target, andscript. Template jobs need at leastscriptorextends.
Can't find the failure in logs
- Symptom: You're reading full stdout/stderr logs but can't identify what failed.
- Fix: Read
loom logs <run_id> --state, then match the failed operation's IDs in the run'sevents.jsonl. See the Diagnostics ladder for the full triage flow.
Unexpected execution order
- Symptom: Jobs run in a different order than expected.
- Fix: Jobs that omit
needswait for every earlier-stage job. An explicitneedslist replaces that barrier, andneeds: []can start immediately. Ready jobs may run concurrently. Runloom compileto inspect the resolved dependency graph.
What to read next
- Workflow structure: Workflow, Workflow schema v1
- Execution environments: Providers
- Parameterize jobs: Variables
- Failure triage: Diagnostics ladder, Runtime logs contract
- Sharing debug info: What to share