Skip to main content

loom check

Validate a Loom or GitLab workflow and emit a receipt without executing jobs.

Use repeatable --job <source-or-id> to validate selected jobs and their prerequisite closure. The receipt and console include job_selection with sorted requested_jobs, effective_jobs and a stable scope identity. A selected check does not verify the entire workflow. Empty, whitespace-padded and unknown IDs are rejected; commas are literal and duplicate IDs form one set.

Without selectors or integer fanout, native checks retain schema-only validation. With explicit jobs, native checks also compile the dependency graph to validate the selected closure. GitLab checks retain capability-aware planning in both cases. No jobs execute.

For integer fanout, --job shard selects all 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 check synopsis, flags, and displayed defaults.

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.

Without flags, Loom selects .loom/workflow.yml, then .loom/workflow.yaml, before the repository root .gitlab-ci.yml. --format loom|gitlab selects exactly one parser; read or parser errors never fall back to the other format. --workflow accepts a project-relative trusted path. Without --format, Loom infers the format from the file content: a root version key or a job with target means Loom; other ordinary mappings mean GitLab. Include-only mappings are indeterminate so a native version may come from an included source. When content is indeterminate, an explicit context envelope hints its frontend; otherwise the exact .gitlab-ci.yml basename selects GitLab. A Loom-schema failure on a GitLab-shaped file ends with this file looks like GitLab CI; pass --format gitlab.

GitLab sources may use !reference [job, key, ...] tags; they resolve after include: and before extends:, nest at most 10 levels, and are the only explicit YAML tag Loom accepts (see the keyword compatibility matrix).

Native --context accepts the strict loom.variable-context.v1 envelope, including an empty variables map. Unscoped native checks validate scalar semantics and cycles with or without context. Explicit format/content wins over a context hint. Config defaults.context remains GitLab-only.

For GitLab, --context accepts a project-relative JSON file with schema loom.gitlab-pipeline-context.v1, or a short name without a path separator or .json suffix that resolves to .loom/contexts/<name>.json (generate one with loom context init). Its variables and changes drive GitLab rules and expansion. runner_tags describes local runner capabilities used to satisfy or report plan requirements; it is not job-selection data. Ambient CI variables and runtime settings are ignored. Context files are limited to 1 MiB.

changes needs base and head; paths is optional. When a matched rule has rules:changes and the context supplies no paths, Loom resolves the changed paths with git diff --merge-base in the repository root, mirroring GitLab's compare_to semantics (diff of merge_base(compare_to, head)..head). The base is the rule's compare_to (variables resolve in the rule phase) or changes.base; the head is changes.head, or the working tree when compare_to is set and the context has no changes (recorded as head HEAD with worktree: true: staged and unstaged tracked files, untracked excluded). Explicit paths always win and git is never consulted. An unknown ref, a shallow clone without a merge base, or a missing git fails closed with GL_RULE_CHANGES_UNAVAILABLE and a non_executable plan. rules:changes accepts the list shorthand (changes: ["docs/**/*"]) and the paths/compare_to mapping; glob brace alternation ({a,b}) and character classes ([ab]) are rejected as GL_SOURCE_UNSUPPORTED.

Outputs​

ArtifactLocationDescription
Receipt.loom/.runtime/receipts/<timestamp>-check-<pid>.jsonStructured result, selected source, safe GitLab plan facts, and errors
Schema errorsstderrHuman-readable error lines on validation failure

Exit codes​

CodeMeaning
0Loom source is valid, GitLab plan is executable, or GitLab workflow is skipped
1Source is invalid, GitLab plan is non-executable, or the command fails

Output streams​

All output goes to stderr — stdout is not used.

StreamContent
stderrloom check passed or loom check skipped, diagnostic lines, and receipt path
stdout(unused)

Example output​

Success​

loom check passed
receipt: .loom/.runtime/receipts/20260304T183012Z-check-48291.json

Failure​

workflow.yml:12 missing required key "stage" in job "build"
workflow.yml:18 expected sequence for "script", got string

receipt: .loom/.runtime/receipts/20260304T183012Z-check-48291.json
loom check failed (exit code 1)

GitLab failure​

.gitlab-ci.yml:43:13 GL_SOURCE_UNSUPPORTED: rule when is preserved but not executable yet [/deploy/rules/0/when]
hint: use when: on_success, never, manual or always, or omit when to keep the GitLab default

receipt: .loom/.runtime/receipts/20260304T183012Z-check-48291.json
loom check failed (exit code 1)

Blocking GitLab diagnostics print PATH:LINE:COL CODE: MESSAGE [pointer]: 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 are presentation only; the receipt records each diagnostic's code, pointer, location, and message unchanged.

When the plan is executable or skipped by workflow: rules, advisory GitLab diagnostics for the plan or automatically selected jobs print as warning: lines before the result. Every blocking finding remains visible on failure. Diagnostics that share a code, message, and source location print once with an eligible job count, for example (7 jobs) for a default: interruptible: true entry, so a default applied to every job takes one line, naming the first eligible job's pointer. The receipt retains all findings, including those for inactive jobs.

Receipt structure​

The receipt JSON captures the validation result so downstream tools can parse it programmatically.

{
"schema_version": "v1",
"kind": "loom-check",
"loom_version": "0.4.2",
"command": ["loom", "check", "--workflow", ".loom/workflow.yml"],
"started_at": "2026-03-04T18:30:12.104Z",
"finished_at": "2026-03-04T18:30:12.131Z",
"duration_ms": 27,
"duration_us": 27412,
"exit_code": 0,
"status": "success",
"workflow_schema_v1": {
"workflow_path": "/repo/.loom/workflow.yml",
"valid": true,
"errors": []
}
}

command is the invocation as typed, with the binary path replaced by loom, so a receipt can be replayed with the same format, workflow, and context flags. duration_ms and duration_us measure the same span from started_at to finished_at, which covers source resolution, planning, and the git lookup; duration_us is the precise value when a check finishes in under a millisecond.

GitLab receipts replace workflow_schema_v1 with source and gitlab_plan. The plan projection includes state, selection identity, selection records, requirements, diagnostics, and resolved_changes without scripts or resolved context values. Each resolved_changes entry records one git comparison as {base, head, paths, worktree}; the list is empty when every matched rules:changes used explicit context paths, and the selection identity covers the resolved paths so plans over different diffs never share an identity. rules:changes globs follow GitLab's fnmatch flags: * and ? never cross a slash, **/ recurses, a leading dot is an ordinary character, [abc] and [!abc] classes and {a,b} alternation (nested) are supported, and \ escapes the next character. Variables in changes.paths are still reported as unsupported. workflow_skipped records status skipped and exits 0. source_invalid and non_executable record status failure and exit nonzero.

GitLab validation reports missing runner tags, privileged services, hosted macOS, OIDC tokens, Pages, and environments as requirements. Check never grants privileged-service consent. Optional manual jobs remain visible but are not executed. Report processing and artifact expiry are reported limitations. A cache key, key.prefix, or fallback_keys entry that still holds a $ after pipeline variables are substituted (for example $$ for a literal $) fails with GL_VARIABLE_EXPANSION_UNSUPPORTED at that pointer, because the local runtime cannot expand it.

include: local: is resolved beneath the repository root as GitLab does: included files merge first, in order, and the including file merges last; mappings such as variables and default deep-merge while sequences and scalars are replaced by the later file. Diagnostics inside an included file name that file's path, line and column. Local wildcard paths, public HTTP(S) remote sources, file-scoped inputs and ordinary include if/when rules are supported. Include rules use explicit caller context before target expansion, reads or input binding; excluded sources contribute no jobs. project, template, component, include exists/changes and variable addresses remain unsupported. Selected local paths retain rooted containment and nesting/count/byte bounds. See includes for selection order, frontend null/false rules and anonymous remote context.

Hidden keys (names starting with .) are validated only through the jobs that extends them, matching GitLab. An unreferenced hidden key is ignored whatever its shape, so YAML anchor lists and other non-job values under hidden keys are accepted. Diagnostics for inherited fields point at the extending job (/job/<field>) and carry the template's source line. Extending a hidden key that is not a mapping is a GL_SOURCE_COMPOSITION error at the extends value.

Root-level image, before_script, after_script and cache are the deprecated GitLab aliases of the same keys under default:; Loom folds them into default: before defaults apply, so they behave exactly like default: entries. Defining the same key at both levels is a GL_SOURCE_COMPOSITION error at the root key. Root services is still reported as unsupported.

GitLab after_script compiles to finalization commands. Job values replace inherited commands; [] or null clears them. Like script, commands accept a string or nested string arrays up to ten array levels, preserving multiline bytes. References preserve their target shape until the consuming keyword normalizes it. A main script remains required: a blank scalar or [] is invalid, while [''] and [[]] are valid. The latter runs setup and finalization with zero main commands. See execution behavior.

Rule when follows GitLab's per-scope allowlists. workflow: rules: accept only always and never; any other value is a GL_SOURCE_TYPE error at the value. Job rules accept on_success, never, manual and always. A job selected through when: always runs like on_success, because a local run has no failed upstream job to run after, and the plan records the non-blocking GL_LIMIT_WHEN_ALWAYS_NOT_ENFORCED limitation at the rule's when. on_failure and delayed are reported as GL_SOURCE_UNSUPPORTED until their execution semantics exist.

Rule if uses the ordinary compile expression contract, including unary negation, scalar/null/boolean values, operand-valued AND/OR, reversed/grouped operands and literal or held RE2 patterns. GitLab if lookup preserves raw winning root/job/caller strings and present-empty values; it does not recursively expand aliases or pattern contents. Other field consumers keep their documented expansion phases.

Common schema errors​

ErrorSymptomFix
Missing required keysErrors about missing version, stages, job stage, target, or scriptCompare against Syntax (v1) and add the missing keys
Type mismatchexpected mapping / expected sequenceCheck YAML types — script must be a list of strings, stages must be a list
Invalid include pathErrors resolving include.local templatesEnsure includes live under .loom/templates/ with no .. traversal (see Syntax (v1) → include)
Cache shape errorsErrors about cache.paths, cache.key, or multi-cache fieldsCross-check Cache and Syntax (v1) → cache

When reporting a schema validation failure, share the receipt path and a small excerpt. See What to share.

  • loom compile — resolve includes/templates and inspect the compiled graph
  • loom run — execute the workflow after validation passes