The Error
Your playbook stops dead with:
fatal: [host]: FAILED! => {
"msg": "The task includes an option with an undefined variable. The error was: 'variable_name' is undefined"
}
AnsibleUndefinedVariable: 'variable_name' is undefined
Ansible hit a Jinja2 template that referenced a variable it couldn't find. Maybe it was never defined. Maybe it's defined in the wrong scope. Or there's a typo in the name.
Why This Happens
- The variable isn't defined anywhere โ no
vars:, nodefaults/main.yml, no inventory, no-eflag - Wrong scope: defined inside a block or in host-specific vars that don't apply to this task
- A typo (
db_portvsdb_portsโ easy to miss) - A nested key doesn't exist (
result.outputwhenresulthas nooutputkey) - A
registered variable used before its task runs, or when that task was skipped
Step-by-Step Fix
Step 1 โ Find where the variable should be defined
Start with -v to get more detail in the error output:
ansible-playbook site.yml -v
Then hunt for the variable across your project:
grep -r "variable_name" roles/ group_vars/ host_vars/ inventory/
No results? That's your answer โ it's not defined anywhere.
Step 2 โ Define the variable in the right place
Pick the location that matches how broadly the variable should apply:
In the playbook directly (task-level scope):
- name: Deploy app
hosts: webservers
vars:
app_port: 8080
app_user: deploy
tasks:
- name: Start app
ansible.builtin.systemd:
name: myapp
environment:
PORT: "{{ app_port }}"
In group_vars (applies to every host in that group):
# group_vars/webservers.yml
app_port: 8080
app_user: deploy
In role defaults (lowest priority โ easily overridden by anything else):
# roles/myapp/defaults/main.yml
app_port: 8080
app_user: deploy
At runtime with -e (highest priority โ overrides everything):
ansible-playbook site.yml -e "app_port=8080 app_user=deploy"
Step 3 โ Handle optional variables with defaults
Not every variable needs to be required. Use Jinja2's default() filter to supply a fallback value instead of letting the playbook crash:
- name: Configure timeout
ansible.builtin.template:
src: config.j2
dest: /etc/myapp/config.conf
vars:
timeout: "{{ connection_timeout | default(30) }}"
debug_mode: "{{ enable_debug | default(false) }}"
Need to skip the parameter entirely when it's not set? Use default(omit):
- name: Create user
ansible.builtin.user:
name: "{{ username }}"
password: "{{ user_password | default(omit) }}"
Step 4 โ Fix nested variable access
Accessing a key inside a dict? That key might not exist. Guard it:
# Unsafe โ fails if result has no stdout key
- debug:
msg: "{{ result.stdout }}"
# Safe โ returns empty string if stdout is missing
- debug:
msg: "{{ result.stdout | default('') }}"
# Or check first
- debug:
msg: "{{ result.stdout }}"
when: result.stdout is defined
Step 5 โ Fix registered variables used after skipped tasks
Classic footgun: a task gets skipped, the variable it was supposed to register never exists, then a later task tries to use it.
# Problem: if this task is skipped, check_result is undefined
- name: Check service
ansible.builtin.command: systemctl status myapp
register: check_result
when: run_checks | default(false)
# Fix: guard the usage
- name: Show result
debug:
msg: "{{ check_result.stdout }}"
when: check_result is defined and check_result.stdout is defined
Verify the Fix
Re-run the playbook. The task should pass instead of hitting FAILED. Before running for real, confirm Ansible can actually see your variable:
# Check a specific variable for all hosts in a group
ansible -m debug -a "var=app_port" webservers
# Dump all variables for one specific host
ansible -m debug -a "var=hostvars[inventory_hostname]" webservers --limit web01
A dry-run catches issues without making any changes:
ansible-playbook site.yml --check --diff
Quick Tips
- Variable precedence: Ansible has 22 levels of precedence. Getting an unexpected value? Check the precedence order. Extra vars (
-e) always win; role defaults always lose. - Strict mode: Set
error_on_undefined_vars = Trueinansible.cfgto catch undefined variables earlier โ before they silently produce wrong output instead of a clean error. - Dump all vars mid-play: Add a temporary debug task to see exactly what's available at that point in the play:
- name: Debug all vars debug: var: vars
- **Bare variables in `when:`**: `when: my_var` blows up if `my_var` is undefined. Write it as `when: my_var is defined and my_var` instead.

