unit 5 · Logic

Conditionals with when

The one place braces are wrong

essential · 6 min · linux + windows

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 braces
branch on a fact — bare expression, no braces

The 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:

old — don't write this · brace-wrapped when: — templated twice
- name: Old answers wrap the condition
  ansible.builtin.ping:
  when: "{{ my_var }}" # when: is already Jinja — the braces are the error

It 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 15
act only where the check passed

Any 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 safely
a list under when: is AND — every line must pass

is 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 task
the condition runs for every item

htop 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. 1 Why is when: "{{ my_var }}" wrong while msg: "{{ my_var }}" is right?

  2. 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. 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.

next up 19 · Loops, and the legacy with_* 6 min · unit 5