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.
~/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:
- 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:
whenis 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:
when: env_name == "staging"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:
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):
- 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.
- 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 == 0failed_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:
- 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:
- name: and example
ansible.builtin.debug:
msg: "Staging and feature disabled"
when: env_name == "staging" and not optional_feature- name: or example
ansible.builtin.debug:
msg: "Staging or production"
when: env_name == "staging" or env_name == "production"- 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.
- 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) }}"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.
- 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"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:
- 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:
- 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:
- name: Include role with when
ansible.builtin.include_role:
name: when_role
when: run_roleWith 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:
- 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:
mkdir -p ~/ansible-project/roles/when_role/taskscat > ~/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"
EOFCreate the demo playbook:
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
EOFRun the demo:
cd ~/ansible-project
ansible-playbook playbooks/when-demo.ymlSample 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=0One 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:
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 }}"
EOFRun it:
ansible-playbook playbooks/when-status-demo.ymlSample 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=0when 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 |
Recommended Conditional Style
- Keep each
whenshort and readable; prefer list-style conditions for multiple checks. - Use state-aware modules first, then add
whenonly for real branching. - Guard optional values with
is defined/defaultto avoid runtime breaks. - Use facts for host-specific branching only when facts are actually gathered.
- Keep status control separate:
whenselects tasks;failed_when/changed_whendescribe results. - 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
- Ansible conditionals (
when) — official guide - Ansible facts and variables — facts in conditions
- Ansible registered variables — using task results
- Operators and tests in expressions — practical operator coverage
- Ansible loop — loop syntax beyond this conditional primer

