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

ansible illustration for: 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).

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:

				
					
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]

...

				
			

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:

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

}

...

				
			

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

				
					
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 &gt;= 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.

				
					
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](#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: