Ansible Tags Explained: Run and Skip Tasks in Playbooks

Learn Ansible tags to run or skip selected playbook tasks at runtime with --tags, --skip-tags, always, never, play and role tags, and safe partial runs.

Published

Updated

Read time 13 min read

Reviewed byDeepak Prasad

Ansible tags for selective playbook runs on Rocky Linux 10

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.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/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

  1. Define tags in the playbook with the tags: keyword.
  2. Run the playbook normally to execute all tasks (except never tasks).
  3. Pass --tags name to run only matching tasks (plus always unless skipped).
  4. Pass --skip-tags name to 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:

yaml
- name: Install packages slice
  ansible.builtin.debug:
    msg: "install slice"
  tags:
    - install

- name: Configure app slice
  ansible.builtin.debug:
    msg: "config slice"
  tags: config

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

bash
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
EOF

Add a small role for later examples:

bash
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
EOF

Run only install-tagged tasks:

bash
cd ~/ansible-project
ansible-playbook playbooks/tags-demo.yml --tags install

Sample output:

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=0

always ran even though you asked for install. Pass multiple tags as a comma-separated list: --tags install,config.

WARNING
Partial runs with --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:

bash
ansible-playbook playbooks/tags-demo.yml --skip-tags verify

Sample output:

output
PLAY RECAP *********************************************************************
rocky2                     : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

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

bash
ansible-playbook playbooks/tags-demo.yml --tags config --skip-tags always

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

bash
ansible-playbook playbooks/tags-demo.yml --list-tags

Sample output:

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:

bash
ansible-playbook playbooks/tags-demo.yml --list-tasks

Sample output:

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:

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

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

bash
ansible-playbook playbooks/tags-demo.yml --tags blocktag

Sample output:

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=0

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

yaml
roles:
  - role: tag_role
    tags:
      - deploy
yaml
- name: Static role import with tags
  ansible.builtin.import_role:
    name: tag_role
  tags:
    - deploy

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

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

yaml
- name: Include role with apply tags
  ansible.builtin.include_role:
    name: tag_role
    apply:
       tags:
        - role_install
       tags:
    - role_install

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

yaml
- name: Include task file with inherited tags
  ansible.builtin.include_tasks:
    file: deploy_steps.yml
    apply:
      tags:
        - deploy
  tags:
    - deploy

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

bash
ansible-playbook playbooks/tags-demo.yml --tags config

Sample output:

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:

bash
ansible-playbook playbooks/tags-demo.yml --tags cleanup

Sample output:

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

bash
ansible-playbook playbooks/tags-demo.yml --tags tagged

untagged

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:

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

bash
ansible-playbook playbooks/tags-demo.yml --check --tags config

Sample output:

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 (Configconfig)
  • Stick to letters, numbers, and underscores for clarity

  1. Start with task-level tags on install, config, verify, and restart groups.
  2. Run --list-tags before the first partial run on a shared inventory.
  3. Use import_role or apply: when a tag must reach inner role or include tasks.
  4. Use always only for steps that must not be skipped accidentally.
  5. Use never for destructive or rare tasks that need explicit opt-in.
  6. Avoid play-wide tags unless the entire play is one logical slice.
  7. Combine --tags with --check on 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

Frequently Asked Questions

1. Do tags inside a playbook select tasks automatically?

No. Tags are labels only. You choose which tagged tasks run at runtime with ansible-playbook --tags or --skip-tags.

2. What is the difference between --tags and --skip-tags?

--tags runs only tasks that match the given tag names (plus always tasks unless skipped). --skip-tags runs everything except tasks with the listed tags.

3. What does the always tag do?

Tasks tagged always run even when you pass --tags, unless you explicitly skip always with --skip-tags always.

4. What does the never tag do?

Tasks tagged never are skipped by default. They run only when you request that tag with --tags.

5. Does a tag on include_role run every task in the role?

No. The tag on include_role selects the include statement. Inner tasks run only if they also match the tag, or if you use apply: tags or import_role for inheritance.

6. What is the difference between tags and --limit?

Tags select which tasks run. --limit selects which hosts run. They solve different problems and can be combined.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …