Introduction

Large Ansible projects split automation across multiple playbook files. import_playbook and task-level includes (import_tasks/include_tasks) are the two mechanisms for composing them. Understanding when each is processed — parse time vs runtime — is essential for conditional logic, tags, and debugging.

Quick Comparison

Featureimport_playbookinclude_tasks (in a play)
Processed atParse time (static)Runtime (dynamic)
Conditional (when)❌ Cannot conditionally import✅ Can conditionally include
Tags✅ Tags flow into imported plays⚠️ Tags on include apply to all tasks
Loops❌ Cannot loop✅ Can loop
Variables from runtime❌ Not available✅ Available
Error if file missing✅ Immediate error✅ Runtime error
--list-tasks visibility✅ Shows all tasks❌ Shows include line only

import_playbook — Static Inclusion

# site.yml — main entry point
---
- 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
  roles:
    - nginx
    - app_deploy

- hosts: webservers
  tasks:
    - name: Verify deployment
      ansible.builtin.uri:
        url: "http://localhost:{{ app_port }}/health"

Project Structure

ansible-project/
├── site.yml                    # Main entry point
├── playbooks/
│   ├── common.yml              # Base setup for all hosts
│   ├── webservers.yml          # Web server deployment
│   ├── dbservers.yml           # Database setup
│   └── monitoring.yml          # Monitoring agents
├── roles/
│   ├── common/
│   ├── nginx/
│   ├── postgresql/
│   └── deploy/
├── inventory/
│   ├── production/
│   └── staging/
└── group_vars/

Using Tags with import_playbook

# Run only web server tasks
ansible-playbook site.yml --tags webservers

# Skip database tasks
ansible-playbook site.yml --skip-tags database
# site.yml with tagged imports
- import_playbook: playbooks/common.yml
  tags: common

- import_playbook: playbooks/webservers.yml
  tags: webservers
  # ⚠️ This tags the ENTIRE imported playbook
  # All tasks inside get the 'webservers' tag

- import_playbook: playbooks/dbservers.yml
  tags: database

Composing Plays Within Playbooks

# deploy.yml — multi-stage deployment
---
# Stage 1: Pre-deployment checks
- hosts: all
  gather_facts: true
  tasks:
    - name: Validate environment
      ansible.builtin.assert:
        that: env is defined

# Stage 2: Database migration (run once)
- hosts: dbservers
  tasks:
    - name: Run migrations
      ansible.builtin.command: /opt/app/migrate.sh
      run_once: true

# Stage 3: Rolling deploy
- hosts: webservers
  serial: 2
  roles:
    - deploy

# Stage 4: Smoke tests
- hosts: webservers
  tasks:
    - name: Health check
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:{{ app_port }}/health"

Dynamic Task Inclusion Within Plays

# When you need runtime decisions, use include_tasks WITHIN a play
- hosts: webservers
  tasks:
    # Static import — processed at parse time
    - import_tasks: tasks/validate.yml

    # Dynamic include — processed at runtime
    - include_tasks: "tasks/{{ ansible_os_family | lower }}.yml"

    # Conditional include
    - include_tasks: tasks/ssl-setup.yml
      when: ssl_enabled | default(false)

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

Environment-Based Composition

# site.yml — always runs
- import_playbook: playbooks/common.yml
- import_playbook: playbooks/security.yml
- import_playbook: playbooks/app.yml

# deploy-production.yml — production-specific
- import_playbook: playbooks/pre-deploy-checks.yml
- import_playbook: playbooks/backup.yml
- import_playbook: playbooks/app.yml
- import_playbook: playbooks/post-deploy-verify.yml
- import_playbook: playbooks/notify.yml

# deploy-staging.yml — simpler staging deploy
- import_playbook: playbooks/app.yml
- import_playbook: playbooks/post-deploy-verify.yml

Common Mistakes

# ❌ Trying to conditionally import a playbook
- import_playbook: playbooks/optional.yml
  when: run_optional  # This does NOT work as expected!
  # 'when' is applied to every TASK inside, not the import itself

# ✅ Use separate entry point files instead
# Full deploy: ansible-playbook deploy-full.yml
# Quick deploy: ansible-playbook deploy-quick.yml

# ❌ import_playbook inside a play (not allowed)
- hosts: all
  tasks:
    - import_playbook: other.yml  # SYNTAX ERROR!

# ✅ import_playbook is top-level only
- import_playbook: other.yml  # Must be at the same level as '- hosts:'

# ❌ Deeply nested imports (hard to follow)
# site.yml → imports base.yml → imports sub-base.yml → imports...

# ✅ Keep it flat — max 2 levels deep
# site.yml → imports playbooks/*.yml (each is self-contained)

Debugging

# List all tasks (imports are expanded, includes are not)
ansible-playbook site.yml --list-tasks

# List all hosts
ansible-playbook site.yml --list-hosts

# Syntax check
ansible-playbook site.yml --syntax-check

# Dry run
ansible-playbook site.yml --check --diff

Conclusion

import_playbook for static project composition — your site.yml imports webservers.yml, dbservers.yml, etc. It's processed at parse time, so tags and --list-tasks work perfectly. For runtime decisions within a play, use include_tasks (dynamic) or import_tasks (static). Keep project structure flat: one site.yml that imports focused playbook files, each targeting specific host groups. Create separate entry points (deploy-production.yml, deploy-staging.yml) instead of trying to conditionally import playbooks.