Loops, Filters and when
Loops don’t have to iterate over a plain, hand-written list. This chapter combines everything from the manipulating-data section directly into loop: itself, clarifies a genuinely non-obvious detail about how when: behaves on a looped task, and introduces zip for looping two lists together in lockstep.
Feeding A Filter Pipeline Directly Into loop:
The selectattr + map pipeline from the manipulating-data section can be loop:’s source directly, filtering which items ever become iterations at all:
- name: Filtered Loop Demo
hosts: servers
vars:
apps:
- name: web1
status: running
- name: web2
status: stopped
- name: db1
status: running
tasks:
- name: "Restart: {{ item }}"
ansible.builtin.debug:
msg: "Would restart {{ item }}"
loop: "{{ apps | selectattr('status', 'equalto', 'running') | map(attribute='name') | list }}"ok: [ubuntu] => (item=web1) => { "msg": "Would restart web1" }
ok: [ubuntu] => (item=db1) => { "msg": "Would restart db1" }web2 never appears at all — not even as a skipping line — because it was filtered out of the loop’s own source list before the loop ever began. Compare this to looping over every app and using when: to skip the stopped ones instead: functionally similar, but the output looks meaningfully different — one approach never mentions the excluded items, the other explicitly reports each one as skipped. Which you want depends on whether that visibility is useful to you or just noise.
when: Inside A Loop Runs Per Item, Not Once
Worth stating plainly, since it’s easy to assume otherwise: a when: attached to a looped task is evaluated independently for every single iteration, using that iteration’s own item — not once, up front, for the whole task.
- name: "Process Even Numbers"
ansible.builtin.debug:
msg: "Processing {{ item }}"
loop: [1, 2, 3, 4, 5, 6]
when: item is evenskipping: [ubuntu] => (item=1)
ok: [ubuntu] => (item=2) => { "msg": "Processing 2" }
skipping: [ubuntu] => (item=3)
ok: [ubuntu] => (item=4) => { "msg": "Processing 4" }Each number gets its own fresh evaluation of item is even — this is a per-item gate, not a single condition deciding whether the entire loop runs at all.
Looping Two Lists In Parallel With zip
zip, from the lists chapter, pairs corresponding elements from two lists — useful for looping over both together, in lockstep:
- name: Filtered Loop Demo
hosts: fedora
vars:
names: [alice, bob, carol]
roles_list: [admin, viewer, editor]
tasks:
- name: "Assign corresponding roles"
ansible.builtin.debug:
msg: "{{ item[0] }} gets role {{ item[1] }}"
loop: "{{ names | zip(roles_list) | list }}"ok: [ubuntu] => (item=['alice', 'admin']) => { "msg": "alice gets role admin" }
ok: [ubuntu] => (item=['bob', 'viewer']) => { "msg": "bob gets role viewer" }
ok: [ubuntu] => (item=['carol', 'editor']) => { "msg": "carol gets role editor" }Since zip produces plain pairs, not dictionaries, access them by position — item[0], item[1] — rather than named keys. You can also use item.0, item.1.
A Realistic Combination: Filter, Then Loop, Then Conditionally Skip
Both techniques from this chapter, layered together:
- name: Filtered Loop Demo
hosts: fedora
vars:
servers:
- name: web1
status: running
needs_restart: true
- name: web2
status: running
needs_restart: false
- name: db1
status: stopped
needs_restart: true
tasks:
- name: "Restart web server if needed"
ansible.builtin.debug:
msg: "Restarting {{ item.name }}"
loop: "{{ servers | selectattr('status', 'equalto', 'running') | list }}"
when: item.needs_restartok: [fedora] => (item={'name': 'web1', 'status': 'running', 'needs_restart': True}) => {
"msg": "Restarting web1"
}
skipping: [fedora] => (item={'name': 'web2', 'status': 'running', 'needs_restart': False})db1 never becomes an iteration at all — it’s filtered out of loop:’s own source, since it isn’t running. web2 does become an iteration, and is explicitly reported as skipped, since needs_restart is false for it. Only web1 actually runs. Two different exclusion mechanisms, doing two genuinely different jobs, both visible in the same task.
Best Practices
- Filter inside
loop:’s own source expression when you don’t want excluded items to show up in output at all. Use a separatewhen:when the skip itself is meaningful to see, or when the condition genuinely needs each item’s own data evaluated individually after the loop’s source is already otherwise fixed. - Remember
when:on a looped task evaluates fresh, per item — never assume it’s gating the whole task as a single unit. - Use
zipwith positional access (item.0,item.1) for looping two related lists together, and reach for a list of dictionaries instead if the pairing needs named, self-documenting fields.