Skip to content

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:

filtered_loop.yaml
- 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 even
skipping: [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_restart
ok: [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 separate when: 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 zip with 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.
Last updated on