Open-Source Wikis

/

Ansible

/

Systems

/

Templating

ansible/ansible

Templating

Variable interpolation, conditional evaluation, and {{ ... }} rendering are powered by a hardened wrapper around upstream Jinja2. The new internal engine lives in lib/ansible/_internal/_templating/; the legacy public surface at lib/ansible/template/ still exists and is being phased out.

Files

File Role
lib/ansible/_internal/_templating/_engine.py TemplateEngine and TemplateOptions — primary entry point
lib/ansible/_internal/_templating/_jinja_bits.py AnsibleEnvironment, AnsibleTemplate, _TemplateCompileContext
lib/ansible/_internal/_templating/_jinja_common.py Shared _TemplateConfig, marker errors, call-context support
lib/ansible/_internal/_templating/_jinja_plugins.py Bridges Ansible filter/test/lookup plugins into Jinja
lib/ansible/_internal/_templating/_jinja_patches.py Jinja2 monkey-patches required for the trust model
lib/ansible/_internal/_templating/_marker_behaviors.py What to do when a name is undefined (FAIL_ON_UNDEFINED, etc.)
lib/ansible/_internal/_templating/_lazy_containers.py Lazy-rendering wrappers for dicts/lists
lib/ansible/_internal/_templating/_chain_templar.py Compatibility shim for legacy code calling Templar
lib/ansible/_internal/_templating/_transform.py Post-render type transforms (e.g., bool, list, dict coercion)
lib/ansible/_internal/_templating/_template_vars.py Variable scope helpers
lib/ansible/_internal/_templating/_datatag.py DeprecatedAccessAuditContext and tag-related helpers
lib/ansible/template/__init__.py Legacy Templar class (still public)
lib/ansible/_internal/_datatag/_tags.py Origin, TrustedAsTemplate, VaultedValue tag definitions

What's new in the trust model

Pre-2.18, every YAML scalar was a plain Python str. To template it, code called Templar.template(value), which looked for {{ / {% markers and ran them through Jinja. The trouble: a value coming back from a remote host (a fact, a registered task result, a piece of stdout) might contain literal {{ ... }} and accidentally be re-templated, with the variable scope of the controller. That's a server-side template injection.

The new model wraps every YAML scalar (and many other primitives) in an AnsibleTaggedObject carrying tags. Two tags matter for templating:

  • TrustedAsTemplate — set when the value originated from a YAML file the user authored (a playbook, a vars file, a role's defaults). Set by the YAML loader.
  • Origin — set everywhere; tracks file + line + column.

TemplateEngine.template() checks for TrustedAsTemplate before running Jinja. Strings without it pass through unchanged, even if they contain {{.

The check happens at the level of "this string", not "this object graph". A list or dict can contain a mixture of trusted and untrusted strings; only the trusted ones are templated.

The marker class TemplateTrustCheckFailedError (in lib/ansible/errors/__init__.py) is raised when something tries to bypass the check incorrectly.

TemplateEngine

TemplateEngine (_engine.py) is the new primary entry point. Construct it with a DataLoader, a variable scope, and optional TemplateOptions. Call template(value) to render.

Key surface area (visible at the top of _engine.py):

  • template(value, options) — render a tagged value.
  • template_object(graph, options) — recursively render a list/dict with mixed templates.
  • transform_to_native_type(value) — bool/int/float/list/dict coercion when _native_type is set.
  • is_template(value) — predicate.

The engine wraps a jinja2.Environment (custom subclass AnsibleEnvironment) with:

  • Ansible's filter, test, and lookup plugins bound (via _jinja_plugins.py).
  • A Origin-aware compile context that propagates source location through the template AST.
  • Custom marker behavior for Undefined, Omit, and the _legacy Jinja behavior switches.

How a value is rendered

graph TD
    VALUE[Value with tags] --> CHECK{TrustedAsTemplate?}
    CHECK -->|no| RETURN_AS_IS[Return value unchanged]
    CHECK -->|yes| HAS_MARKER{Contains {{ or {%?}
    HAS_MARKER -->|no| RETURN_TRIM[Return value as-is]
    HAS_MARKER -->|yes| COMPILE[Compile via AnsibleEnvironment]
    COMPILE --> EVAL[Render with current vars]
    EVAL --> TRANSFORM{Native type requested?}
    TRANSFORM -->|no| RETURN_STR[Return rendered string]
    TRANSFORM -->|yes| COERCE[Coerce via _transform.py]
    COERCE --> RETURN_TYPED[Return bool/int/list/dict]

The transform step (_transform.py) handles the case where a when: expression should return a bool, or a vars: value should be a real int. There's a chain limit (TRANSFORM_CHAIN_LIMIT = 10 in _engine.py) that prevents pathological recursive transforms.

Filter, test, and lookup integration

Ansible's filter, test, and lookup plugins aren't ordinary Jinja extensions — they're discovered via the plugin loader and dynamically bound into the template environment.

  • Filters: {{ x | ansible.builtin.to_yaml }} calls lib/ansible/plugins/filter/to_yaml.py. _jinja_plugins.py:_invoke_filter wraps the plugin call so undefined-handling and tag propagation work.
  • Tests: {{ x is defined }}, {{ x is succeeded }}. Implemented in lib/ansible/plugins/test/.
  • Lookups: {{ lookup('file', '/etc/hosts') }} and {{ q('file', '/etc/hosts') }} (the shorter alias q). The lookup is invoked via _invoke_lookup in _jinja_plugins.py, which routes through lookup_loader.

For collections, community.general.json_query etc. resolve through the same FQCN flow that the rest of the plugin loader uses.

Lazy containers

Templating an entire variable graph eagerly would be expensive — variables for one host can include thousands of facts. _lazy_containers.py defines lazy-rendering wrappers (_AnsibleLazyTemplateMixin) so a host_vars access doesn't render ansible_facts until something actually references it. The TQM hands these lazy containers off to workers.

Conditionals

Conditional (lib/ansible/playbook/conditional.py) wraps when: and failed_when: expressions. It strips the implicit {{ ... }} braces (these are bare expressions in YAML, not template strings) and runs them through TemplateEngine.template() with template_options.bare_jinja=True. The result is coerced to a bool, or AnsibleBrokenConditionalError is raised if it's not coercible.

This is why when: my_var works (truthy check), when: my_var == "expected" works (binary expression), and when: "{{ my_var }} == 'expected'" was deprecated long ago (you don't put template braces in when).

Origin propagation in errors

When a template error fires, the engine attempts to attach the underlying value's Origin to the exception. The result is errors like:

ERROR! the field 'name' has an invalid value (something), and could not be converted
to an str. The error was: ... encountered while templating ...
playbook.yml: line 5, column 7

The Origin tag is what makes the file:line precision possible. See lib/ansible/_internal/_errors/_handler.py.

Markers and the omit token

Omit (lib/ansible/_internal/_templating/_utils.py) is a sentinel that means "this argument should be removed entirely". Used in module argument processing:

- name: maybe-set-mode
  copy:
    src: foo
    dest: /tmp/foo
    mode: '{{ desired_mode | default(omit) }}'

If desired_mode is undefined, omit ends up in the rendered argument and the mode parameter is dropped before the module runs. Equivalent to "don't pass this arg".

Integration points

  • Imports from: ansible.parsing.dataloader, ansible.module_utils._internal._datatag, ansible._internal._datatag._tags, ansible.errors, ansible.utils.display, jinja2 (vendored-style; specific subset).
  • Imported by: every place that templates anything — the playbook base class, the executor, lookup plugins, action plugins, conditionals.
  • Patches Jinja2 through _jinja_patches.py to intercept undefined access and template compilation.

Entry points for modification

  • Adding a filter or test — write a plugin in lib/ansible/plugins/filter/ or lib/ansible/plugins/test/ (or in a collection). The auto-discovery picks it up.
  • Changing trust semantics — work in _jinja_bits.py:_TemplateCompileContext and lib/ansible/_internal/_datatag/_tags.py. This is delicate; the trust model is part of the supported security boundary.
  • Adding a marker behavior — extend _marker_behaviors.py:MarkerBehavior.
  • Tracking down a "template error" without a useful location — wire up additional Origin.propagate calls in the templating code, or add display.vvv calls in _jinja_plugins.py.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Templating – Ansible wiki | Factory