Chapter 7: Control Flow
Topic 44

include vs import

ConceptControl Flow

import_* and include_* both pull in another file of tasks, a role, or a playbook, and they look interchangeable until they aren't — they are the single most confused pair in Ansible. import_* is static: the content is read and inlined at parse time, before the play runs. include_* is dynamic: the content is pulled in at run time, when execution reaches that line.

That one difference — parse time versus run time — decides how tags, loops, and when behave on the included content. Get it wrong and you produce a playbook that runs the wrong tasks, loops once instead of per item, or ignores your --tags entirely. The whole topic is one distinction and its three consequences.

Parse Time versus Run Time

import_tasks and import_playbook are processed when Ansible reads the playbook, before anything executes — so the imported tasks exist as real, individual tasks in the plan from the start. include_tasks and include_role are processed when the play reaches them at run time, so their tasks do not exist until that moment of execution. That is the entire root cause: a statically imported task is visible before the run; a dynamically included one is invisible until the line is hit.

Static vs dynamic — one timing difference, three consequences
import_* — static
Inlined at parse time. Tags propagate to every inner task; when stamps onto each task; cannot loop.
include_* — dynamic
Resolved at run time. --tags can't reach inside; when gates the whole file once; can loop.

The Tags Consequence

Tags on an import propagate to every imported task — they are inlined at parse time, so --list-tags sees them and --tags can select them. A tag on an include applies only to the include statement itself, and --tags cannot select tasks inside a dynamic include, because those tasks do not exist yet at selection time. This is the classic trap: include_tasks: deploy.yml plus --tags deploy runs nothing, because there is nothing tagged deploy in the plan when the filter is applied.

The Loop Consequence

You can loop: over an include_tasks to run a whole task file once per item, because the include is evaluated at run time and the loop drives repeated evaluation. You cannot loop an import_tasks — it is inlined at parse time, before any loop exists to drive it, so Ansible rejects a loop on an import outright with a parse-time error: You cannot use loops on 'import_tasks' statements. You should use 'include_tasks' instead. When you need a task file applied per element of a list, the include is the only form that works.

Loop an include to run a task file per site — a loop on an import is a parse-time error
- name: Configure each Larkspur site from one task file
  ansible.builtin.include_tasks: configure_site.yml
  loop: "{{ larkspur_sites }}"
  loop_control:
    loop_var: site

Each iteration pulls configure_site.yml in fresh with site bound to the current entry. Swap include_tasks for import_tasks here and the file runs once with whatever site happens to be — the loop is ignored because the import was already resolved before the loop could matter.

The when Consequence

when on an import is copied onto every imported task individually, and each one is then evaluated per host — the condition is distributed, not applied once. when on an include gates the single include statement, deciding once whether to pull the file in at all. Same keyword, different reach: an import with a when stamps the condition onto each inner task, while an include with a when is a single yes-or-no on the whole file. The surprise is usually a set_fact inside an imported file that you expected to skip wholesale but which gets the condition stamped on it individually instead.

Choosing Between Them

Use import for static structure you always run and want visible to --tags and --list-tasks — fixed scaffolding that never changes shape between runs. Use include when the file to pull in, or whether to pull it in, depends on run-time data: a variable computed earlier, a loop over a list, or a registered result. The rule of thumb is simple — when the structure is static, import; the moment you need a loop or a run-time-computed target, include.

import vs include

import_* (static) — inlined at parse time. Tags propagate to every imported task, --list-tasks shows them, when is applied per imported task, and you cannot loop it. Choose it for fixed structure you always run and want fully visible to selection.

include_* (dynamic) — resolved at run time. A tag covers only the include line, --tags cannot reach the inner tasks, when gates the whole include once, and you can loop: it. Choose it when the file or the decision to run it depends on run-time data. When in doubt and the structure is static, import; the moment you need a loop or a run-time target, include.

Common Mistakes
  • Using include_tasks: deploy.yml then running --tags deploy and finding nothing runs — the included tasks do not exist at tag-selection time, so --tags cannot see them; use import_tasks if tag selection must reach inside.
  • Trying to loop: over import_tasks to run a task file per item — it silently runs once, because the import was inlined at parse time before the loop existed; switch to include_tasks for the loop.
  • Putting when on import_tasks and expecting it to skip the file as a unit — instead the condition is stamped onto each imported task individually, which surprises you when an inner set_fact you expected to skip still gets evaluated against the condition.
  • Using include_role deep in a loop and then discovering the dynamic role cannot be targeted with --tags the way a statically imported one can.
  • Wiring up includes and imports and never running --list-tasks to check what is visible — dynamic includes do not appear until they execute, so the plan you think you have is not the plan that runs.
Best Practices
  • Default to import_* for static, always-run structure so --tags, --skip-tags, and --list-tasks see every task in the plan.
  • Switch to include_* the moment you need to loop over the inclusion or decide at run time which file to pull in.
  • Keep when on an import only when per-task gating is genuinely what you want; use when on an include to skip the whole file as one decision.
  • Run --list-tasks after wiring includes and imports to confirm which tasks are actually visible, since dynamic includes will not appear until they execute.
  • Reach for import first by habit and treat include as the deliberate exception you take on when run-time behavior demands it, rather than mixing the two without a reason.
Comparable tools Terraform modules are always statically resolved into the graph — no run-time-inlined counterpart No direct equivalent — this static/dynamic split is specific to Ansible's parse-then-run model

Knowledge Check

What is the root difference between import_tasks and include_tasks?

  • import is static — inlined at parse time before the run; include is dynamic — resolved at run time when execution reaches the line
  • import is restricted to pulling in entire roles, while include is restricted to pulling in standalone task files only
  • import runs the pulled-in tasks locally on the control node itself, while include ships every one of them out to run on each managed node in turn
  • import copies the task file to the host just once, while include recopies it to every host individually on each pass

You write include_tasks: deploy.yml and run with --tags deploy. Why does nothing run?

  • The included tasks do not exist at tag-selection time, so --tags cannot see anything tagged deploy inside a dynamic include
  • A tag placed directly on an include statement is treated as a reserved keyword and gets silently ignored by Ansible during tag selection at run time
  • --tags only ever applies to whole roles and never reaches tasks sitting inside an included task file
  • The included file name must match the requested tag name exactly, and here deploy.yml does not line up with it

Why can you loop: over an include_tasks but not an import_tasks?

  • The include is evaluated at run time, so the loop drives repeated evaluation; the import was inlined once at parse time before any loop existed
  • import_tasks explicitly rejects the loop keyword and raises a hard syntax error the moment the playbook is parsed at load time
  • Imported task files all run in parallel across the whole batch and so can never be ordered by a loop, whereas includes always run strictly one after another in series
  • Loops are only ever valid on whole roles, and include_tasks qualifies because it is internally treated as a lightweight role

How does when behave differently on an import versus an include?

  • On an import the condition is copied onto every imported task individually; on an include it gates the single include statement as one decision
  • On an import the when condition is ignored entirely and has no effect; on an include it instead gates each individual inner task separately
  • Both apply the condition exactly once to the whole referenced file, so there is no observable difference at all in how when behaves
  • On an import the when condition is evaluated once on the control node, while on an include it is evaluated separately on each individual managed node

You got correct