Registering Results From Loop
Back in the variables section, register gave you a flat structure — stdout, rc, changed, all sitting right at the top level. Combine register with a loop, and that shape changes completely. This is the single most common surprise anyone hits the first time they try it, and it’s worth a full chapter to get right.
The Surprise: register Inside A Loop Gives You A List
- name: Loop And Register
hosts: servers
tasks:
- name: "Echo: {{ item }}"
ansible.builtin.command: "echo {{ item }}"
loop:
- one
- two
- three
register: loop_resultloop_result here is not shaped like a normal registered result. There’s no top-level stdout at all — instead, everything lives under a results key, holding one entry per loop iteration, each shaped the way a single task’s registered result normally would be.
Inspecting The Full Structure
Same habit from the variables section — dump it with var: before assuming anything about its shape:
- name: Dump the whole structure
ansible.builtin.debug:
var: loop_resultloop_result: {
"changed": true,
"results": [
{"item": "one", "stdout": "one", "rc": 0, "changed": true, ...},
{"item": "two", "stdout": "two", "rc": 0, "changed": true, ...},
{"item": "three", "stdout": "three", "rc": 0, "changed": true, ...}
]
}Notice there’s still a top-level changed — but it’s an aggregate, true if any single iteration changed, not the detailed per-item information. That detail is all inside results.
Accessing Individual Results
Index into results directly, exactly like any other list:
msg: "{{ loop_result.results[0].stdout }}"oneChecking Which Items Succeeded Or Failed
Since results is just a list of dictionaries, the filtering tools from the manipulating-data section apply directly:
- name: Show which items reported a change
ansible.builtin.debug:
msg: "{{ loop_result.results | selectattr('changed') | map(attribute='item') | list }}"["one", "two", "three"]Filter, then extract — the exact same selectattr + map pattern from that section’s collections chapter, applied here to a loop’s own results instead of a hand-written list.
A Gotcha: Looping Over .results Itself
Here’s where the real confusion sets in. If you loop over results in a later task to process each outcome individually, the loop variable item in that new loop refers to a whole per-iteration result object — not the original value from the first loop.
- name: Run something for each value
ansible.builtin.command: "echo {{ item }}"
loop:
- one
- two
- three
register: results
- name: Loop over the results themselves
ansible.builtin.debug:
msg: "Original value: {{ item.item }}, stdout was: {{ item.stdout }}"
loop: "{{ results.results }}"ok: [ubuntu] => (item={'item': 'one', 'stdout': 'one', ...}) => {
"msg": "Original value: one, stdout was: one"
}In this second task, item is the whole result dictionary — {'item': 'one', 'stdout': 'one', ...} — and item.item reaches inside it to get back the original value that was looped over the first time around. Two different items, meaning two genuinely different things, one nested inside the other. This is confusing purely because of the name collision, not because the underlying idea is complicated — worth reading twice if it doesn’t click immediately.
Best Practices
- Dump a freshly registered looped result with
debug: var:before writing anything that uses it — confirm the.resultsshape is really there before assuming what’s inside it. - Use
selectattr/mapon.resultsto filter and extract per-item outcomes, rather than writing a second loop with a manualwhen:check when a filter pipeline does the same job more directly. - Remember: looping over
.resultsmeansitemis the per-iteration result object, and the original loop value is atitem.item— say it out loud if it helps; the naming collision is the whole source of confusion here, not the underlying mechanics.