Skip to main content

Template Syntax

This page walks through ATL syntax the way you'll actually use it, building from simple substitution up to loops, whitespace control, and includes. The terse, complete version lives in the syntax reference.

Variables

Wrap a context variable in {{ }} and it's replaced with its value:

name = "{{ project_name }}"
authors = ["{{ author_full }}"]

Dotted access reaches into nested tables that scripts publish into the context — for example, structured data contributed by composed components:

version = "{{ components.xtask.version }}"

There's also optional chaining, ?., which yields nothing instead of raising an error when an intermediate value is missing:

{% if components?.license?.spdx %}
license = "{{ components.license.spdx }}"
{% endif %}

Keys that aren't valid identifiers work too — {{ project-name }} looks up the kebab-cased key exactly as written.

Filters

Filters transform a value. Chain them with |, left to right:

{{ project_name | pascal_case }}
{{ description | trim | truncate(72) }}

Every filter is also callable as a plain function — same implementation, two surface forms. Use whichever reads better:

{{ pascal_case(project_name) }}
{{ description | default(project_name) }}
{{ default(description, project_name) }}

The pipe form tends to win when the input dominates ({{ items | join(", ") }}); the function form when the arguments do. See Filters & Functions for a tour of the built-ins.

Conditionals

{% if use_github %}
[badges]
github = { repository = "{{ github_slug }}" }
{% endif %}

With alternatives:

{% if license == "MIT" %}
license = "MIT"
{% elseif license == "Apache-2.0" %}
license = "Apache-2.0"
{% else %}
license-file = "LICENSE"
{% endif %}

Conditions are ordinary boolean expressions. Comparison and logic operators follow Lua conventions — the statement bodies of ATL compile to Lua:

OperatorMeaning
==equal
~=not equal
and, or, notboolean logic
<, <=, >, >=comparison

A bare variable is truthy when it is set and not false, so {% if use_docker %} works exactly as you'd expect for a confirmation prompt. For membership tests, use the contains builtin:

{% if contains(features, "metrics") %}
metrics = { enabled = true }
{% endif %}

Blocks close with {% endif %} / {% endfor %}, or the shorter {% end %} — both are accepted; pick one style and stay consistent.

Loops

Iterate a list:

[workspace]
members = [
".",
{% for m in workspace_members %}
"{{ m }}",
{% endfor %}
]

Iterate with an index (1-based), or over a map's key/value pairs:

{% for i, step in enumerate(steps) %}
{{ i }}. {{ step }}
{% endfor %}

{% for alias, cmd in cargo_aliases %}
{{ alias }} = "{{ cmd }}"
{% endfor %}

Numeric ranges use range(n) (0 to n−1, exclusive upper bound) or range(start, stop):

{% for i in range(3) %}
worker-{{ i }}
{% endfor %}

Comments

{# ... #} is stripped entirely from output — notes to future maintainers of the archetype, invisible to its users:

{# The trailing comma matters: workspace_members can be empty. #}

Whitespace control

Block tags like {% if %} sit on their own lines, and by default those lines leave residue: the newline after the tag and the indentation before it both land in the output. For generated code, that residue matters.

Given this template:

dependencies = [
{% if use_serde %}
"serde",
{% endif %}
]

With default settings the rendered output keeps the tag lines' whitespace:

dependencies = [

"serde",

]

Two manifest settings fix this globally. In archetype.yaml:

templating:
trim_blocks: true # strip the newline right after a block tag
lstrip_blocks: true # strip indentation before a tag on its own line

Now the same template renders cleanly:

dependencies = [
"serde",
]

This is why real-world code-generating archetypes enable both — see Templating Configuration for the full rationale.

You can also control whitespace per-tag with a - just inside the delimiter: {{- value }} eats whitespace before the expression, {% if x -%} eats whitespace (through the newline) after the tag. These work regardless of the manifest settings and are handy for one-off tight spots:

Hello {{- " " -}} world

Escaping literal delimiters

If a generated file must itself contain {{ or {% (a CI config with its own templating, or a meta-archetype that generates archetypes), you have two tools. For a block of content, {% raw %}:

{% raw %}
image: {{ .Values.image }} # emitted verbatim, not rendered
{% endraw %}

For inline spots, the built-in escape constants LEFT_EXPR/RIGHT_EXPR and LEFT_STMT/RIGHT_STMT (short aliases LE/RE/LS/RS) expand to the literal delimiter text:

{{ LE }} project_name {{ RE }}

renders as {{ project_name }}.

Includes

{% include "path.atl" %} splices another template file's rendered body in place. Paths resolve against your archetype's includes/ directory (and, when you compose libraries, against their shared partials — see Organizing Templates):

{% include "license-header.atl" %}

pub struct {{ ProjectName }} {
// ...
}

Included fragments see the same flat context as the including template, and includes can nest (cycles are detected and reported as errors). Paths must stay inside the includes tree — absolute paths and .. segments are rejected.