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: .loom/templates/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: .loom/templates/common.yml
- local: .loom/templates/languages/node.yml
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 start with .loom/templates/ | Rejected otherwise |
Path must end with .yml or .yaml | Rejected otherwise |
Path must not contain .. | Path traversal is rejected |
| Cycles are detected | Error shows the full 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 -> .loom/templates/a.yml -> .loom/templates/b.yml -> .loom/templates/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:
.loom/
templates/
common.yml # shared defaults
languages/
node.yml # Node.js toolchain setup
go.yml # Go toolchain setup
jobs/
lint.yml # reusable lint job templates
test.yml # reusable test job templates
Name templates by what they do (languages/node.yml, jobs/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 path restrictions exist
include.local is restricted to .loom/templates/ and rejects .. path traversal to keep includes:
- Local and reviewable — all included files are checked into the repo under a known directory.
- Harder to smuggle — files from unrelated directories cannot be pulled in accidentally.
- Auditable — reviewers can inspect exactly what a workflow includes by looking at one directory.
To use a template from outside the repo, vendor it into .loom/templates/ 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 (wrong prefix, missing extension, 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