skip to content

In an Ansible task, what does the when keyword do, and why is its expression written without the {{ }} delimiters?

level: juniorimportance: should knowfreq 66%

answer

  1. per-host gate, evaluated for each host
  2. raw Jinja expression, not a string
  3. braces are for interpolating into text
  4. a list of conditions means AND
  5. skipped tasks notify no handlers

basics

~20 s

The when keyword makes a task run only if its expression is true; otherwise the task is skipped for that host. The value is already evaluated as a Jinja expression, so wrapping it in curly braces is redundant and Ansible warns about it.

solid answer

~40 s

`when` is per-host task control: Ansible evaluates the expression for each host in the play and skips the task on hosts where it is false. Because the value is *already* treated as a Jinja2 expression, you write `when: ansible_facts['os_family'] == 'RedHat'`, not `when: "{{ ... }}"` — the delimiters are templating syntax for strings, and adding them here produces a warning and can turn a boolean into the string `"True"`. A YAML list of conditions is implicitly ANDed. Two things worth saying out loud: a skipped task notifies no handlers, and variables arriving from `--extra-vars` or inventory are strings, so `when: enable_tls | bool` is the safe form rather than trusting bare truthiness. `when` on a `block` applies to every task inside it, and `when` combined with `loop` is evaluated once per item.

go deeper

for a junior

Be able to write a task that runs only on one OS family, state that when is evaluated per host, and say plainly that the expression is written without curly braces.

for a middle

Explain why: the value is handed to Jinja as a bare expression, so braces are string-interpolation syntax that can yield the truthy string "False", and describe how a list of conditions ANDs.

for a senior

Show the operational traps — string values from --extra-vars, downstream tasks referencing a registered result from a skipped task, and handlers that silently stop firing because the notifying task now skips.

for a principal

Own the readability standard: when host membership should decide instead of a per-task conditional, when a guarded block beats five repeated conditions, and how much branching a playbook may carry before it should be split.

## What when does `when` is the conditional on an Ansible task. Ansible evaluates its expression separately **for each host** in the play, using that host's variables and facts, and runs the task only where the result is truthy. Where it is false, the task reports `skipped` for that host and the play moves on. ```yaml - name: Install the RPM package ansible.builtin.package: name: nginx state: present when: ansible_facts['os_family'] == 'RedHat' ``` Because evaluation is per host, one play can branch across a mixed fleet without splitting into separate plays. ## Why no curly braces In Jinja2, `{{ ... }}` means "substitute the value of this expression into surrounding text". It is for building strings. The `when` value is not a string to be templated — Ansible hands it to the template engine as a **bare expression** and uses the resulting object directly. So the variable name goes in raw: ```yaml when: install_nginx # right when: "{{ install_nginx }}" # warns, and can misbehave ``` Modern ansible-core emits a warning for the braced form ("conditional statements should not include jinja2 templating delimiters"). The practical hazard is that templating a boolean can hand back the *string* `"False"`, and a non-empty string is truthy — so the task runs when you expected it to skip. The same reasoning applies to `changed_when`, `failed_when`, `until` and `assert`'s `that`: all of them take raw expressions. ## The string-truthiness trap Variables that arrive from the command line (`-e enable_tls=false`), from an INI inventory, or from some environment sources are strings, not YAML booleans. `when: enable_tls` is then testing a non-empty string and is always true. The defensive form is explicit: ```yaml when: enable_tls | bool ``` A value defined in YAML as `enable_tls: false` really is a boolean and does not need it, but the filter is harmless and makes the task robust to how the value was supplied. ## Combining conditions A YAML list of conditions is ANDed: ```yaml when: - ansible_facts['os_family'] == 'Debian' - ansible_facts['distribution_major_version'] | int >= 12 ``` This is the readable way to express several requirements. For OR you write one expression with `or`. Jinja tests are available and idiomatic: `is defined`, `is not defined`, `is truthy`, and — against a registered result — `is failed`, `is succeeded`, `is skipped`, `is changed`. ```yaml - name: Check the health endpoint ansible.builtin.uri: url: http://localhost:8080/health register: health failed_when: false - name: Report the outage ansible.builtin.debug: msg: "health check failed" when: health is failed ``` ## when with loop, and when on a block With `loop`, the condition is evaluated **per item** — items that fail it are skipped individually, and the task as a whole may report a mix. That is a common source of confusion: `when` does not gate the loop, it gates each iteration. `when` on a `block` is distributed to every task in the block, which is the tidiest way to guard a group of related tasks with a single condition rather than repeating it five times. ## Consequences of a skip A skipped task **notifies no handlers**, because notification requires a `changed` result. It also registers a result object whose `skipped` field is true — so a later `when: earlier_task is skipped` works, but a later task that assumes `earlier_task.stdout` exists will fail with an undefined-attribute error. Guard downstream uses with `is not skipped` or `is defined`. ## Where when is the wrong tool If the condition is really "only do this on hosts in group X", the host pattern on the play or a group-scoped play is clearer than a `when` on every task. And if the condition is "only if this file does not exist yet", most modules are already idempotent — checking the state yourself with a `stat` task and a `when` re-implements, usually worse, what the module does natively.

  • A boolean passed with --extra-vars as false is not skipping the task. Why?
    Values supplied on the command line arrive as strings, so `false` is the four-character string `"false"`, and a non-empty string is truthy in Jinja. The task therefore runs. Write the condition as `when: my_flag | bool`, which coerces common string spellings to a real boolean, or pass structured data as JSON/YAML via `-e '{"my_flag": false}'`.
  • How does when behave on a task that also has a loop?
    The condition is evaluated once per item, not once for the task. Items whose condition is false are skipped individually and the remaining items still run, so the task can report a mixture of skipped and changed results. If you want to gate the whole loop, put the condition on an enclosing block or compute the list itself with a filter such as `selectattr`.
  • What does putting when on a block do?
    The condition is applied to every task inside the block, so one guard covers a group of related tasks instead of being repeated on each. It is evaluated per task at run time, using the variables current at that point, so a fact set earlier in the block can change the outcome for later tasks in the same block.

saying these in an interview costs you the question

  • Wraps the when expression in {{ }} because "it is a variable"
  • Assumes when is evaluated once for the whole play
  • Trusts bare truthiness of a flag passed via --extra-vars
  • Expects a skipped task to still notify its handler
  • Thinks when on a loop skips the entire task rather than each item

context