Introduction

As playbooks grow, splitting them into reusable files is essential. Ansible provides two mechanisms: import (static, parsed at load time) and include (dynamic, processed at runtime). Understanding the difference is key to organizing scalable automation.

import vs include

import (static)include (dynamic)
When parsedAt playbook load timeAt runtime when reached
TagsInherited by all tasksOnly on the include line
ConditionalsApplied to each taskApplied once to the include
Loops❌ Cannot loop✅ Can loop
VariablesResolved at parse timeResolved at runtime
HandlersCan be notified by nameCannot be notified by name
Best forPredictable, always-run tasksConditional or looped includes

import_tasks (Static)

# main.yml
---
- hosts: webservers
  become: true
  tasks:
    - import_tasks: tasks/packages.yml
    - import_tasks: tasks/config.yml
    - import_tasks: tasks/services.yml
# tasks/packages.yml
- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Install Redis
  ansible.builtin.apt:
    name: redis-server
    state: present

Tags flow through to all imported tasks:

- import_tasks: tasks/packages.yml
  tags: packages
# ALL tasks in packages.yml get the 'packages' tag

include_tasks (Dynamic)

# Conditional include
- include_tasks: tasks/debian.yml
  when: ansible_os_family == 'Debian'

- include_tasks: tasks/redhat.yml
  when: ansible_os_family == 'RedHat'

# Loop include
- include_tasks: tasks/setup-vhost.yml
  loop: "{{ virtual_hosts }}"
  loop_control:
    loop_var: vhost

# Include with variables
- include_tasks: tasks/deploy.yml
  vars:
    app_version: "2.1.0"
    app_env: production

Playbook workflow

Generate a complete playbook from this tutorial

Use Ansible include and import Tasks as the starting context and generate an editable Ansible playbook in the playground.

One anonymous generation is available. Do not paste passwords, private keys, tokens, or customer secrets.

Generate a playbook

import_playbook

# site.yml — master playbook
---
- import_playbook: playbooks/common.yml
- import_playbook: playbooks/webservers.yml
- import_playbook: playbooks/dbservers.yml
- import_playbook: playbooks/monitoring.yml
# playbooks/webservers.yml
---
- hosts: webservers
  become: true
  roles:
    - nginx
    - myapp
  tasks:
    - import_tasks: tasks/ssl.yml

include_role / import_role

# Static — parsed at load time
- import_role:
    name: nginx

# Dynamic — processed at runtime
- include_role:
    name: "{{ web_server }}"    # Can use variables!

# With variables
- include_role:
    name: myapp
  vars:
    app_port: 8080
    app_env: production

# Conditional role
- include_role:
    name: monitoring
  when: enable_monitoring | default(true)

Practical Patterns

OS-Specific Tasks

# main.yml
- name: Include OS-specific tasks
  include_tasks: "{{ ansible_os_family | lower }}.yml"
# Loads debian.yml or redhat.yml based on OS
# debian.yml
- ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true

# redhat.yml
- ansible.builtin.yum:
    name: nginx
    state: present

Task File Per Component

project/
├── site.yml
├── tasks/
│   ├── users.yml
│   ├── packages.yml
│   ├── firewall.yml
│   ├── nginx.yml
│   ├── app-deploy.yml
│   └── monitoring.yml
# site.yml
---
- hosts: webservers
  become: true
  tasks:
    - import_tasks: tasks/users.yml
      tags: users
    - import_tasks: tasks/packages.yml
      tags: packages
    - import_tasks: tasks/firewall.yml
      tags: firewall
    - import_tasks: tasks/nginx.yml
      tags: nginx
    - import_tasks: tasks/app-deploy.yml
      tags: deploy
    - import_tasks: tasks/monitoring.yml
      tags: monitoring

Reusable Task with Parameters

# tasks/create-vhost.yml
- name: Create vhost directory
  ansible.builtin.file:
    path: "/var/www/{{ vhost.domain }}"
    state: directory
    owner: www-data

- name: Deploy vhost config
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "/etc/nginx/sites-enabled/{{ vhost.domain }}.conf"
  notify: Reload nginx

# main.yml
- include_tasks: tasks/create-vhost.yml
  loop: "{{ virtual_hosts }}"
  loop_control:
    loop_var: vhost

Environment-Specific Playbooks

# deploy.yml
---
- import_playbook: playbooks/pre-deploy-checks.yml
- import_playbook: playbooks/deploy-app.yml
- import_playbook: playbooks/post-deploy-verify.yml
- import_playbook: playbooks/notify.yml

Handlers in Separate File

# handlers/main.yml
- name: Reload nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded

- name: Restart app
  ansible.builtin.service:
    name: myapp
    state: restarted

# playbook.yml
- hosts: webservers
  handlers:
    - import_tasks: handlers/main.yml
  tasks:
    - name: Deploy config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Reload nginx

When to Use Which

Use import_tasks when:

  • Tasks should always run
  • You need tags to flow through
  • You need handlers to be notifiable
  • Task list is known at parse time

Use include_tasks when:

  • Tasks are conditional (when)
  • You need to loop over a task file
  • Task file name comes from a variable
  • You want lazy evaluation

Use import_playbook when:

  • Composing a master playbook from smaller ones
  • Each playbook targets different host groups

Use include_role when:

  • Role selection is dynamic (variable-based)
  • Role is conditionally applied

Troubleshooting

"Task not found" with Handlers and include_tasks

Handlers defined in include_tasks files can't be notified by name. Use import_tasks for handlers:

# WRONG
handlers:
  - include_tasks: handlers.yml

# RIGHT
handlers:
  - import_tasks: handlers.yml

Tags Not Working with include_tasks

Tags only apply to the include line itself, not inner tasks:

# Tags don't flow into included tasks
- include_tasks: tasks.yml
  tags: mytag
# Tasks inside tasks.yml DON'T get 'mytag'

# Use import_tasks for tag inheritance
- import_tasks: tasks.yml
  tags: mytag
# ALL tasks inside tasks.yml get 'mytag'

"Recursive loop detected"

A task file includes itself. Check for circular includes.

Conclusion

Use import_tasks (static) for predictable, always-run task files where tags and handlers need to work. Use include_tasks (dynamic) for conditional includes, loops, and variable-driven file selection. Use import_playbook to compose a master playbook from smaller targeted ones. The key rule: import = static (parse time), include = dynamic (runtime). Start with imports, switch to includes when you need dynamic behavior.