Colin documents support Jinja2 templating, giving you dynamic content through expressions, filters, control structures, and specialized blocks. Templating transforms static Markdown into compiled output that can reference other documents, invoke LLMs, and adapt based on context.
Jinja
Colin uses standard Jinja2 templating. This section covers the core features available in any Jinja environment.
Expressions
Double braces output values into your document:
---
name: Project Status
---
# Project Status
Last updated: {{ now().strftime('%Y-%m-%d') }}
Expressions can access variables, call functions, and use filters. The result replaces the expression in the compiled output.
Control Structures
Standard Jinja control flow works in documents. Use {% %} tags for logic:
---
name: Team Roster
---
# Team
{% for member in ['Alice', 'Bob', 'Charlie'] %}
- {{ member }}
{% endfor %}
Conditionals control what content appears:
{% if include_details %}
## Additional Details
This section only appears when include_details is truthy.
{% endif %}
Filters
Filters transform values using the pipe syntax. Apply them after any expression:
---
name: Summary
---
{{ ref('report').content | upper }}
{{ ref('data').content | length }} characters
Colin extends standard Jinja filters with LLM-powered transformations like extract() for intelligent content processing. See the LLM Provider documentation for details.
Section Blocks
Named sections mark parts of a document that other templates can reference independently.
Sections have the following properties:
- Duplicate names: If multiple sections share a name, the last one wins
- Empty sections: Sections can be empty, useful as placeholders
- Special characters: Use quoted strings for names with spaces, hyphens, or other characters
Creating Sections
Define a section with {% section %}:
---
name: Strategic Plan
---
{% section strategy %}
## Our Strategy
Focus on growth through innovation and customer success.
Target enterprise market with AI-powered solutions.
{% endsection %}
{% section "key metrics" %}
## Key Metrics
- Revenue: $1M ARR
- Customers: 50 enterprise
- Team size: 12
{% endsection %}
Section names support identifiers (strategy) or quoted strings ("key metrics") for names with spaces.
Accessing Sections
Access sections from other documents through ref():
---
name: Executive Summary
---
## Strategy Overview
{{ ref('plan.md').sections.strategy }}
## Performance
{{ ref('plan.md').sections['key metrics'] }}
Use dot access (.sections.name) for simple identifiers or dict access (.sections['name']) for any name including those with spaces.
Format-Aware Content
Sections adapt to your output format automatically:
- Markdown output: Returns raw string content
- JSON/YAML output: Parses markdown structure into data (headers become keys, lists become arrays)
For a document configured with JSON output:
---
colin:
output:
format: json
---
{% section database %}
## host
localhost
## port
5432
{% endsection %}
Accessing ref('config.json.md').sections.database returns:
{
"host": "localhost",
"port": 5432
}
Defer Blocks
Documents render sequentially from top to bottom, but often you’ll want to write content at the top that references the complete document, such as table of contents, executive summaries, desciprionts, and tags.
Colin introduces defer blocks to solve this. A defer block lets you access the entire rendered content of the document by deferring the block until the rest of the document is finished. When Colin detects a defer block, it renders the document in two passes:
- First pass: Normal rendering that excludes defer blocks
- Second pass: Renders only the defer blocks with access to the complete first-pass output
Each part of the document renders exactly once.
Accessing Content
Inside defer blocks, the rendered variable contains the full first-pass output of the document, including its sections:
rendered.content — the complete document output from the first pass
rendered.sections — format-aware access to named sections
Here’s how you might use a defer block to build a table of contents:
{% defer %}
## Table of Contents
{% for name in rendered.sections.keys() %}
- [{{ name | title }}](#{{ name }})
{% endfor %}
{% enddefer %}
{% section introduction %}
## Introduction
Welcome to the guide.
{% endsection %}
{% section concepts %}
## Core Concepts
The fundamental ideas behind the system.
{% endsection %}
Patterns
Variables from First Pass
Jinja variables defined outside defer blocks remain accessible inside them:
{% set title = "Strategic Plan" %}
{% defer %}
Title: {{ title }}
Word count: {{ rendered.content.split() | length }}
{% enddefer %}
Auto-Generated Frontmatter
Use a defer block to generate frontmatter values dynamically, based on the complete document content. This is especially useful when combined with Colin’s LLM provider to generate values based on the document content.
---
name: {% defer %}{{ rendered.content | llm_extract("A concise title") }}{% enddefer %}
tags: {% defer %}{% llm %}Suggest 3-5 tags: {{ rendered.content }}{% endllm %}{% enddefer %}
---
Table of Contents
Use a defer block to build a table of contents from sections or markdown headers:
{% defer %}
## Contents
{% for name in rendered.sections.keys() %}
- [{{ name | title }}](#{{ name | replace(' ', '-') }})
{% endfor %}
{% enddefer %}
Summaries
Combine a defer block with an LLM block to generate summaries based on complete content that appears later in the document:
{% defer %}
## Executive Summary
{% llm %}
Write a 2-sentence executive summary of this document:
{{ rendered.content }}
{% endllm %}
{% enddefer %}
Change Tracking
Use the output() function to compare against previous versions:
{% defer %}
{% set prev = output(cached=True) %}
{% if prev %}
**Changes since last compile:**
- Content: {{ rendered.content | length - prev.content | length }} characters difference
{% else %}
*First version of this document.*
{% endif %}
{% enddefer %}
This pattern is useful for documents that need to track their own evolution, like changelogs or audit trails.
Output Function
The output() function lets any template access its own previous output. This enables workflows where documents evolve over time rather than being regenerated from scratch.
{{ output() }} {# reads from published output (output/) #}
{{ output(cached=True) }} {# reads from Colin's artifact cache (.colin/compiled/) #}
Both variants return a RenderedOutput object with .content and .sections properties, or None if no previous output exists.
Published vs Cached
The default output() reads from the published output directory (output/). If someone manually edits the file after Colin compiles it, output() sees those changes. If the published file doesn’t exist (first compile, private doc, or deleted file), it falls back to the cached version.
The .content property reads from the file specified by the cached parameter, but .sections always reflects the section structure from the last compilation. Manual edits to section content won’t appear in .sections until the document is recompiled.
{% if output() %}
Current content: {{ output().content | length }} chars
{% endif %}
Use output(cached=True) to read exactly what Colin produced, ignoring any manual edits:
{% set cached = output(cached=True) %}
{% set published = output() %}
{% if cached and published and cached.content != published.content %}
Warning: Published file has been manually edited
{% endif %}
Edit-in-Place Workflow
The output() function enables collaborative editing where Colin and humans both contribute:
- Colin compiles a document
- A user manually edits the published output
- Colin recompiles—the template sees and preserves the manual edits
---
name: Evolving Document
---
{% set prev = output() %}
{% if prev %}
{{ prev.content }}
---
*Updated by Colin on {{ now().strftime('%Y-%m-%d') }}*
{% else %}
# Initial Version
This document will evolve over time.
{% endif %}
The output() function does not create a dependency that triggers recompilation. Reading your own output won’t cause infinite rebuild loops.
LLM Blocks
For complex LLM interactions, use {% llm %} blocks. The content inside becomes the prompt, and Colin replaces the block with the LLM’s response:
---
name: Competitive Analysis
---
# Analysis
{% llm %}
Compare these two products and identify key differentiators:
Product A: {{ ref('product-a').content }}
Product B: {{ ref('product-b').content }}
Focus on pricing, features, and target market.
{% endllm %}
LLM blocks can span multiple lines and include other template expressions. The entire block renders first (resolving refs and variables), then the rendered content goes to the LLM.
Override the default model on specific blocks:
{% llm model="anthropic:claude-sonnet-4-5" %}
Summarize this content for an executive audience.
{% endllm %}
See LLM Provider for complete documentation on instructions, structured output, caching, and cost management.
File Blocks
A single Colin document normally produces one output file. File blocks let you generate multiple outputs from one source—perfect for creating related files, generating content from data, or building auxiliary outputs alongside your main document.
Creating Files
Use {% file %} to create additional output files:
---
name: Config Generator
---
# Generator
{% file "config.json" format="json" %}
## database
localhost
## port
5432
{% endfile %}
Generator complete.
This produces two files:
output/generator.md — the main document output
output/config.json — the file block output
Arguments
File blocks accept three arguments that match the document-level output configuration:
| Argument | Required | Default | Description |
|---|
path | Yes | — | Relative path for the output file |
format | No | markdown | Renderer to apply (markdown, json, yaml) |
publish | No | inherit | Whether to copy to output/ |
{% file "data/users.json" format="json" publish=true %}
...content...
{% endfile %}
The path can be any Jinja expression, enabling dynamic file names:
{% for user in users %}
{% file "profiles/" ~ user.name ~ ".md" %}
# {{ user.name }}
{{ user.bio }}
{% endfile %}
{% endfor %}
File blocks support the same renderers as document output. When you specify a format, the content goes through that renderer’s transformation pipeline:
---
name: Data Generator
---
{% file "users.json" format="json" %}
{% for user in ["Alice", "Bob", "Charlie"] %}
{% item %}
## name
{{ user }}
## role
Engineer
{% enditem %}
{% endfor %}
{% endfile %}
This produces valid JSON with an array of user objects:
[
{"name": "Alice", "role": "Engineer"},
{"name": "Bob", "role": "Engineer"},
{"name": "Charlie", "role": "Engineer"}
]
Item blocks, section markers, and all other template features work inside file blocks just as they do in the main document.
Publish Control
By default, file outputs inherit their publish setting from the source document. Override this with the publish argument:
{# Public source document #}
{% file "public-output.json" format="json" %}
This file publishes to output/ (inherits from source)
{% endfile %}
{% file "private-cache.json" format="json" publish=false %}
This file stays in .colin/compiled/ only
{% endfile %}
Private generators can produce public outputs—useful when the generator logic shouldn’t appear in final output but the generated files should:
---
name: Private Generator
---
{# This document doesn't publish (underscore prefix) #}
{% file "public/config.json" format="json" publish=true %}
## setting
value
{% endfile %}
The generator document stays in .colin/compiled/ while the config file publishes to output/public/config.json.
Section Scoping
Sections defined inside file blocks are scoped to that file only—they don’t leak into the parent document:
---
name: Report
---
{% section summary %}
## Summary
Main report summary.
{% endsection %}
{% file "data.md" %}
{% section data_summary %}
## Data Summary
This section belongs to data.md only.
{% endsection %}
{% endfile %}
Access scoped sections through refs:
{{ ref("report.md").sections.summary }} {# Main doc section #}
{{ ref("data.md").sections.data_summary }} {# File block section #}
The parent document’s sections only contains summary. The file output’s sections contains data_summary.
Referencing File Outputs
Other documents can reference file block outputs using ref(). Since file outputs can’t be statically detected, use depends_on to ensure correct compilation order:
---
name: Consumer
colin:
depends_on:
- generator.md
---
Using the generated config:
{{ ref("config.json").content }}
The depends_on hint tells Colin to compile generator.md first, ensuring config.json exists before consumer.md tries to reference it.
Colin uses content-addressed staleness for file outputs. If the generator recompiles but produces identical file content, downstream documents that reference it won’t recompile unnecessarily.
Patterns
Generate Files from Data
Loop over data to create multiple related files:
models/skill-generator.md
---
name: Skill Generator
---
{% for tool in colin.mcp.server.tools() %}
{% file "skills/" ~ tool.name ~ ".md" %}
# {{ tool.name }}
{{ tool.description }}
## Parameters
{% for param in tool.inputSchema.properties.items() %}
- **{{ param[0] }}**: {{ param[1].description }}
{% endfor %}
{% endfile %}
{% endfor %}
This creates a separate skill file for each MCP tool.
Auxiliary Data Files
Keep structured data alongside human-readable output:
---
name: Market Analysis
---
# Market Analysis
{% llm %}
Analyze the competitive landscape for {{ ref("company.md").content }}
{% endllm %}
{% file "analysis-data.json" format="json" publish=false %}
## generated_at
{{ now().isoformat() }}
## source_company
{{ ref("company.md").name }}
## model
{{ colin.llm.default_model }}
{% endfile %}
The main analysis publishes as Markdown while the metadata stays in the build cache for debugging or downstream processing.
Configuration Generation
Generate configuration files from templates:
models/_config-builder.md
---
name: Config Builder
colin:
output:
publish: false
---
{% file "app.config.json" format="json" publish=true %}
## database
{{ vars.database_host }}
## port
{{ vars.database_port }}
## environment
{{ vars.environment }}
{% endfile %}
{% file "docker-compose.yml" format="yaml" publish=true %}
## services
### app
#### image
myapp:{{ vars.version }}
#### environment
##### DATABASE_HOST
{{ vars.database_host }}
##### DATABASE_PORT
{{ vars.database_port }}
{% endfile %}
A single source generates multiple configuration files in different formats.