Why Ansible include_tasks Variables Are Not Defined in the Parent Play
You define a variable inside an include_tasks file. The task runs successfully. Then the very next task in your parent play fails with variable is undefined. What just happened?
This is one of the most confusing Ansible behaviors, and it bites people repeatedly because it looks like a bug. It’s not — it’s a scoping decision — but understanding why it works this way helps you fix it fast.
The Problem in Code
| |
| |
You’d expect my_result to be available after the include. Sometimes it is. Sometimes it isn’t. The inconsistency is maddening.
Why This Happens
include_tasks is dynamic — Ansible resolves it at runtime, and the included tasks share the play’s variable space in most cases. However, set_fact inside an included task file can behave differently depending on Ansible version, whether the include is inside a loop, and how deeply nested it is.
The reliable pattern for returning a value from an included task file is set_fact — but you need to be deliberate about it.
Case 1: include_tasks Inside a Loop
This is the most common failure scenario:
| |
| |
The variable last_processed gets set on each iteration, but the final value after the loop is unpredictable. Each loop iteration runs in its own include scope. The last iteration’s value may or may not propagate correctly to the parent.
Fix: Accumulate results explicitly:
| |
Case 2: Nested Includes
| |
| |
| |
Variables set two levels deep sometimes fail to propagate to the top level, especially in older Ansible versions (pre-2.8).
Fix: Explicitly re-set the variable at each level, or restructure to avoid deep nesting.
The Reliable Fix: import_tasks
If you don’t need dynamic includes (conditionals on the include itself, loops over the include), switch to import_tasks:
| |
import_tasks is static — Ansible processes it at parse time, not runtime. All tasks from the imported file are treated as if they’re inline in the parent play. Variables set via set_fact in imported tasks are always visible to subsequent tasks.
When you can’t use import_tasks (you need when: on the include, or you’re looping over includes), keep reading.
Fix for Dynamic Includes: Initialize First
If you must use include_tasks, initialize the variable before the include:
| |
This guarantees my_result exists in the play scope. When helper.yml sets it via set_fact, it overwrites the initialized value, and the parent play sees the update.
Fix for Loop Includes: Use a Results Accumulator
| |
| |
This pattern works reliably across all Ansible versions. Each iteration appends to a list defined in the parent scope.
Quick Diagnostic
When you hit an undefined variable after an include:
- Is the include inside a loop? → Use an accumulator pattern
- Is it nested more than one level? → Flatten or use import_tasks
- Is it conditional (
when:on the include)? → Initialize the variable before the include - None of the above? → Switch to
import_tasksif possible; it’s always more predictable
| |
The core rule: import_tasks shares scope unconditionally. include_tasks shares scope usually, but not reliably inside loops or deep nesting. When in doubt, initialize first and accumulate explicitly.