unit 5 · Logic
Conditionals with when
The one place braces are wrong
you may want to read 15 · Registering output & debug , 17 · Facts, set_fact & caching first — but nothing is locked
tl;dr
- when: takes a bare Jinja2 expression — no {{ }}. Wrapping the condition in braces is the classic mistake.
- Condition on facts or registered fields (.rc, .changed); a false condition reports skipping, not failure.
- A YAML list under when: means AND; combined with loop, the condition is evaluated per item.
why it matters Real playbooks branch constantly — per OS family, per check result — and a brace-wrapped when: or an undefined variable inside one are the first two conditional failures you will hit.
Lesson 13 promised you one exception to the {{ }} rule, and lesson 15
ended with a registered result waiting on a decision. This is that lesson.
when: is a task keyword: attach a condition to any task, and the task
runs only where the condition is true.
One rule outranks everything else here: when: takes a bare Jinja2
expression — no braces. The keyword already puts you inside Jinja, so
{{ }} has no job to do:
- name: Install Apache the Red Hat way
ansible.builtin.dnf:
name: httpd # Red Hat family calls it httpd, not apache2
when: ansible_facts['os_family'] == "RedHat" # bare expression — no bracesThe condition reads a fact from lesson 17. os_family
rolls distributions up into families — "RedHat" covers RHEL, Rocky and
Alma; "Debian" covers Debian and Ubuntu; Windows hosts report
"Windows". Per-OS branching is the most common when: you
will read at work. In a mixed Linux + Windows estate it is how one play
serves both.
On a Debian host that task does not fail — it skips. The run output
prints skipping, which joins ok and changed from lesson 12’s
vocabulary: three verdicts, all normal. Because facts are per host,
the same task can run on half your inventory and skip the other half. A
feature, not a bug.
Now the mistake every old tutorial makes look natural:
- name: Old answers wrap the condition
ansible.builtin.ping:
when: "{{ my_var }}" # when: is already Jinja — the braces are the errorIt looks consistent with every other templated value, which is exactly
why people write it. Ansible flags it and ansible-lint rejects it —
the same tell as a bare module name.
Deciding on registered results
The second thing to branch on is a registered result. Lesson 15’s
cfg_check was built for this moment:
- name: Reload nginx # runs only where nginx -t succeeded
ansible.builtin.service:
name: nginx
state: reloaded
when: cfg_check.rc == 0 # cfg_check: the result you registered in lesson 15Any field in the result dict is fair game — .rc, .stdout, .changed.
One restraint: if you catch yourself writing when: something.changed to
trigger a restart, hold that thought. Handlers, lesson 20, are the
purpose-built tool for exactly that.
Combining conditions
It is Jinja, so and, or, not, comparisons and parentheses all work
inline. For readable AND-chains there is a second form — a YAML list,
where every item must be true:
- name: Report the custom port
ansible.builtin.debug:
msg: 'Port is {{ app_port }}' # braces belong HERE — a value, not a when
when:
- app_port is defined # guard first…
- app_port != 80 # …then use it safelyis defined and is not defined are Jinja tests. They exist because of
a sharp edge: an undefined variable inside when: does not skip the
task — it errors and fails the host.
when + loop: per item, not per task
Loops get their own lesson next, but one interaction belongs here: when:
does not gate the loop as a whole. It runs once per item, filtering:
- name: Install only the wanted tools
ansible.builtin.package:
name: '{{ item.name }}'
state: present
loop:
- { name: htop, wanted: true }
- { name: tmux, wanted: false } # this item alone reports skipping
when: item.wanted # evaluated once per item, not once per taskhtop installs, tmux skips, and the output shows both verdicts under
one task. That per-item behavior surprises people who expect the whole
task to be gated — lesson 19’s loops will hold no surprises. From here: handlers in lesson 20 for the .changed-triggered
restarts you just resisted writing, and blocks in lesson 21 for applying
one condition to a whole group of tasks at once.
self-check — recall, not a test
1 Why is when: "{{ my_var }}" wrong while msg: "{{ my_var }}" is right?
2 Lesson 15 registered nginx -t as cfg_check. Write the condition that reloads nginx only on success, and say what the other hosts report.
3 Your when: references a variable that may not exist. What happens without a guard, and what is the fix?
lab (optional — never required)
Against localhost: register a command result, gate one debug on rc == 0 and another on a variable you never set (with and without is defined) — watch ok, skipping and the undefined error.
needs: The control node from lesson 5.