| name | jinja-expert |
| description | Author, read, and debug Jinja2 templates across the three places Jinja lives in 2026 — HuggingFace `chat_template.jinja` (rendered by `apply_chat_template` for vLLM / sglang), Ansible playbooks + `.j2` files, and Jinja-adjacent Kubernetes workflows (`values.yaml.j2`, `kubernetes.core.k8s + template`, Helm post-renderers). Companion to the `helm` skill — Helm charts are Go `text/template` + Sprig, not Jinja, and this skill makes that disambiguation explicit. |
| when_to_use | Triggers on the phrases "chat template", "chat_template.jinja", "j2 file", ".j2", "jinja filter", "jinja2 filter", "ansible template", "ansible jinja", "ansible playbook template", "ansible .j2", "values.yaml jinja", "jinja helm", "jinja2-cli", "helm-jinja", "kubernetes jinja", or any request to write / read / fix a Jinja template. Also triggers on symptoms — `TemplateError`, `SecurityError`, `AnsibleUndefinedVariable`, `add_generation_prompt` misbehavior, `tool_call.arguments` rendering wrong, `{% set %}` not persisting outside a `{% for %}` loop (the namespace/loop-scope trap), double-templating via `!unsafe` / `{% raw %}`, whitespace drift shifting tokenization, content-format bifurcation (`message.content` list vs string), `when:` conditional delimiters, `hostvars`, `lookup('template', ...)`, `jinja2_native` types, `ansible_managed`, custom filter plugins, `kubernetes.core.k8s`, `community.kubernetes.helm`, "render a jinja template", "fix chat template", and Helm questions that turn out to be Go/Jinja mix-ups. |
Jinja Expert
Jinja2 shows up in three mostly-disjoint ecosystems in 2026. Each has its
own dialect — different sandbox, different extensions, different filters,
different failure modes. Using chat-template idioms in an Ansible playbook
will silently misbehave. Using Ansible !unsafe in a chat template is a
syntax error. This skill is the map.
Which dialect am I in?
Pick the row that matches the file under review. The rightmost column
points to the next file to read.
| Signal | Dialect | Go to |
|---|
File named chat_template.jinja; apply_chat_template, tokenizer_config.json, vLLM/sglang in play; messages/tools/add_generation_prompt in scope | HF chat template (sandboxed, loopcontrols, tojson ensure_ascii=False) | references/chat-template/ |
File ends in .j2; inside an Ansible playbook/role/collection; {{ ansible_managed }}, hostvars, lookup(...), ` | default(omit); when:` bare expressions | Ansible Jinja (recursive templating, native-types flag, !unsafe) |
values.yaml.j2 / deployment.yaml.j2 feeding Helm or kubectl; jinja2-cli; community.kubernetes.helm / kubernetes.core.k8s; ArgoCD config management plugin | Helm-adjacent Jinja (Jinja around Helm, not inside) | references/helm-and-jinja.md |
File inside a Helm chart (templates/*.yaml, _helpers.tpl, Chart.yaml); {{ .Values.x }}, {{ toYaml ... }}, {{ include "mychart.name" . }}, Sprig functions | Go text/template + Sprig — not Jinja | Reach for the helm skill instead |
Syntax looks like {{ }} but something else — .gotmpl, helmfile.yaml, Flask, Saltstack state, dbt model, Home Assistant config | Not covered here; use the core-language cheatsheet | references/language-surface.md + upstream docs |
When the dialect is uncertain, start at references/language-surface.md —
dialect-neutral, covers the statements, expressions, filters, tests,
globals, and whitespace rules shared by every Jinja deployment.
Quick decision guide (chat-template default)
Chat templates are the dialect most likely to bring someone here, so the
body below stays biased toward them. When the task is Ansible or
Helm-adjacent, branch to the matching reference and ignore the rest.
| Task | Go to |
|---|
| Look up a statement, filter, test, or global | references/language-surface.md |
Understand the exact env apply_chat_template builds | references/chat-template/transformers-dialect.md |
| Diagnose a specific category of chat-template bug | references/chat-template/seven-failure-categories.md |
| Write a new chat template (or patch an existing one) | references/chat-template/authoring-patterns.md |
| Find a "clean" chat template to study, or a broken one to learn from | references/chat-template/exemplars-and-antiexemplars.md |
| Debug a chat template or set up coverage/parity testing | references/chat-template/debugging-and-testing.md |
Write Ansible playbooks or .j2 files — filters, lookups, !unsafe, native types, k8s manifests | references/ansible-dialect.md |
Jinja around Helm — values.yaml.j2, raw manifests, post-renderers, the Helm-is-Go disambiguation | references/helm-and-jinja.md |
Author a Helm chart (Go templates, _helpers.tpl, Sprig) | cross to the helm skill |
The chat-template dialect at a glance
Every chat template in the HuggingFace ecosystem is rendered through an
ImmutableSandboxedEnvironment (trim_blocks=True, lstrip_blocks=True,
loopcontrols extension loaded, tojson filter forced to
ensure_ascii=False, raise_exception + strftime_now globals). vLLM
and sglang both delegate to tokenizer.apply_chat_template, so the
sandbox is the whole surface. .append / .pop / .update raise
SecurityError; ext.do / ext.i18n / ext.debug are NOT loaded.
Variables in scope: messages, tools, documents,
add_generation_prompt, all special_tokens_map entries, date_string,
plus caller chat_template_kwargs.
Full env config, sandbox rules, and canonical skeleton →
references/chat-template/transformers-dialect.md (env) and
references/chat-template/authoring-patterns.md (skeleton).
The seven chat-template failure categories
Roughly 90% of real chat-template bugs. Templates written against these
defensive idioms survive the next model release.
| # | Category | Symptom | Defensive idiom |
|---|
| 1 | Content-format bifurcation | Template assumes content is a string; caller passes a list of parts. Python repr of the list lands in the prompt. | Branch is string → else is sequence and is not string → else is mapping → catch-all. Never isinstance. |
| 2 | Loop-scope escape | {% set flag = true %} inside {% for %} doesn't persist after the loop. | {% set ns = namespace(flag=false) %} then {% set ns.flag = true %}. |
| 3 | add_generation_prompt bimodal | Template hardcodes the assistant primer, breaking training; or reads the flag via **kwargs, so vLLM's AST detection can't see it. | Explicit {% if add_generation_prompt %} at the tail; render both modes in tests and diff. |
| 4 | Tool-call shape variance | tool_call.function.arguments is sometimes a JSON string, sometimes a dict. tool_call.id is optional. Unguarded access crashes. | `{{ args if args is string else args |
| 5 | Whitespace control | Stray \n before `< | im_start |
| 6 | Special-token doubling | BOS emitted by tokenizer (add_bos_token=True) AND by template ({{ bos_token }}) → double BOS. | Emit BOS in exactly one place. Check tokenizer_config.json before adding {{ bos_token }}. |
| 7 | Sandbox mutation attempts | .append, .pop, .update raise SecurityError. {% do x.append(...) %} doesn't parse (ext.do not loaded). | namespace() for accumulators; filter chains (selectattr, map, join) for transformations. |
Full examples, before/after code, canonical skeleton, and real-world
incidents → references/chat-template/seven-failure-categories.md +
references/chat-template/authoring-patterns.md.
The Ansible dialect at a glance
Ansible is the largest Jinja-in-production deployment outside web frameworks.
It adds a long list of things to stock Jinja — recursive templating, lazy
variable resolution, the !unsafe tag, native-types mode, hostvars,
conditional expressions without delimiters (when:, failed_when:,
changed_when:, until:), custom filter/test/lookup plugins, and a 22-level
variable precedence system. None of it lives in Pallets Jinja.
Four rules carry most of the distance:
when: takes a bare expression, not a Jinja template. when: "{{ foo }}" is always wrong; write when: foo. Same for failed_when:,
changed_when:, until:.
!unsafe is all-or-nothing. It disables templating for the whole
value — including lookups. For mixed literal + templated content, use
{% raw %}...{% endraw %}.
- Recursive templating amplifies errors. A variable whose value is
itself Jinja-looking renders again. An undefined reference in
round-two produces an error message that mentions neither round.
| default(omit) removes a task parameter entirely. The idiomatic
way to make a parameter conditional without duplicating task
definitions.
Everything else (filters, lookups, kubernetes.core.k8s + template,
custom plugins, ansible_managed, debugging with ansible-playbook --check --diff, j2lint, ten gotchas) → references/ansible-dialect.md.
Helm and Jinja — what really runs where
Helm charts do not use Jinja. They use Go's text/template + Sprig. Any
{{ ... }} inside a chart is Go syntax. The two languages look identical
for the first ten seconds and diverge hard after that — . semantics,
pipe argument shape (default 3 vs default(3)), Sprig's toYaml /
required / lookup, range rebinding. Chart authoring is owned by the
helm skill, not this one.
Where Jinja does show up in Kubernetes workflows:
- Ansible wraps Helm —
values.yaml.j2 rendered by Ansible,
installed via community.kubernetes.helm. Most common pattern.
jinja2-cli preprocessor — same shape without Ansible. Makefile
or CI step renders values.yaml.j2 → values.yaml, then
helm upgrade --install.
- Raw Jinja manifests, no Helm — small apps or air-gapped sites.
Ansible
kubernetes.core.k8s with template: path/to.j2.
- Helm post-renderer chained with Jinja — niche; kustomize is
usually the better fit here.
- kluctl — actually Jinja-native for Kubernetes GitOps. One of the
few 2026 tools that isn't Go underneath.
helm-jinja plugins — historically tried to swap Helm's engine;
never thrived. Don't adopt in 2026. Use patterns 1/2/3 instead.
Full patterns, decision matrix, and the ten Go-vs-Jinja gotchas (default
arity, . scope, toYaml, required vs mandatory, lookup, CRD
ordering, genCA / genSignedCert, whitespace trim, double-templating)
→ references/helm-and-jinja.md.
Out of scope
Generic Flask web templating, custom ext.Extension authoring, async Jinja
(enable_async), bytecode caching, custom loaders, and Helm chart authoring
itself (Go text/template + Sprig — use the helm skill). For anything
in this list, the Pallets docs at https://jinja.palletsprojects.com/ are
authoritative for the core language.