Skip to content

when With import_tasks and include_tasks


Here’s the sharpest, most practical consequence of the static/dynamic split: when:, attached to import_tasks versus include_tasks, behaves in two genuinely different ways — and the difference can silently change which tasks actually run.

when: On import_tasks: Applied To Every Expanded Task Individually

Since import_tasks splices the imported file’s tasks directly into the play at parse time, a when: attached to it gets copied onto every single task that came from that file — as if you’d manually added the same condition to each one separately. Each copy is evaluated independently, at the moment that specific task actually runs.

when: On include_tasks: Evaluated Once, For The Whole Include

include_tasks, being a genuine runtime node in the task list, evaluates its when: exactly once — deciding whether the entire include happens at all. If true, every task inside runs, as a unit, with no further rechecking of that specific condition per task. If false, none of them run.

Why This Distinction Matters In Practice

Here’s where it actually bites: what happens when a task inside the included file changes the very variable the when: condition depends on?

conditional_tasks.yaml
- name: Task A — sets should_continue to false
  ansible.builtin.set_fact:
    should_continue: false

- name: Task B — depends on should_continue
  ansible.builtin.debug:
    msg: "Task B ran"

With import_tasks:

- name: Play
  hosts: fedora
  vars:
    should_continue: true
  tasks:
    - name: Import with condition
      ansible.builtin.import_tasks: conditional_tasks.yaml
      when: should_continue
ok: [ubuntu] => (Task A ran, should_continue is now false)
skipping: [ubuntu] (Task B — its own copy of the when: is rechecked, now false)

Task A runs, since should_continue was true at that point — and sets it to false. Task B has its own independent copy of when: should_continue, rechecked fresh at the moment it runs — and by then, the condition is false, so Task B is skipped.

With include_tasks, the identical scenario behaves completely differently:

- name: Play
  hosts: fedora
  vars:
    should_continue: true
  tasks:
    - name: Import with condition
      ansible.builtin.include_tasks: conditional_tasks.yaml
      when: should_continue
ok: [ubuntu] => (Task A ran, should_continue is now false)
ok: [ubuntu] => (Task B ran anyway)

should_continue was checked once, when Ansible reached the include_tasks line — it was true then, so the decision “run this whole file” was made once and for all. Task B runs regardless of Task A changing the variable, because the include itself was never rechecked — only the individual tasks inside import_tasks’s expanded form get that per-task treatment.

Visibility: --list-tasks And --list-tags

A related consequence: ansible-playbook site.yaml --list-tasks previews every task that would run, without actually running anything. Since import_tasks (and import_playbook, import_role) is fully resolved at parse time, every individual task it brings in shows up in that listing, by name. include_tasks (and include_role) hasn’t been resolved yet at that point — it shows up as just the include statement itself, with none of its actual contents visible until the playbook genuinely runs and reaches that point.

A Combined Example

Given everything above, the choice isn’t just stylistic:

- name: "Import — full visibility, but when: applies per-task"
  ansible.builtin.import_tasks: setup.yaml
  when: should_run_setup

- name: "Include — single all-or-nothing gate, but hidden from --list-tasks"
  ansible.builtin.include_tasks: setup.yaml
  when: should_run_setup

Same file, same condition, meaningfully different behavior and meaningfully different visibility, depending purely on which keyword you chose.

Best Practices

  • Remember when: on import_tasks rechecks per expanded task — a task inside the imported content can genuinely change the outcome for later tasks in that same file.
  • Remember when: on include_tasks is a single all-or-nothing gate, decided once, for the whole file.
  • Use --list-tasks to preview a playbook before running it, but remember dynamic includes won’t reveal their contents there — reach for import_tasks specifically when full preview visibility matters to you.
  • Choose based on the actual behavior you want, not just which keyword happens to be shorter to type — the two aren’t interchangeable the moment a when: or a variable-dependent filename is involved.
Last updated on