Skip to content

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

loop_register.yaml
- name: Loop And Register
  hosts: servers
  tasks:
    - name: "Echo: {{ item }}"
      ansible.builtin.command: "echo {{ item }}"
      loop:
        - one
        - two
        - three
      register: loop_result

loop_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_result
loop_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 }}"
one

Checking 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 .results shape is really there before assuming what’s inside it.
  • Use selectattr/map on .results to filter and extract per-item outcomes, rather than writing a second loop with a manual when: check when a filter pipeline does the same job more directly.
  • Remember: looping over .results means item is the per-iteration result object, and the original loop value is at item.item — say it out loud if it helps; the naming collision is the whole source of confusion here, not the underlying mechanics.
Last updated on