Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsAnsible’s loop keyword repeats one task for every element in a list. The current element is available as item by default, so a single task can install many packages, create many users, inspect many files, or process inventory hosts. The important constraints are that the loop expression must produce a list, nested data should usually be reshaped before iteration, and retry settings such as until apply independently to each loop item.
Antihost offers Linux VPS plans with full root access, unlimited traffic on a 1 Gbps port and a 14-day free trial.
What an Ansible loop does
A task with loop is still one task definition, but Ansible executes it once for each value in the supplied list. During each execution, the current value is assigned to item. When the list is empty, the task has no iterations; when it has five elements, Ansible performs five task executions.
As an Amazon Associate I earn from qualifying purchases.
The simplest form is a literal list:
- name: Ensure each package is installed
ansible.builtin.package:
name: '{{ item }}'
state: present
loop:
- nginx
- postgresql
- redis
Ansible runs the package task three times, once with each package name. The loop keyword is the preferred concise form for ordinary list iteration. The older with_* family remains available for compatibility and for cases where a lookup form is clearer.
Loop over a variable safely
Most playbooks keep data in variables rather than embedding it in a task. The variable used by loop must resolve to a list:
#1 Best Overall
vars:
users:
- name: alice
shell: /bin/bash
- name: bob
shell: /bin/zsh
tasks:
- name: Create users
ansible.builtin.user:
name: '{{ item.name }}'
shell: '{{ item.shell }}'
state: present
loop: '{{ users }}'
Each element here is a dictionary, so fields are accessed with expressions such as item.name. Dot notation is convenient when keys are simple identifiers; bracket notation, such as item['display-name'], is safer for keys containing punctuation or names that could collide with dictionary methods.
Why “value is not a list” appears
loop rejects a string. A lookup plugin can return a comma-separated string unless it is explicitly asked for a list, which commonly produces an error saying that the loop value is not a list. Prefer query(), because it always returns a list:
- name: Run against every host in the inventory
ansible.builtin.debug:
msg: '{{ item }}'
loop: '{{ query("inventory_hostnames", "all") }}'
The equivalent form with lookup() supplies wantlist=True:
- name: Run against every host in the inventory
ansible.builtin.debug:
msg: '{{ item }}'
loop: '{{ lookup("inventory_hostnames", "all", wantlist=True) }}'
Use the same rule for any lookup plugin: verify whether it returns one value or a collection, and request list semantics when necessary. Do not fix the error by blindly splitting a string; a plugin may return values whose contents legitimately include commas.
Looping through dictionaries
A dictionary list is the usual shape for structured configuration. The task can use any fields from the current record:
- name: Create accounts with their primary groups
ansible.builtin.user:
name: '{{ item.name }}'
group: '{{ item.primary_group }}'
state: present
loop: '{{ users }}'
If a field may be absent, decide explicitly whether to provide a default, skip the record, or fail validation. A conditional skip can be attached to the task:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →- name: Create only enabled accounts
ansible.builtin.user:
name: '{{ item.name }}'
state: present
loop: '{{ users }}'
when: item.enabled | default(false)
Keep the data shape consistent. Mixing strings and dictionaries in one list forces every task to branch on the element type and makes failures harder to diagnose.
Control labels, variable names, and indexes
loop_control manages how a loop is displayed and which variables it exposes. A label keeps output readable without changing the value passed to the module:
- name: Show a concise user label
ansible.builtin.debug:
msg: '{{ item.name }}'
loop: '{{ users }}'
loop_control:
label: '{{ item.name }}'
For nested tasks or records where item is ambiguous, assign a descriptive loop variable and, when needed, an index variable:
Rank #2
- name: Display users with their positions
ansible.builtin.debug:
msg: '{{ user_index }}: {{ user_record.name }}'
loop: '{{ users }}'
loop_control:
loop_var: user_record
index_var: user_index
| Control | Purpose | Example |
|---|---|---|
label |
Shows a concise value in task output while the full element remains available. | label: '{{ item.name }}' |
loop_var |
Renames the variable holding the current element. | loop_var: user_record |
index_var |
Provides the current zero-based index in a separate variable. | index_var: user_index |
Use a custom variable when a task includes another task file or when an outer and inner loop would otherwise both use item. Giving variables meaningful names makes conditions and registered-result processing much easier to read.
Shape nested data before looping
Ansible loops are simplest when each iteration receives one complete record. Instead of trying to nest several task-level loops, reshape the data with Jinja filters and then iterate once.
Preserve one-level flattening
The older with_items form implicitly flattens one level of nested lists. To preserve that behavior while migrating to loop, use flatten(1):
- name: Process values from the old with_items shape
ansible.builtin.debug:
msg: '{{ item }}'
loop: '{{ items | flatten(1) }}'
The argument 1 matters: it removes exactly one nested list level rather than recursively flattening every list. Recursive flattening can destroy structure that later tasks need.
Generate a Cartesian product
Use product when every value in one list must be paired with every value in another list. Convert the result to a list before passing it to loop:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minute- name: Show every user and database combination
ansible.builtin.debug:
msg: 'user={{ item.0 }}, database={{ item.1 }}'
loop: '{{ users | product(databases) | list }}'
Each loop item is a two-element pair. For three input lists, the product produces three-element tuples, so the task must use the corresponding indexes.
Pair parallel lists with zip
zip combines elements at the same position in multiple lists. It is useful when one list contains names and another contains matching identifiers or settings:
- name: Assign each account its matching numeric ID
ansible.builtin.debug:
msg: '{{ item.0 }} receives ID {{ item.1 }}'
loop: '{{ usernames | zip(user_ids) | list }}'
Make sure the source lists have the intended lengths. A short list limits the resulting pairs, so validate input when every source value is required.
Nested loops with include_tasks
When a loop includes a task file that has its own loop, the default variable name item would be reused. Ansible warns about the collision, and the inner loop can hide the outer value. Rename the outer variable with loop_control.loop_var:
Rank #3
- name: Apply settings for each application
ansible.builtin.include_tasks: application-settings.yml
loop: '{{ applications }}'
loop_control:
loop_var: outer_application
Inside application-settings.yml, the inner task can use its normal item while still referring to outer_application:
- name: Create each application directory
ansible.builtin.file:
path: '/srv/{{ outer_application.name }}/{{ item }}'
state: directory
mode: '0755'
loop: '{{ outer_application.directories }}'
This pattern is safer than relying on a warning or trying to recover an overwritten item. For many cases, flattening or building a combined list first is clearer than nesting task loops at all.
Retry every loop item with until
until repeats a task until its condition evaluates to true. retries limits the retry count and delay sets the wait between attempts:
- name: Wait for the web service
ansible.builtin.command: systemctl is-active nginx
register: service_status
until: service_status.rc == 0
retries: 5
delay: 10
The current Ansible guide documents defaults of three retries and five seconds when until is used. Set both values explicitly when the timing matters. A registered result includes an attempts value, which lets later tasks report how many tries were needed.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
When until and loop appear on the same task, Ansible evaluates the condition separately for each item. One item can succeed on its first attempt while the next item receives all configured retries:
- name: Wait for each endpoint to answer
ansible.builtin.command: 'curl --fail --silent http://{{ item }}/health'
register: endpoint_check
until: endpoint_check.rc == 0
retries: 5
delay: 10
loop: '{{ service_hosts }}'
loop_control:
label: '{{ item }}'
Use a condition based on the registered result for the current task. Do not test a variable that is set only after the loop has completed, because that would not describe the current attempt.
Use registered results from a loop
A looped task registered with register produces a result object containing a results collection. Each element contains the original loop value and the module’s return data. A file check can therefore be followed by a task that handles only missing files:
- name: Check required files
ansible.builtin.stat:
path: '{{ item }}'
loop: '{{ paths }}'
register: file_checks
- name: Report missing files
ansible.builtin.debug:
msg: '{{ item.item }} is missing'
when: not item.stat.exists
loop: '{{ file_checks.results }}'
In the second task, the outer item is one result record. Its original loop value is therefore available as item.item, while module fields such as item.stat.exists describe the check. A custom loop_var on the second task can make this less visually confusing.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Loop over inventory hosts
Ansible exposes inventory groups as lists. To display every host in the inventory, use groups['all']:
Rank #4
- name: Show every inventory host
ansible.builtin.debug:
msg: '{{ item }}'
loop: '{{ groups["all"] }}'
ansible_play_batch represents the hosts in the current play batch, which is useful when serial execution or failure handling means that the batch is smaller than the complete inventory:
- name: Show hosts in the current batch
ansible.builtin.debug:
msg: '{{ item }}'
loop: '{{ ansible_play_batch }}'
For an inventory subset selected by a pattern, use the inventory-hostnames lookup through query():
- name: Show database hosts
ansible.builtin.debug:
msg: '{{ item }}'
loop: '{{ query("inventory_hostnames", "databases") }}'
Remember that a host loop inside a play is different from the play’s normal host iteration. A task already runs once per host targeted by the play; looping over groups['all'] inside that task makes every play host process every inventory host. Use this deliberately, for example when building a cross-host report.
Migrate from with_* to loop
Most older with_* constructs can be expressed with loop, but migration is not always a text-only replacement. Preserve lookup behavior and flattening semantics.
| Older form | Loop equivalent | Important detail |
|---|---|---|
with_list |
loop |
Direct mapping when the input is already a flat list. |
with_items |
loop: '{{ values | flatten(1) }}' |
Preserves its implicit single-level flattening. |
with_indexed_items |
loop: '{{ values | flatten(1) }}' with loop_control.index_var |
Use the index variable instead of relying on a two-element indexed item shape. |
Lookup-specific forms such as with_fileglob |
Often keep the original form | The with_* syntax can express the lookup more clearly; a forced conversion may obscure intent. |
After migration, check three things: whether the new expression returns a list, whether nested lists were flattened to the same depth, and whether the task still refers to the correct element fields. Run the playbook in a safe environment before changing a broad set of tasks.
Debug loops and diagnose common failures
When iteration behaves unexpectedly, inspect the actual value and increase Ansible’s verbosity:
ansible-playbook site.yml -v
ansible-playbook site.yml -vv
Use -v or -vv when you need module arguments, returned values, or retry details that are hidden at normal verbosity. A temporary debug task can reveal both the type and content of a variable:
Free tools Windows power users keep installed
One-click scans. No signup required.
- name: Inspect loop input
ansible.builtin.debug:
var: users
- “The loop value must be a list.” The expression resolved to a string, undefined value, or another non-list type. Use
query()orlookup(..., wantlist=True)for lookup results, and verify variable definitions. - Fields such as
item.namefail. The current element is not a dictionary with that key. Inspect one element and make the input shape consistent. - The outer value disappears in an included task. Both loops are using
item. Set an outerloop_varand refer to that name inside the included file. - Too many or too few iterations occur after migration. Compare the old and new flattening behavior.
with_itemsflattened one nested level; plainloopdoes not. - A retry condition never succeeds. Check the registered variable and return code for the current item. Confirm that the command is idempotent enough to repeat and that
delayandretriesmatch the service’s startup time. - Output is unreadable. Add
loop_control.labelor rename the variable withloop_varso task output identifies the record being processed.
Choose the right looping approach
| Situation | Recommended approach | Reason |
|---|---|---|
| Flat package, path, or name list | loop |
Minimal syntax and direct item access. |
| List of dictionaries | loop plus a descriptive label |
Fields remain structured and output stays readable. |
| Lookup output | query() or lookup(..., wantlist=True) |
Guarantees list input. |
| Nested lists from an old task | flatten(1) |
Matches with_items’s single-level flattening. |
| Every combination of two collections | product(...) | list |
Creates a Cartesian product before iteration. |
| Parallel values at matching positions | zip(...) | list |
Pairs corresponding elements. |
| Task file containing another loop | include_tasks with an outer loop_var |
Prevents item collisions. |
| Eventually consistent service | until with explicit retries and delay |
Retries independently for each loop item. |
A lookup-specific legacy construct such as with_fileglob |
Often retain with_* |
The older syntax may communicate the lookup’s intent more clearly. |
Practical loop checklist
- Confirm that the expression after
loop:resolves to a list, not a string. - Use
query()for lookup plugins unless you have deliberately requested list output another way. - Keep each iteration’s data in a predictable shape, preferably one dictionary per record.
- Use
loop_control.labelfor readable output andloop_varfor nested task files. - Shape nested data with
flatten,product, orzipbefore the task runs. - When using
until, register the task, set retry timing explicitly, and remember that the condition is evaluated per item. - Process a registered loop through its
resultscollection; the original value is commonly found atitem.item. - During a migration from
with_*, preserve lookup semantics and the exact flattening depth. - Use
ansible-playbook -vor-vvto inspect values, module results, and retry attempts.
For most new tasks, start with loop and a list. Add loop_control when names or output need clarification, reshape data before iteration when the input is nested, and combine until with explicit retry settings when each item may need time to become ready.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




