From agenticat
Askama Rust templating (0.16): syntax, filters, inheritance, macros, web-framework integration via askama_web, rinja migration. Use when writing or debugging Askama templates or upgrading from older askama/rinja.
How this skill is triggered — by the user, by Claude, or both
Slash command
/agenticat:askamaThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> _Captured 2026-07-10 (askama 0.16.0). To update: re-verify against the askama book + docs.rs/askama, then diff for changes._
Captured 2026-07-10 (askama 0.16.0). To update: re-verify against the askama book + docs.rs/askama, then diff for changes.
Askama is a compile-time, Jinja-like template engine for Rust. Templates are parsed at build time and generate type-checked Rust code. This skill targets the unified askama crate, version 0.16.x (MSRV 1.88, raised in 0.15.0 from 1.83; was 1.81 through 0.13.x, then 1.83 in 0.14.0). Anything older than 0.13 wired web frameworks through dedicated askama_* crates that no longer exist; see the breaking-changes section if migrating.
Quick history: Askama was forked into
rinjain 2024, then the two projects re-unified asaskamain early 2025. If the user mentionsrinjaorrinja_axum, those are deprecated. Migrate toaskama/askama_web.
[dependencies]
askama = "0.16"
# For web frameworks, ADD askama_web separately (see §3):
askama_web = { version = "0.16", features = ["axum-0.8"] }
Do not depend on askama_axum, askama_actix, askama_warp, or askama_rocket (deprecated when 0.13 unified them), nor askama_gotham / askama_tide (never deprecated; just abandoned since 2023 on askama ^0.12). All dead now. There is no askama_actix_web crate.
templates/hello.html:
Hello, {{ name }}!
use askama::Template;
#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> { name: &'a str }
HelloTemplate { name: "world" }.render()? // -> Result<String, askama::Error>
Templates live in templates/ at the crate root (next to Cargo.toml) by default. Override via askama.toml (see §11).
Prefer these over .to_string() or format!(), which route through a vtable at runtime and run 100-200% slower:
| Method | Output | When to use |
|---|---|---|
tmpl.render() | Result<String, askama::Error> | Default. Allocates a new String. |
tmpl.render_into(&mut writer) | writes into impl fmt::Write | Reusing a buffer, nested rendering. |
tmpl.write_into(&mut writer) | writes into impl io::Write | Writing directly to a socket / file. |
The Template trait exposes render_with_values(&dyn Values) for runtime-injected values that aren't on the struct.
The derive auto-computes a SIZE_HINT const from the template's static text; render() / render_with_values preallocate their String with it (no effect on the caller-owned writers of render_into / write_into). There is no size_hint attribute; #[template(size_hint = ...)] is a compile error (unsupported template attribute). Override the hint only by hand-implementing Template with a custom const SIZE_HINT.
Removed in 0.13+: Template::EXTENSION and Template::MIME_TYPE associated fields no longer exist. Do not reference them.
Two paths. Pick one.
Call .render() and wrap the string yourself.
// axum
use axum::response::{Html, IntoResponse};
async fn index() -> impl IntoResponse {
Html(IndexTemplate { title: "Home" }.render().unwrap())
}
For proper error handling, return Result<Html<String>, AppError> where AppError: IntoResponse and AppError::from(askama::Error) exists.
askama_web with WebTemplateOne derive implements the response trait for whichever feature flag you turned on (see the list below).
askama_web = { version = "0.16", features = ["axum-0.8"] }
use askama::Template;
use askama_web::WebTemplate;
#[derive(Template, WebTemplate)]
#[template(path = "hello.html")]
struct HelloTemplate { name: String }
async fn hello() -> HelloTemplate {
HelloTemplate { name: "world".into() }
}
Returns 200 OK, Content-Type: text/html; charset=utf-8. Render errors become 500.
Per-target feature flags for askama_web:
axum-0.8 / axum-0.7 / axum-core-0.5 / axum-core-0.4actix-web-4rocket-0.5warp-0.4 / warp-0.3poem-3trillium-0.2cot-0.3 / cot-0.4 / cot-0.5 / cot-0.6 / cot_core-0.6derive (WebTemplate derive, on by default) and eprintln. Standalone logging-backend flags log-0.4 / tracing-0.1 (one of each, shared across all integrations)#[template(...)] Attribute Options| Key | Example | Purpose |
|---|---|---|
path | path = "hello.html" | File in templates dir. Extension drives escaping. |
source | source = "Hi {{ name }}" | Inline template; requires ext. |
ext | ext = "html" | Content-type hint; "jinja" / "jinja2" aliases for editor syntax highlighting. |
escape | escape = "none" or "html" | Override auto-escape. |
whitespace | whitespace = "suppress" | Per-template WS mode ("preserve" / "suppress" / "minimize"); overrides askama.toml. |
syntax | syntax = "foo" | Use a custom syntax defined in askama.toml. |
config | config = "config.toml" | Override config path (default askama.toml). |
print | print = "code" / "ast" / "all" / "none" | Compile-time debug: prints generated code to stdout. |
block | block = "hello" | Render just one block. Fields outside that block aren't required. Good for HTMX fragments. |
blocks | blocks = ["title", "content"] | Generates as_<block>() accessor methods on the struct, one per block. |
in_doc | in_doc = true | Source is in the struct's doc comment (inside an ```askama fenced block). Requires ext. |
askama | askama = $crate::__askama | Override the askama crate path. Needed when re-exporting from a macro-defining crate. |
Cannot combine path and source.
{{ MyStruct { field1: 1, field2: "x" }.to_string() }}{{ (1, 2) }}, array: {{ [1, 2, 3] }}, array-repeat (0.15): {{ [0; 4] }}.to_string() (0.15): {{ Point { x: 1, y: 2 } }}{{ name }}: field on template struct{{ user.name }}: dotted path (fields or methods){{ crate::MAX_USERS }}: use constants from your crate| -> bitor& -> bitand^ -> xor{% if my_bitset bitand 1 != 0 %}set!{% endif %}~: {{ a ~ b ~ c }} is shorthand for {{ a }}{{ b }}{{ c }}. Must have spaces around it to disambiguate from whitespace control.as cast: {{ x as i64 }}. Only primitive types. Automatically derefs &&&bool etc.{{ method() }} -> self.method() (method on the template struct){{ self::function() }} -> free function in the current module{{ super::b::f() }} -> function in another module{{ (closure)(12) }} -> calling a closure stored in a field; parens required{{ some_macro!(field) }} -> Rust macro. Askama won't infer field references inside macro args. Pass them explicitly, or the macro sees the literal token instead.{% if a %}…{% else if b %}…{% else %}…{% endif %}
{% for user in users %}<li>{{ user.name }}</li>{% endfor %}
{% for user in users if user.active %}{% for u in users %}...{% else %}No one here.{% endfor %}
loop.index (1-based)loop.index0 (0-based)loop.firstloop.lastloop.cycle([...]) (method call, array-literal arg): {{ loop.cycle(["a", "b"]) }} cycles per iterationloop.length, loop.revindex, loop.changed, loop.previtem, loop.nextitem. If you need these, compute in Rust or use modulo on loop.index.{% match item %}
{% when Some with ("foo") %}
Found literal foo
{% when Some with (val) %}
Found {{ val }}
{% when None %}
Nothing
{% endmatch %}
{% when Variant with (a, b) %}{% when Variant { field } %} or {% when Variant { field: val } %} to rename (with-less form since 0.11; 0.8 through 0.10 required the with keyword: {% when Variant with { field } %}){% when Self::Circle { radius } %}. #[derive(Template)] works on the enum itself, giving each variant its own #[template(...)].{% when [first, ..] %}{% when _ %} or {% else %}{% when 3 %}{% let name = user.name %} {# immutable binding #}
{% let mut it = xs.iter() %} {# mutable binding #}
{% set x = 4 %} {# alias for let (Jinja compat) #}
{% decl val %} {# declare WITHOUT value; let/set can't since 0.16 #}
{% if cond %}
{% let val = "a" %}
{% else %}
{% let val = "b" %}
{% endif %}
{{ val }}
decl (alias declare) is the only valueless declaration since 0.16. Bare {% let x %} / {% set x %} with no = now starts a let/set block instead (below).{% let heading %}{{ title }} on {{ site }}{% endlet %}
{{ heading }}
{% mut %} tag (0.16), not bare let: {% mut counter += i %}. Every Rust compound operator (+=, -=, *=, …) works; the target must be a mut binding.__askama, name one caller (reserved since 0.15), or use Rust keywords.Apply one or more filters to a whole block:
{% filter lower | capitalize %}
{{ t }} / HELLO / {{ u }}
{% endfilter %}
| Operator | Name | Effect |
|---|---|---|
- | suppress | Remove all whitespace on that side |
~ | minimize | Collapse to a single space/newline |
+ | preserve | Keep whitespace as-is (overrides config) |
Usage: put right after {%/{{ or right before %}/}}.
<div>
{%- if x %} {# strip before this tag #}
<p>{{ x }}</p>
{%- endif %}
</div>
{% for x in xs ~%} {# minimize trailing whitespace #}
{{ x }}
{%~ endfor %}
Global default comes from askama.toml's whitespace = "preserve" | "suppress" | "minimize". Inline operators override globals. When two inline controls point at the same span, Suppress always wins; between Minimize and Preserve, whichever operator sits on the tag AFTER the gap wins (positional, not a fixed priority). The book's flat Suppress > Minimize > Preserve table is wrong vs 0.16.0 for the Minimize/Preserve case.
Whitespace-control operators (-/+/~) on {% extends %} are accepted but a no-op, not a compile error. The child's top-level content is dropped anyway, so a trim has nothing to act on. (The book still describes this as "rejected"; that's stale vs 0.16.0.)
Nested block comments are supported:
{# outer {# nested #} still in outer #}
base.html:
<title>{% block title %}{{ title }} · Site{% endblock %}</title>
<main>{% block content %}<p>default</p>{% endblock %}</main>
page.html:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
{{ super() }} {# inject the parent block's contents #}
<h1>Hello!</h1>
{% endblock %}
Rules:
if/else/for: the book forbids this, but 0.16.0 compiles it and inheritance overrides correctly (verified empirically). Top-level or block-nested stays the safe form.block / blocks (see §4): block = "content" renders one block (fields outside it not required, good for HTMX partials); blocks = ["title", "content"] generates as_<block>() accessor methods on the struct itself, e.g. page.as_title().render() / page.as_content().render() (no new struct types).
{% include %}{% for item in items %}
{% include "item.html" %}
{% endfor %}
Define with {% macro name(args) %}...{% endmacro %}, call with {% call name(args) %}:
{% macro heading(arg) %}
<h1>{{ arg }}</h1>
{% endmacro %}
{% call heading("Title") %}
{{ heading("Title") }} (named args allowed too). Only works for macros that don't require a caller body.{% endmacro heading %}{% macro h(arg, bold) %}<h1>{{arg}}<b>{{bold}}</b></h1>{% endmacro %}
{% call h(bold="x", arg="y") %}
{%- import "macros.html" as m -%}
{% call m::heading("hi") %}
{% macro card() %}
<div class="card">{{ caller() }}</div>
{% endmacro %}
{% call card() %}
<p>Body passed into the macro.</p>
{% endcall %}
caller() inside the macro renders the block passed to {% call %}...{% endcall %}. To pass arguments, the call-block declares them up front: {% call(user) dump_users(list) %}...{% endcall %} makes the macro's caller(user) render the body once per invocation. Guard an optional body with {% if caller is defined %}. caller is a reserved variable name since 0.15.
Embed a Template-derived type as a field; it auto-renders via its Display impl:
#[derive(Template)]
#[template(source = "Section 1: {{ s1 }}", ext = "txt")]
struct Outer { s1: Inner }
#[derive(Template)]
#[template(source = "A={{ a }}", ext = "txt")]
struct Inner { a: String }
Self-include does NOT work. Do recursion with an explicit .render() call:
#[derive(Template)]
#[template(source = r#"
{{ name }} {
{% for child in children %}
{{ child.render()? }}
{% endfor %}
}
"#, ext = "txt", escape = "none")]
struct Item<'a> {
name: &'a str,
children: &'a [Item<'a>],
}
(Prefer a custom iterator + plain loop when possible.)
askama.tomlPlace at crate root (next to Cargo.toml). All sections are optional.
[general]
dirs = ["templates"] # default; globs OK (0.16): ["templates/*"], ["templates/**"]
whitespace = "preserve" # or "suppress" | "minimize"
# Custom delimiters (e.g. to avoid collisions with LaTeX or Vue)
[[syntax]]
name = "vue"
block_start = "[%"
block_end = "%]"
expr_start = "[["
expr_end = "]]"
comment_start = "[#"
comment_end = "#]"
# Custom escaper: apply askama::filters::Text (no escaping) to .js files
[[escaper]]
path = "askama::filters::Text"
extensions = ["js"]
Use a custom syntax per-template with #[template(path = "...", syntax = "vue")].
Default escaping extensions:
askama, html, htm, j2, jinja, jinja2, rinja, svg, xmlmd, yml, none, txt (and the empty extension)Escape modes override via #[template(escape = "html")] or escape = "none".
Askama escapes <, >, &, ", ' per OWASP recommendations when the extension implies HTML.
Per-expression bypass:
{{ value | safe }}: don't escape this value{{ value | escape }} or {{ value | e }}: force escape in an unescaped contextAll filters use value | filter_name(args). Named arguments are supported: {{ count | pluralize(plural = "gies") }}.
| Filter | Purpose |
|---|---|
capitalize | First char upper, rest lower |
center | Center in a field of given width |
escape / e | Force HTML escape |
filesizeformat | Bytes -> "1.4 KB" |
fmt(fmtstr) | Apply Rust format string |
format(fmtstr, ...) | Like format!() |
indent(n) / indent(prefix) | Indent each line |
join(sep) | Join iterable into string |
linebreaks | Plain text -> <p>/<br> |
linebreaksbr | Newlines -> <br> |
lower / lowercase | Lowercase |
paragraphbreaks | Only \n\n -> <p> |
pluralize(singular="", plural="s") | Suffix based on ±1 |
reject / reject_with | Filter iterator |
safe | Mark as HTML-safe |
title / titlecase | Title Case |
trim | Strip whitespace |
truncate(n) | Truncate with "…" |
unique | Dedup iterator (requires std) |
upper / uppercase | Uppercase |
wordcount | Count words |
assigned_or(fallback) | Fallback if the value equals its type default (0.15) |
defined_or(fallback) | Fallback if the identifier is undefined; LHS must be an identifier (0.15) |
default(val[, bool]) | Jinja-compat: acts as defined_or, or as assigned_or when 2nd arg is true. Prefer the two above (0.15) |
| Filter | Feature flag |
|---|---|
json (indent arg selects pretty: `{{ value | json(2) }}`) |
urlencode (does NOT encode /) | urlencode |
urlencode_strict (encodes /) | urlencode |
Add to Cargo.toml: askama = { version = "0.16", features = ["serde_json", "urlencode"] }
humansize: always available now, no flag neededmarkdown: removed, use comrak directlyserde-yaml: removed, use yaml-rust2 directlyserde-json renamed to serde_jsonSince 0.15 a custom filter is a plain fn annotated with #[askama::filter_fn]. Put it in a module named filters in scope of the #[derive(Template)] struct, or call it via an explicit path ({{ x | mymod::myfilter }}).
Mandatory signature: first arg is the piped-in value (any type; impl Display or a generic bound is typical, accepting owned and borrowed), second is &dyn askama::Values (askama's runtime-values env). Returns askama::Result<T>. In a chain only the last filter's T must be Display; earlier ones may return any T.
use askama::Template;
#[derive(Template)]
#[template(source = "{{ s | shout }}", ext = "txt")]
struct Msg<'a> { s: &'a str }
mod filters {
#[askama::filter_fn]
pub fn shout<T: std::fmt::Display>(
s: T,
_env: &dyn askama::Values,
) -> askama::Result<String> {
Ok(s.to_string().to_uppercase() + "!")
}
}
Extra arguments come after the two mandatory ones:
fn repeat(s: impl Display, _: &dyn Values, n: usize) invoked as {{ s | repeat(4) }}.#[optional(...)] attribute:
#[askama::filter_fn]
pub fn f(
value: impl Display,
_env: &dyn askama::Values,
#[optional(None)] a: Option<&str>, // omitted -> None
#[optional("hi")] b: &str, // omitted -> "hi"
) -> askama::Result<String> { /* ... */ }
{{ x | f(b = "yo") }}. Lifetimes on the fn are allowed since 0.15.1, where bounds since 0.15.2.Notes:
{{ x | filters::myfilter }} if you shadow one.#[askama::filter_fn]; it's what unlocked the named/optional args the old form couldn't express.askama_axum / askama_actix / etc. are gone — use #[derive(WebTemplate)] (askama_web) or manual .render() + Html(...).|, &, ^ are not bitwise in templates — use bitor, bitand, xor.{{ child.render()? }}.if/for: book forbids it, 0.16.0 compiles it (§9); top-level/block-nested is safe.{% extends %} is a silent no-op, not a compile error (§7).__askama, the reserved caller, or Rust keywords are banned.self infinite-loops via Display; don't write {{ self }}.{{ my_macro!(field) }} sees field, not self.field..render() / .render_into() over .to_string() (2-3x slower).~ is concat (with spaces) vs. whitespace-minimize (no spaces) — keep spaces around concat..html escaped, .txt not); inline source needs explicit ext / escape.rinja / rinja_axum / rinja_derive -> askama / askama_web (see intro).#[askama::filter_fn] since 0.15; a bare fn in mod filters no longer compiles (§14).{% let x %} / {% set x %} now opens a block (use {% decl x %}); compound assignment moved to {% mut %}; duplicate block names are now a hard compile error (was a warning).file:line:column location. Lifetime / ownership errors in generated code usually mean a field got moved. Take a reference, or use & in the template.Expressions: {{ expr }} Tags: {% stmt %} Comments: {# ... #}
WS control: {%- strip -%} {%~ min ~%} {%+ keep +%}
Inheritance: {% extends "base.html" %} {% block x %}...{% endblock %} {{ super() }}
Loops: {% for x in xs if cond %}...{% else %}...{% endfor %} loop.{index,index0,first,last,cycle([…])}
Conditionals: {% if %}...{% else if %}...{% else %}...{% endif %}
Match: {% match v %}{% when Some with (x) %}...{% when None %}...{% endmatch %}
Vars: {% let x = y %} {% let mut it = … %} {% set x = 1 %} {% decl x %} {% mut x += 1 %}
Let block: {% let s %}…rendered…{% endlet %} (captures block output into `s`)
Macros: {% macro m(a) %}...{% endmacro %} {% call m(a) %} expr-call: {{ m(a) }}
{% call(x) m(a) %}body{% endcall %} inside macro: {{ caller() }} / {{ caller(x) }}
Imports: {% import "m.html" as ns %} {% call ns::m(a) %}
Include: {% include "part.html" %}
Filters: {{ x | lower | trim }} block: {% filter lower %}...{% endfilter %}
Concat: {{ a ~ b ~ c }} (spaces required)
Cast: {{ x as i64 }} (primitives only)
Bitwise: a bitor b, a bitand b, a xor b (NOT |, &, ^)
Crate consts: {{ crate::MY_CONST }}
Struct ctor: {{ Foo { a: 1 }.to_string() }}
Escape: {{ x | safe }} {{ x | e }} or #[template(escape = "none"|"html")]
npx claudepluginhub uwuclxdy/agenticat --plugin skillsGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.