AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

ansible.builtin.set_fact Module: Set Variables Dynamically (Complete Guide)

By Luca Berton · Published 2026-04-03 · Category: linux-administration

Complete guide to ansible.builtin.set_fact module. Set variables dynamically during playbook execution, cache facts across plays, use with conditionals.

The ansible.builtin.set_fact module creates or modifies variables (facts) at runtime. Unlike vars, facts set with set_fact persist across tasks and can be cached across playbook runs.

Basic Usage

- name: Set a simple variable
  ansible.builtin.set_fact:
    app_version: "2.5.1"
    deploy_path: "/opt/myapp"

- name: Use the variable ansible.builtin.debug: msg: "Deploying version {{ app_version }} to {{ deploy_path }}"

See also: Ansible Facts: Gather, Use, and Create Custom Facts (Complete Guide)

Set Facts from Registered Variables

- name: Get current user
  ansible.builtin.command: whoami
  register: whoami_result

- name: Store as fact ansible.builtin.set_fact: current_user: "{{ whoami_result.stdout }}"

Conditional set_fact

- name: Set environment-specific config
  ansible.builtin.set_fact:
    db_host: "{{ 'db-prod.example.com' if env == 'production' else 'db-dev.example.com' }}"
    db_port: "{{ 5432 if env == 'production' else 5433 }}"
- name: Set fact based on OS
  ansible.builtin.set_fact:
    package_manager: apt
  when: ansible_os_family == 'Debian'

- name: Set fact for RedHat ansible.builtin.set_fact: package_manager: dnf when: ansible_os_family == 'RedHat'

See also: Ansible set_fact Module: Set Variables Dynamically (Complete Guide 2026)

Complex Data Structures

Set a dictionary

- name: Set database config
  ansible.builtin.set_fact:
    db_config:
      host: db.example.com
      port: 5432
      name: myapp
      user: appuser

Set a list

- name: Build server list dynamically
  ansible.builtin.set_fact:
    server_list: "{{ server_list | default([]) + [item] }}"
  loop: "{{ groups['webservers'] }}"

Merge dictionaries

- name: Merge with existing config
  ansible.builtin.set_fact:
    app_config: "{{ app_config | default({}) | combine({'new_key': 'new_value'}) }}"

Cacheable Facts

Persist facts across playbook runs using fact caching:

- name: Set cacheable fact
  ansible.builtin.set_fact:
    last_deploy_time: "{{ ansible_date_time.iso8601 }}"
    cacheable: true

Configure fact caching in ansible.cfg:

[defaults]
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

See also: Ansible facts: Gather, Use, and Create Custom Facts

set_fact vs vars vs register

| Feature | set_fact | vars | register | |---------|----------|------|----------| | Scope | Host-level, persists across plays | Play/block/task level | Task-level | | Dynamic | Yes — computed at runtime | Limited with Jinja2 | Output only | | Cacheable | Yes (with cacheable: true) | No | No | | Overrides | Higher precedence than vars | Lower precedence | N/A |

Practical Examples

Calculate derived values

- name: Get filesystem usage
  ansible.builtin.command: df -BG / --output=avail
  register: disk_result

- name: Parse available disk space ansible.builtin.set_fact: disk_available_gb: "{{ disk_result.stdout_lines[1] | regex_search('([0-9]+)') | int }}" disk_low: "{{ (disk_result.stdout_lines[1] | regex_search('([0-9]+)') | int) < 10 }}"

Build configuration from inventory

- name: Build upstream server list for nginx
  ansible.builtin.set_fact:
    upstream_servers: "{{ groups['appservers'] | map('extract', hostvars, 'ansible_host') | list }}"

FAQ

When should I use set_fact vs register?

Use register to capture command output. Use set_fact to create computed/derived variables or transform registered data.

Do set_fact variables survive between plays?

Yes! Facts set with set_fact persist for the host across all plays in the same playbook run. With cacheable: true, they survive across runs.

How do I unset a fact?

You can't directly unset a fact, but you can set it to omit or an empty value: my_fact: ""

Basic set_fact

- name: Set a variable
  ansible.builtin.set_fact:
    app_version: "2.5.0"
    deploy_path: "/opt/myapp-2.5.0"

- debug: msg: "Deploying v{{ app_version }} to {{ deploy_path }}"

Set Facts from Task Results

- name: Get hostname
  ansible.builtin.command: hostname -f
  register: hostname_result
  changed_when: false

- name: Store as fact ansible.builtin.set_fact: server_fqdn: "{{ hostname_result.stdout }}"

Conditional Facts

- name: Set environment-specific config
  ansible.builtin.set_fact:
    db_host: "{{ 'db.prod.com' if env == 'production' else 'db.dev.com' }}"
    debug_mode: "{{ env != 'production' }}"

Computed Facts

- name: Calculate values
  ansible.builtin.set_fact:
    total_memory_mb: "{{ ansible_memtotal_mb }}"
    java_heap_mb: "{{ (ansible_memtotal_mb * 0.75) | int }}"
    worker_count: "{{ ansible_processor_vcpus * 2 }}"

Build Complex Data Structures

- name: Build server config
  ansible.builtin.set_fact:
    server_config:
      hostname: "{{ inventory_hostname }}"
      ip: "{{ ansible_default_ipv4.address }}"
      os: "{{ ansible_distribution }}"
      roles: "{{ group_names }}"

- name: Build list incrementally ansible.builtin.set_fact: all_ips: "{{ all_ips | default([]) + [hostvars[item].ansible_host] }}" loop: "{{ groups['webservers'] }}"

Cacheable Facts (Persistent)

- name: Set persistent fact
  ansible.builtin.set_fact:
    last_deploy_time: "{{ ansible_date_time.iso8601 }}"
    cacheable: true  # Survives between plays

Requires fact_caching in ansible.cfg:

[defaults]
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts

set_fact vs vars vs register

| Method | Scope | When Set | |--------|-------|----------| | set_fact | Host, rest of play | Runtime (dynamic) | | vars | Task/play/role | Before execution (static) | | register | Host, rest of play | From task output | | group_vars | Group hosts | Inventory load time | | include_vars | Host, rest of play | File load time |

Common Patterns

Default values

- set_fact:
    config: "{{ user_config | default(default_config) | combine({'timestamp': now}) }}"

Cross-host facts

- set_fact:
    primary_ip: "{{ hostvars[groups['primary'][0]].ansible_host }}"

FAQ

Does set_fact persist between playbook runs?

Only with cacheable: true and fact_caching configured. Otherwise, facts reset each run.

Can I unset a fact?

No direct way. Set it to "", null, or omit:

- set_fact:
    my_var: null

set_fact in a loop — does it accumulate?

Each iteration replaces the fact. To accumulate, build on the previous value:

- set_fact:
    items: "{{ items | default([]) + [item] }}"
  loop: [a, b, c]

Basic set_fact

- set_fact:
    app_version: "2.5.0"
    deploy_time: "{{ ansible_date_time.iso8601 }}"
    full_name: "{{ first_name }} {{ last_name }}"

From Command Output

- command: git rev-parse --short HEAD
  register: git_output
  changed_when: false

- set_fact: git_commit: "{{ git_output.stdout }}" deploy_tag: "deploy-{{ git_output.stdout }}-{{ ansible_date_time.epoch }}"

Computed Values

- set_fact:
    worker_count: "{{ ansible_processor_vcpus * 2 }}"
    memory_limit: "{{ (ansible_memtotal_mb * 0.75) | int }}m"
    is_production: "{{ env == 'production' }}"
    app_url: "{{ 'https' if enable_ssl else 'http' }}://{{ server_name }}:{{ app_port }}"

Build Lists and Dicts

# Append to list
- set_fact:
    server_list: "{{ server_list | default([]) + [inventory_hostname] }}"

# Build dict - set_fact: host_info: hostname: "{{ inventory_hostname }}" ip: "{{ ansible_default_ipv4.address }}" os: "{{ ansible_distribution }}" cpu: "{{ ansible_processor_vcpus }}"

Conditional set_fact

- set_fact:
    package_manager: apt
  when: ansible_os_family == "Debian"

- set_fact: package_manager: dnf when: ansible_os_family == "RedHat"

# Ternary (one-liner) - set_fact: db_host: "{{ 'db.prod.com' if env == 'production' else 'localhost' }}"

Cacheable Facts (Persist Across Plays)

- set_fact:
    discovered_version: "2.5.0"
    cacheable: true  # Survives between plays when fact_caching is enabled
# ansible.cfg (required for cacheable)
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-facts
fact_caching_timeout = 86400

Transform Data

# Filter list
- set_fact:
    active_hosts: "{{ all_hosts | selectattr('status', 'eq', 'active') | list }}"

# Extract field - set_fact: hostnames: "{{ servers | map(attribute='name') | list }}"

# Merge dicts - set_fact: final_config: "{{ default_config | combine(env_config) | combine(host_config) }}"

Loop with set_fact

# Build a dict from loop
- set_fact:
    service_ports: "{{ service_ports | default({}) | combine({item.name: item.port}) }}"
  loop:
    - { name: web, port: 80 }
    - { name: api, port: 8080 }
    - { name: db, port: 5432 }
# Result: { web: 80, api: 8080, db: 5432 }

set_fact vs vars vs register

| Method | Scope | Timing | Use Case | |--------|-------|--------|----------| | set_fact | Host, persists in play | Runtime | Dynamic values | | vars | Task/play/role | Compile time | Static values | | register | Host, current play | After task | Capture output | | group_vars | Group hosts | Load time | Inventory config |

FAQ

Does set_fact persist across plays?

By default, yes — within the same playbook run. For cross-playbook persistence, use cacheable: true with fact caching enabled.

Can I unset a fact?

No direct way. Set it to null or "", or use meta: clear_facts to clear all facts.

set_fact vs vars?

set_fact creates host-level facts at runtime. vars are evaluated at parse time. Use set_fact when the value depends on runtime data (command output, conditionals).

Basic set_fact

- ansible.builtin.set_fact:
    app_url: "https://{{ inventory_hostname }}:{{ app_port }}"
    full_name: "{{ first_name }} {{ last_name }}"

Conditional set_fact

- set_fact:
    package_manager: apt
  when: ansible_os_family == "Debian"

- set_fact: package_manager: yum when: ansible_os_family == "RedHat"

From Registered Results

- command: cat /opt/myapp/VERSION
  register: version_output
  changed_when: false

- set_fact: app_version: "{{ version_output.stdout | trim }}"

Complex Data Structures

- set_fact:
    server_config:
      host: "{{ ansible_default_ipv4.address }}"
      port: "{{ app_port }}"
      workers: "{{ ansible_processor_vcpus * 2 }}"
      env: "{{ 'production' if is_prod else 'staging' }}"

Cacheable Facts

# Persists across plays (with fact caching enabled)
- set_fact:
    deployment_timestamp: "{{ ansible_date_time.iso8601 }}"
    cacheable: true

Append to List

- set_fact:
    my_list: "{{ my_list | default([]) + [new_item] }}"

# Build list from loop - set_fact: server_ips: "{{ server_ips | default([]) + [hostvars[item]['ansible_default_ipv4']['address']] }}" loop: "{{ groups['webservers'] }}"

Merge Dictionaries

- set_fact:
    config: "{{ config | default({}) | combine({'new_key': 'new_value'}) }}"

set_fact vs vars

| Feature | set_fact | vars | |---------|-----------|--------| | Scope | Per-host, persists across plays | Per-play or per-task | | Dynamic | ✅ Computed at runtime | ❌ Static (evaluated once) | | Conditional | ✅ Can use when | ❌ Always set | | Cacheable | ✅ With cacheable: true | ❌ |

Cross-Host Access

# set_fact on web1
- set_fact:
    app_version: "2.5.0"
  when: inventory_hostname == "web1"

# Access from any host - debug: msg: "Web1 runs version {{ hostvars['web1']['app_version'] }}"

FAQ

set_fact persists between plays?

Yes — set_fact values persist for the host across all plays in the same playbook run. With cacheable: true and fact caching, they persist across runs.

How to unset a fact?

You can't truly unset it, but you can set it to undefined: this isn't directly supported. Set it to empty or use default() pattern.

set_fact in a loop?

Each iteration overwrites the previous value. To accumulate, use the append pattern: "{{ list + [item] }}".

Related Articles

dynamic config with Ansible templatetask gating with Ansible whenAnsible inventory complete referencefree_form parameter in Ansible commandnested loops in Ansible

Category: linux-administration

Browse all Ansible tutorials · AnsiblePilot Home