Skip to main content
RunBook Academy

AnsibleXXIII · Tags, Blocks and Error HandlingError handling

The tag that selected nothing

Advanced⏱ ~19 minansible-playbook

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

Not yet marked complete on this device.

An operator runs the monthly patch play:

Read-only / Safethe run that looks fine
$ ansible-playbook -i inventory.ini tagtrap.yml --tags patch
PLAY [the tag that selected nothing] *******************************************

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Exit 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:

Read-only / Safethe tag surface, as the parser sees it
$ ansible-playbook -i inventory.ini dynonly.yml --list-tags
playbook: 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:

Read-only / Safestatic import: the inner tag is reachable
$ ansible-playbook -i inventory.ini import-local.yml --tags alpha_inner
TASK [alpha : alpha task one] **************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
Static import_*Dynamic include_*
Tags on the statementApplied to every task insideApplied to the statement only
Tags declared insideVisible to --list-tags, individually selectableInvisible; unreachable unless the statement is selected
--tags naming an inner tagSelects that taskSelects 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:

Read-only / Safeapply: stamps the tag onto every inner task
$ ansible-playbook -i inventory.ini tagtrap-local.yml --tags patch
TASK [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=0

delta 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

Read-only / Safedynamic includes whose statement carries no tags
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:

Read-only / Safedoes the tag this job uses actually exist
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

  1. Q1. A play consists of one untagged include_role. The role contains a task tagged patch. What does --tags patch do?

  2. 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.

  3. 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.

  4. 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.