Choosing Between import_tasks and include_tasks in Ansible
Learn how to choose between Ansible's import_tasks and include_tasks for playbook modularity, including trade-offs around loops, tags, and task visibility.
12 Mar 2026, 04:35 UTC

The Problem: Static Structure vs. Dynamic Execution
When you break an Ansible playbook into reusable files, you must decide how those files are loaded. import_tasks and include_tasks both pull external task files into a play, but they resolve at different times. Choosing the wrong one can hide tasks from your debugging tools, prevent looping, or make execution order harder to predict. The practical rule: use import_tasks for a fixed task list that is known before the run starts; use include_tasks when the tasks to run depend on inventory facts or must be repeated.
Decision Constraints
Before picking a directive, answer three questions:
- Do you know every task that will run before execution begins?
- Does the set of tasks depend on facts discovered at runtime, such as the operating system or host group?
- Do you need to iterate over a list of files or variables?
If the answers are yes, no, and no, import_tasks is usually the safer default. If task selection is dynamic or iterative, include_tasks is required.
Comparing import_tasks and include_tasks
| Aspect | import_tasks | include_tasks |
|---|---|---|
| Load time | Playbook parse time | Runtime, when the task is reached |
Visible in --list-tasks | Yes | No |
Supports loop | No | Yes |
Works with --start-at-task | Yes | No |
| Tag binding | Import time | Runtime |
| Best for | Stable, predictable structure | Dynamic selection based on facts |
Trade-offs in Practice
Task Visibility and Debugging
Because import_tasks expands the task list during parsing, tools like --list-tasks and --start-at-task can see every imported task. With include_tasks, the included file is not read until the play reaches that step, so those tasks remain invisible to parse-time inspection. If you rely on dry-run task listings or partial reruns, this difference matters.
Loops and Conditional Inclusion
import_tasks cannot be used directly inside a loop; the playbook parser rejects it. include_tasks accepts a loop keyword, letting you process multiple variable files or host groups. The trade-off is that loop variables must be scoped carefully to avoid unexpected task duplication.
Tags and Handlers
Both directives support tags and apply tags, but the timing differs. Tags on import_tasks are bound when the file is imported, while tags on include_tasks are applied at runtime. Mixing the two in one play can make handler ordering and pre_tasks/post_tasks sequences harder to reason about.
Concrete Implementation and Validation
The following example shows a play that imports a stable baseline and dynamically includes an OS-specific task file. Run these commands from your Ansible control node with read access to the playbook and inventory; no elevated privileges are required for the checks shown.
# site.yml
- hosts: all
tasks:
- name: Load common baseline
import_tasks: common.yml
- name: Load OS-specific tasks
include_tasks: "{{ ansible_facts['os_family'] }}.yml"
when: ansible_facts['os_family'] is defined
Replace common.yml and {{ ansible_facts['os_family'] }}.yml with your actual file names. To validate behavior without changing managed nodes, run:
ansible-playbook --syntax-check site.yml
ansible-playbook --list-tasks site.yml
Expected check: the --list-tasks output should show the tasks from common.yml because they are imported at parse time. The OS-specific tasks will not appear individually; only the include step itself is listed. To confirm the --start-at-task limitation, try targeting a task that exists only inside the included file. The command should fail to find it, because the task name is not known until runtime.
If you need to iterate over several files, replace the single include with a loop:
- name: Load multiple variable files
include_tasks: "{{ item }}"
loop:
- network.yml
- storage.yml
Risk: a typo in the included file path will not surface during --syntax-check or --list-tasks; it fails only when the play reaches that task.
Limitations and Verification
Tasks inside include_tasks are unreachable by --start-at-task. If your workflow depends on resuming from a specific task, keep that task in an imported file or restructure the play. Also avoid the legacy include directive; its behavior changed in Ansible 2.4, and older playbooks may break on modern Ansible Core. The import_tasks and include_tasks directives are stable in Ansible Core 2.11 and later.
After writing the playbook, run ansible-playbook --syntax-check followed by --list-tasks. If dynamic tasks are missing from the list, that confirms they are included at runtime rather than parse time. To check your engine version, run ansible --version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.