Skip to main content
RunBook Academy

AnsibleXXIII · Tags, Blocks and Error HandlingError handling

always, never, tagged and untagged

Intermediate⏱ ~17 minansible-playbook

What you'll learn

  • Predict what each reserved tag does for a given selection
  • Use never to keep a destructive task out of every ordinary run
  • State how always is suppressed, and why that matters for a dry run
  • Choose between never and a required variable as a guard

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.

Four tag names are reserved, and their behaviour is easier to state by running it than by describing it. This playbook has one task carrying each of the interesting cases:

- name: always and never
  hosts: local
  gather_facts: false
  tasks:
    - name: always task
      ansible.builtin.debug: {msg: always}
      tags: [always]

    - name: never task
      ansible.builtin.debug: {msg: never}
      tags: [never, destroy]

    - name: plain task
      ansible.builtin.debug: {msg: plain}
      tags: [routine]

    - name: untagged task
      ansible.builtin.debug: {msg: untagged}

Five selections, executed against ansible-core 2.21.3:

Read-only / Safethe complete behaviour table, executed
$ for t in '--tags routine' '--tags destroy' '--tags all' '--tags untagged' '--skip-tags always'; do echo "== $t"; ansible-playbook -i inventory.ini specialtags.yml $t | grep '^TASK'; done
== --tags routine
TASK [always task]
TASK [plain task]

== --tags destroy
TASK [always task]
TASK [never task]

== --tags all
TASK [always task]
TASK [plain task]
TASK [untagged task]

== --tags untagged
TASK [always task]
TASK [untagged task]

== --skip-tags always
TASK [plain task]
TASK [untagged task]

Everything about the four reserved tags is in those five blocks.

always

Runs under every selection — including one that names a completely different tag, and including --tags selections that match nothing else at all.

The one thing that suppresses it is naming it explicitly: --skip-tags always. That is the escape hatch, and the last block above is it working.

Where it belongs: preconditions and assertions. A task that verifies the host is what you think it is should run whatever subset the operator selected, because a partial run against the wrong host is not safer than a full one.

- name: refuse to run against a host outside the intended environment
  ansible.builtin.assert:
    that: ansible_facts['hostname'] in groups['web']
    fail_msg: >-
      This play configures the web tier. {{ inventory_hostname }} is not in
      the web group; check --limit before retrying.
  tags: [always]

Part XXII lesson 4 showed the generated role-argument validation task carrying exactly this tag, for exactly this reason.

Where it does not belong: anything that changes state. A task tagged always that installs a package makes every tag selection wider than the operator asked for, and --tags certificates stops being a statement about blast radius.

never

The inverse. A task tagged never is skipped unless something explicitly requests one of its tags — including never itself.

Look at the second and third blocks. --tags destroy runs the never task, because destroy was named. --tags all does not, because all is not a name — it is the wildcard, and the wildcard deliberately does not reach tasks marked never.

The production use is a destructive operation that lives in the playbook, next to the code it belongs with, and cannot execute by accident:

- name: drop and recreate the reporting schema
  community.postgresql.postgresql_db:
    name: reporting
    state: absent
  tags: [never, reset_reporting_schema]

An operator who needs it types --tags reset_reporting_schema. Nobody gets there by running site.yml, by running --tags all, or by a --tags database that happens to overlap. The tag name is the request, and the request has to be exact.

tagged and untagged

Two selectors rather than tags you apply. They are how you answer “what did I forget to tag”:

  • --tags untagged — every task carrying no tags at all, plus always
  • --tags tagged — every task carrying at least one tag
  • --skip-tags untagged — everything that has been tagged

The fourth block above is --tags untagged selecting the untagged task and the always task, and nothing else.

Read-only / Safefind the tasks a tag scheme cannot reach
ansible-playbook -i inventory/production.ini site.yml --list-tasks --tags untagged

That command is the audit for a partially tagged playbook. Anything it lists is work no --tags invocation can select, which means it can only ever be run as part of the whole play — usually the opposite of what somebody intended when they started adding tags.

--tags all is the fifth selector and it is the default: with no --tags at all, Ansible behaves as --tags all, which is everything except never.

The complete table

Selectionalwaysnever + destroytagged routineuntagged
(none) / --tags allrunsskippedrunsruns
--tags routinerunsskippedrunsskipped
--tags destroyrunsrunsskippedskipped
--tags untaggedrunsskippedskippedruns
--skip-tags alwaysskippedskippedrunsruns

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task carries tags: [never, destroy]. Which invocation runs it?

  2. Q2. A task tagged always runs even under a --tags selection that matches nothing else in the playbook, and the only way to suppress it is --skip-tags always.

  3. Q3. Which of these belong on an always-tagged task? Select all that apply.

  4. Q4. A destructive task must not run without a deliberate second confirmation. What does tags: [never, reset_schema] give you, and what does it not?

Passing score: 75%. Answers are checked in this browser.