AnsibleXXIII · Tags, Blocks and Error HandlingError handling
always, never, tagged and untagged
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
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:
$ 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, plusalways--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.
ansible-playbook -i inventory/production.ini site.yml --list-tasks --tags untaggedThat 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
| Selection | always | never + destroy | tagged routine | untagged |
|---|---|---|---|---|
(none) / --tags all | runs | skipped | runs | runs |
--tags routine | runs | skipped | runs | skipped |
--tags destroy | runs | runs | skipped | skipped |
--tags untagged | runs | skipped | skipped | runs |
--skip-tags always | skipped | skipped | runs | runs |
Knowledge check
Knowledge check · 4 questions
Q1. A task carries tags: [never, destroy]. Which invocation runs it?
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.
Q3. Which of these belong on an always-tagged task? Select all that apply.
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.