Includes and templates
Split large workflows into reusable pieces. Includes import template files into your workflow at parse time. Templates (extends) let jobs inherit configuration from hidden job definitions, reducing duplication across similar jobs.
include:
- local: ci/shared/common.yml
.node-base:
image: node:20
variables:
NODE_ENV: production
lint:
extends: .node-base
stage: ci
target: linux
script:
- npm run lint
Resolution pipeline
Loom resolves includes and templates in a fixed order before validation:
1. Parse main workflow YAML
2. Resolve includes (recursive, depth-first)
3. Merge included definitions into the root
4. Apply default configuration to all jobs
5. Resolve extends chains (template inheritance)
6. Validate the fully-resolved workflow
This means included files can define templates that jobs in the main file extend, and default values are applied before template merging so both parent and child jobs see defaults.
Includes (include.local)
Use include to import local YAML template files into your workflow.
Syntax
include:
- local: ci/shared/common.yml
- local: tooling/jobs/node.yaml
Rules
| Constraint | Enforcement |
|---|---|
include must be a YAML sequence | Schema error if not a sequence |
Each entry must be a mapping with only the key local | Unknown keys are rejected |
| Path must be normalized and relative to the repository root | Rejected before content is read |
Path must end with .yml or .yaml | Rejected otherwise |
Absolute paths and any .. segment are forbidden | Path traversal is rejected |
| Target must be a regular file beneath the rooted reader | Links, directories, and special files are checked safely |
| Cycles are detected | Error shows the full include chain |
Each compilation accepts at most 32 include edges, 128 include expansions, and 4 MiB of cumulative YAML input. The byte budget includes the root workflow and counts every repeated include occurrence. Limit errors show the repository-relative include chain.
Nested includes
Included files can themselves contain include entries. Loom resolves them recursively. Cycles are detected and rejected with a descriptive error showing the full chain:
resolve include cycle by removing one include edge:
.loom/workflow.yml -> ci/a.yml -> tooling/b.yaml -> ci/a.yml
Merge order
Included files are merged in declaration order: earlier includes first, then later includes, then the main workflow file. When the same key appears in multiple files:
- Scalars and sequences (strings, numbers, lists): the last definition wins.
- Mappings (like
variables): keys merge recursively — later files override specific keys while preserving others.
Recommended directory structure
Keep includes small, composable, and organized by intent:
ci/
shared/
common.yml # shared defaults
tooling/
jobs/
node.yaml # Node.js toolchain setup
go.yml # Go toolchain setup
.loom/
workflow.yml # root workflow
Name templates by what they do (tooling/jobs/node.yaml, ci/shared/lint.yml) rather than by team or project names. Prefer several small files over one large include — this makes templates easier to find, review, and compose.
Why rooted path checks exist
include.local accepts YAML files from any directory in the repository. Loom resolves and
opens every root and nested include through the repository's rooted reader. It rejects
absolute paths, traversal, escaping or dangling links, directories, special files, and
in-flight replacement outside the root before consuming their content. These checks keep includes:
- Local and reviewable — all included files remain inside the repository root.
- Harder to smuggle — files from unrelated directories cannot be pulled in accidentally.
- Auditable — diagnostics and cycle chains use repository-relative paths without exposing host paths.
To use a template from outside the repo, vendor it into the repository and review it like any other code change.
Templates (extends)
A job whose name starts with . is a template job. Template jobs are not executed directly — they provide reusable configuration via extends.
Syntax
.base:
image: loom:nix-local
variables:
PNPM_STORE_DIR: .pnpm-store
check:
extends: .base
stage: ci
target: linux
script:
- pnpm i --frozen-lockfile
- pnpm run check
Rules
| Constraint | Enforcement |
|---|---|
extends must be a single template job name | Lists/multiple inheritance not supported |
The parent name must start with . | Rejected otherwise |
| The parent must exist in the workflow | Error if template is not found |
| Cycles are detected | Error shows the full extends chain |
Templates can chain (.a extends .b extends .c) | Resolved recursively |
Merge semantics
When a child job extends a parent template, configuration is merged with these rules:
| Value type | Behavior | Example |
|---|---|---|
| Scalars (strings, numbers, booleans) | Child replaces parent | target: linux in child overrides parent |
| Sequences (lists) | Child replaces parent entirely | script: ["echo child"] replaces parent's script |
Mappings (like variables) | Keys merge recursively — child overrides, parent provides defaults | Child adds BAR; parent's FOO is preserved |
| Cache | Special merge — named caches merge by name, unnamed caches follow mapping/sequence rules | See Cache |
Example showing merge behavior:
.base:
variables:
FOO: "from-base"
BAR: "from-base"
script:
- echo "base"
child:
extends: .base
stage: ci
target: linux
variables:
BAR: "from-child"
NEW: "child-only"
script:
- echo "child"
Effective resolved configuration for child:
| Key | Resolved value | Why |
|---|---|---|
variables.FOO | "from-base" | Inherited from parent (mapping merge) |
variables.BAR | "from-child" | Child overrides parent (mapping merge) |
variables.NEW | "child-only" | Added by child |
script | ["echo child"] | Child replaces parent (sequence replacement) |
Template requirements
Template jobs (names starting with .) must include at least one of:
script— so they provide runnable commands.extends— so they chain to another template.
A template with only image and variables but neither script nor extends produces a schema error. Add a script to fix it.
Debugging resolution
Step 1: Validate structure
Run loom check to catch YAML and schema issues before resolution:
loom check
Common errors at this stage:
- Invalid
include.localpath (absolute, unnormalized, missing extension, or path traversal). - Missing template reference (
extendspoints to a name that doesn't exist). - Include cycle or extends cycle.
Step 2: Inspect the resolved workflow
Run loom compile to see the fully-resolved workflow after includes are merged, defaults are applied, and extends chains are resolved:
loom compile --workflow .loom/workflow.yml
This shows the effective configuration each job will run with. Use it to verify that template merging produced the expected result — especially for variables (mapping merge) and script (sequence replacement).
Step 3: Diagnose runtime failures
If the workflow resolves and validates but a job fails at runtime, the issue is in execution, not resolution. Use the diagnostics ladder:
- Diagnostics ladder — pointer-first failure triage.
- Common failures — frequent issues and fixes.
Planned
- Remote includes — load templates from catalogs or registries outside the local repo.
- Multi-template composition — extend from multiple templates with explicit conflict rules.
- Template parameters — pass arguments to templates for more flexible reuse.
- Template versioning — pin included templates to specific versions for reproducibility.
Next steps
- Syntax (v1) →
include— canonical schema reference for includes - Syntax (v1) →
extends— canonical schema reference for extends - Variables — how variables merge across defaults, templates, and jobs
- Cache — cache merge behavior with templates