Ansible Loop: Basics and Looping Across Multiple Tasks

1 October, 2026

Dirk Wening
Dirk Wening
Technical Writer

by | Oct 1, 2026

Last updated: October 1, 2026 · Reading time: 4–6 minutes

With an Ansible loop, you can run a task for each item in a list. Here you’ll find the basics, along with practical examples, and learn how to use ` include_tasks ` to loop through multiple tasks that depend on one another.

The most important points in 30 seconds

  • An Ansible loop executes a task for each element in a list, wher {{ item }} represents the current element.
  • The list can be included directly in the task, come from a variable, or consist of dictionaries with multiple values per iteration.
  • when is evaluated individually in a loop on each iteration.
  • For multiple interdependent tasks, save them in a separate file and append the ” loop ” to include_tasks.
  • Having your own loop_var via loop_control prevents name conflicts with item.

The loop is one of the most useful tools in Ansible. It ensures that a task is executed multiple times—once for each item in a list. This reduces paperwork and makes playbooks much easier to maintain. This article first covers the basics and then delves into an advanced use case: looping through an entire chain of related tasks.

Using an Ansible Loop in a Single Task

Loop is most commonly used to call a single module multiple times with different values.

- name: Mehrere Pakete installieren
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - git
    - curl

This is equivalent to three separate calls to the apt module. Within the loop, it refers to {{ item }} on the current element.

Loop Using a Variable

You can also retrieve the list from a variable instead of writing it out directly:

vars:
  paketliste:
    - nginx
    - git
    - curl

tasks:
  - name: Pakete installieren
    ansible.builtin.apt:
      name: "{{ item }}"
      state: present
    loop: "{{ paketliste }}"

Loop through a list of dictionaries

This is very commonly used when multiple values are needed per iteration:

- name: Nutzer anlegen
  ansible.builtin.user:
    name: "{{ item.name }}"
    uid: "{{ item.uid }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: "alice", uid: 1001, groups: "admins" }
    - { name: "fabian",   uid: 1002, groups: "finance" }

Conditions in Ansible Loops

“when ” is evaluated separately for each iteration:

- name: Nur bestimmte Pakete installieren
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - git
    - docker
  when: item != "docker"

Ansible Loops Across Multiple Tasks: Multiple Interrelated Loops

A simple loop is ideal when a single task needs to be executed multiple times. It gets more complicated when several tasks depend on one another. For example, a task may need the result (register) of a previous task for a when condition.

A typical example of this is setting up multiple websites. For each website, a directory should be created, a vHost configuration should be set up, and the website should be activated as soon as the vHost file has been modified.

- name: "create {{ site }} directory"
  ansible.builtin.file:
    path: "/var/www/{{ site }}"
    state: directory

- name: "create {{ site }}"
  ansible.builtin.template:
    src: vhost.j2
    dest: "/etc/apache2/sites-available/{{ site }}"
  register: vhost

- name: "enable {{ site }}"
  ansible.builtin.command: /usr/sbin/a2ensite "{{ site }}"
  register: result
  when: vhost.changed
  changed_when: "'Enabling site' in result.stdout"
  notify: apache_reload

If you were to use a loop to process each task individually here, you would have to correctly map the respective register results to the individual iterations. That makes the code unnecessarily complicated.

Learn how to reliably automate servers, software, and entire IT workflows in our hands-on Ansible training courses.

The Solution: `include_tasks` with a loop

This is where `include_tasks` comes into play. This allows you to move several related tasks to a separate file.
The `include_tasks` task itself is then placed inside the `loop` loop. This causes the entire task file to be executed once for each item on the list.

Here’s what the main playbook looks like:

- ansible.builtin.set_fact:
    sites:
      - default
      - icingaweb2

- name: create vhosts
  ansible.builtin.include_tasks: create-vhosts.yml
  loop: "{{ sites }}"
  loop_control:
    loop_var: site

Using a custom `loop_var` name (`site` instead of the default `item`) ensures that no naming conflicts occur if the included file eventually gets its own loop with `item`.

The create-vhosts.yml task file

The other tasks are stored in create-vhosts.yml:

- name: "create {{ site }} directory"
  ansible.builtin.file:
    path: "/var/www/{{ site }}"
    state: directory

- name: "create {{ site }}"
  ansible.builtin.template:
    src: vhost.j2
    dest: "/etc/apache2/sites-available/{{ site }}"
  register: vhost

- name: "enable {{ site }}"
  ansible.builtin.command: /usr/sbin/a2ensite "{{ site }}"
  register: result
  when: vhost.changed
  changed_when: "'Enabling site' in result.stdout"
  notify: apache_reload

The result in the Playbook output

The result: All three tasks are executed in full for every element in Sites, including the correctly assigned register context.

TASK [set_fact] *********************************************
ok: [localhost]
TASK [create vhosts] ****************************************
included: /Users/dwening/Documents/netways/ansible_test/create-vhosts.yml for localhost => (item=default)
included: /Users/dwening/Documents/netways/ansible_test/create-vhosts.yml for localhost => (item=icingaweb2)
TASK [create default directory] *****************************
ok: [localhost]
TASK [create default] ***************************************
ok: [localhost]
TASK [enable default] ***************************************
ok: [localhost]
TASK [create icingaweb2 directory] **************************
ok: [localhost]
TASK [create icingaweb2] ************************************
ok: [localhost]
TASK [enable icingaweb2] ************************************
ok: [localhost]
PLAY RECAP **************************************************
localhost                  : ok=10   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Conclusion: `include_tasks` makes entire task sequences loopable

Loops are easy to explain in their simplest form: a list, a task, and multiple iterations. However, as soon as multiple tasks with dependencies need to be repeated, simply looping through the task is no longer sufficient. The combination of `include_tasks` and `loop` fills exactly this gap. It treats an entire task sequence as a single unit and makes it repeatable without having to duplicate code or use Jinja2 in `when` conditions.

Do you want to use Ansible in your environment or clean up existing playbooks? Our consultants will be happy to help you with that!

How did you like our article?