Ansible import_tasks vs include_tasks: Key Differences (2026 Guide)
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible import_tasks vs include_tasks. Understand static vs dynamic task loading, when to use each, performance differences, and migration.
Ansible provides two ways to load external task files: import_tasks (static) and include_tasks (dynamic). They look similar but behave very differently at parse time vs runtime. Choosing the wrong one causes subtle bugs with tags, handlers, and conditionals.
Quick Comparison
# Static import — parsed at playbook load time
- name: Import database tasks
ansible.builtin.import_tasks: database.yml
# Dynamic include — loaded at runtime when reached
- name: Include database tasks
ansible.builtin.include_tasks: database.yml
See also: Ansible delegate_to: Run Tasks on Different Hosts (Complete Guide)
Key Differences
| Feature | import_tasks (static) | include_tasks (dynamic) |
|---------|------------------------|--------------------------|
| When loaded | Playbook parse time | Runtime (when task is reached) |
| Tags | Applied to all imported tasks | Only on the include task itself |
| when condition | Applied to each imported task | Evaluated once before loading |
| Handlers | Can notify imported handlers | Cannot notify included handlers |
| Loops | ❌ Cannot use with loop | ✅ Can use with loop |
| Variables | Cannot use task-level vars in filename | Can use runtime variables in filename |
| --list-tasks | Shows imported tasks | Shows only the include task |
| --list-tags | Shows tags from imports | Does not show include tags |
| Error detection | Syntax errors caught early | Syntax errors caught at runtime |
import_tasks (Static)
Tasks are inserted into the play at parse time, as if you copied them directly into the playbook.
Basic Usage
# main.yml
- hosts: webservers
tasks:
- name: Import common setup
ansible.builtin.import_tasks: tasks/common.yml
- name: Import web server config
ansible.builtin.import_tasks: tasks/webserver.yml
vars:
http_port: 8080
# tasks/common.yml
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install common packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- curl
- wget
- vim
Tags with import_tasks
Tags cascade to ALL imported tasks:
- name: Import monitoring setup
ansible.builtin.import_tasks: tasks/monitoring.yml
tags: ['monitoring']
# ALL tasks in monitoring.yml now have the 'monitoring' tag
# ansible-playbook site.yml --tags monitoring ← runs all of them
when with import_tasks
The when condition is applied to each imported task individually:
- name: Import Debian tasks
ansible.builtin.import_tasks: tasks/debian.yml
when: ansible_os_family == 'Debian'
# Each task in debian.yml gets: when: ansible_os_family == 'Debian'
❌ Cannot Use Loops
# This FAILS
- name: Import tasks in a loop
ansible.builtin.import_tasks: "tasks/{{ item }}.yml"
loop:
- users
- packages
# ERROR: import_tasks does not support loops
See also: Ansible Playbook Structure: Anatomy, Best Practices & Examples (2026)
include_tasks (Dynamic)
Tasks are loaded at runtime when the include task is reached.
Basic Usage
- hosts: all
tasks:
- name: Include OS-specific tasks
ansible.builtin.include_tasks: "tasks/{{ ansible_os_family | lower }}.yml"
Dynamic Filenames
- name: Include tasks based on variable
ansible.builtin.include_tasks: "tasks/{{ app_type }}/deploy.yml"
vars:
version: "{{ deploy_version }}"
- name: Include tasks based on fact
ansible.builtin.include_tasks: "tasks/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"
Loops with include_tasks
- name: Configure each application
ansible.builtin.include_tasks: tasks/configure_app.yml
loop: "{{ applications }}"
loop_control:
loop_var: app
vars:
app_name: "{{ app.name }}"
app_port: "{{ app.port }}"
Tags with include_tasks
Tags apply only to the include task itself, NOT to the tasks inside:
- name: Include monitoring
ansible.builtin.include_tasks: tasks/monitoring.yml
tags: ['monitoring']
# Only the include task has the tag
# The tasks INSIDE monitoring.yml do NOT get the tag
# ansible-playbook site.yml --tags monitoring ← loads the file but tasks inside may not run
To make tags work with includes, add tags inside the included file:
# tasks/monitoring.yml
- name: Install monitoring agent
ansible.builtin.package:
name: node-exporter
tags: ['monitoring']
Or use apply:
- name: Include monitoring
ansible.builtin.include_tasks:
file: tasks/monitoring.yml
apply:
tags: ['monitoring']
tags: ['monitoring'] # Needed so the include itself runs when filtering by tag
When to Use Each
Use import_tasks When:
• You need tags to work across the included file • You need handlers defined in the imported file • The filename is static (known at parse time) • You want --list-tasks and --list-tags to show everything • You want early error detection (syntax errors caught before execution)# Good use of import_tasks
- ansible.builtin.import_tasks: tasks/common.yml
tags: ['common', 'setup']
- ansible.builtin.import_tasks: tasks/security.yml
tags: ['security']
Use include_tasks When:
• The filename is dynamic (uses variables or facts) • You need to run includes in a loop • You want conditional loading (load different files based on conditions) • The included file might not exist (useignore_errors)
• You want tasks to load only when needed (performance)
# Good use of include_tasks
- ansible.builtin.include_tasks: "tasks/{{ ansible_os_family }}.yml"
- ansible.builtin.include_tasks: tasks/configure_app.yml
loop: "{{ app_list }}"
See also: Ansible Tags: Run Specific Tasks and Roles Selectively (Guide)
Roles: import_role vs include_role
The same static/dynamic distinction applies to roles:
# Static (parsed at load time)
- name: Import webserver role
ansible.builtin.import_role:
name: webserver
tags: ['web']
# Dynamic (loaded at runtime)
- name: Include role based on variable
ansible.builtin.include_role:
name: "{{ server_role }}"
vars:
http_port: 8080
import_playbook
For including entire playbooks (only works at the play level, not inside tasks):
# site.yml
- ansible.builtin.import_playbook: webservers.yml
- ansible.builtin.import_playbook: databases.yml
- ansible.builtin.import_playbook: monitoring.yml
> Note: There is no include_playbook. Playbook inclusion is always static.
Common Patterns
OS-Specific Task Loading
# Recommended: include_tasks with dynamic filename
- name: Include OS-specific tasks
ansible.builtin.include_tasks: "{{ ansible_os_family | lower }}.yml"
# Alternative: import with when
- name: Import Debian tasks
ansible.builtin.import_tasks: debian.yml
when: ansible_os_family == 'Debian'
- name: Import RedHat tasks
ansible.builtin.import_tasks: redhat.yml
when: ansible_os_family == 'RedHat'
Role Structure
# roles/webserver/tasks/main.yml
- name: Import installation tasks
ansible.builtin.import_tasks: install.yml
tags: ['install']
- name: Import configuration
ansible.builtin.import_tasks: configure.yml
tags: ['configure']
- name: Include TLS setup (only if enabled)
ansible.builtin.include_tasks: tls.yml
when: enable_tls | default(false)
tags: ['tls']
Common Mistakes
Tags Not Working with include_tasks
# Bug: --tags deploy doesn't run tasks inside deploy.yml
- ansible.builtin.include_tasks: deploy.yml
tags: ['deploy']
# Fix: use import_tasks if you need tag filtering
- ansible.builtin.import_tasks: deploy.yml
tags: ['deploy']
# Or use apply with include_tasks
- ansible.builtin.include_tasks:
file: deploy.yml
apply:
tags: ['deploy']
tags: ['deploy']
Variable Filename with import_tasks
# Bug: fails because import is static
- ansible.builtin.import_tasks: "{{ os_family }}.yml"
# ERROR: variable not available at parse time
# Fix: use include_tasks for dynamic filenames
- ansible.builtin.include_tasks: "{{ os_family }}.yml"
FAQ
What is the difference between import_tasks and include_tasks?
import_tasks is static — tasks are loaded at playbook parse time and behave as if written directly in the playbook. include_tasks is dynamic — tasks are loaded at runtime when the task is reached. This affects how tags, conditions, and loops work.
Can I use loops with import_tasks?
No, import_tasks does not support loops. Use include_tasks when you need to loop over a task file multiple times.
Why don't my tags work with include_tasks?
Tags on include_tasks only apply to the include task itself, not to the tasks inside the file. Use import_tasks for tag-based filtering, or add apply: with tags to pass them through to included tasks.
Which is better for performance?
include_tasks can be slightly better for large playbooks because tasks are only loaded when needed. import_tasks loads everything upfront. In practice, the difference is negligible for most playbooks.
Should I use import_tasks or include_tasks by default?
Use import_tasks as the default for static, predictable task loading. Switch to include_tasks when you need dynamic filenames, loops, or conditional file loading. If you use tags extensively, prefer import_tasks.
Conclusion
•import_tasks — Static, parsed at load time. Tags and when cascade. No loops. Best for predictable structure.
• include_tasks — Dynamic, loaded at runtime. Supports loops and variable filenames. Tags need special handling.
• Rule of thumb: Use import_tasks unless you need dynamic behavior.
Related Articles
• Ansible Roles: Create Reusable Automation • Ansible Handlers: Trigger Actions on Change • Ansible Playbook Examples: Complete Guide • Ansible Collections: Install, Use & CreateSee also
• Ansible import_role vs include_role: Static vs Dynamic Role LoadingCategory: installation