Introduction

loop repeats a task for each item in a list. It replaces the older with_items, with_dict, and with_fileglob keywords with a single, consistent syntax. Each iteration, the current item is available as {{ item }}.

Basic Loop

---
- name: Loop examples
  hosts: all
  become: true
  tasks:
    # Install multiple packages
    - name: Install packages
      ansible.builtin.apt:
        name: "{{ item }}"
        state: present
      loop:
        - nginx
        - redis
        - postgresql-client
        - curl

    # More efficient — pass the entire list to apt
    - name: Install packages (better)
      ansible.builtin.apt:
        name:
          - nginx
          - redis
          - postgresql-client
        state: present

Loop Over Dictionaries

# List of dicts
- name: Create users
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
    shell: "{{ item.shell | default('/bin/bash') }}"
  loop:
    - { name: alice, groups: "developers,docker" }
    - { name: bob, groups: "developers,sudo" }
    - { name: charlie, groups: "dbadmins" }

# Loop over dict key-value pairs
- name: Set sysctl values
  ansible.posix.sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_set: true
  loop: "{{ sysctl_settings | dict2items }}"
  vars:
    sysctl_settings:
      net.ipv4.ip_forward: 1
      vm.swappiness: 10
      net.core.somaxconn: 65535

Loop Over a Variable

vars:
  packages:
    - nginx
    - redis
    - postgresql

  users:
    - name: alice
      role: admin
    - name: bob
      role: developer

tasks:
  - name: Install packages from variable
    ansible.builtin.apt:
      name: "{{ item }}"
    loop: "{{ packages }}"

  - name: Create users from variable
    ansible.builtin.user:
      name: "{{ item.name }}"
    loop: "{{ users }}"

Loop Index

Access the loop index and other metadata with loop_var attributes:

- name: Show loop info
  ansible.builtin.debug:
    msg: >
      Item {{ ansible_loop.index }} of {{ ansible_loop.length }}:
      {{ item }}
  loop:
    - alpha
    - beta
    - gamma
  loop_control:
    extended: true

Available variables with extended: true:

VariableDescription
ansible_loop.indexCurrent iteration (1-based)
ansible_loop.index0Current iteration (0-based)
ansible_loop.firstTrue on first iteration
ansible_loop.lastTrue on last iteration
ansible_loop.lengthTotal number of items
ansible_loop.revindexIterations remaining (1-based)
ansible_loop.previtemPrevious item
ansible_loop.nextitemNext item

Loop with Condition

# Only process items matching a condition
- name: Install production packages only
  ansible.builtin.apt:
    name: "{{ item.name }}"
  loop:
    - { name: nginx, envs: [production, staging] }
    - { name: debug-tools, envs: [development] }
    - { name: redis, envs: [production, staging, development] }
  when: env in item.envs

# Skip certain items
- name: Create active users only
  ansible.builtin.user:
    name: "{{ item.name }}"
  loop: "{{ users }}"
  when: item.active | default(true)

Register with Loop

- name: Check service status
  ansible.builtin.command: "systemctl is-active {{ item }}"
  loop:
    - nginx
    - redis
    - postgresql
  register: service_status
  ignore_errors: true

# Results are in service_status.results (list)
- name: Show failed services
  ansible.builtin.debug:
    msg: "{{ item.item }} is DOWN"
  loop: "{{ service_status.results }}"
  when: item.rc != 0

Nested Loops

# Loop over nested lists
- name: Create directories for each user
  ansible.builtin.file:
    path: "/home/{{ item.0 }}/{{ item.1 }}"
    state: directory
    owner: "{{ item.0 }}"
  loop: "{{ users | product(directories) | list }}"
  vars:
    users: [alice, bob]
    directories: [documents, projects, scripts]
# Creates: /home/alice/documents, /home/alice/projects, etc.

# With subelements
- name: Add SSH keys for each user
  ansible.posix.authorized_key:
    user: "{{ item.0.name }}"
    key: "{{ item.1 }}"
  loop: "{{ users | subelements('ssh_keys') }}"
  vars:
    users:
      - name: alice
        ssh_keys:
          - "ssh-ed25519 AAAA... alice@laptop"
          - "ssh-ed25519 BBBB... alice@desktop"
      - name: bob
        ssh_keys:
          - "ssh-ed25519 CCCC... bob@laptop"

Loop with Fileglob

# Copy all config files
- name: Copy config files
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: /etc/myapp/
    mode: "0644"
  loop: "{{ lookup('fileglob', 'files/configs/*.conf') | split(',') }}"

# Template all .j2 files
- name: Deploy templates
  ansible.builtin.template:
    src: "{{ item }}"
    dest: "/etc/myapp/{{ item | basename | regex_replace('\\.j2$', '') }}"
  with_fileglob:
    - templates/myapp/*.j2

Loop Control

# Custom loop variable name (for nested loops)
- name: Outer loop
  ansible.builtin.include_tasks: inner.yml
  loop:
    - alice
    - bob
  loop_control:
    loop_var: username    # Use 'username' instead of 'item'

# Pause between iterations
- name: Rolling restart
  ansible.builtin.service:
    name: myapp
    state: restarted
  loop: "{{ groups['webservers'] }}"
  loop_control:
    pause: 30             # Wait 30 seconds between each host

# Custom label for output
- name: Deploy configs
  ansible.builtin.template:
    src: "{{ item.src }}"
    dest: "{{ item.dest }}"
  loop:
    - { src: app.conf.j2, dest: /etc/app.conf, env: prod }
    - { src: db.conf.j2, dest: /etc/db.conf, env: prod }
  loop_control:
    label: "{{ item.dest }}"  # Shows just the dest in output (not the whole dict)

until (Retry Loop)

# Retry until condition is met
- name: Wait for application to start
  ansible.builtin.uri:
    url: "http://localhost:8080/health"
    status_code: 200
  register: result
  until: result.status == 200
  retries: 10         # Max attempts
  delay: 5            # Seconds between retries

# Retry command until output matches
- name: Wait for cluster to be ready
  ansible.builtin.command: kubectl get nodes
  register: nodes
  until: "'Ready' in nodes.stdout"
  retries: 30
  delay: 10

Migration from with_* Keywords

Old SyntaxModern Syntax
with_items: listloop: list
with_list: listloop: list
with_dict: dictloop: "{{ dict | dict2items }}"
with_fileglob: patternloop: "{{ lookup('fileglob', pattern, wantlist=True) }}"
with_sequence: start=1 end=5loop: "{{ range(1, 6) | list }}"
with_nested: [a, b]loop: "{{ list_a | product(list_b) | list }}"
with_subelementsloop: "{{ list | subelements('key') }}"

Troubleshooting

"item is undefined"

You're using item outside a loop, or inside an include_tasks with a different loop_var:

# If outer loop uses loop_var: outer_item
# inner tasks must use outer_item, not item

Loop Over Empty List

# Avoid errors on empty lists
- name: Process items
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ my_list | default([]) }}"

Conclusion

loop is the modern way to iterate in Ansible. Use it with simple lists, dictionaries (dict2items), nested structures (product, subelements), and file patterns (fileglob). Key tips: use loop_control for custom variable names in nested loops, label to clean up output, and pause for rolling operations. Use until + retries for retry loops. For modules that accept lists natively (like apt), pass the whole list instead of looping.