The essential ansible when conditional examples are fact comparisons, defined-variable checks, Boolean flags, AND/OR expressions, loop filters, and registered-result tests. Put when alongside the module, write a Jinja expression without {{ }}, and make it return true or false. Ansible evaluates that condition separately for each host before running the task. Ansible documents this task-level behavior.
Start with a runnable conditional playbook
You need a working Ansible installation and a terminal. Save this as when-demo.yml; it prints branch decisions on localhost and requires no sudo.
---
- name: Try conditional branches locally
hosts: localhost
connection: local
gather_facts: false
vars:
deployment_env: lab
deploy_enabled: true
tasks:
- name: Report the lab branch
ansible.builtin.debug:
msg: "The lab branch ran"
when: deployment_env == "lab"
- name: Report the enabled branch
ansible.builtin.debug:
msg: "Deployment is enabled"
when: deploy_enabled | bool
Run it, then change the inputs:
ansible-playbook -i localhost, when-demo.yml
ansible-playbook -i localhost, when-demo.yml \
-e '{"deployment_env":"production","deploy_enabled":false}'
The default run selects both tasks. The overrides make both conditions false, so both tasks are skipped.
Keep when at the same indentation level as the module name. Variable names belong directly in the expression; {{ deployment_env }} adds unnecessary templating inside a condition. Ordinary module arguments, such as a templated msg, still use braces. See Ansible’s templating FAQ.
Add the remaining task snippets under tasks:. Fact-based examples require gathering; the include example adds a separate file.
Combine conditions with AND, OR, and NOT
A YAML list under when means AND: every entry must pass. Ansible’s conditionals reference documents the list behavior.
- name: Report an approved deployment
ansible.builtin.debug:
msg: "Environment and deployment flag both passed"
when:
- deployment_env == "lab"
- deploy_enabled | bool
For OR, write an expression with or:
- name: Report a nonproduction environment
ansible.builtin.debug:
msg: "This environment is approved"
when: deployment_env == "lab" or deployment_env == "staging"
The equivalent membership test is deployment_env in ['lab', 'staging']. Use parentheses when mixing operators, for example (deployment_env == 'lab' or deployment_env == 'staging') and (deploy_enabled | bool). Negate a flag with not (deploy_enabled | bool). These are Jinja comparison and logical operators.
To select inventory members, use when: "'webservers' in group_names". The outer quotes make the expression valid YAML; group_names lists the current host’s groups. For where those groups and flags come from, see Ansible inventory groups and variables.
Check operating-system facts
Use gathered facts when the decision depends on the managed host’s operating system. Enable gather_facts: true in the demo before adding this task:
- name: Report Debian-family hosts
ansible.builtin.debug:
msg: "The Debian-family branch ran"
when: ansible_facts['os_family'] == "Debian"
Prefer the ansible_facts dictionary to top-level fact aliases: those aliases depend on fact-injection configuration. Facts are gathered at the start of a play by default, but disabling gathering without supplying cached or explicitly collected facts can leave your condition without its input. The facts and magic variables documentation covers both behaviors and group_names.
For numeric strings, cast before comparing: when: (retry_count | int) > 0. For dotted version strings, use the version test, such as app_version is version('1.0.0', '>='), rather than alphabetical comparison. See Ansible’s version tests.
Handle undefined variables and Boolean flags
An optional variable needs an explicit absence policy. Skip a task when a label is missing or empty:
- name: Report an optional label
ansible.builtin.debug:
msg: "Label: {{ optional_label }}"
when: optional_label is defined and (optional_label | length) > 0
This assumes the label is a string. The defined test checks existence, not type or content. Validate required inputs rather than silently skipping their tasks.
For an optional feature flag, default to disabled:
- name: Report an optional feature
ansible.builtin.debug:
msg: "The feature branch ran"
when: feature_enabled | default(false) | bool
The bool filter understands conventional Boolean values, including strings such as "true" and "false". It is not a general-purpose test for arbitrary text; invalid inputs produce a deprecation warning. Prefer native YAML Booleans where you control the variables. See the official bool filter documentation.
The default filter normally replaces undefined inputs, leaving defined empty or null values intact. Decide which values your inventory accepts.
Branch on registered results and file existence
Use register to capture a module result, then check its documented fields. For a path on the managed host, collect metadata with stat:
- name: Inspect an application configuration path
ansible.builtin.stat:
path: /etc/app/config.yml
get_checksum: false
register: config_info
when: inspect_config | default(true) | bool
- name: Report an existing configuration path
ansible.builtin.debug:
msg: "The configuration path exists"
when:
- config_info is not skipped
- config_info.stat.exists
The existence field is config_info.stat.exists, including the nested stat key. This proves the path exists; checking for a regular file additionally requires the documented isreg field. The stat module reference describes the result structure.
A task skipped by when still registers a skipped result, so config_info is defined does not establish that metadata was collected. Guard against skipping before accessing module-specific fields.
For other registered results, these task-status tests express the branch clearly:
| Condition | What it checks |
|---|---|
result is failed | The result reports failure |
result is changed | The task reports a change |
result is skipped | Execution was skipped |
For a registered command named probe, use probe is not skipped and probe.rc == 0 before consuming successful output. A read-only probe can use changed_when: false to avoid reporting a configuration change.
Ordinary task failure stops later tasks for that host. A follow-up when: result is failed therefore needs deliberate error handling, such as a rescue block or narrowly scoped ignore_errors. See Ansible’s error-handling documentation.
Filter individual loop items
With a loop, Ansible evaluates when for each item. Give optional lists an empty default, and give optional per-item flags an explicit default:
- name: Report enabled services
ansible.builtin.debug:
msg: "Selected {{ item.name }}"
loop: "{{ services | default([]) }}"
when: item.enabled | default(false) | bool
vars:
services:
- name: metrics
enabled: true
- name: dashboard
enabled: false
This selects metrics and skips dashboard. Using default([]) supplies an empty iterator when the list is undefined, as described in Ansible’s conditionals reference.
When you register a loop, inspect the resulting results list for individual outcomes. Do not assume a loop’s aggregate result has the same fields as a single module invocation. See the Ansible loops guide.
Share conditions with blocks and task files
A block removes repeated conditions:
- name: Report an enabled deployment
when: deploy_enabled | bool
block:
- name: Report the flag
ansible.builtin.debug:
msg: "Deployment is enabled"
- name: Report the environment
ansible.builtin.debug:
var: deployment_env
Each enclosed task inherits the condition, which Ansible evaluates before each task. Changing the tested variable can therefore make a later task skip. See Ansible blocks.
Task files need a different distinction. Add this to the demo:
- name: Initialize a missing marker
ansible.builtin.include_tasks: bootstrap.yml
when: bootstrap_marker is undefined
Create bootstrap.yml beside the playbook:
---
- name: Set the marker
ansible.builtin.set_fact:
bootstrap_marker: ready
- name: Report the marker
ansible.builtin.debug:
var: bootstrap_marker
With include_tasks, the condition gates the include itself. If you replace it with import_tasks, child tasks inherit the condition: setting the marker makes the later debug task’s inherited test false. The reuse documentation explains dynamic and static task options.
Keep that scope in mind when moving tasks into roles. Roles versus collections explains the organizational choices.
Debug the expression before changing the logic
Print the input and the comparison separately:
- name: Inspect the deployment flag
ansible.builtin.debug:
var: deploy_enabled
- name: Check whether the flag is a Boolean
ansible.builtin.debug:
var: deploy_enabled is boolean
- name: Inspect the environment decision
ansible.builtin.debug:
var: deployment_env == 'lab'
The debug module’s var parameter already has implicit templating, so these expressions need no braces. See the debug module documentation.
Since ansible-core 2.19, non-Boolean conditional results are errors by default. An expression like when: deployment_env returns a string. Replace it with the intended comparison, or (deployment_env | length) > 0 if nonempty text is the actual requirement. The 2.19 porting guide documents this change.
Quote the whole YAML expression when it contains a colon followed by a space, for example when: 'result.msg == "status: ready"'. Quoting for YAML parsing is fine; wrapping the variable itself in Jinja delimiters is unnecessary.
Exercise both true and false branches with controlled inputs. For deployment runs, consult playbook flags that change a run before relying on check mode or limiting the target hosts.
FAQ
can ansible when retry a task?
No, when decides whether to attempt a task; use until when you need retries until a result meets your condition. Register the result and test it in the until expression. Retrying a task that was excluded by when does not make that branch eligible. See loop retries.
does ansible when run on the control node?
Ansible evaluates conditional expressions on the control node, using the variables and results available for the current host. That matters for filesystem tests: path is exists checks the controller’s filesystem. To inspect a managed host’s path, run ansible.builtin.stat there and test the returned metadata. See Ansible test execution.
what is the difference between when and failed_when?
when controls whether a task runs, while failed_when controls whether its result counts as a failure after execution. Use changed_when to control change reporting. A false when skips execution; a false failed_when allows an executed result to be treated as successful, even when the module’s normal failure rules differ. See result handling.
does ansible when work in check mode?
Yes, when still controls task selection in check mode, but a skipped probe may not provide the output your next condition expects. For a deliberately read-only command, consider check_mode: false after reviewing its effects. Otherwise guard skipped results and confirm the branch in a normal run against a disposable target. See check-mode behavior.