AnsibleXXIII · Tags, Blocks and Error HandlingError handling
The tag that selected nothing
What you'll learn
- Predict which tasks a tag reaches through an import and through an include
- Diagnose a tagged run that succeeded having executed nothing
- Apply the apply: keyword and state exactly what it does to inner tasks
- Decide when to make a dynamic include static instead of patching the tags
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
An operator runs the monthly patch play:
$ ansible-playbook -i inventory.ini tagtrap.yml --tags patchPLAY [the tag that selected nothing] *******************************************
PLAY RECAP *********************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Exit code 0. No failures. No warnings. And no patches, on any host, this
month or last month.
The play is this:
- name: the tag that selected nothing
hosts: web
gather_facts: false
tasks:
- name: include delta dynamically
ansible.builtin.include_role:
name: delta
and roles/delta/tasks/main.yml is this:
- name: delta patch task
ansible.builtin.debug:
msg: delta-patch
tags: [patch]
- name: delta untagged task
ansible.builtin.debug:
msg: delta-untagged
There is a task tagged patch. The operator asked for patch. Nothing ran.
Why
Part XXII lesson 5 established the mechanism: include_role is a task, and
what it pulls in does not exist until that task runs. Tag selection is
evaluated against the parse-time tree, so the only thing available to be
selected is the include statement itself — and the include statement carries
no tags.
The selection excludes the include. The include never runs. The tasks inside never come into existence. Nothing to select, nothing to skip, nothing to report.
--list-tags says the same thing, and this is the diagnostic:
$ ansible-playbook -i inventory.ini dynonly.yml --list-tagsplaybook: dynonly.yml
play #1 (local): local TAGS: []
TASK TAGS: []If the tag you are about to use does not appear in --list-tags, it cannot
select anything. That is the check, it takes a second, and it is the
difference between finding this in a terminal and finding it in a patch
compliance report.
The contrast, side by side
The same role, imported statically:
$ ansible-playbook -i inventory.ini import-local.yml --tags alpha_innerTASK [alpha : alpha task one] **************************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Static import_* | Dynamic include_* | |
|---|---|---|
| Tags on the statement | Applied to every task inside | Applied to the statement only |
| Tags declared inside | Visible to --list-tags, individually selectable | Invisible; unreachable unless the statement is selected |
--tags naming an inner tag | Selects that task | Selects nothing |
Fixing it
Fix one: tag the include statement
- name: patch the fleet
ansible.builtin.include_role:
name: delta
tags: [patch]
--tags patch now selects the include, the include runs, and the tasks
inside are evaluated against the same selection as they arrive. The task
tagged patch runs.
The task not tagged patch does not. That is usually correct and is
worth stating, because it is different from the apply: behaviour below.
Fix two: apply:
include_role and include_tasks take an apply: keyword that pushes
directives onto every task the include brings in:
- name: patch the fleet
ansible.builtin.include_role:
name: delta
apply:
tags: [patch]
tags: [patch]
Note that the outer tags: is still required — apply: affects the included
content, not the statement, so without it the statement is still unselected
and nothing happens.
Now look at what apply: actually does:
$ ansible-playbook -i inventory.ini tagtrap-local.yml --tags patchTASK [include delta dynamically with apply] ************************************
included: delta for localhost
TASK [delta : delta patch task] ************************************************
ok: [localhost]
TASK [delta : delta untagged task] *********************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0delta untagged task ran. It has no tags of its own. apply: gave it
patch, so the selection reached it.
This is the same additive inheritance a static import performs, which makes
it consistent — but it is not what people expect from a keyword they reached
for to fix a tag problem. apply: tags: does not make the inner tags
reachable; it overwrites the question by tagging everything.
Fix three: make it static
If the include was dynamic out of habit rather than necessity — no loop, no
templated path, no run-time conditional — the best fix is import_role.
Inner tags become reachable, --list-tags becomes complete, and --list-tasks
starts telling you what the run will do.
Part XXII lesson 5 has the decision criteria. The short version: dynamic costs you the dry run, so pay it only for a loop or a run-time value.
Auditing for the trap
grep -rn -A4 'ansible.builtin.include_\(role\|tasks\)' playbooks/ roles/*/tasks/ \
| grep -B1 -A3 'include_'Then, for anything a scheduled job depends on, the definitive check:
ansible-playbook -i inventory/production.ini site.yml --list-tags | grep -q 'patch' \
&& echo "tag present" || echo "TAG ABSENT - the scheduled run is a no-op"Knowledge check
Knowledge check · 4 questions
Q1. A play consists of one untagged include_role. The role contains a task tagged patch. What does --tags patch do?
Q2. Which recap values together form the signature of a tag selection that matched nothing, as opposed to a when that excluded everything? Select all that apply.
Q3. Adding apply: tags: [patch] to an include_role makes the tasks inside the role that already carry the patch tag individually selectable, without affecting the others.
Q4. Which single command most directly confirms that a tag a scheduled job passes can select anything at all?
Passing score: 75%. Answers are checked in this browser.