Ansible tags let you run or skip selected parts of a playbook without editing the file. You label tasks in YAML, then choose slices at runtime with ansible-playbook --tags or --skip-tags.
This guide focuses on execution control: task, play, block, and role tags, special tags (always, never, tagged, untagged, all), listing tags before risky runs, and combining tags with check mode. It assumes you can run a playbook from how to run Ansible playbooks and playbook structure—this page does not repeat every ansible-playbook flag.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
What are Ansible Tags?
A tag is a label attached to a task, block, play, role, or import. Tags live in the playbook; they do nothing until you pass --tags or --skip-tags on the command line.
The official tags guide describes where tags can be defined and how runtime selection works.
Why Use Tags in Playbooks?
Tags save time on large playbooks:
- Rerun only install or config tasks during practice
- Skip slow reboot or validation steps while debugging
- Run verification slices without touching production config
- Opt in to dangerous cleanup tasks only when you mean to
The playbook stays one file; runtime flags choose the slice.
How Ansible Tags Work
- Define tags in the playbook with the
tags:keyword. - Run the playbook normally to execute all tasks (except
nevertasks). - Pass
--tags nameto run only matching tasks (plusalwaysunless skipped). - Pass
--skip-tags nameto run everything except matching tasks.
Tags do not replace conditionals—use when for host-specific logic. Tags select tasks at the CLI.
Add Tags to Tasks
Task tags are the safest place to start. Align tags: with the same indentation as module keys such as name or ansible.builtin.debug:
- name: Install packages slice
ansible.builtin.debug:
msg: "install slice"
tags:
- install
- name: Configure app slice
ansible.builtin.debug:
msg: "config slice"
tags: configA task can have one tag or a list. Multiple tasks can share one tag name so --tags install runs every install-labelled task.
Run Tasks with --tags
Create a demo playbook:
cat > ~/ansible-project/playbooks/tags-demo.yml << 'EOF'
---
- name: Ansible tags demo
hosts: lab
gather_facts: false
tags:
- playtag
tasks:
- name: Always gather baseline
ansible.builtin.debug:
msg: "always task runs unless skipped"
tags:
- always
- name: Install packages slice
ansible.builtin.debug:
msg: "install slice"
tags:
- install
- name: Configure app slice
ansible.builtin.debug:
msg: "config slice"
tags:
- config
- name: Validate deployment
ansible.builtin.debug:
msg: "verify slice"
tags:
- verify
- name: Dangerous cleanup (opt-in)
ansible.builtin.debug:
msg: "cleanup would run here"
tags:
- cleanup
- never
- name: Block tagged tasks
tags:
- blocktag
block:
- name: Block inner one
ansible.builtin.debug:
msg: "block inner one"
- name: Block inner two
ansible.builtin.debug:
msg: "block inner two"
- name: Include role with tags
ansible.builtin.include_role:
name: tag_role
tags:
- role_install
EOFAdd a small role for later examples:
mkdir -p ~/ansible-project/roles/tag_role/tasks
cat > ~/ansible-project/roles/tag_role/tasks/main.yml << 'EOF'
---
- name: Role install step
ansible.builtin.debug:
msg: "tag_role install task"
tags:
- install
- name: Role config step
ansible.builtin.debug:
msg: "tag_role config task"
tags:
- config
EOFRun only install-tagged tasks:
cd ~/ansible-project
ansible-playbook playbooks/tags-demo.yml --tags installSample output:
TASK [Always gather baseline] **************************************************
ok: [rocky2] => {
"msg": "always task runs unless skipped"
}
TASK [Install packages slice] **************************************************
ok: [rocky2] => {
"msg": "install slice"
}
PLAY RECAP *********************************************************************
rocky2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0always ran even though you asked for install. Pass multiple tags as a comma-separated list: --tags install,config.
--tags can skip prerequisites. A --tags config slice does not run install unless that task shares the tag or carries always—directories, packages, or baseline settings another tagged task assumes may never run. Audit with --list-tasks before production slices, and test tagged runs on a lab host end to end, not only the slice you think you need.
Skip Tasks with --skip-tags
Skip a slice while leaving the rest of the playbook available:
ansible-playbook playbooks/tags-demo.yml --skip-tags verifySample output:
PLAY RECAP *********************************************************************
rocky2 : ok=7 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0The verify task was skipped; other tagged tasks still ran. Use --skip-tags when excluding a few slow or risky tasks is easier than naming every tag you still want.
Skip always tasks when you truly want a narrow slice:
ansible-playbook playbooks/tags-demo.yml --tags config --skip-tags alwaysOnly the config task runs—no automatic always tasks.
List Tags Before Running a Playbook
Before a partial run on production, inspect what Ansible will consider:
ansible-playbook playbooks/tags-demo.yml --list-tagsSample output:
playbook: playbooks/tags-demo.yml
play #1 (lab): Ansible tags demo TAGS: [playtag]
TASK TAGS: [always, blocktag, cleanup, config, install, never, playtag, role_install, verify]See task names and their tags together:
ansible-playbook playbooks/tags-demo.yml --list-tasksSample output:
play #1 (lab): Ansible tags demo TAGS: [playtag]
tasks:
Always gather baseline TAGS: [always, playtag]
Install packages slice TAGS: [install, playtag]
Configure app slice TAGS: [config, playtag]
Validate deployment TAGS: [playtag, verify]
Block inner one TAGS: [blocktag, playtag]
Block inner two TAGS: [blocktag, playtag]
Include role with tags TAGS: [playtag, role_install]Run --list-tags or --list-tasks before risky --tags runs on shared inventories.
Add Tags to Plays
Play-level tags are inherited by every task in that play. If a task also defines its own tags, Ansible appends the play tag instead of replacing the task tag:
- name: Web tier rollout
hosts: lab
tags:
- playtag
tasks:
- name: Task inherits playtag
ansible.builtin.debug:
msg: "runs with play-level tag"
tags:
- install--list-tasks shows both tags on that task: [install, playtag]. Avoid broad play tags unless you intend --tags playtag to select the whole play.
Add Tags to Blocks
Tag a block to label every task inside it:
- name: Block tagged tasks
tags:
- blocktag
block:
- name: Block inner one
ansible.builtin.debug:
msg: "block inner one"
- name: Block inner two
ansible.builtin.debug:
msg: "block inner two"Run the block slice:
ansible-playbook playbooks/tags-demo.yml --tags blocktagSample output:
TASK [Block inner one] *********************************************************
ok: [rocky2] => {
"msg": "block inner one"
}
TASK [Block inner two] *********************************************************
ok: [rocky2] => {
"msg": "block inner two"
}
PLAY RECAP *********************************************************************
rocky2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0The always task ran as well (ok=3 total). For block rescue patterns, tag the block—not each inner task—when the whole group shares one purpose.
Tags with Roles, Imports, and Includes
For dynamic includes, the tag on the include selects the include task itself. It does not automatically tag every task inside the included file or role. If you want inner tasks to inherit the tag, use a static import or apply: with dynamic includes.
| Pattern | Behavior |
|---|---|
import_tasks with tags |
Imported tasks inherit the tag |
import_role with tags |
Role tasks inherit the tag |
include_tasks with tags |
Only the include task gets the tag unless apply: is used |
include_role with tags |
Only the include task gets the tag unless apply: is used |
Static roles and import_role
For static roles under roles: or import_role, a tag on the role is inherited by role tasks:
roles:
- role: tag_role
tags:
- deploy- name: Static role import with tags
ansible.builtin.import_role:
name: tag_role
tags:
- deployWith import_role, --list-tasks shows inner role tasks carrying the import tag—for example [deploy, install] on a role task that also defines install. Prefer import_role when full tag inheritance at parse time matters.
Dynamic include_role
A tag on include_role applies to the include statement itself:
- name: Include role with tags
ansible.builtin.include_role:
name: tag_role
tags:
- role_install--tags role_install selects the include_role task. After the role is included, normal tag filtering still applies to the role's inner tasks, so those tasks need the selected tag too unless you used apply: or a static import.
To push tags into a dynamically included role, use apply::
- name: Include role with apply tags
ansible.builtin.include_role:
name: tag_role
apply:
tags:
- role_install
tags:
- role_installThe include_role module docs note that task-level keywords on include_role apply to the include statement unless you pass them through apply:.
import_tasks vs include_tasks
The same static-vs-dynamic rule applies to task files:
- name: Include task file with inherited tags
ansible.builtin.include_tasks:
file: deploy_steps.yml
apply:
tags:
- deploy
tags:
- deployThe official tag inheritance guide also documents a block workaround for dynamic includes when apply: is not enough. For full static-vs-dynamic task loading detail, see include_tasks vs import_tasks. Role layout and deeper import/include behavior are covered in the roles guide.
Special Tags in Ansible
Ansible reserves several tag names for runtime selection.
always
Tasks tagged always run even when you pass --tags, unless you skip them explicitly:
ansible-playbook playbooks/tags-demo.yml --tags configSample output:
TASK [Always gather baseline] **************************************************
ok: [rocky2] => {
"msg": "always task runs unless skipped"
}
TASK [Configure app slice] *****************************************************
ok: [rocky2] => {
"msg": "config slice"
}Use always sparingly for essential setup steps that must not be accidentally skipped.
Fact gathering is also tagged always internally. If you run a very narrow tag slice and skip always, tasks that depend on facts may fail because facts were not gathered.
never
Tasks tagged never are skipped on a normal run. Request them explicitly:
ansible-playbook playbooks/tags-demo.yml --tags cleanupSample output:
TASK [Dangerous cleanup (opt-in)] **********************************************
ok: [rocky2] => {
"msg": "cleanup would run here"
}Use never for dangerous cleanup, destructive tests, or optional maintenance you do not want in a default run.
tagged
Run every task that has at least one tag (still respects never unless you request those tags):
ansible-playbook playbooks/tags-demo.yml --tags taggeduntagged
Run only tasks that have no tags. Rare in well-tagged playbooks; useful when migrating legacy content.
all
all selects the default runnable task set, similar to running without --tags. Tasks tagged never still require explicit opt-in through --tags never or another tag on that task.
Tags vs --limit
| Control | Selects | Example |
|---|---|---|
--tags / --skip-tags |
Which tasks run | --tags config |
--limit |
Which hosts run | --limit rocky2 |
Combine them when you need both:
ansible-playbook playbooks/tags-demo.yml --tags verify --limit rocky2--limit does not replace tags—it narrows the host list while tags narrow the task list. Host patterns and inventory groups are covered in the inventory guide.
Tags with Check Mode and Diff Mode
Tags combine with check mode and diff for safer partial previews. Confirm ok vs changed on reruns with idempotency before you trust a tagged slice in production. For task-file reuse patterns, see include_tasks vs import_tasks.
ansible-playbook playbooks/tags-demo.yml --check --tags configSample output:
TASK [Configure app slice] *****************************************************
ok: [rocky2] => {
"msg": "config slice"
}
PLAY RECAP *********************************************************************
rocky2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0--check shows what would change for the selected tag slice without applying it. Add --diff when file or template tasks should print differences. Check mode is a preview—not a substitute for testing tagged tasks on a lab host first.
Common Tag Use Cases
Run only package installation tasks
Tag package tasks install and run ansible-playbook site.yml --tags install.
Run only configuration tasks
Tag template, copy, and lineinfile work config for iterative config tuning without reinstalling packages.
Skip reboot or restart tasks
Tag service restarts restart and pass --skip-tags restart while debugging install logic.
Run only validation tasks
Tag smoke tests and uri checks verify for a post-change validation pass.
Run only one role
For static roles under roles: or import_role, a tag on the role can select the whole role because tags are inherited by role tasks.
For include_role, a tag on the include selects the include statement. Inner role tasks still need matching tags, or you need apply: tags, or you should use import_role when full tag inheritance is required.
Run dangerous cleanup tasks only when requested
Combine cleanup with never so default runs skip cleanup; pass --tags cleanup when you intend to run it.
Common Tag Mistakes
Why does --tags not run my included tasks?
This is the most common tag confusion on forums: --tags role_install runs the include_role line, but inner role tasks are skipped unless they also match the tag (or you used import_role / apply:).
Audit with --list-tasks. Static imports show inner tasks at parse time; dynamic includes often list only the include wrapper until runtime.
| Symptom | Likely cause | Fix |
|---|---|---|
| Tagged tasks never run alone | Expecting tags in YAML to filter by themselves | Pass --tags or --skip-tags at runtime |
| Partial run still executes everything | No --tags passed |
Add --tags slice or --skip-tags slow on the CLI |
--tags runs more than expected |
Broad play or block tag inherited everywhere | Prefer task-level tags; audit with --list-tasks |
--tags install does not enter include_tasks |
The include task itself is not tagged | Tag the include task too, or use static import_tasks |
--tags role_install enters role but skips role tasks |
include_role tag did not inherit to inner tasks |
Use import_role, duplicate inner task tags, or apply: tags |
--list-tasks only shows the include, not all inner tasks |
Dynamic includes are evaluated at runtime | Use static imports when you need parse-time visibility |
| Inconsistent slices | Tag sprawl (deploy, deployment, deploy_app) |
Standardize names; document in README or vars |
| Wrong hosts change | Used --limit expecting task filter |
Tags select tasks; --limit selects hosts |
always tasks ignore your slice |
always runs unless skipped |
Add --skip-tags always for a strict slice |
never tasks run unexpectedly |
Passed matching --tags |
Remove the tag from CLI unless opt-in is intended |
| Tagged handler does not run | Handler was not notified by a changed selected task | Tag the notifying task; handlers run from notifications, not direct tag selection |
| Risky run without preview | Skipped --list-tags |
List tags/tasks before production partial runs |
Tag Naming Best Practices
- Use short, action-oriented names:
install,config,verify,restart - Reuse the same tag across tasks that belong to one slice
- Keep tag count small—tag for runtime slices you actually run
- Match case consistently; tags are case-sensitive (
Config≠config) - Stick to letters, numbers, and underscores for clarity
Recommended Tag Strategy
- Start with task-level tags on install, config, verify, and restart groups.
- Run
--list-tagsbefore the first partial run on a shared inventory. - Use
import_roleorapply:when a tag must reach inner role or include tasks. - Use
alwaysonly for steps that must not be skipped accidentally. - Use
neverfor destructive or rare tasks that need explicit opt-in. - Avoid play-wide tags unless the entire play is one logical slice.
- Combine
--tagswith--checkon lab hosts before production partial runs.
Summary
Ansible tags label tasks so you can run or skip playbook slices at runtime with --tags and --skip-tags—without editing the playbook. Define tags on tasks, blocks, plays, and roles; use always and never for special behavior; list tags before risky runs; and remember that tags select tasks while --limit selects hosts. That workflow keeps large playbooks manageable during practice and production reruns.
References
- Ansible tags — official tag guide
- Ansible tag inheritance — imports vs includes
- Run Ansible playbooks — check mode, diff, limits, verbosity
- Ansible blocks — block-level grouping
- Ansible roles — role layout
- include_tasks vs import_tasks — static vs dynamic loading

