Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Ansible By James Joyner IV · · 8 min read

Ansible Error Guide: 'recursive loop detected in template string' — Fix Self-Referential Vars

Quick answer

Fix Ansible's 'recursive loop detected in template string' error by breaking self-referential variables, renaming shadowed vars, and restructuring group_vars.

Part of the Ansible Playbook & Module Errors hub
  • #ansible
  • #automation
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

Ansible raises this when templating a variable eventually requires templating itself, creating a cycle Jinja2 cannot resolve:

fatal: [app01]: FAILED! => {"changed": false, "msg": "recursive loop detected in template string: {{ app_url }}"}

You will often see it in a variable definition where a name refers back to itself, directly or through another variable:

An unhandled exception occurred while templating '{{ base_path }}/{{ app_name }}'. Error was: recursive loop detected in template string

The task fails during variable resolution, before it does any work. The problem is not the host or the module — it is a self-referential variable graph that has no fixed point for Jinja2 to settle on.

Symptoms

  • A task or debug fails with recursive loop detected in template string naming a specific variable.
  • The variable references itself, e.g. path: "{{ path }}/bin".
  • Two variables reference each other, forming a cycle across group_vars/host_vars.
  • A default(...) filter references the same variable it is defaulting.
  • The error appears only after merging an inventory layer that redefines a name using its own prior value.

Common Root Causes

  • Direct self-referencelog_dir: "{{ log_dir }}/app"; the right-hand log_dir re-triggers templating of the same variable.
  • Mutual referencea: "{{ b }}" and b: "{{ a }}" across files.
  • Shadowing while extending — trying to “append” to a variable by redefining it in terms of itself in a higher-precedence layer.
  • Self-referential defaultport: "{{ port | default(port) }}".
  • Loop var reusing the outer name — a loop_var or set_fact that reuses a name it reads from.
  • Registered var re-templated — feeding a variable’s templated output back into a definition of the same variable.

Diagnostic Workflow

Find where the offending variable is defined across all precedence layers:

grep -rn "app_url" inventory/ group_vars/ host_vars/ roles/*/defaults/ roles/*/vars/

Trace the resolved value (or the failure) with a targeted debug and a verbose run:

ansible app01 -i inventory -m debug -a "var=app_url" -vvv

Break the cycle by introducing a distinct base variable that the derived one references — never itself:

# BROKEN: self-referential
# app_url: "{{ app_url }}/health"

# FIXED: derive from a separate base variable
app_base_url: "https://{{ app_host }}:{{ app_port }}"
app_health_url: "{{ app_base_url }}/health"     # references a different name, no cycle

For a value you meant to extend across layers, rename rather than redefine in terms of itself:

# group_vars/all.yml
base_flags: "--verbose"

# group_vars/prod.yml
extra_flags: "--metrics"
run_flags: "{{ base_flags }} {{ extra_flags }}"   # composed from two distinct names

Example Root Cause Analysis

A play failed with recursive loop detected in template string: {{ java_opts }}. The role’s defaults/main.yml set java_opts: "-Xmx512m", and a group_vars/prod.yml file tried to add a flag with java_opts: "{{ java_opts }} -XX:+UseG1GC". Because group_vars outranks role defaults, the higher-precedence java_opts referenced a java_opts that Ansible resolved to the same higher-precedence definition — a cycle with no base value to anchor it.

The fix separated the base from the extension: the role kept java_opts_default: "-Xmx512m", and group_vars/prod.yml set java_opts: "{{ java_opts_default }} -XX:+UseG1GC". Now the composed variable references a different name, the graph is acyclic, and each environment can extend the base without redefining a name in terms of itself. The tell for this whole class of bug is a variable whose right-hand side mentions its own name.

Prevention Best Practices

  • Never define a variable in terms of itself; derive from a separately named base variable instead.
  • Keep a clear base-plus-derived naming convention (x_basex_url) so extension never requires self-reference.
  • Avoid mutual references between variables; factor shared pieces into a third, independent variable.
  • Use distinct loop_var names in nested loops so an inner loop cannot read and rewrite the outer name.
  • When overriding across group_vars/host_vars layers, override with a literal or a different base var, not the same name referencing itself.

Quick Command Reference

# Find every definition of a variable across precedence layers
grep -rn "<var>" inventory/ group_vars/ host_vars/ roles/*/defaults/ roles/*/vars/

# Resolve (or fail) a single variable with detail
ansible <host> -i inv -m debug -a "var=<var>" -vvv

# Dump a host's fully merged variables to inspect the graph
ansible-inventory -i inventory --host <host>

# Syntax-check before running
ansible-playbook -i inventory site.yml --syntax-check

Conclusion

“recursive loop detected in template string” means a variable’s definition ultimately depends on itself, so Jinja2 has no value to settle on. The fix is always structural: separate a base variable from the derived one, rename instead of redefining a name in terms of itself, and factor out shared pieces so no cycle exists. Grep every precedence layer for the named variable, break the self-reference, and templating resolves cleanly.

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.