ansible/ansible · error · NotALoopError

The current task is not a loop.

Error message

The current task is not a loop.

What it means

NotALoopError raised by build_loop_result when task.is_loop is False. The method assembles an aggregate UnifiedTaskResult from _raw_loop_results, which only exist for loop tasks; calling it on a loop-free task is a programming error in the caller, so it fails fast instead of fabricating an empty loop result.

Source

Thrown at lib/ansible/_internal/_task.py:360

        if item_label_template := self.task.loop_control.label:
            try:
                item_label = self.task_templar.template(item_label_template)
            except AnsibleTemplateError as ex:
                display.error_as_warning('Failed to template loop_control label.', ex, obj=item_label_template)

        if item_label is ...:
            if self._loop_var is None:
                # _loop_var will be None if start_loop was not called due to a field attribute error (even if the error is for another field)
                item_label = ''
            else:
                item_label = self.task_templar.resolve_variable_expression(self._loop_var)

        utr.loop_item_label = item_label

    def build_loop_result(self, preview: bool = False) -> UnifiedTaskResult:
        if not self.is_loop:
            raise NotALoopError()

        if not preview and (self._item_index is None or len(self._raw_loop_results) not in (self._item_index, self._item_index + 1)):
            # RPFIX-9: FUTURE: can we ditch preview while retaining this safety check?
            # Loop results can be queried before or after loop results are recorded, so we need to accept a range of results.
            raise RuntimeError(f"Mismatch between item index {self._item_index} and loop result count {len(self._raw_loop_results)}.")

        # create the overall result item
        utr = UnifiedTaskResult.from_action_result_dict()
        utr.loop_results = self._raw_loop_results

        # RPFIX-5: IMPL: all the fields set in this loop could be converted to properties
        for item in self._raw_loop_results:
            if item.no_log:
                utr.no_log = True  # ensure no_log processing recognizes at least one item needs to be censored

            utr._extend_warnings(item.warnings)
            utr._extend_deprecations(item.deprecations)

View on GitHub (pinned to 9cf16a4aca)

Solutions

  1. Branch on task.is_loop (or the loop keyword) before calling build_loop_result.
  2. For non-loop tasks use the ordinary single-result path (latest_result / recorded result dict).
  3. If the task was supposed to loop, fix the templating error on the loop field that prevented loop setup.
  4. Preview mode also requires is_loop — do not use preview to bypass the check.

Example fix

# before
utr = task.build_loop_result()  # NotALoopError for normal tasks

# after
if task.is_loop:
    utr = task.build_loop_result()
else:
    utr = task.latest_result
Defensive patterns

Strategy: type-guard

Validate before calling

if task.is_loop:
    utr = task_ext.build_loop_result()

Type guard

def is_loop_task(task) -> bool:
    return bool(task.loop)

Try / catch

try:
    utr = task_ext.build_loop_result()
except NotALoopError:
    utr = task_ext.latest_result  # single-result path

Prevention

When it happens

Trigger: Plugin/strategy code calling build_loop_result() unconditionally for every task; handling a task whose `loop` keyword failed to template so is_loop ended up False even though loop semantics were expected; copying loop-specific handling code to a non-loop path.

Common situations: Custom callbacks or result processors branching incorrectly; tasks where the loop field had a templating error (the error path noted in _record_result's comment means start_loop never ran).

Related errors


AI-assisted analysis of ansible/ansible@9cf16a4aca (2026-08-15). Data as JSON: /api/errors/875bbd7fe86eacb9. Report an issue: GitHub.