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 template • task gating with Ansible when • Ansible inventory complete reference • free_form parameter in Ansible command • nested loops in AnsibleCategory: linux-administration