loom compile
Compile a Loom workflow into Graph IR v1 JSON or project a GitLab workflow into a value-safe plan report, without executing jobs.
Use repeatable --job <source-or-id> for a bounded graph containing those jobs and
their complete ordering and artifact prerequisites. Both native Graph IR and the
GitLab report expose optional job_selection with sorted requested_jobs, optional requested_instances,
effective_jobs and a stable scope identity. This is partial-workflow evidence.
Omitting the flag preserves full-workflow output. IDs are exact: empty and
whitespace-padded IDs fail, commas are literal, and duplicates form one set.
Unknown or unavailable selections fail without execution; rules still apply.
For integer fanout, --job shard selects available automatic source members before prerequisite
closure. --instance 'shard 2/3' selects only that exact final node, even when its
name is also a source alias. Repeat or combine both flags. job_selection binds
both request sets and effective jobs into its identity. See
integer fanout.
Canonical command usage
Use the
core CLI command reference for the exact
loom compile synopsis, flags, and displayed defaults.
Output summary for GitLab sources
For GitLab sources, stderr ends with one line,
N executable jobs, M job decisions skipped, state <state> when an executable
graph exists. N is its final node count, including selected fanout instances;
M counts excluded, manual or unevaluated job decisions. Without a graph the
summary explicitly reports job decisions selected/skipped instead. Stdout remains
report JSON only, so jq pipelines are unaffected.
Workflow source and context
Omitted flags fall back to the defaults block of the layered runtime config
(see runtime config syntax); explicit flags
always win.
Default discovery selects .loom/workflow.yml, then .loom/workflow.yaml,
before the repository root .gitlab-ci.yml. --format loom|gitlab selects
exactly one parser. Without --format, an explicit --workflow file is sniffed:
a root version key or a job target means Loom; ordinary other mappings mean
GitLab. An include-only mapping is ambiguous, and an explicit context envelope
hints its frontend for indeterminate content, as
described for loom check.
Parser and read failures do not cause fallback; a Loom-schema failure on a
GitLab-shaped file ends with this file looks like GitLab CI; pass --format gitlab.
Native --context accepts the strict
caller variable envelope, including
empty maps and values. Explicit format/content remains authoritative; configured
defaults.context remains GitLab-only.
Use --context with GitLab sources to load a strict
loom.gitlab-pipeline-context.v1 JSON document, by project-relative path or by
short name (.loom/contexts/<name>.json, see
loom context init). The file is project-relative,
limited to 1 MiB, and keeps variables, changes, and runner_tags separate.
changes.paths is optional: without it, changed paths are resolved from git as
described for loom check
and reported under resolved_changes.
Ambient CI variables and runtime settings do not affect selection.
Workflow path trust boundary
--workflow paths are resolved relative to the repository root. Absolute paths,
paths containing .., and paths that resolve through escaping or dangling symlinks
are rejected with WF_TRUSTED_PATH before workflow content is read. The same
boundary applies to include.local.
Use cases
- Diff compiled plans across branches or commits to spot unexpected job changes
- Verify include/template resolution — confirm
extendsandinclude.localmerge correctly - Inspect the job graph (nodes +
needsedges) before running - Feed Graph IR to external tools for visualization or analysis
Exit codes
| Code | Meaning |
|---|---|
0 | Loom compilation succeeded, or GitLab plan is executable or skipped |
1 | Source is invalid, GitLab plan is non-executable, or the command fails |
Output streams
| Stream | Content |
|---|---|
| stdout | Graph IR v1 JSON, or a GitLab plan report once a plan exists |
| stderr | Warnings, diagnostics, and fatal errors |
For GitLab, stdout uses loom.gitlab-plan-report.v1. It contains the plan
schema and state, selection identity, selection records, requirements,
diagnostics, and executable job topology. It excludes scripts, variables, and
resolved context values. source_invalid and non_executable still write this
report before returning nonzero; workflow_skipped writes a skipped report and
returns 0.
Compile reports privileged-service requirements but cannot grant consent. It also exposes unsupported hosted macOS, OIDC token, Pages, and environment requirements; optional manual jobs remain visible but are not executed. Report processing and artifact expiry are diagnostic limitations.
Graph IR output format
loom compile outputs a JSON object with these top-level keys:
| Key | Type | Description |
|---|---|---|
schema | string | IR schema identifier — currently loom.graph-ir.v1 |
stages | string[] | Ordered list of stage names |
integer_fanout | object[] | Optional source groups with original totals and selected member IDs |
nodes | object[] | Executable jobs (template-only jobs are excluded) |
edges | object[] | Dependency edges (currently: needs relationships) |
Example output
{
"schema": "loom.graph-ir.v1",
"stages": ["ci"],
"nodes": [
{
"id": "docs-lint",
"stage": "ci",
"target": "linux",
"image": "alpine:3.20",
"script": ["pnpm i --frozen-lockfile", "pnpm nx run loom-platform:docs-mdlint"],
"allow_failure": false,
"variables": [{ "key": "PNPM_STORE_DIR", "value": ".pnpm-store" }],
"cache": {
"paths": [".pnpm-store", ".nx/cache"],
"policy": "pull-push",
"when": "always",
"key": { "prefix": "loom-cache", "files": ["pnpm-lock.yaml"] }
}
}
],
"edges": []
}
Notes on node fields:
scriptmust be present. An explicit empty array represents a zero-command main phase; missing or null is invalid. Source validation still determines which authored command forms are valid; native required scripts remain nonempty.image,variables, andcacheare omitted when not present or not effective for a node.cachecan be a single object or a list, matching the workflow's single-cache vs. multi-cache shape.allow_failureis always a boolean and defaults tofalse. Whentrue, only an ordinary nonzero exit from the main script may satisfy dependency ordering. The command result keeps its actual status, exit code, and reason. Setup, cancellation, timeout, provider, publication, finalization, and cleanup errors remain fatal.- Workflow source may declare
allow_failureon jobs and template jobs. Resolved template values are emitted in Graph IR, and an explicit child value overrides its template.
Warnings
Warnings are non-fatal. They are written to stderr in this format:
warning: <code> <path> <message>
Known warning codes
| Code | Meaning |
|---|---|
WF_PLANNER_VARIABLE_NON_SCALAR | A variable value was not a scalar; it was ignored while building the Graph IR |
Invalid needs targets, self-dependencies, and dependency cycles are compilation
errors. They are reported at the offending needs source path and exit with code
1.
This list may grow. If you see an unknown warning code, include it when reporting issues.
GitLab diagnostics
GitLab console diagnostics include every blocking finding and advisory findings
that apply to the plan or automatically selected jobs. Blocking findings carry
the error: prefix and advisory findings carry warning::
error: ci/.gitlab-ci.yml:8:9 GL_SOURCE_UNSUPPORTED: rule when is preserved but not executable yet [/job/rules/0/when]
hint: use when: on_success, never, manual or always, or omit when to keep the GitLab default
warning: ci/.gitlab-ci.yml:3:18 GL_LIMIT_INTERRUPTIBLE_IGNORED: GitLab interruptible cancellation is ignored locally [/job/interruptible]
Each line names the source path relative to the repository root, the line and
column, the diagnostic code, the message, and the JSON pointer into the document.
Diagnostics without a recorded source location fall back to
CODE POINTER: MESSAGE. Findings with a known fix add an indented hint: line.
Hints never appear in the stdout report, whose diagnostics entries keep the
code, pointer, location, and message unchanged.
Diagnostics that share a code, message, and source location print once. A
default: entry that GitLab applies to every job, such as interruptible: true,
is reported at the default's location with the first eligible job's pointer and
a count of eligible jobs instead of one line per job:
warning: .gitlab-ci.yml:67:18 GL_LIMIT_INTERRUPTIBLE_IGNORED: GitLab interruptible cancellation is ignored locally [/pnpm-deps/interruptible] (7 jobs)
The scope and count are presentation only. The stdout report retains all
diagnostics entries, including findings for inactive jobs.
Examples
Compile the default workflow
loom compile
Compile a workflow at a custom path
loom compile --workflow ci/staging-workflow.yml
Pipe Graph IR to jq for inspection
loom compile | jq '.nodes[] | {id, stage, image}'
Diff compiled plans between branches
diff <(git stash && loom compile) <(git stash pop && loom compile)
Reporting issues
When loom compile fails or emits unexpected warnings, include:
- The command you ran (including
--workflowif used) - Warning or error lines from stderr
- The first ~20 lines of JSON output (redacted if needed)
See What to share for the full checklist.
Related commands
loom check— validate schema without compilingloom run— execute the compiled workflow
Related
Native compile selection
Use root/job selection with explicit
caller context to filter native jobs before graph edges and --job closure:
loom compile --workflow .loom/conformance/compile-selection/native-host.yml --format loom --context .loom/conformance/compile-selection/caller-native-blue.json
This selects blue, common, raw-phase, scalar-logic and consumer.
The same fixture with the green caller selects green in place of blue.
Native output preserves optional compile_selection decision evidence through
Graph IR JSON roundtrips. Compile inclusion does not bypass retained runtime rules.