By Erika Heidi and Vinayak Baranwal

In Ansible, tasks execute on every host in the inventory unless you tell them not to. Without conditionals, a task that uses apt will attempt to run on RedHat hosts and fail; a task that assumes a /data volume exists will error on hosts where it is not mounted. The when keyword lets you attach a condition to any task so it runs only on hosts where that condition is true and is skipped everywhere else.
This guide shows how to use conditionals in Ansible playbooks for basic task control, register-based checks, host facts, multiple conditions, loops, and how to avoid common errors. For broader playbook structure, see the How To Write Ansible Playbooks series. The official Ansible conditionals documentation is the authoritative reference.
when keyword controls task execution: the task runs only when the expression evaluates to true.register).ansible_os_family, ansible_memtotal_mb, and ansible_mounts (and other facts) to make playbooks adapt to host OS, memory, and disk.and, or, and not for production-grade logic; avoid complex inline Jinja2 in when.when with loops to run a task only for items that match a condition.--check and use assert for precondition checks; inspect register output with debug when troubleshooting.Use an inventory file and user that match your setup; the examples use an inventory file named inventory and user sammy.
You use a basic ansible when condition by adding a when clause to a task so that the task runs only when the expression is true. This gives you simple task control without writing separate playbooks for each scenario.
The following example defines two variables, create_user_file and user. When create_user_file is true, a file is created in that user’s home directory.
Create a new file called playbook-04.yml in your ansible-practice directory:
- nano ~/ansible-practice/playbook-04.yml
Add the following content:
---
- hosts: all
vars:
- create_user_file: yes
- user: sammy
tasks:
- name: create file for user
file:
path: /home/{{ user }}/myfile
state: touch
when: create_user_file
Save and close the file.
Run the playbook (adjust -i and -u for your inventory and user):
- ansible-playbook -i inventory playbook-04.yml -u sammy
When the condition is met, the task shows a changed status:
...
TASK [create file for user] *****************************************************************************
changed: [203.0.113.10]
...
If you set create_user_file to no, the ansible when condition is false and the task is skipped:
...
TASK [create file for user] *****************************************************************************
skipping: [203.0.113.10]
...
Ansible coerces when values using Jinja2 boolean rules. Truthy values are yes, true, on, and 1. Falsy values are no, false, off, 0, empty string, and null. Strings that spell out a boolean are also coerced: a variable set to the string "false" evaluates as false even though it is a non-empty string. Use | bool when a variable might arrive as a string to force correct coercion: when: create_user_file | bool.
If you reference a variable in when that was never defined (e.g., you removed it from vars or it is only set for some hosts), Ansible fails with an undefined variable error before running the task. You avoid that by checking that the variable exists before using its value.
Example: a playbook that uses enable_feature in when but does not define it:
- hosts: all
tasks:
- name: Run optional feature
debug:
msg: Feature enabled
when: enable_feature
Running this playbook produces:
fatal: [203.0.113.10]: FAILED! => {"msg": "The conditional check 'enable_feature' failed. The error was: error while evaluating conditional (enable_feature): 'enable_feature' is undefined"}
Fix it by requiring the variable to exist and be truthy:
- hosts: all
tasks:
- name: Run optional feature
debug:
msg: Feature enabled
when: enable_feature is defined and enable_feature | bool
Now the task runs only when enable_feature is defined and evaluates to true; it is skipped when the variable is undefined or falsy, and Ansible does not raise an error.
You combine ansible task control with the result of a previous task by using register to capture module or command output, then using that registered variable in a when condition. The idiomatic way to check file or directory state in Ansible is the stat module, which returns structured data and does not rely on command exit codes.
The following example creates a file in the user’s home directory only if it does not exist, using stat to check. If the file exists, a debug message is shown instead.
Create playbook-05.yml in your ansible-practice directory:
- nano ~/ansible-practice/playbook-05.yml
Add the following content:
---
- hosts: all
vars:
- user: sammy
tasks:
- name: Check if file already exists
stat:
path: /home/{{ user }}/myfile
register: file_stat
- name: create file for user
file:
path: /home/{{ user }}/myfile
state: touch
when: not file_stat.stat.exists
- name: show message if file exists
debug:
msg: The user file already exists.
when: file_stat.stat.exists
Save and close the file.
Run the playbook:
- ansible-playbook -i inventory playbook-05.yml -u sammy
The first run (file missing): the stat task succeeds and reports that the file does not exist, so the create task runs and the message task is skipped:
...
TASK [Check if file already exists] *********************************************************************
ok: [203.0.113.10]
TASK [create file for user] *****************************************************************************
changed: [203.0.113.10]
TASK [show message if file exists] **********************************************************************
skipping: [203.0.113.10]
...
Run the playbook again. The file now exists, so the create task is skipped and the message is shown:
- ansible-playbook -i inventory playbook-05.yml -u sammy
...
TASK [Check if file already exists] *********************************************************************
ok: [203.0.113.10]
TASK [create file for user] *****************************************************************************
skipping: [203.0.113.10]
TASK [show message if file exists] **********************************************************************
ok: [203.0.113.10] => {
"msg": "The user file already exists."
}
...
A variable registered from the stat module holds a dictionary. The stat key contains the file metadata. Use a debug task with var: file_stat to inspect it. Example output:
ok: [203.0.113.10] => {
"file_stat": {
"changed": false,
"failed": false,
"stat": {
"exists": true,
"mode": "0664",
"size": 0,
"path": "/home/sammy/myfile",
...
}
}
}
You can use file_stat.stat.exists, file_stat.stat.size, file_stat.stat.mode, and other keys under stat in your when conditions or templates. Other modules put their result in different keys (e.g., stdout and rc for command), so inspecting the registered variable with debug is the way to see what is available.
When you cannot use a module (e.g., you need to run a custom command and branch on its exit code), you can run a command, register its output to a variable (e.g., register: cmd_result), set ignore_errors: yes so the play continues on failure, and use when: cmd_result is failed or when: cmd_result is succeeded in the following task. Use ignore_errors: yes only when failure is expected and you handle it in a later task (e.g., “file not found” is acceptable). Do not use it to hide real failures (e.g., a missing package or a broken config); that makes debugging hard and can leave systems inconsistent.
For finer control over what counts as a failure, use failed_when instead of ignore_errors. failed_when lets you define a condition so the task is marked failed only when that condition is true (e.g., when return code is not 0 or when stdout contains an error string). That way you can treat specific outcomes as failures while letting others pass.
You use host facts in conditionals so that tasks run only on hosts that match the right OS, memory, or disk layout. Facts are gathered automatically (unless you disable gathering) and give you a reliable way to adapt playbooks across different environments without extra variables.
To see which facts are available on your hosts, run the setup module. It dumps all gathered facts so you can look up exact variable names (e.g., ansible_os_family, ansible_memtotal_mb).
- ansible all -m setup -i inventory -u sammy
ansible_os_family groups operating systems (e.g., Debian, RedHat, Suse). Use it to run OS-specific tasks in a single playbook. The typical real-world pattern is one task that uses apt when the family is Debian and another that uses dnf (or yum) when the family is RedHat, so the same playbook works on both.
Example: install a text editor on Debian-family hosts with apt and on RedHat-family hosts with dnf:
- name: Install editor on Debian-family systems
apt:
name: vim
state: present
when: ansible_os_family == "Debian"
- name: Install editor on RedHat-family systems
dnf:
name: vim-enhanced
state: present
when: ansible_os_family == "RedHat"
On a Debian or Ubuntu host the first task runs and the second is skipped; on a RedHat or Rocky host the opposite happens:
TASK [Install editor on Debian-family systems] **********************************************************
changed: [203.0.113.10]
TASK [Install editor on RedHat-family systems] **********************************************************
skipping: [203.0.113.10]
On a RedHat-family host:
TASK [Install editor on Debian-family systems] **********************************************************
skipping: [203.0.113.11]
TASK [Install editor on RedHat-family systems] **********************************************************
changed: [203.0.113.11]
ansible_memtotal_mb is total RAM in megabytes. Use it to run memory-sensitive tasks only on hosts with enough RAM (e.g., skip a heavy service on small instances).
Example: install Elasticsearch only when total RAM is at least 2048 MB:
- name: Install Elasticsearch (only if RAM >= 2GB)
package:
name: elasticsearch
state: present
when: ansible_memtotal_mb >= 2048
ansible_mounts is a list of mounted filesystems. Use it to run tasks only when a specific mount point exists (e.g., configure or clean a volume only when it is present).
The expression ansible_mounts | map(attribute='mount') | list is used to get a list of mount points. Here is what each part does. ansible_mounts is a list of dictionaries; each dictionary has keys such as mount, device, fstype, and size_total. The Jinja2 filter map(attribute='mount') takes that list and returns an iterator of the mount value from each dictionary (e.g., /, /data, /home). The | list converts that iterator into a list so you can use the in operator in when. Without | list, you cannot test membership with in in the same way.
Example: run a task only when /data is mounted:
- name: Ensure data directory exists on mounted volume
file:
path: /data/app
state: directory
mode: '0755'
when: "'/data' in (ansible_mounts | map(attribute='mount') | list)"
Real-world use: skip log rotation or backup tasks when the target filesystem is not mounted, avoiding errors and unnecessary work.
You can combine ansible when multiple conditions in a single task using and, or, and not. This keeps logic in one place and avoids duplicating tasks. Use parentheses when mixing and and or so evaluation order is clear.
Example: run a task only on Debian-family hosts with at least 1 GB RAM:
- name: Install optional tools on Debian with enough memory
apt:
name: build-essential
state: present
when:
- ansible_os_family == "Debian"
- ansible_memtotal_mb >= 1024
List form is equivalent to AND: all conditions must be true.
and, or, and not explicitlyansible_check_mode is a magic variable Ansible sets to true automatically when the playbook runs with --check; using not ansible_check_mode in a condition prevents a task from being evaluated during dry runs.
- name: Configure app only when data volume is mounted and not in check mode
template:
src: app.conf.j2
dest: /etc/app/app.conf
when: >
('/data' in (ansible_mounts | map(attribute='mount') | list))
and (not ansible_check_mode)
A block in Ansible is a way to group multiple tasks together so you can apply shared directives, such as when, become, or error handling, to all of them at once. When the same set of conditions applies to several tasks, repeating the when on every task is error-prone and hard to maintain. Use a block with a single when so the conditions are defined once and apply to all tasks in the block.
Bad: the same three conditions repeated on two tasks:
---
- hosts: all
vars:
enable_data_service: true
tasks:
- name: Install data service package on RedHat
dnf:
name: my-data-service
state: present
when:
- ansible_os_family == "RedHat"
- enable_data_service | bool
- "'/data' in (ansible_mounts | map(attribute='mount') | list)"
- name: Start and enable data service
systemd:
name: my-data-service
state: started
enabled: true
when:
- ansible_os_family == "RedHat"
- enable_data_service | bool
- "'/data' in (ansible_mounts | map(attribute='mount') | list)"
Good: one block with one when:
---
- hosts: all
vars:
enable_data_service: true
tasks:
- name: Install and start data service when conditions are met
block:
- name: Install data service package on RedHat
dnf:
name: my-data-service
state: present
- name: Start and enable data service
systemd:
name: my-data-service
state: started
enabled: true
when:
- ansible_os_family == "RedHat"
- enable_data_service | bool
- "'/data' in (ansible_mounts | map(attribute='mount') | list)"
This keeps ansible task control readable and avoids repeating the same conditions.
You use when with loop so that a task runs once per loop item but only for items that satisfy a condition. The when clause is evaluated per item; items that fail the condition are skipped for that task.
Example: create directories only for listed paths that exist as mount points:
---
- hosts: all
vars:
data_dirs:
- /data
- /cache
- /logs
tasks:
- name: Create app dirs only on existing mounts
file:
path: "{{ item }}/app"
state: directory
mode: '0755'
loop: "{{ data_dirs }}"
when: item in (ansible_mounts | map(attribute='mount') | list)
Run the playbook. For a host where only /data and /logs are mounted, you see two changes and one skip:
TASK [Create app dirs only on existing mounts] **********************************************************
changed: [203.0.113.10] => (item=/data)
skipping: [203.0.113.10] => (item=/cache)
changed: [203.0.113.10] => (item=/logs)
ansible when in list style: run a task only when the current item is in an allowed list. Define both the list of services to consider and the allowed subset in vars so the snippet is self-contained:
---
- hosts: all
vars:
list_of_services:
- nginx
- redis
- postfix
allowed_services:
- nginx
- redis
tasks:
- name: Start only allowed services
systemd:
name: "{{ item }}"
state: started
loop: "{{ list_of_services }}"
when: item in allowed_services
ansible when not in list: run when the item is not in a list (e.g., skip installation for certain packages). Define both lists in vars:
---
- hosts: all
vars:
packages_to_upgrade:
- nginx
- redis-server
- kernel
hold_list:
- kernel
tasks:
- name: Install packages except those on hold list
apt:
name: "{{ item }}"
state: present
loop: "{{ packages_to_upgrade }}"
when: item not in hold_list
Undefined variable errors in when expressions and how to avoid them with is defined and | default() are covered in Step 1: Checking whether a variable exists before using it.
ignore_errors: yes makes Ansible continue after a task failure. Use it only when failure is expected and handled (e.g., a check command that fails when a file is missing). Do not use it to hide real failures; that makes playbooks hard to debug and can leave systems in a bad state.
An expected failure is one you design for: for example, “does this file exist?” where a missing file returns a non-zero exit code and you use that in a following when. A real failure is one that indicates something wrong: for example, apt install nginx failing because the package does not exist or the repo is broken. If you set ignore_errors: yes on the package install, the play continues and later tasks may assume the package is installed; the system is left inconsistent and the real error is buried in the log. Use ignore_errors only for tasks whose failure you explicitly handle in the next steps.
When you need to treat only certain outcomes as failures, use failed_when instead of ignore_errors. failed_when marks a task as failed only when a specific condition is true, so you get precise control without suppressing all errors:
- name: Check if service is active
command: systemctl is-active myapp
register: svc_status
failed_when: svc_status.rc not in [0, 3]
| Directive | What it does | When to use it | Risk |
|---|---|---|---|
ignore_errors: yes |
Continues the play regardless of why the task failed | Task failure is expected and fully handled in a later step | Hides real failures; system can be left in an inconsistent state |
failed_when: <condition> |
Marks the task as failed only when your condition is true | You need to treat specific exit codes or output as failures | None when condition is precise; overly broad conditions can still mask errors |
Return code 0 means active; 3 means inactive but not an error. Any other return code is treated as a real failure. Use the registered result in a following when to branch on the outcome.
In YAML, strings that look like booleans or numbers can be parsed as such. Use quotes when you need a literal string in a comparison:
when: ansible_os_family == "RedHat"
In when with Jinja2, ensure list/map expressions are quoted so the parser does not break on characters like : or [:
when: "'/data' in (ansible_mounts | map(attribute='mount') | list)"
When a conditional does not behave as expected, inspect the registered variable. Run a playbook with a debug task that prints it:
- name: Check if file already exists
stat:
path: /home/{{ user }}/myfile
register: file_stat
- name: Show register output for troubleshooting
debug:
var: file_stat
when: always_show_debug | default(false) | bool
Or run once without the conditional to see the full structure:
- name: Show register output
debug:
var: file_stat
Then use the correct attribute in your when (e.g., file_stat.stat.exists, or for a command result result.rc, result.stdout, or result is failed).
Keep when expressions readable. Prefer list form or a variable that holds the result of a complex expression instead of a long Jinja2 one-liner. That makes playbooks easier to maintain and debug.
# Prefer: list form or vars
when:
- ansible_os_family == "RedHat"
- ansible_memtotal_mb >= 1024
For mandatory preconditions (e.g., required variable or fact), use assert so the play fails fast with a clear message instead of failing later in a different task:
- name: Require app_environment to be set
assert:
that: app_environment is defined
fail_msg: "app_environment must be set for this playbook."
Use ansible-playbook --check to see which tasks would run or be skipped without applying any changes. That helps verify ansible when condition logic and avoids surprises in production.
- ansible-playbook -i inventory playbook-04.yml -u sammy --check
In check mode, Ansible reports what would change but does not modify the host. Tasks that would make changes still show as changed, but the summary indicates that the run was in check mode and no actual changes were made:
...
TASK [create file for user] *****************************************************************************
changed: [203.0.113.10] # reported as changed, but no file was created
PLAY RECAP **********************************************************************************************
203.0.113.10 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
# check mode: no changes were applied to the host
The task reports changed because it would have made a change, but nothing was written to the host. Run the same playbook without --check to apply changes. Tasks that would be skipped still show as “skipping” in check mode, so you can confirm your conditionals.
A conditional in Ansible is an expression attached to a task with the when keyword. Ansible evaluates the expression before running the task; if it is false, the task is skipped. Conditionals let you run tasks only when variables, facts, or previous task results match the conditions you define.
Add a when key to the task with a single expression or a list of expressions. Use variables, facts (e.g., ansible_os_family), or registered results (e.g., result is failed). For variables that might be undefined, use when: var is defined and var or a default.
Yes. Use a list under when (all items are ANDed), or use and, or, and not in one expression. Use parentheses when mixing and and or so the order of evaluation is clear.
Use fact names directly in when, for example ansible_os_family == "Debian", ansible_memtotal_mb >= 2048, or membership in ansible_mounts. Facts are gathered automatically unless gathering is disabled. Run ansible all -m setup -i inventory -u sammy to list available facts. See Step 3: Using Ansible Facts in Conditionals for examples.
when: var evaluates the value of var (true/false, empty/non-empty). If var is not defined, Ansible can raise an undefined variable error. when: var is defined is true only when the variable exists; it does not check its value. For optional variables, use when: var is defined and var so the task runs only when the variable exists and is truthy.
| Syntax | Behavior | Use case | Risk if misused |
|---|---|---|---|
when: var |
Evaluates the variable’s value as truthy or falsy | Variable is always defined and you control its value | Fails with undefined variable error if var is not set |
when: var is defined |
True only when the variable exists, regardless of its value | Check existence before acting; value does not matter | Runs even when var is empty or false |
when: (var | default(false)) | bool |
Returns false safely when variable is undefined or falsy | Optional variables set by external sources or extra vars | None; this is the safest form for optional variables |
Common mistakes include: using an undefined variable in when without is defined or a default; overusing ignore_errors and hiding real failures; quoting or syntax errors in Jinja2 (e.g., in list/map expressions); and not inspecting registered output with debug when debugging. Use the Common Errors and Troubleshooting section and test with --check to avoid these.
Conditionals in Ansible give you precise ansible task control using the when keyword with variables, facts, and registered results. You can adapt tasks by OS, memory, or mounts; combine conditions with and, or, and not; and use when with loops to run tasks only for matching items. Follow the best practices and troubleshooting tips above to keep playbooks maintainable and reliable.
Next steps:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Ansible is a modern configuration management tool that doesn’t require the use of an agent software on remote nodes, using only SSH and Python to communicate and execute commands on managed servers. This series will walk you through the main Ansible features that you can use to write playbooks for server automation. At the end, we’ll see a practical example of how to create a playbook to automate setting up a remote Nginx web server and deploy a static HTML website to it.
Browse Series: 11 tutorials
Dev/Ops passionate about open source, PHP, and Linux. Former Senior Technical Writer at DigitalOcean. Areas of expertise include LAMP Stack, Ubuntu, Debian 11, Linux, Ansible, and more.
Building future-ready infrastructure with Linux, Cloud, and DevOps. Full Stack Developer & System Administrator. Technical Writer @ DigitalOcean | GitHub Contributor | Passionate about Docker, PostgreSQL, and Open Source | Exploring NLP & AI-TensorFlow | Nailed over 50+ deployments across production environments.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.