Skip to main content
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:
models/status.md
---
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:
models/team-roster.md
---
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:
models/summary.md
---
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 %}:
models/plan.md
---
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():
models/summary.md
---
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:
models/config.json.md
---
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:
  1. First pass: Normal rendering that excludes defer blocks
  2. 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:
models/guide.md
{% 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:
  1. Colin compiles a document
  2. A user manually edits the published output
  3. 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:
models/analysis.md
---
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:
models/generator.md
---
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:
ArgumentRequiredDefaultDescription
pathYesRelative path for the output file
formatNomarkdownRenderer to apply (markdown, json, yaml)
publishNoinheritWhether 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 %}

Format Rendering

File blocks support the same renderers as document output. When you specify a format, the content goes through that renderer’s transformation pipeline:
models/data-generator.md
---
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:
output/users.json
[
  {"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:
models/_generator.md
---
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:
models/report.md
---
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:
models/consumer.md
---
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:
models/analysis.md
---
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.