Skip to content

include_tasks vs import_tasks


include_tasks has already shown up twice in this course — fixing the nested-loop collision, and working around the fact that block: can’t be looped directly — both times without a full explanation of what it actually is or why it was the right tool. This chapter delivers that, alongside its counterpart, import_tasks, and the one fundamental difference between them that explains every practical consequence covered in this section.

Two Ways To Pull In Tasks From Another File

Both import_tasks and include_tasks let you split a play’s tasks into a separate file and pull them in. The difference isn’t in what they let you do — it’s entirely in when that pulling-in actually happens.

import_tasks: Resolved At Parse Time

Ansible reads and expands import_tasks before the play starts running at all — at parse time, the imported file’s tasks are spliced directly into the play’s task list, effectively as if you’d copy-pasted them there yourself. This happens before a single task has executed, before facts have been gathered, before anything set during a run exists yet.

include_tasks: Resolved At Runtime

include_tasks is resolved during execution, exactly when the play actually reaches that point in its task list. At that moment, Ansible goes and reads whatever file is specified — which, critically, can be determined by something only known by then, like a fact or a registered result.

Basic Syntax, Side By Side

# import (static)
- name: Import setup tasks
  ansible.builtin.import_tasks: setup.yaml
# include (dynamic)
- name: Include setup tasks
  ansible.builtin.include_tasks: setup.yaml

For a fixed filename like this, they look identical and behave identically too. The difference only becomes visible — and important — the moment the filename itself depends on something.

The Practical Consequence: A Runtime-Only Path

Since import_tasks is resolved before the play runs at all, its filename cannot depend on anything only known at runtime — a fact, a registered variable, anything gathered during execution. At parse time, none of that exists yet. include_tasks, resolved later, has no such restriction.

Proving It Concretely

tasks_debian.yaml
- name: Debian-specific task
  ansible.builtin.debug:
    msg: "Running Debian-specific setup"
tasks_redhat.yaml
- name: RedHat-specific task
  ansible.builtin.debug:
    msg: "Running RedHat-specific setup"
broken_import.yaml
- name: Play
  hosts: servers
  tasks:
    - name: Try import with a runtime-dependent path (BROKEN)
      ansible.builtin.import_tasks: "tasks_{{ ansible_facts['os_family'] | lower }}.yaml"
ansible-playbook broken_import.yaml
[ERROR]: Error when evaluating variable in import path 
"tasks_{{ ansible_facts['os_family'] | lower }}.yaml": 'ansible_facts' is undefined

This fails — typically with an error about the referenced file not being found, since ansible_facts doesn’t exist yet at the point import_tasks tries to resolve its filename. Facts are gathered during the play; import_tasks is expanded before the play even starts.

working_include.yaml
- name: Include with the same runtime-dependent path (WORKS)
  ansible.builtin.include_tasks: "tasks_{{ ansible_facts['os_family'] | lower }}.yaml"
TASK [Include with the same runtime-dependent path (WORKS)] ************************
included: /home/alice/codes/ansible/include_import/tasks_debian.yaml for ubuntu
included: /home/alice/codes/ansible/include_import/tasks_redhat.yaml for fedora

TASK [Debian-specific task] ********************************************************
ok: [ubuntu] => {
    "msg": "Running Debian-specific setup"
}

TASK [RedHat-specific task] ********************************************************
ok: [fedora] => {
    "msg": "Running RedHat-specific setup"
}

This works, because by the time execution actually reaches this line, facts have already been gathered — ansible_facts['os_family'] genuinely exists, and the correct file gets included based on it.

Best Practices

  • Use import_tasks for content that’s always the same, known entirely in advance — no dependency on facts, registered results, or anything else only available once the play is actually running.
  • Use include_tasks any time the included file’s identity depends on something only known at runtime — this is exactly why the nested-loop and block-looping workarounds earlier in this course both needed it.
  • Default to include_tasks when in doubt — it’s the more flexible of the two. import_tasks’s stricter, earlier resolution has real, specific benefits of its own, covered fully in this section’s next chapter.
Last updated on