when decides whether a task runs. The value is a Jinja2 expression built from comparison operators (==, >, in), logical operators (and, or, not), and tests (is defined, is mapping). Get the expression wrong and tasks skip silently—or fail on undefined variables.
This guide covers operators and tests for when conditions: strings, numbers, booleans, lists, dictionaries, registered results, and facts. It assumes variables, YAML syntax, and playbook structure. For failed_when and changed_when task syntax see conditionals; for loops see Ansible loop; for facts see custom facts; for result.rc patterns see register variables.
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 are Operators and Tests in Ansible when Conditions?
Operators compare or combine values:
app_env == "production"port >= 1024 and port <= 65535"httpd" in packages
Tests evaluate one value and return true or false:
my_var is definedapp_config is mappingenable_debug is boolean
Ansible uses standard Jinja2 tests plus Ansible-specific tests. Both appear inside when, failed_when, and changed_when expressions.
How when Conditions Work in Ansible
when is a task keyword. Its value is a Jinja2 expression without surrounding {{ }}:
- name: Production-only task
ansible.builtin.debug:
msg: "Production branch"
when: app_env == "production"Avoid this—when already expects a Jinja expression, and wrapping the whole condition in {{ }} can cause warnings or confusing parsing:
when: "{{ app_env == 'production' }}"Indent when at the same level as the module name (debug, dnf, …). Full task conditional patterns live in conditionals—here we focus on the expression inside when.
Operators vs Jinja2 Tests
| Kind | Role | Examples |
|---|---|---|
| Operators | Compare or combine values | ==, !=, in, and, or |
| Tests | Ask a true/false question about one value | is defined, is string, is mapping |
Use operators for equality and logic. Use tests before accessing optional variables or checking value types.
Which Operator or Test Should You Use?
| Need | Use |
|---|---|
| Compare two values | ==, !=, >, < |
| Match one of many values | var in ["a", "b"] |
| Require multiple conditions | List-form when or and |
| Allow either condition | or |
| Check optional variable | var is defined |
| Check dictionary before nested key | parent is mapping and parent.key is defined |
| Check registered command success | result.rc == 0 |
Comparison and Membership Operators in when Conditions
| Operator | Meaning |
|---|---|
== |
Equal to |
!= |
Not equal to |
> |
Greater than |
< |
Less than |
>= |
Greater than or equal to |
<= |
Less than or equal to |
in |
Value exists in a string, list, or dictionary keys |
Example playbook:
---
- name: Comparison operators demo
hosts: localhost
connection: local
gather_facts: false
vars:
app_env: production
port: 8080
tasks:
- name: Match environment
ansible.builtin.debug:
msg: "Production branch"
when: app_env == "production"
- name: Port in valid range
ansible.builtin.debug:
msg: "Port {{ port }} is in range"
when: port >= 1024 and port <= 65535Run it:
ansible-playbook playbooks/operators-demo.ymlSample output:
ok: [localhost] => {
"msg": "Production branch"
}
ok: [localhost] => {
"msg": "Port 8080 is in range"
}Logical Operators in when Conditions
Use logical operators when one variable is not enough to decide whether a task should run. A single comparison such as app_env == "production" answers one question. Real playbooks often need two or more checks—OS family and a debug flag, or environment name in a small set of allowed values.
Keep the condition readable. If the expression grows past two operators, split it into separate tasks or use list-form when for AND logic.
| Operator | Runs the task when… | Typical use |
|---|---|---|
and |
Every part is true | OS matches and feature flag is on |
or |
At least one part is true | Staging or development deploy |
not |
The following test or comparison is false | Maintenance mode is off |
and
Both sides must be true. Ansible evaluates the full expression before deciding to run or skip:
when: ansible_os_family == "RedHat" and enable_debugUse and when two independent facts must line up—for example Red Hat family packages and verbose logging enabled. If either side is false, the task is skipped.
or
At least one side must be true:
when: app_env == "staging" or app_env == "development"Use or for a small set of named values. When the list grows, app_env in ["staging", "development"] is often easier to read than chaining or.
not
Negate a test or comparison:
when: not enable_maintenance
when: missing_var is not definednot flips the result of what follows. when: not enable_maintenance runs when enable_maintenance is false or undefined. Pair not with default(false) when an optional flag may be missing:
when: not (maintenance_mode | default(false))Combined example:
---
- name: Logical operators demo
hosts: localhost
connection: local
gather_facts: true
vars:
enable_debug: true
app_env: staging
tasks:
- name: Red Hat family with debug enabled
ansible.builtin.debug:
msg: "Run verbose tasks on {{ ansible_os_family }}"
when: ansible_os_family == "RedHat" and enable_debug
- name: Staging or development deploy path
ansible.builtin.debug:
msg: "Non-production deploy checks"
when: app_env == "staging" or app_env == "development"
- name: Skip maintenance window tasks
ansible.builtin.debug:
msg: "Normal operations"
when: not (maintenance_mode | default(false))ansible-playbook playbooks/logical-operators-demo.ymlSample output:
ok: [localhost] => {
"msg": "Run verbose tasks on RedHat"
}
ok: [localhost] => {
"msg": "Non-production deploy checks"
}
ok: [localhost] => {
"msg": "Normal operations"
}The first task needs both ansible_os_family == "RedHat" and enable_debug. The second runs because app_env is staging. The third runs because maintenance_mode was never set and defaults to false inside not (...).
Check if a Variable is Defined
Optional variables come from inventory, group_vars, extra vars (-e), or prior tasks. Before you read backup_path or app_config.cache, confirm the name exists—otherwise Ansible errors instead of skipping the task.
is defined and is not defined answer whether Ansible knows the variable name. They do not tell you whether the value is empty: optional_var: "" is still defined.
is defined
Run a task only when the caller supplied a value:
when: optional_var is definedUse this for optional overrides—custom backup_path, feature flags, or keys that appear only on some hosts.
is not defined
Skip configuration that depends on a missing key:
when: missing_var is not defined
when: missing_var is undefinedis not defined and is undefined are equivalent for optional keys.
default filter for safe fallback values
In when, test definition first—you are deciding whether to run a task. In templates and module arguments where you need a display value, use default:
msg: "Cache={{ app_config.cache | default('disabled') }}"default supplies a fallback when the variable or key is undefined. It does not replace is defined in when when the goal is to skip the whole task.
Example playbook:
---
- name: Defined checks demo
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: optional_flag is defined
ansible.builtin.debug:
msg: "Caller set optional_flag"
when: optional_flag is defined
- name: backup_path is not defined
ansible.builtin.debug:
msg: "Use default backup location"
when: backup_path is not definedansible-playbook playbooks/defined-checks-demo.ymlSample output:
skipping: [localhost]
ok: [localhost] => {
"msg": "Use default backup location"
}optional_flag was never set, so the first task is skipped. backup_path is not defined either, so the second task runs and applies your default path logic.
Check Strings in when Conditions
Use in for simple substring checks. Use match or search only when you need a regular expression. match checks from the beginning of the string, while search can match anywhere in the value.
Compare string values
Quote string literals:
when: app_env == "production"
when: ansible_os_family == 'RedHat'Check string contains text
when: "'ssl' in cert_path"
when: cert_path is search('\\.pem$')Check string starts or ends with text
when: hostname is match('^web')
when: log_path is search('/var/log')Check empty strings
An empty string is defined but falsy in Jinja:
when: optional_var | length > 0
when: optional_var != ""Check Numbers in when Conditions
Compare numbers without quotes:
when: port == 8080
when: memory_mb >= 2048Avoid comparing numbers as strings—port == "8080" fails when port is an integer. Use filters if types might differ: when: port | int == 8080.
Check Booleans in when Conditions
Use real booleans in YAML when possible:
vars:
enable_debug: truewhen: enable_debug
when: not enable_debugString "true" and "false" are not booleans—when: debug == "true" is a string comparison, not a YAML boolean.
This matters when values come from inventory, extra vars, or external files. If a value may arrive as a string, normalize it before relying on boolean logic:
when: enable_debug | boolThe bool filter has its own rules—use it only when you know the input shape. See the conditionals guide for task-level patterns.
| Test | Use |
|---|---|
is boolean |
Value is a real boolean |
is true / is false |
Strict boolean checks |
Check Lists in when Conditions
Check if item exists in a list
when: "'httpd' in packages"
when: item in allowed_packagesCheck list length
when: packages | length > 0Check empty lists
when: packages | length == 0Example:
vars:
packages:
- httpd
- mod_ssl
tasks:
- name: httpd is listed
ansible.builtin.debug:
msg: "httpd is in package list"
when: "'httpd' in packages"Check Dictionaries in when Conditions
Dictionary checks are safest when you verify the parent object before reading a child key. Otherwise Ansible may fail before it gets a chance to skip the task.
Check if key exists
when: app_config is mapping
when: "'region' in app_config"Check nested dictionary values
when: app_config.region == "us-east"Avoid undefined nested variable errors
If the parent might be missing, test the parent first:
when: app_config is mapping and app_config.region is definedNot safe when app_config itself is undefined:
when: app_config.region == "us-east"Use Multiple when Conditions
YAML list form uses AND logic. The task runs only when every condition is true:
when:
- app_env == "production"
- port == 443This is the same as:
when: app_env == "production" and port == 443For OR logic, use or in one expression:
when: app_env == "production" or app_env == "staging"For membership-style OR checks, use in:
when: app_env in ["production", "staging"]Use list form for readable AND conditions. Use or, in, or parentheses when one of several values should match.
Use Parentheses for Complex Conditions
Without parentheses, mixed and / or expressions can be read incorrectly during review, even when Jinja evaluates them consistently.
Group logic with parentheses:
when: (app_env == "production" or app_env == "staging") and port >= 1024Parentheses clarify precedence when mixing and and or.
Use when with Registered Variables
After register: probe, use fields from the result hash. Details: register variables.
Check rc
when: probe.rc == 0Check stdout
when: probe.stdout != ""
when: "'READY' in probe.stdout"Check failed or changed status
when: not probe.failed
when: probe.changedExample:
- name: Probe file
ansible.builtin.command: test -f /etc/hosts
register: probe
changed_when: false
failed_when: false
- name: Continue when hosts file exists
ansible.builtin.debug:
msg: "rc={{ probe.rc }}, changed={{ probe.changed }}"
when: probe.rc == 0 and not probe.failedSample output:
ok: [localhost] => {
"msg": "rc=0, changed=False"
}Use when with Facts
Facts are variables after gathering—use them like any other value:
when: ansible_os_family == "RedHat"
when: ansible_distribution_major_version | int >= 9
when: ansible_virtualization_role == "guest"Do not re-teach fact gathering here—see facts and custom facts. With gather_facts: false, fact names are undefined unless you call setup.
Use when with Loops
when on a looping task is evaluated per item:
- name: Configure web packages only
ansible.builtin.debug:
msg: "Would configure {{ item }}"
loop: "{{ packages }}"
when: item in ['httpd', 'nginx']Sample output:
ok: [localhost] => (item=httpd) => { "msg": "Would configure httpd" }
ok: [localhost] => (item=nginx) => { "msg": "Would configure nginx" }
skipping: [localhost] => (item=php)Loop mechanics (loop_control, until) belong in the loop guide.
Safe Conditional Expressions
| Technique | Why |
|---|---|
var is defined before use |
Avoid undefined variable errors |
default('fallback') |
Safe display when a key may be missing |
parent is mapping and parent.key is defined |
Safe nested access |
| Parentheses | Clear mixed and / or logic |
| Short expressions | Easier review in Git and exams |
Common tests:
| Test | Use |
|---|---|
is defined |
Variable exists |
is not defined / is undefined |
Variable does not exist |
is string |
Value is a string |
is number |
Value is numeric |
is boolean |
Value is boolean |
is iterable |
Value can be iterated; not necessarily a list |
is mapping |
Value behaves like a dictionary |
Common when Condition Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
when: "{{ expr }}" |
Parsing errors or always true string | Drop outer {{ }} |
| Compare number to string | Condition never matches | Use port == 8080 or port | int |
| Access nested key on undefined parent | Task fails before skip | Test parent is mapping first |
"true" string vs boolean |
Wrong branch | Use YAML true / false |
One 120-character when |
Hard to debug | Split tasks or use parentheses |
Use facts with gather_facts: false |
Undefined fact | Call setup or enable gathering |
when on wrong indent |
YAML error | Align with module name |
Recommended Conditional Style
In a small Ansible project:
- Write
whenas a plain Jinja expression—no{{ }}. - Check
is definedbefore optional inventory or registered keys. - Use
and/orsparingly; split tasks when readability suffers. - Branch on
ansible_os_familyand version facts instead of hard-coding host names. - Leave
failed_when/changed_whenoverrides to the conditionals guide when task status needs customization.
References
- Ansible: Tests
- Ansible: Conditionals
- Jinja2: Expressions
Summary
Ansible when conditions are Jinja2 expressions: comparison operators (==, in, …), logical operators (and, or, not), and tests (is defined, is mapping). Do not wrap the whole expression in {{ }}. Check types and defined state before nested access; use default for fallbacks in values. Combine with registered results, facts, and loop item filters using the same rules. Keep expressions short and readable.

