The Problem

Your playbook uses set_fact to define variables dynamically, but you encounter errors:

bash
fatal: [server]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'my_var' is undefined"}

Or the fact you set doesn't exist in later tasks:

bash
fatal: [server]: FAILED! => {"msg": "'hostvars[inventory_hostname']['my_fact']' is undefined"}

Or facts set on one host aren't available on others.

Why This Happens

set_fact errors occur because:

Variable in value undefined - Referencing a variable that doesn't exist.

Scope misunderstanding - Facts are host-specific, not global.

Jinja2 syntax errors - Incorrect template syntax in the value.

Facts not available across hosts - Need hostvars to access other hosts' facts.

Timing issues - Using facts before they're set.

Understanding set_fact

set_fact creates host-specific variables that persist for the playbook run:

```yaml - hosts: webservers tasks: - name: Set a fact set_fact: my_config: "value"

  • name: Use the fact
  • debug:
  • msg: "{{ my_config }}" # Works on same host
  • `

Diagnosing the Issue

Debug the fact value:

yaml
- name: Debug fact
  debug:
    msg: "my_fact is {{ my_fact | default('UNDEFINED') }}"

Check hostvars for cross-host access:

yaml
- name: Check other host facts
  debug:
    msg: "{{ hostvars['other_host']['my_fact'] | default('UNDEFINED') }}"

Run with verbosity:

bash
ansible-playbook playbook.yml -vv

The Fix

Fix 1: Handle Undefined Variables in set_fact

Use defaults for potentially undefined variables:

```yaml # WRONG - fails if source_var undefined - name: Set fact set_fact: my_fact: "{{ source_var }}"

# CORRECT - use default - name: Set fact set_fact: my_fact: "{{ source_var | default('default_value') }}" ```

Complex defaults:

yaml
- name: Set fact with complex default
  set_fact:
    app_config:
      port: "{{ app_port | default(8080) }}"
      name: "{{ app_name | default('myapp') }}"
      enabled: "{{ app_enabled | default(true) }}"

Fix 2: Understand Host-Specific Scope

Facts are per-host, not global:

```yaml - hosts: web1,web2 tasks: - name: Set fact on each host set_fact: host_id: "{{ inventory_hostname | regex_replace('web', '') }}"

  • name: Show host's own fact
  • debug:
  • msg: "My ID: {{ host_id }}" # Works
  • name: Show other host's fact (WRONG)
  • debug:
  • msg: "Other ID: {{ hostvars.web2.host_id }}"
  • when: inventory_hostname == 'web1'
  • # Works because we access hostvars
  • `

Fix 3: Access Facts Across Hosts

Use hostvars for cross-host fact access:

```yaml - hosts: web1,web2 tasks: - name: Set shared config on first host set_fact: shared_token: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters') }}" run_once: yes delegate_to: web1

  • name: Access from other hosts
  • debug:
  • msg: "Token: {{ hostvars['web1']['shared_token'] }}"
  • # All hosts can access web1's facts via hostvars
  • `

Fix 4: Set Facts for All Hosts

Use run_once with delegate_facts:

```yaml - hosts: webservers tasks: - name: Generate common fact once set_fact: deployment_id: "{{ ansible_date_time.epoch }}" run_once: yes delegate_to: "{{ groups['webservers'][0] }}" # Only sets on first host

  • name: Access from all hosts
  • debug:
  • msg: "{{ hostvars[groups['webservers'][0]]['deployment_id'] }}"
  • `

Or set on all hosts:

yaml
- name: Set on all hosts
  set_fact:
    deployment_id: "{{ hostvars[groups['webservers'][0]]['deployment_id'] }}"
  when: hostvars[groups['webservers'][0]]['deployment_id'] is defined

Fix 5: Fix Jinja2 Syntax in set_fact

Complex values need proper syntax:

```yaml # WRONG - unquoted colon - name: Set fact set_fact: my_url: http://example.com:8080 # YAML parsing error

# CORRECT - quote the value - name: Set fact set_fact: my_url: "http://example.com:8080" ```

Dictionary values:

yaml
- name: Set dictionary fact
  set_fact:
    config:
      host: "{{ inventory_hostname }}"
      port: 8080
      enabled: true
    # Proper YAML dictionary

JSON strings:

yaml
- name: Parse JSON into fact
  set_fact:
    parsed_data: "{{ raw_json | from_json }}"

Fix 6: Handle Fact Dependencies

Order matters for dependent facts:

```yaml - hosts: webservers tasks: # WRONG - second_fact depends on first_fact before it's set - name: Set second fact set_fact: second_fact: "{{ first_fact }}/subdir"

  • name: Set first fact
  • set_fact:
  • first_fact: "/opt/app"

# CORRECT - set in correct order - hosts: webservers tasks: - name: Set first fact set_fact: first_fact: "/opt/app"

  • name: Set second fact
  • set_fact:
  • second_fact: "{{ first_fact }}/subdir"
  • `

Fix 7: Use cacheable Facts

Make facts survive playbook runs:

yaml
- name: Set cacheable fact
  set_fact:
    my_persistent_fact: "value"
    cacheable: yes
  # Fact available in later playbook runs via hostvars

Fix 8: Convert Between Types

Type conversion in set_fact:

```yaml - name: Convert to integer set_fact: port_int: "{{ port_string | int }}"

  • name: Convert to list
  • set_fact:
  • items_list: "{{ items_string | split(',') }}"
  • name: Convert to boolean
  • set_fact:
  • enabled_bool: "{{ enabled_string | bool }}"
  • `

Verifying the Fix

Test fact persistence:

```yaml # test_facts.yml - hosts: localhost gather_facts: no tasks: - name: Set fact set_fact: test_fact: "hello world"

  • name: Verify fact
  • assert:
  • that:
  • - test_fact == "hello world"
  • success_msg: "Fact set correctly"
  • name: Check fact in hostvars
  • debug:
  • msg: "{{ hostvars['localhost']['test_fact'] }}"
  • `

Run:

bash
ansible-playbook test_facts.yml -v

Prevention

Validate facts before using:

yaml
- name: Verify required facts exist
  assert:
    that:
      - my_fact is defined
      - my_fact | length > 0
    fail_msg: "Required fact my_fact must be set"

Document fact dependencies:

yaml
# Requires: first_fact must be set in previous task
- name: Set dependent fact
  set_fact:
    second_fact: "{{ first_fact }}/path"