AnsibleXV · Conditionals and LoopsConditionals
when expressions that hold up
What you'll learn
- Write when expressions without the templating braces and explain why
- Predict what ansible-core 2.21 does with a non-boolean conditional result
- Apply bool and int where a comparison needs a type it does not have
- Distinguish undefined, empty and false when writing a guard
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
when decides whether a task runs on a host. It is the smallest piece
of control flow Ansible has and it is responsible for a
disproportionate share of the incidents in this course, because the
characteristic failure of a when is not an error. It is a task that
quietly does not run.
ansible-core 2.21 changes a large part of that, and changes it for
the better. This lesson covers the mechanics, then the 2.21 behaviour,
then the traps that are still traps.
when is raw Jinja
The value of when is a Jinja expression, evaluated directly. It
does not go through the templating braces:
# Correct.
- name: Install with apt
ansible.builtin.package:
name: chrony
when: ansible_facts.os_family == 'Debian'
# Wrong. The braces are already implied.
- name: Install with apt
ansible.builtin.package:
name: chrony
when: "{{ ansible_facts.os_family == 'Debian' }}"The second form has historically been the source of a specific bug: the
braces render the expression to the string "True" or "False",
and both of those strings are truthy, so the task runs unconditionally.
That is precisely the failure mode 2.21 now refuses to have.
A list under when is ANDed:
when:
- ansible_facts.os_family == 'Debian'
- ansible_facts.distribution_major_version | int >= 12
Both must be true. There is no list form for OR — write or in the
expression, or restructure the inventory, which lesson 7 argues is
usually the real answer.
What 2.21 does with a non-boolean
Here is a play designed to hit the classic trap. s_false is the
string "false", which in every version of Jinja is truthy because it
is a non-empty string.
$ ansible-playbook truth.ymlTASK [Report how each value evaluates in a raw Jinja conditional] **************
ok: [localhost] => {
"msg": "s_false=TRUE s_no=TRUE s_zero=TRUE s_empty=FALSE b_false=FALSE n_zero=FALSE"
}The string "false" is truthy. The string "no" is truthy. The string
"0" is truthy. Only the empty string, the real boolean false and
the integer 0 are falsy.
Now the same value used as a conditional:
$ ansible-playbook truth.ymlTASK [This task runs even though s_false looks false] ***************************
[ERROR]: Task failed: A 'when' expression failed: Conditional result (True)
was derived from value of type 'str' at 'truth.yml:5:14'. Conditionals must
have a boolean result.
Origin: truth.yml:25:13
23 ansible.builtin.debug:
24 msg: "I ran. when was the string false."
25 when: s_false
^ column 13
Broken conditionals can be temporarily allowed with the
'ALLOW_BROKEN_CONDITIONALS' configuration option.
fatal: [localhost]: FAILED!Read the error carefully, because it is unusually good. It reports:
- the result it derived (
True), - the type it came from (
str), - the origin of the value —
truth.yml:5:14, wheres_falsewas defined, - the origin of the conditional — line 25, where it was used.
Two file positions, one for the definition and one for the use. That is exactly the pair you need to fix this class of bug and exactly the pair older versions did not give you.
The escape hatch, and its expiry
$ ansible-config list | grep -A 17 '^ALLOW_BROKEN_CONDITIONALS'ALLOW_BROKEN_CONDITIONALS:
default: false
description:
- When enabled, this option allows conditionals with non-boolean results to be used.
- A deprecation warning will be emitted in these cases.
- By default, non-boolean conditionals result in an error.
- Such results often indicate unintentional use of templates where they are not
supported, resulting in a conditional that is always true.
- When this option is enabled, conditional expressions which are a literal 'None'
or empty string will evaluate as true for backwards compatibility.
env:
- name: ANSIBLE_ALLOW_BROKEN_CONDITIONALS
ini:
- key: allow_broken_conditionals
section: defaults
type: boolean
version_added: '2.19'Turning it on downgrades the error to a deprecation warning that names the removal version:
$ ANSIBLE_ALLOW_BROKEN_CONDITIONALS=True ansible-playbook truth.ymlTASK [This task runs even though s_false looks false] ***************************
[DEPRECATION WARNING]: Conditional result (True) was derived from value of
type 'str' at 'truth.yml:5:14'. Conditionals must have a boolean result.
This feature will be removed from ansible-core version 2.23.
Broken conditionals are currently allowed because the
'ALLOW_BROKEN_CONDITIONALS' configuration option is enabled.
ok: [localhost] => {
"msg": "I ran. when was the string false."
}
TASK [With the bool filter is skipped] *****************************************
skipping: [localhost]Treat this as a migration tool with a deadline, not a setting. Turn it on to get an upgrade over the line, collect the deprecation warnings into a list of files to fix, fix them, turn it off. It stops working in 2.23.
The traps that are still traps
The 2.21 change catches conditionals that evaluate to the wrong type. It does not catch conditionals that evaluate to a boolean by a wrong route.
String comparison where you meant numeric
$ ansible-playbook truth.ymlTASK [String comparison against an integer-looking fact] ***********************
ok: [localhost] => {
"msg": "compare: False but int-compared: True"
}'26' > '9' is false, because string comparison is character by
character and '2' sorts before '9'. '26' | int > '9' | int is
true.
This matters because ansible_facts.distribution_major_version is a
string. A conditional written as:
when: ansible_facts.distribution_major_version > '9'
is a perfectly valid boolean expression that returns false on Debian 12, RHEL 10 and Ubuntu 26 — every host you actually wanted it to match. Nothing errors. The task skips. The recap is green.
Always cast:
when: ansible_facts.distribution_major_version | int >= 12
is defined on something that is defined and empty
Covered in Part XIV lesson 3 and it belongs here too, because it is a
conditional problem: ansible_facts.default_ipv4 is {} on a host
with no default route. is defined is true. The next line fails.
Test the value you are about to read.
| bool on a string that is not a boolean word
| bool maps a specific set of words. 'true', 'yes', 'on', '1'
become true; 'false', 'no', 'off', '0' become false. Anything
else — 'enabled', 'TRUE ' with a trailing space, 'y' — is not
guaranteed and does not error in a way you will notice.
If a value’s source is a text file, INI fact, or environment variable, normalise at the boundary rather than at each use:
- name: Normalise once, near the source
ansible.builtin.set_fact:
app_maintenance_mode: >-
{{ (ansible_facts.ansible_local.app.maintenance | default('false'))
| trim | lower | bool }}
Assembling a conditional that holds up
- name: Apply the modern configuration on supported releases
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
mode: '0644'
when:
- ansible_facts.os_family == 'Debian'
- ansible_facts.distribution_major_version | int >= 12
- name: Record hosts excluded by the release check
ansible.builtin.debug:
msg: >-
SKIPPED {{ inventory_hostname }}:
{{ ansible_facts.distribution }}
{{ ansible_facts.distribution_major_version }} is below the
minimum supported release
when:
- ansible_facts.os_family == 'Debian'
- ansible_facts.distribution_major_version | int < 12The second task is not decoration. On a fleet where five hosts are on
an old release, that debug is the only thing that will tell you so —
and it is the difference between “the run was green” and “the run was
green and here are the five hosts it deliberately did not touch”.
Knowledge check
Knowledge check · 4 questions
Q1. On ansible-core 2.21, what happens to a task with when: some_var where some_var holds the string "false"?
Q2. A conditional written as when: ansible_facts.distribution_major_version > '9' matches no hosts on a fleet of Debian 12 and Ubuntu 26 machines. Why?
Q3. Which of these produce a conditional that is a valid boolean but potentially the wrong answer? Select all that apply.
Q4. A when: expression is written as raw Jinja and should not be wrapped in templating braces.
Passing score: 75%. Answers are checked in this browser.