when: Running a Task Only Where It Applies

Scenario: A task guarded by when: inventory_hostname in dbservers has been skipping on every host for a year. Nobody noticed, because a skipped task is green.

New words, in plain English

when is the mechanism for "this task applies to some hosts and not others". It is evaluated per host, so one play can converge a heterogeneous estate.

The single most important syntax rule: when is already a Jinja2 expression context, so you do not wrap it in {{ }}. Write when: app_env == 'production', not when: "{{ app_env == 'production' }}". The braced form usually still works, but it produces a string that Ansible then re-evaluates, and the failure modes are confusing.

The scenario above shows the dangerous pattern. dbservers is a group name, not a variable. In a Jinja2 expression it is an undefined name, so the condition is false everywhere - forever, silently, greenly. The correct forms are:

Conditions combine with and, or, not, and a list of conditions is implicitly ANDed, which reads more clearly than a long single line.

Analogy: when is the bouncer's list. A name that is not on the list is turned away quietly - no announcement, no complaint. If you wrote the list wrong, everyone gets turned away and the venue simply stays empty all night.

A worked example

# WRONG - `dbservers` is a group name, not a variable. Always false.
- name: Install DB tooling
  ansible.builtin.package:
    name: postgresql-client
  when: inventory_hostname in dbservers

# RIGHT - two equivalent, correct forms
  when: "'dbservers' in group_names"
  when: inventory_hostname in groups['dbservers']

# A list of conditions is ANDed - and reads far better
- name: Restart only healthy production web servers
  ansible.builtin.service:
    name: app
    state: restarted
  when:
    - app_env == 'production'
    - "'webservers' in group_names"
    - health.rc == 0

# Branching on a fact
  when: ansible_facts['os_family'] == 'Debian'

Because a skipped task is reported green, a broken condition is invisible in normal output. Two habits catch it. Run with -v and read the skipping: lines - if a task skips on every single host, that is a red flag, not a success. And in ansible.cfg, display_skipped_hosts = False hides skips to reduce noise, which is fine for a converged estate and actively harmful while you are still developing a play.

When a task depends on a registered result, remember that the result exists only if the earlier task ran. A when on the first task that skips it leaves the second task referencing an object with no stdout, which fails with a message about a dict having no attribute.

Warning: A when that is false on every host produces a completely green run in which nothing happened. Green is not the same as correct.
Goal: Put this to work in the ansible-loops-conditionals lab. Open /labs/ansible, pick ansible-loops-conditionals, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.