Introduction

Tags let you run a subset of tasks from a playbook. Instead of running everything, target only what you need — just the config deployment, just the packages, or just the service restarts. Essential for large playbooks and iterative development.

Tag a Task

tasks:
  - name: Install packages
    ansible.builtin.apt:
      name: [nginx, redis]
    tags: packages

  - name: Deploy config
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    tags: config

  - name: Restart nginx
    ansible.builtin.service:
      name: nginx
      state: restarted
    tags: services

Run and Skip Tags

# Run only tasks tagged 'config'
ansible-playbook site.yml --tags config

# Run multiple tags
ansible-playbook site.yml --tags "config,services"

# Skip specific tags
ansible-playbook site.yml --skip-tags packages

# List all tags in a playbook
ansible-playbook site.yml --list-tags

# List tasks that would run
ansible-playbook site.yml --tags config --list-tasks

Multiple Tags on a Task

- name: Deploy app config
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/myapp/config.yml
  tags:
    - config
    - deploy
    - myapp

# This task runs with --tags config OR --tags deploy OR --tags myapp

Special Tags

always

Tasks tagged always run even when you use --tags (unless explicitly skipped):

- name: Gather facts
  ansible.builtin.setup:
  tags: always    # Runs no matter what

- name: Install packages
  ansible.builtin.apt:
    name: nginx
  tags: packages

- name: Deploy config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  tags: config
ansible-playbook site.yml --tags config
# Runs: "Gather facts" (always) + "Deploy config" (config)

ansible-playbook site.yml --skip-tags always
# Skips "Gather facts"

never

Tasks tagged never only run when explicitly requested:

- name: Dangerous cleanup
  ansible.builtin.shell: rm -rf /tmp/old-data
  tags: never    # Only runs with --tags cleanup

- name: Actually give it a usable tag too
  ansible.builtin.shell: rm -rf /tmp/old-data
  tags:
    - never
    - cleanup
ansible-playbook site.yml              # Does NOT run cleanup
ansible-playbook site.yml --tags cleanup  # DOES run cleanup

Tag a Block

- name: Database tasks
  tags: database
  block:
    - name: Install PostgreSQL
      ansible.builtin.apt:
        name: postgresql

    - name: Configure PostgreSQL
      ansible.builtin.template:
        src: pg_hba.conf.j2
        dest: /etc/postgresql/16/main/pg_hba.conf

    - name: Start PostgreSQL
      ansible.builtin.service:
        name: postgresql
        state: started
# All three tasks inherit the 'database' tag

Tag a Role

roles:
  - role: nginx
    tags: [webserver, nginx]

  - role: postgresql
    tags: [database, postgresql]

  - role: monitoring
    tags: monitoring
# Run only the nginx role
ansible-playbook site.yml --tags nginx

# Run webserver and database roles
ansible-playbook site.yml --tags "webserver,database"

Tag a Play

---
- name: Web server setup
  hosts: webservers
  tags: web
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx

- name: Database setup
  hosts: dbservers
  tags: db
  tasks:
    - name: Install PostgreSQL
      ansible.builtin.apt:
        name: postgresql
ansible-playbook site.yml --tags web  # Only web play runs

Tag with include/import

# import_tasks — tags apply to ALL imported tasks
- name: Import web tasks
  ansible.builtin.import_tasks: web.yml
  tags: web
# Every task in web.yml gets the 'web' tag

# include_tasks — tag applies to the include itself
- name: Include web tasks
  ansible.builtin.include_tasks: web.yml
  tags: web
# Only the include task is tagged; tasks inside web.yml keep their own tags

This is an important distinction:

import_tasksinclude_tasks
Tag inheritanceTags flow to all imported tasksTags only on the include line
When evaluatedParse time (static)Runtime (dynamic)
Best forApplying tags to a whole fileConditional includes

Practical Patterns

Deployment Workflow

tasks:
  - name: Pull latest code
    ansible.builtin.git:
      repo: https://github.com/myorg/myapp.git
      dest: /opt/myapp
    tags: [deploy, code]

  - name: Install dependencies
    ansible.builtin.pip:
      requirements: /opt/myapp/requirements.txt
    tags: [deploy, deps]

  - name: Run migrations
    ansible.builtin.command: python manage.py migrate
    args:
      chdir: /opt/myapp
    tags: [deploy, migrate]

  - name: Deploy config
    ansible.builtin.template:
      src: config.yml.j2
      dest: /opt/myapp/config.yml
    tags: [deploy, config]

  - name: Restart app
    ansible.builtin.service:
      name: myapp
      state: restarted
    tags: [deploy, restart]
# Full deploy
ansible-playbook site.yml --tags deploy

# Just update config and restart
ansible-playbook site.yml --tags "config,restart"

# Just run migrations
ansible-playbook site.yml --tags migrate

Debug Tag

- name: Show all variables
  ansible.builtin.debug:
    var: hostvars[inventory_hostname]
  tags:
    - never
    - debug

- name: Show network info
  ansible.builtin.debug:
    var: ansible_default_ipv4
  tags:
    - never
    - debug
# Normal run — debug tasks don't run
ansible-playbook site.yml

# Debug run — includes debug output
ansible-playbook site.yml --tags "all,debug"

Best Practices

  1. Use consistent tag names across playbooks: packages, config, services, deploy, security
  2. Tag at the right level — don't over-tag individual tasks; tag blocks or roles
  3. Use always sparingly — only for truly essential tasks (fact gathering, validation)
  4. Use never for dangerous tasks — cleanup, destructive operations
  5. Document your tags — list them in README

Troubleshooting

Tagged Task Doesn't Run

Check if the task is inside an include_tasks — tags don't flow into dynamic includes:

# Tags DON'T flow in
- include_tasks: tasks.yml
  tags: mytag
# Tasks inside tasks.yml won't have 'mytag'

# Tags DO flow in
- import_tasks: tasks.yml
  tags: mytag
# All tasks inside tasks.yml get 'mytag'

Handler Not Triggered with --tags

Handlers only run if their notifying task ran AND changed something. If you skip the notifying task with --tags, the handler won't trigger.

Conclusion

Tags give you surgical control over playbook execution. Tag tasks, blocks, roles, and plays — then use --tags and --skip-tags to run exactly what you need. Use always for essential tasks that must run regardless, and never for dangerous operations that require explicit opt-in. Remember that import_tasks inherits tags (static) while include_tasks doesn't (dynamic). Keep tag names consistent across your project for predictable behavior.