ADR 011: Multi-File Output
Status: Partially Implemented Date: 2024-12-29 Updated: 2026-01-10
Implementation Status
{% file %}directive: Implemented. Supportspath,format, andpublisharguments. See architecture.md for usage details.- Static file mappings: Not yet implemented.
- Output plugin-generated files: Not yet implemented.
Context
Colin's current model is 1:1—one template produces one output file. However, several use cases require generating multiple outputs from a single template or copying static files:
- A template that generates multiple related files (e.g., a skill plus supporting scripts)
- Static assets that need to be copied to the target directory
- Output plugins that generate supporting files (e.g.,
__init__.pyalongside Python modules)
Decision
Multi-file output is supported through three mechanisms:
1. {% file %} Directive
A Jinja block directive that creates additional output files:
{% file "path/relative/to/output.json" %}
{
"name": "{{ name }}",
"generated": true
}
{% endfile %}
File blocks can contain any Jinja directives, including MCP resources:
{% file "tools/analyzer.py" %}
{{ colin.mcp.scripts.resource('scripts://analyzer') }}
{% endfile %}
2. Static File Mappings in colin.toml
For known static assets, define mappings in project configuration:
[[static]]
src = "resources/utils.py"
dest = "lib/utils.py"
[[static]]
src = "resources/utils.py" # Same source
dest = "backup/utils.py" # Different destination
[[static]]
src = "resources/templates/" # Directory
dest = "templates/"
Array-of-tables syntax ([[static]]) allows one source to be copied to multiple destinations.
3. Output Plugin-Generated Files
Output plugins can emit additional files beyond the primary document:
- A
skillplugin could generate__init__.pyalongside skill files - A
pythonplugin could generate type stubs - A plugin could copy required assets referenced in the document
This is already supported by OutputPlugin.emit() -> list[Path].
Architecture Changes
CompiledDocument model:
class CompiledDocument(BaseModel):
uri: str
output: str # Primary output (existing)
additional_outputs: dict[str, str] # path → content from {% file %} blocks
Compilation flow:
- Static files copied first (from
[[static]]config) - Templates compiled,
{% file %}blocks captured inadditional_outputs - Output plugins receive
CompiledDocument, write primary + additional outputs - Output plugins may generate their own additional files
Rationale
{% file %}for dynamic content: Templates can generate multiple related files with full Jinja power[[static]]for known assets: Configuration-driven, no template needed for static files- Output plugins for format-specific: Plugins know what supporting files their format needs
- Composable: These mechanisms work together—a skill template can use
{% file %}while the skill plugin adds__init__.py
Alternatives Considered
{% copy %}directive: Inline copying in templates- Problem: Static file mappings don't need template execution; config is cleaner
{source: dest}in config: Simple key-value mapping- Problem: One source can't map to multiple destinations
- Separate manifest file: List of all outputs per template
- Problem: Duplicates information, harder to maintain
Consequences
CompiledDocumentgainsadditional_outputsfield- New
{% file %}Jinja extension needed - Static file copying added to compilation pipeline
- Output plugins already support multi-file via
list[Path]return - Primary output remains at URI-derived path; additional outputs at specified paths