Report this

What is the reason for this report?

How To Use Conditionals in Ansible Playbooks

Updated on February 27, 2026
How To Use Conditionals in Ansible Playbooks

Introduction

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.

Key Takeaways

  • The when keyword controls task execution: the task runs only when the expression evaluates to true.
  • You can base conditions on variables, facts, or the result of a previous task (via register).
  • Use ansible_os_family, ansible_memtotal_mb, and ansible_mounts (and other facts) to make playbooks adapt to host OS, memory, and disk.
  • Combine conditions with and, or, and not for production-grade logic; avoid complex inline Jinja2 in when.
  • Use when with loops to run a task only for items that match a condition.
  • Test conditionals with --check and use assert for precondition checks; inspect register output with debug when troubleshooting.

Prerequisites

  • One or more remote hosts configured in an Ansible inventory file.
  • SSH key-based access to the remote hosts (or password-based if you prefer).
  • Basic familiarity with running playbooks (e.g., from the How To Write Ansible Playbooks series).

Use an inventory file and user that match your setup; the examples use an inventory file named inventory and user sammy.

Step 1: Using a Basic when Condition

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:

  1. 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):

  1. 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]
...

How Ansible evaluates truthiness

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.

Checking whether a variable exists before using it

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.

Step 2: Using register with Conditionals

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:

  1. 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:

  1. 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:

  1. 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."
}
...

What does a registered variable contain?

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.

Alternative: using register with commands

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.

Step 3: Using Ansible Facts in Conditionals

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).

  1. ansible all -m setup -i inventory -u sammy

Using ansible_os_family

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]

Using ansible_memtotal_mb

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

Using ansible_mounts

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.

Step 4: Combining Multiple Conditions

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.

Using and, or, and not explicitly

ansible_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.

Step 5: Using Conditionals with Loops

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

Common Errors and Troubleshooting

Undefined variable errors

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 misuse

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.

Quoting pitfalls in when expressions

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)"

Register output inspection with debug

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).

Best Practices

Avoid complex inline Jinja2 in when

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

Use assert for precondition checks

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."

Test conditionals with --check mode

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.

  1. 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.

FAQ

What is a conditional in Ansible?

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.

How do I use when statements in Ansible playbooks?

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.

Can I combine multiple conditions in a single task?

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.

How do I use host facts in Ansible conditionals?

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.

What is the difference between when: var and when: var is defined?

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

What are the most common mistakes when writing Ansible conditionals?

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.

Conclusion

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.

Learn more about our products

Tutorial Series: How To Write Ansible Playbooks

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.

About the author(s)

Erika Heidi
Erika Heidi
Author
Developer Advocate
See author profile

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.

Vinayak Baranwal
Vinayak Baranwal
Editor
Technical Writer II
See author profile

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.

Still looking for an answer?

Was this helpful?


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!

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.