include vs import
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.
import_* — staticwhen stamps onto each task; cannot loop.include_* — dynamic--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.
- 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_* (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.
- Using
include_tasks: deploy.ymlthen running--tags deployand finding nothing runs — the included tasks do not exist at tag-selection time, so--tagscannot see them; useimport_tasksif tag selection must reach inside. - Trying to
loop:overimport_tasksto run a task file per item — it silently runs once, because the import was inlined at parse time before the loop existed; switch toinclude_tasksfor the loop. - Putting
whenonimport_tasksand expecting it to skip the file as a unit — instead the condition is stamped onto each imported task individually, which surprises you when an innerset_factyou expected to skip still gets evaluated against the condition. - Using
include_roledeep in a loop and then discovering the dynamic role cannot be targeted with--tagsthe way a statically imported one can. - Wiring up includes and imports and never running
--list-tasksto 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.
- Default to
import_*for static, always-run structure so--tags,--skip-tags, and--list-taskssee 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
whenon animportonly when per-task gating is genuinely what you want; usewhenon anincludeto skip the whole file as one decision. - Run
--list-tasksafter wiring includes and imports to confirm which tasks are actually visible, since dynamic includes will not appear until they execute. - Reach for
importfirst by habit and treatincludeas the deliberate exception you take on when run-time behavior demands it, rather than mixing the two without a reason.
Knowledge Check
What is the root difference between import_tasks and include_tasks?
importis static — inlined at parse time before the run;includeis dynamic — resolved at run time when execution reaches the lineimportis restricted to pulling in entire roles, whileincludeis restricted to pulling in standalone task files onlyimportruns the pulled-in tasks locally on the control node itself, whileincludeships every one of them out to run on each managed node in turnimportcopies the task file to the host just once, whileincluderecopies 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
--tagscannot see anything taggeddeployinside 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
--tagsonly 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.ymldoes 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_tasksexplicitly rejects theloopkeyword 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_tasksqualifies 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
whencondition 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
whenbehaves - On an import the
whencondition is evaluated once on the control node, while on an include it is evaluated separately on each individual managed node
You got correct