Start with the input contract
A list of names does not by itself define a safe change. Specify each service’s fields, accepted types, and the meaning of enabled before building the loop. A comma-separated string is not necessarily a file list; query or lookup with wantlist=True can obtain lists where the plugin provides that result. Also check duplicates and unexpected values. In a fictional funds example, two entries for the same service could repeat an action with persistent effects. This validation should explain the set being processed and the conditions that exclude each element.
Read the aggregate and then each result
Register on a loop produces a structure containing results for its iterations. Aggregate changed can be true because only one item reported a change. Aggregate failed can indicate failure in an item; aggregate skipped represents all items being skipped. Do not use these summaries to assign the same state to every service. Traverse the results while retaining the name or identifier and check which fields are actually present. An executed command may have rc, whereas a skipped item may lack it. Acceptance should distinguish change, no change, failure, and no execution according to the service contract.
Conditions and identity in nested loops
When is evaluated for each loop item. If enabled arrives as text, confirm the contract and convert explicitly where appropriate rather than relying on implicit coercion. Do not add Jinja delimiters to a condition that is already an expression. An include_tasks iterating over services can run a second loop over ports; use different loop_var names, such as service and port, to retain both identities. Without that distinction, a condition or message can refer to the port while the operator believes it describes the service. Review expression context before attributing the problem to host concurrency.
Skipped results and attempts
A task skipped because of when still creates its registered result. To determine whether it ran, inspect skip state rather than looking only for an undefined variable. When loop is combined with until, attempts apply to each item; this is not a transaction that repeats the entire set. If the action has effects, document what another attempt could repeat. For modules that directly accept a package list, evaluate that interface before making one call per package. The choice depends on module semantics, the need for per-element results, and constraints on the shared resource.
Workshop: three services without external infrastructure
This course’s lab uses three fictional services on localhost: eligible alpha, excluded beta, and eligible gamma. A debug task reports a change only for alpha, making the aggregate observable without changing real services. Assertions check three results, each item’s identity, beta being skipped, and aggregate changed being true. Another skipped task confirms that its registered result remains defined. Run this lab in a temporary environment with the documented version and compare observations. The change report is deliberately artificial in this exercise: it demonstrates result mechanics without proving a functional change in any application.
Summary and evidence handover
Prepare a matrix containing expected service, applied condition, observed action, and acceptance decision. Identify elements left out of execution and whether that exclusion was authorized. Use loop_control.label to improve readability, but do not present it as token or password protection; sensitive data needs appropriate handling, including no_log where applicable and control of later outputs. Give the next colleague a sanitized summary with identifiers that enable investigation of differences. Connect this work with inventory, secret protection, and recovery: before repeating execution, distinguish an unchanged service from a service that was never processed.
# Local fixture: inspect per-item results, not real service health.
loop: "{{ services }}"
loop_control:
loop_var: service
label: "{{ service.name }}"
when: service.enabled
register: service_results
# service_results.results retains each item and its own state.Three services produce one changed, one unchanged, and one skipped result. changed=true in the summary identifies a change in at least one item without establishing coverage of all items.
Common pitfalls
Treating aggregate changed as global success; reading rc at the wrong level; reusing item in nested loops; using label as secret protection.
Related topics: Inventory and execution context · Batch orchestration · Failures and recovery
The acceptance unit is the expected service. Preserve its identity, distinguish states, and explain differences between intended and observed scope.
Reference: Loops · Ansible community 14 / ansible-core 2.21 documentation consulted 2026-09-30; executor, collection and plugin compatibility requires confirmation