Ansible when Conditional Explained with Facts and Variables

Learn Ansible when conditionals with variables, facts, registered output, lists, dictionaries, loops, block and role level conditions, and common mistakes.

Published

Updated

Read time 10 min read

Reviewed byDeepak Prasad

Ansible when conditionals with facts and variables on Rocky Linux 10

Ansible when lets you run the same playbook safely across different hosts and states. Instead of splitting logic into many separate playbooks, you keep one task list and execute each task only when its condition matches.

This guide focuses on practical when usage with variables, facts, registered output, and readable multi-condition patterns. It does not deep-dive every operator/test, full fact discovery internals, loop result structures, or precedence tables—those have separate guides.

Read variables, facts, and operators and Jinja2 tests as companion pages. Playbook structure shows where when: sits beside module keys in a task.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.

What is when in Ansible?

when is a task-selection condition. If the expression is true, Ansible runs the task. If false, Ansible marks it as skipped and continues.

Think of it as a guard clause around a task:

yaml
- name: Install package only on staging
  ansible.builtin.package:
    name: httpd
    state: present
  when: env_name == "staging"

How Ansible when Conditions Work

when expressions are evaluated per host at runtime, using that host's variables/facts and previously registered task results. For register field names and loop output shapes, see register and magic variables.

Important behavior:

  • when is a raw Jinja expression (no {{ }} around the whole condition)
  • A false condition does not fail the playbook; it skips the task
  • Conditions are evaluated after variables/facts/register data are available

Basic when Conditional Syntax

Two common forms:

yaml
when: env_name == "staging"
yaml
when:
  - env_name == "staging"
  - ansible_facts.os_family == "RedHat"

List form means logical and between lines and reads better for multi-check conditions.


Use when with Variables

Play, inventory, and group variables are the most common when inputs:

yaml
vars:
  env_name: staging

tasks:
  - name: Run only in staging
    ansible.builtin.debug:
      msg: "Environment is {{ env_name }}"
    when: env_name == "staging"

This is the fastest way to branch behavior by environment without forking playbooks.


Use when with Facts

Facts are system values gathered from managed nodes (os_family, distribution, architecture, memory values, and more):

yaml
- name: Run only on RedHat family hosts
  ansible.builtin.debug:
    msg: "OS family is {{ ansible_facts.os_family }}"
  when: ansible_facts.os_family == "RedHat"

If you set gather_facts: false, fact-based conditions fail unless you gathered facts another way.


Use when with Registered Output

register captures command/module output so later tasks can branch on rc, stdout, stderr, changed, or failed.

yaml
- name: Check if /etc/passwd exists
  ansible.builtin.command: test -f /etc/passwd
  register: passwd_check
  changed_when: false
  failed_when: false

- name: Use registered output in when
  ansible.builtin.debug:
    msg: "/etc/passwd check rc={{ passwd_check.rc }}"
  when: passwd_check.rc == 0

failed_when: false lets the play continue even if the probe returns a non-zero exit code. The next task decides what to do by checking passwd_check.rc. Use changed_when: false so read-only probes do not inflate changed counts.


Use Multiple Conditions

Readable list style:

yaml
- name: Multiple conditions as list style
  ansible.builtin.debug:
    msg: "List-style conditions matched"
  when:
    - env_name == "staging"
    - ansible_facts.os_family == "RedHat"

This is easier to maintain than long one-line expressions.


Use and, or and not in when

Boolean operators work directly in when:

yaml
- name: and example
  ansible.builtin.debug:
    msg: "Staging and feature disabled"
  when: env_name == "staging" and not optional_feature
yaml
- name: or example
  ansible.builtin.debug:
    msg: "Staging or production"
  when: env_name == "staging" or env_name == "production"
yaml
- name: not example
  ansible.builtin.debug:
    msg: "Not a production run"
  when: not (env_name == "production")

For more operators and Jinja2 tests (in, is defined, is mapping), see operators and Jinja2 tests.


Check Defined and Undefined Variables

Optional variables should be guarded to avoid "undefined" errors.

yaml
- name: Run only if optional_port is defined
  ansible.builtin.debug:
    msg: "optional_port={{ optional_port }}"
  when: optional_port is defined
yaml
- name: Use default for undefined variable
  ansible.builtin.debug:
    msg: "optional_port safe value={{ optional_port | default(8080) }}"

Use is defined and default(...) together when values may not exist in every environment.


Use when with Lists and Dictionaries

Before indexing a list or reading a dictionary key, confirm the variable exists and has the shape you expect. Unguarded package_names[0] or app_config.service can error before Ansible skips the task.

yaml
- name: List check
  ansible.builtin.debug:
    msg: "First package is {{ package_names[0] }}"
  when:
    - package_names is defined
    - package_names | length > 0
    - package_names[0] == "httpd"
yaml
- name: Dictionary key check
  ansible.builtin.debug:
    msg: "App listens on {{ app_config.port }}"
  when:
    - app_config is mapping
    - app_config.service is defined
    - app_config.service == "httpd"

List-style when: lines combine with logical and—each guard must pass. Deep data-model design belongs in the variables guide.


Use when with Loops

when is evaluated per loop item. That makes item-level filtering clean:

yaml
- name: when with loop items
  ansible.builtin.debug:
    msg: "Would manage package {{ item.name }}"
  loop: "{{ web_packages }}"
  when: item.enabled | default(false)

Use default(false) when some loop items may not define the flag. Loop strategy itself is covered in Ansible loop.


Use when at Task, Block and Role Level

when is not task-only. You can apply it to a single task, a block, or a role include.

A block when skips every task inside the block when the condition is false—useful when several related steps share the same guard:

yaml
- name: Block with when condition
  when: env_name == "staging"
  block:
    - name: Block task one
      ansible.builtin.debug:
        msg: "Block task one ran"
    - name: Block task two
      ansible.builtin.debug:
        msg: "Block task two ran"

A role include when decides whether that role runs for the host:

yaml
- name: Include role with when
  ansible.builtin.include_role:
    name: when_role
  when: run_role

With include_role, the condition is checked when Ansible reaches the include task. If it is false, the role is not included for that host. Static role imports and the roles: keyword apply conditions differently—keep advanced role-loading behavior in the roles article.


when vs failed_when vs changed_when

These are different controls:

Keyword Purpose
when Select whether a task should run
failed_when Override whether a task result counts as failure
changed_when Override whether a task result counts as changed

Example pattern for command checks:

yaml
- name: Query package without false changed
  ansible.builtin.command: rpm -q httpd
  register: httpd_query
  changed_when: false
  failed_when: httpd_query.rc not in [0, 1]

This task always runs, but reports status cleanly.


Common when Conditional Examples

These are expression patterns you can drop into tasks—not full playbooks:

  • OS-specific package manager steps (when: ansible_facts.os_family == "RedHat")
  • Group-specific service actions (when: inventory_hostname in groups["web"])
  • Guarded config deploy (when: app_config is defined)
  • Follow-up task only on success (when: result.rc == 0)
  • Role include per environment (when: env_name == "production")

If the expression is getting long, move part of it into a variable name that documents intent.


Full Demo Playbook (Variables, Facts, Register, Loops, Block, Role)

Create a tiny role used in the role-level condition example:

bash
mkdir -p ~/ansible-project/roles/when_role/tasks
bash
cat > ~/ansible-project/roles/when_role/tasks/main.yml << 'EOF'
---
- name: Role task runs only when enabled
  ansible.builtin.debug:
    msg: "Role executed because run_role is true"
EOF

Create the demo playbook:

bash
cat > ~/ansible-project/playbooks/when-demo.yml << 'EOF'
---
- name: when conditional demo
  hosts: lab
  gather_facts: true
  vars:
    env_name: staging
    run_role: true
    optional_feature: false
    package_names:
      - httpd
      - nginx
    web_packages:
      - name: httpd
        enabled: true
      - name: nginx
        enabled: false
    app_config:
      service: httpd
      port: 8080
  tasks:
    - name: Run only in staging
      ansible.builtin.debug:
        msg: "Environment is {{ env_name }}"
      when: env_name == "staging"

    - name: Run only on RedHat family hosts
      ansible.builtin.debug:
        msg: "OS family is {{ ansible_facts.os_family }}"
      when: ansible_facts.os_family == "RedHat"

    - name: Check if /etc/passwd exists
      ansible.builtin.command: test -f /etc/passwd
      register: passwd_check
      changed_when: false
      failed_when: false

    - name: Use registered output in when
      ansible.builtin.debug:
        msg: "/etc/passwd check rc={{ passwd_check.rc }}"
      when: passwd_check.rc == 0

    - name: Multiple conditions as list style
      ansible.builtin.debug:
        msg: "List-style conditions matched"
      when:
        - env_name == "staging"
        - ansible_facts.os_family == "RedHat"

    - name: and or not example
      ansible.builtin.debug:
        msg: "Boolean operators matched"
      when: env_name == "staging" and not optional_feature

    - name: Run only if optional_port is defined
      ansible.builtin.debug:
        msg: "optional_port={{ optional_port }}"
      when: optional_port is defined

    - name: Use default for undefined variable
      ansible.builtin.debug:
        msg: "optional_port safe value={{ optional_port | default(8080) }}"

    - name: List check
      ansible.builtin.debug:
        msg: "First package is {{ package_names[0] }}"
      when:
        - package_names is defined
        - package_names | length > 0
        - package_names[0] == "httpd"

    - name: Dictionary key check
      ansible.builtin.debug:
        msg: "App listens on {{ app_config.port }}"
      when:
        - app_config is mapping
        - app_config.service is defined
        - app_config.service == "httpd"

    - name: when with loop items
      ansible.builtin.debug:
        msg: "Would manage package {{ item.name }}"
      loop: "{{ web_packages }}"
      when: item.enabled | default(false)

    - name: Block with when condition
      when: env_name == "staging"
      block:
        - name: Block task one
          ansible.builtin.debug:
            msg: "Block task one ran"
        - name: Block task two
          ansible.builtin.debug:
            msg: "Block task two ran"

    - name: Include role with when
      ansible.builtin.include_role:
        name: when_role
      when: run_role
EOF

Run the demo:

bash
cd ~/ansible-project
ansible-playbook playbooks/when-demo.yml

Sample output:

output
TASK [Run only in staging] *****************************************************
ok: [rocky2] => {
    "msg": "Environment is staging"
}

TASK [Run only on RedHat family hosts] *****************************************
ok: [rocky2] => {
    "msg": "OS family is RedHat"
}

TASK [Run only if optional_port is defined] ************************************
skipping: [rocky2]

TASK [Use default for undefined variable] **************************************
ok: [rocky2] => {
    "msg": "optional_port safe value=8080"
}

TASK [when with loop items] ****************************************************
ok: [rocky2] => (item={'name': 'httpd', 'enabled': True}) => {
    "msg": "Would manage package httpd"
}
skipping: [rocky2] => (item={'name': 'nginx', 'enabled': False})

TASK [when_role : Role task runs only when enabled] ****************************
ok: [rocky2] => {
    "msg": "Role executed because run_role is true"
}

PLAY RECAP *********************************************************************
rocky2                     : ok=14   changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

One playbook now shows task, loop item, block, and role-level conditions in one place.


Status-Control Demo (when vs failed_when vs changed_when)

Create the demo playbook:

bash
cat > ~/ansible-project/playbooks/when-status-demo.yml << 'EOF'
---
- name: when vs failed_when vs changed_when demo
  hosts: lab
  gather_facts: false
  tasks:
    - name: Selection condition with when
      ansible.builtin.debug:
        msg: "This task is selected by when"
      when: 2 > 1

    - name: Check package query without reporting changed
      ansible.builtin.command: rpm -q httpd
      register: httpd_query
      changed_when: false
      failed_when: httpd_query.rc not in [0, 1]

    - name: Show package query return code
      ansible.builtin.debug:
        msg: "httpd query rc={{ httpd_query.rc }}"
EOF

Run it:

bash
ansible-playbook playbooks/when-status-demo.yml

Sample output:

output
TASK [Selection condition with when] *******************************************
ok: [rocky2] => {
    "msg": "This task is selected by when"
}

TASK [Check package query without reporting changed] ***************************
ok: [rocky2]

TASK [Show package query return code] ******************************************
ok: [rocky2] => {
    "msg": "httpd query rc=1"
}

PLAY RECAP *********************************************************************
rocky2                     : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

when controls task selection, while changed_when and failed_when control status reporting of a task that did run.


Common when Mistakes and Fixes

Symptom Likely cause Fix
when: {{ var == "x" }} fails or behaves oddly Wrapped entire condition in {{ }} Use raw expression: when: var == "x"
Undefined variable error Optional variable not present Guard with is defined or default(...)
Condition never matches Type mismatch ("8080" vs 8080) Compare same types; keep YAML typing consistent
Fact-based condition fails gather_facts: false and no facts loaded Enable facts or avoid fact conditions in that play
Expression too hard to read Huge one-line boolean chain Split into list-style when: lines or helper vars
Loop condition surprises when evaluated per item Use `when: item.enabled
List or dict access errors Unguarded [0] or .key in when Guard with is defined, length, or is mapping first

  1. Keep each when short and readable; prefer list-style conditions for multiple checks.
  2. Use state-aware modules first, then add when only for real branching.
  3. Guard optional values with is defined / default to avoid runtime breaks.
  4. Use facts for host-specific branching only when facts are actually gathered.
  5. Keep status control separate: when selects tasks; failed_when/changed_when describe results.
  6. For complex operators/tests, refer to operators and Jinja2 tests.

Summary

when is the core conditional tool in Ansible: it decides whether a task, block, or role include runs for each host. Use it with variables, facts, and registered output; keep expressions raw (no wrapping {{ }}), readable, and guarded for undefined values. Reserve failed_when and changed_when for status control after a task runs. With those patterns, your playbooks stay clear, predictable, and safe to rerun.


References


Frequently Asked Questions

1. What does when do in Ansible?

The when keyword runs a task only if its expression evaluates to true. If the expression is false, Ansible skips that task.

2. Can I use facts in when conditions?

Yes. You can use gathered facts such as ansible_facts.os_family, distribution, architecture, and memory values in when expressions.

3. Can when use registered command output?

Yes. Register task output and test fields like result.rc, result.stdout, result.stderr, result.changed, or result.failed in when.

4. Should I write when: {{ my_var == "x" }}?

No. when already expects a raw Jinja expression. Write when: my_var == "x" without wrapping it in {{ }}.

5. What is the difference between when and failed_when?

when decides whether a task should run. failed_when overrides whether a task result should be marked as failed after it runs.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …