Introduction

The VARIABLE IS NOT DEFINED! error is one of the most common Ansible troubleshooting scenarios. Most of the time the root cause is a misspelled variable name or a variable that was never set. However, there is a special case involving ansible_hostname and other Ansible facts that catches many users off guard — the variable exists, is spelled correctly, and yet Ansible reports it as undefined.

This article explains why this happens, the difference between ansible_hostname and inventory_hostname, how gather_facts controls fact availability, and multiple strategies to fix and prevent this error.

Understanding the Error

When Ansible encounters an undefined variable during template rendering or task execution, it produces output like this:

"ansible_hostname": "VARIABLE IS NOT DEFINED!"

or in newer Ansible versions:

fatal: [demo.example.com]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible_hostname' is undefined."}

This happens because ansible_hostname is an Ansible fact — a variable that is automatically populated when Ansible gathers facts from the target host. If fact gathering is disabled, the variable simply does not exist.

Why ansible_hostname Requires gather_facts

Ansible facts are collected by the setup module, which runs automatically at the beginning of each play when gather_facts: true (the default). Facts include:

Fact VariableDescriptionExample Value
ansible_hostnameShort hostname of the targetwebserver01
ansible_fqdnFully qualified domain namewebserver01.example.com
ansible_os_familyOS familyRedHat, Debian
ansible_distributionDistribution nameUbuntu, CentOS
ansible_default_ipv4.addressPrimary IPv4 address192.168.1.100
ansible_memtotal_mbTotal memory in MB8192

When you set gather_facts: false, none of these variables are available unless you explicitly call the setup module later in the play.

ansible_hostname vs inventory_hostname

A critical distinction that often confuses users:

ansible_hostname — Requires fact gathering. Returns the actual hostname as reported by the target machine (output of hostname -s). This is a runtime fact collected from the remote host.

inventory_hostname — Always available. Returns the hostname as defined in your Ansible inventory file. This is a magic variable that does not require fact gathering.

# inventory
[webservers]
web01.example.com ansible_host=192.168.1.10

In this example:

  • inventory_hostname = web01.example.com (always available)
  • ansible_hostname = whatever hostname -s returns on the target (requires facts)

Error Playbook

The following playbook triggers the error because gather_facts: false prevents fact collection:

---
- name: hostname Playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: print hostname
      ansible.builtin.debug:
        var: ansible_hostname

Error Output

$ ansible-playbook -i inventory troubleshooting/variablenotdefined_error.yml
PLAY [hostname Playbook] **********************
TASK [print hostname] *************************
ok: [demo.example.com] => {
    "ansible_hostname": "VARIABLE IS NOT DEFINED!"
}
PLAY RECAP ************************************
demo.example.com : ok=1 changed=0 unreachable=0 failed=0

Solution 1: Enable gather_facts

The simplest fix — set gather_facts: true (or remove the line, since true is the default):

---
- name: hostname Playbook
  hosts: all
  gather_facts: true
  tasks:
    - name: print hostname
      ansible.builtin.debug:
        var: ansible_hostname

Fixed Output

$ ansible-playbook -i inventory troubleshooting/variablenotdefined_fix.yml
PLAY [hostname Playbook] **********************
TASK [Gathering Facts] ************************
ok: [demo.example.com]
TASK [print hostname] *************************
ok: [demo.example.com] => {
    "ansible_hostname": "webserver01"
}

Solution 2: Use inventory_hostname Instead

If you disabled fact gathering for performance reasons and only need the host identifier, use inventory_hostname:

---
- name: hostname Playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: print hostname from inventory
      ansible.builtin.debug:
        var: inventory_hostname

    - name: print short hostname
      ansible.builtin.debug:
        msg: "{{ inventory_hostname_short }}"

Solution 3: Gather Facts Selectively

If you need ansible_hostname but want to minimize fact-gathering overhead, gather only the subset you need:

---
- name: hostname Playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: gather only network facts
      ansible.builtin.setup:
        gather_subset:
          - network
          - min

    - name: print hostname
      ansible.builtin.debug:
        var: ansible_hostname

Available subsets include: all, min, hardware, network, virtual, ohai, facter.

Solution 4: Use default Filter for Graceful Fallback

When you are unsure whether facts will be available, use the default filter to provide a fallback value:

---
- name: hostname Playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: print hostname with fallback
      ansible.builtin.debug:
        msg: "Host: {{ ansible_hostname | default(inventory_hostname) }}"

Solution 5: Cache Facts Across Plays

If your playbook has multiple plays and only the first gathers facts, enable fact caching so subsequent plays can access them:

# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 3600

With gathering = smart, Ansible only gathers facts if they are not already cached.

Common Scenarios That Trigger This Error

Scenario 1: Disabled gather_facts for Performance

- hosts: all
  gather_facts: false  # <-- facts not collected
  tasks:
    - name: configure hostname
      ansible.builtin.hostname:
        name: "{{ ansible_hostname }}-new"  # FAILS

Fix: Either enable gather_facts or use inventory_hostname.

Scenario 2: Using ansible_hostname in a Role with gather_facts Disabled at Play Level

- hosts: all
  gather_facts: false
  roles:
    - configure_network  # role templates use ansible_hostname

Fix: Add gather_facts: true at the play level, or add a setup task in the role's tasks/main.yml.

Scenario 3: Cross-Play Variable Access

- name: Play 1
  hosts: webservers
  gather_facts: true
  tasks:
    - name: do something
      ansible.builtin.debug:
        msg: "OK"

- name: Play 2
  hosts: dbservers
  gather_facts: false
  tasks:
    - name: reference webserver hostname
      ansible.builtin.debug:
        msg: "{{ hostvars['web01']['ansible_hostname'] }}"  # May fail

Fix: Enable fact caching or gather facts in both plays.

Other Commonly Undefined Ansible Facts

The same VARIABLE IS NOT DEFINED! error applies to all facts when gather_facts: false:

  • ansible_distribution — OS distribution name
  • ansible_os_family — OS family (RedHat, Debian, Suse, etc.)
  • ansible_default_ipv4 — Default IPv4 network info
  • ansible_memtotal_mb — Total RAM
  • ansible_processor_vcpus — CPU count
  • ansible_kernel — Kernel version
  • ansible_env — Environment variables

Debugging Undefined Variables

Use the ansible.builtin.debug module to inspect available variables:

- name: list all facts
  ansible.builtin.debug:
    var: ansible_facts

- name: check if variable is defined
  ansible.builtin.debug:
    msg: "ansible_hostname is {{ 'defined' if ansible_hostname is defined else 'NOT defined' }}"

Run with increased verbosity to see fact gathering:

ansible-playbook -i inventory playbook.yml -vvv

Best Practices

  1. Leave gather_facts: true unless you have a specific reason to disable it — the performance impact is usually minimal
  2. Use inventory_hostname when you need the inventory name, not the actual machine hostname
  3. Use gather_subset to minimize overhead if fact gathering is slow (e.g., large cloud environments)
  4. Enable fact caching for multi-play playbooks and large inventories
  5. Use the default filter when a variable might not be defined: {{ var | default('fallback') }}
  6. Document which plays require facts — add comments when setting gather_facts: false

Conclusion

The VARIABLE IS NOT DEFINED! ansible_hostname error is almost always caused by gather_facts: false in the play definition. The fix is straightforward — either enable fact gathering, use inventory_hostname as an alternative, gather facts selectively with setup and gather_subset, or use the default filter for graceful fallback. Understanding the distinction between runtime facts and magic variables is key to writing robust Ansible playbooks.