Intermediate 7 min · 2026-06-21

Ansible Tags Survival Guide: How We Cut 20-Minute Playbooks to 90 Seconds

Master Ansible tags for selective execution: --tags, --skip-tags, always/never, tag inheritance, CI integration.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Use --tags to run only tagged tasks: ansible-playbook playbook.yml --tags deploy Use --skip-tags to exclude tasks: ansible-playbook playbook.yml --skip-tags smoke-test Tag tasks, roles, and blocks with tags: key; blocks inherit tags to all children Special tags always and never control unconditional execution: tags: always runs even with --tags foo Tags on includes/imports propagate to all contained tasks; use apply: to override In CI, use --tags changed with a custom callback to run only modified components Avoid tag typos: Ansible silently ignores unknown tags, use --list-tags to verify Use --list-tasks --tags deploy to preview which tasks will execute

✦ Definition~90s read
What is Ansible Tags for Selective Execution?

Ansible tags are metadata you attach to tasks, roles, or blocks to control which ones execute during a playbook run. The --tags flag filters tasks to only those with matching tags, while --skip-tags excludes tasks with certain tags. This is essential for large playbooks where you want to run only specific parts—like deploying code without running security scans.

Imagine you're a chef with a massive recipe book.

Tags are also useful in CI/CD pipelines to run only the components that changed, drastically reducing feedback loops. They work with ansible-playbook, ansible-pull, and even within ansible-navigator.

Plain-English First

Imagine you're a chef with a massive recipe book. Normally, you cook the entire meal from appetizer to dessert, which takes hours. But sometimes you only need to reheat the soup because the rest is already perfect. Ansible tags are like sticky notes on each recipe step: you can say 'only do the steps tagged with #reheat.' This saves you from starting the whole cooking process over. In our kitchen, we used this to skip running full server setups when only updating a config file—cutting our deployment time from 20 minutes to 90 seconds.

I still remember the Friday afternoon when our deployment pipeline broke. The playbook took 20 minutes to run, and the team was waiting to push a critical hotfix. The problem? We had no way to skip the 15-minute OS patch check that ran every single time, even when we only changed a config file. That's when I discovered Ansible tags—and it changed everything. Tags let you selectively execute parts of your playbook, turning a monolithic run into a surgical strike. In this article, I'll share how we use tags in production, including a real incident where a missing tag caused a 2-hour outage, and how to integrate tags with CI to run only changed components.

Tagging Tasks, Roles, and Blocks

Tagging is straightforward: add a tags: key to a task, role, or block. For tasks:

``yaml - name: Install nginx apt: name: nginx state: present tags: - packages - nginx ``

``yaml - hosts: webservers roles: - role: nginx tags: - nginx - web ``

``yaml - name: Configure web server block: - name: Copy config template: ... - name: Restart service service: ... tags: - config ``

A gotcha: if you tag a block, the block itself is not a task, so --list-tasks won't show the block tag unless a task inside has it. Always tag individual tasks if you need precise control.

Tag Inheritance Gotcha
Tags on a block apply to all tasks inside, but not to handlers notified by those tasks. Handlers have their own tags—if you use --tags config, a handler without the config tag won't run even if notified.
Production Insight
In our production playbook, we tag every task with at least one of: deploy, config, security, or monitoring. This lets us run --tags deploy during a code push, skipping security scans that take 5 minutes.
Key Takeaway
Tag tasks, roles, and blocks explicitly; remember handlers are not automatically tagged.

Using --tags and --skip-tags CLI Flags

The --tags flag filters execution to only tasks with specified tags. Multiple tags are comma-separated: --tags deploy,config. The --skip-tags flag excludes tasks with those tags. You can combine them: --tags deploy --skip-tags smoke-test runs deploy tasks but not smoke tests.

Important: --tags and --skip-tags are mutually exclusive in the sense that --skip-tags takes precedence. If a task has both a tag you include and a tag you skip, it will be skipped.

Use --list-tasks --tags deploy to preview what will run. This is invaluable for debugging: I once spent an hour wondering why a task wasn't running, only to find a typo in the tag name (deploy vs deply).

``bash ansible-playbook playbook.yml --tags "$TAGS" ``

Where $TAGS is set by your CI system based on changed files.

Use --list-tags First
Run ansible-playbook playbook.yml --list-tags to see all tags in your playbook. This helps avoid typos and confirms your tagging strategy.
Production Insight
We once had a pipeline that used --tags deploy but a new task was tagged as deploy (misspelled). The task never ran, causing a config drift. Now we run --list-tags in CI to validate tags.
Key Takeaway
Always preview with --list-tasks --tags before running; tag names are case-sensitive and typos are silent.

Special Tags: always and never

Ansible provides two special tags: always and never. Tags with always run regardless of the --tags filter (unless --skip-tags always is used). Tags with never are skipped unless explicitly included with --tags never.

Use always for critical setup tasks like creating users or directories that must exist for other tasks to work. Use never for optional or destructive tasks, like database resets.

```yaml - name: Create app user user: name: app tags: always

  • name: Reset database
  • shell: dropdb myapp
  • tags: never
  • ```

Gotcha: always does not override --skip-tags always. If you skip always, it won't run. Also, always tasks are not included when you use --tags without including always explicitly? Actually, they are included automatically. But if you use --tags foo, tasks with always still run. This is by design: always is additive.

However, if you use --skip-tags always, they are skipped. So be careful when skipping.

always vs. never Behavior
always tasks run even with --tags foo; never tasks only run with --tags never or --tags all.
Production Insight
We tag our log rotation setup with always because it's critical for disk space. One time a junior engineer used --skip-tags always to speed up a test and the disks filled up, causing an outage.
Key Takeaway
Use always for essential tasks, never for optional ones; remember --skip-tags always will skip them.

Tag Inheritance in Includes and Imports

When you include or import a file, tags can be applied at the include/import level. These tags propagate to all tasks in the included file. However, there's a nuance: include_tasks vs import_tasks.

With import_tasks, tags are applied statically at parse time. With include_tasks, tags are dynamic but still propagate.

``yaml - name: Include deploy tasks import_tasks: deploy.yml tags: - deploy ``

All tasks in deploy.yml will inherit the deploy tag. You can override this by using the apply keyword:

``yaml - name: Include with custom tags import_tasks: deploy.yml tags: - deploy apply: tags: - custom ``

This applies custom to all tasks, but they still also have deploy? Actually, apply replaces the inherited tags. So tasks will have only custom tag. This is a common source of confusion.

Best practice: Use import_tasks for static includes and apply tags at the include level. For dynamic includes, test carefully because tag inheritance can be unpredictable.

apply Replaces, Not Adds
The apply key on includes replaces the inherited tags, it does not add to them. If you want both, list all tags in apply.
Production Insight
We had a playbook that included a common tasks file with tags: always. Someone added apply: tags: deploy thinking it would add, but it replaced always, so critical setup tasks were skipped during --tags deploy runs.
Key Takeaway
Tag inheritance works with includes/imports; use apply carefully as it overrides inherited tags.

Strategy for Tagging in Large Playbooks

In large playbooks with hundreds of tasks, a consistent tagging strategy is essential. Here's our production approach:

  1. Categorize by function: deploy, config, security, monitoring, cleanup.
  2. Categorize by component: nginx, app, db, cache.
  3. Use compound tags: deploy_app, config_nginx, security_patch.
  4. Tag every task with at least one function tag.
  5. Use always sparingly—only for tasks that must run every time (e.g., user creation).
  6. Document tags in a README or use --list-tags in CI to generate docs.

``yaml tags: - deploy - app - config ``

This allows running --tags deploy for a full deploy, or --tags app for app-specific tasks, or --tags deploy,app for app deploy only.

Production insight: We maintain a tags.yml file that lists all tags and their purpose, checked into the repo. New team members must read it before modifying playbooks.

Tag Naming Convention
Use lowercase and underscores. Avoid spaces. Keep tags short but descriptive: deploy_app is better than dplapp.
Production Insight
We once had a playbook with 50+ tasks and no tags. Running the full playbook took 30 minutes. After implementing a tagging strategy, we reduced deploy time to 2 minutes by using --tags deploy.
Key Takeaway
Create a tagging strategy with functional and component tags; document it for the team.

Using Tags in CI to Run Only Changed Components

In CI, you can determine which components changed and pass corresponding tags to Ansible. For example, if only the nginx directory changed, run only tasks tagged with nginx.

```yaml - name: Detect changes id: changed uses: dorny/paths-filter@v2 with: filters: | nginx: - 'roles/nginx/' app: - 'app/' db: - 'roles/db/**'

  • name: Run Ansible with tags
  • run: |
  • TAGS=""
  • if [[ "${{ steps.changed.outputs.nginx }}" == "true" ]]; then TAGS="$TAGS,nginx"; fi
  • if [[ "${{ steps.changed.outputs.app }}" == "true" ]]; then TAGS="$TAGS,app"; fi
  • if [[ "${{ steps.changed.outputs.db }}" == "true" ]]; then TAGS="$TAGS,db"; fi
  • TAGS=${TAGS#,}
  • ansible-playbook playbook.yml --tags "$TAGS"
  • ```

If no tags are set (no changes), you might want to run a default set of tags, or skip entirely. Use --tags always to run only always tasks, or --tags never to run nothing.

Gotcha: If you use --tags with an empty string, Ansible runs all tasks. So ensure you handle the empty case.

CI Tag Filtering
Use a paths filter to detect changes and map them to Ansible tags. This reduces pipeline time significantly.
Production Insight
We reduced our CI pipeline from 25 minutes to 3 minutes by running only changed component tags. The key was mapping each microservice to its own tag.
Key Takeaway
Integrate tags with CI change detection to run only what's needed; handle empty tags gracefully.

Tagging Handlers Correctly

Handlers are tasks that run when notified, but they have their own tags. If a handler is not tagged, it will only run if no --tags filter is applied. If you use --tags deploy, an untagged handler will not run even if notified.

Solution: Tag handlers with the same tags as the tasks that notify them, or use tags: always for critical handlers.

``yaml handlers: - name: restart nginx service: name: nginx state: restarted tags: - nginx - config ``

Now if you run --tags nginx, this handler will run when notified.

Gotcha: Even with correct tags, handlers only run at the end of the play. If you use --tags that exclude the handler's tag, it won't run. Always verify with --list-tasks.

Handlers Are Not Automatically Tagged
Handlers do not inherit tags from the notifying task. You must tag them explicitly.
Production Insight
We had a handler restart app that was untagged. When we ran --tags deploy, the handler never ran, causing the app to use old config. Now all handlers have tags: always.
Key Takeaway
Always tag handlers, preferably with always or the same tags as notifying tasks.

Using --list-tags and --list-tasks for Debugging

These flags are your best friends for debugging tag issues. --list-tags shows all tags used in the playbook. --list-tasks shows tasks that will run, optionally filtered by --tags.

``bash ansible-playbook playbook.yml --list-tags ansible-playbook playbook.yml --list-tasks --tags deploy ``

The second command shows only tasks tagged with deploy. If a task you expect is missing, check its tag.

Production insight: I always run --list-tasks --tags mytag before a production deploy to confirm exactly what will execute. This caught many issues, like a new task accidentally tagged with never.

Always Preview Before Production
Run --list-tasks --tags <your_tags> as a dry run to verify the scope.
Production Insight
Once, a colleague added a task with tags: deply (typo). Our CI used --tags deploy, so the task never ran. We only caught it because --list-tags showed deply as a separate tag.
Key Takeaway
Use --list-tags and --list-tasks to validate tag usage before running.

Common Pitfall: Tags and `--check` Mode

When using --check mode, tags still apply. So ansible-playbook playbook.yml --check --tags deploy will only check tasks tagged with deploy. This is useful for dry-running a subset.

However, --check does not run handlers. So if you're testing a handler tag, you won't see it in check mode. Use --list-tasks instead.

Production insight: We use --check --tags deploy in CI to validate syntax and task selection before actual deployment.

Check Mode and Tags
Tags work with --check mode; use --list-tasks to see handler tags.
Production Insight
We once had a playbook that passed --check but failed in real run because a handler was missing tags. Now we always run --list-tasks to verify handlers.
Key Takeaway
Combine --check with --tags for safe previews; remember handlers are not executed in check mode.

Tagging with `ansible-navigator`

If you use ansible-navigator (the TUI for Ansible), tags work the same way. You can pass --tags and --skip-tags as CLI arguments. In ansible-navigator's interactive mode, you can filter by tags using the :tags command.

``bash ansible-navigator run playbook.yml --tags deploy ``

Inside navigator, press : and type tags deploy to see only tagged tasks.

Navigator Tag Filtering
In ansible-navigator, use :tags deploy to filter the task list.
Production Insight
We use ansible-navigator for ad-hoc debugging. Filtering by tags helps focus on relevant tasks without scrolling through hundreds.
Key Takeaway
Tags work seamlessly with ansible-navigator; use :tags to filter interactively.

Tagging and Ansible Vault

Tags do not affect vault decryption. If you have vault-encrypted variables, they are decrypted regardless of tags. However, tasks that use those variables will only run if tagged appropriately.

Gotcha: If you run --tags deploy and a task with vault variables is not tagged deploy, it won't run, but the vault password will still be required because Ansible parses the entire playbook.

Vault and Tags
Vault decryption happens before tag filtering, so you'll still be prompted for a password even if no tagged tasks use vault.
Production Insight
We once had a playbook with vault-encrypted secrets. Running --tags deploy still asked for the vault password, which confused new team members. We documented this behavior.
Key Takeaway
Tags don't skip vault decryption; you'll always need the vault password if any vault vars exist.

Advanced: Dynamic Tags with Variables

You can set tags using variables, but be careful: tags are evaluated at parse time for static imports, but dynamically for includes. This can lead to unexpected behavior.

``yaml - name: Run with dynamic tags include_tasks: somefile.yml tags: "{{ my_tags }}" ``

If my_tags is a list, it works. But if the variable is undefined, the task will fail. Use default() to provide a fallback.

Production insight: We avoid dynamic tags in static imports because they cause parse-time errors. For dynamic includes, we use them sparingly.

Dynamic Tags Can Be Unpredictable
Tags from variables are resolved at runtime for includes, but at parse time for imports. Prefer static tags.
Production Insight
We tried using a variable to set tags based on environment, but it led to confusing behavior. Now we use separate playbooks for different environments instead.
Key Takeaway
Stick to static tags for clarity; dynamic tags can introduce hard-to-debug issues.
● Production incidentPOST-MORTEMseverity: high

The 'always' Tag That Never Ran

Symptom
Service didn't restart after config update; playbook reported 'ok' but app was down.
Assumption
Engineer assumed the handler was triggered automatically because it was notified.
Root cause
The handler had tags: never which was skipped because --tags deploy excluded it.
Fix
Changed handler tags to tags: always so it runs regardless of tag filters.
Key lesson
  • Always explicitly tag handlers that must run on notifications, or use always tag for critical actions.
Production debug guideSymptom → Root cause → Fix4 entries
Symptom · 01
Playbook runs but no tasks execute (0 ok, 0 changed)
Fix
Check if your --tags filter is too restrictive. Run ansible-playbook playbook.yml --list-tasks --tags yourtag to see matched tasks. If none, the tag is misspelled or not applied.
Symptom · 02
Handler not running after notification
Fix
Handlers inherit tags from the task that notifies them? No! Handlers have their own tags. Ensure the handler's tag matches your --tags filter, or use tags: always on the handler.
Symptom · 03
Tasks with tags: always are skipped
Fix
always only runs when --tags is explicitly used; if you use --skip-tags always, it will be skipped. Verify you didn't accidentally skip it.
Symptom · 04
Tagged tasks inside a role not executed
Fix
Role tags are applied to all tasks in the role. If you tag a role with tags: foo, all tasks inside get foo. But if you also have --tags bar, only tasks with bar run. Use --tags foo,bar to include both.
★ Ansible Tags for Selective Execution Quick Referenceprint this for your desk
No tasks matched my --tags filter
Immediate action
Check tag spelling and list all tags
Commands
ansible-playbook playbook.yml --list-tags
ansible-playbook playbook.yml --list-tasks --tags mytag
Fix now
Correct tag name in playbook or CLI
Handler didn't run+
Immediate action
Check handler tags
Commands
ansible-playbook playbook.yml --list-tasks --tags all | grep handler_name
Fix now
Add tags: always to handler
Tasks with always tag are skipped+
Immediate action
Check if --skip-tags includes always
Commands
grep -r 'tags: always' playbook.yml
ansible-playbook playbook.yml --tags foo --skip-tags always
Fix now
Remove --skip-tags always or use --tags always
Role tasks not running with --tags+
Immediate action
Verify role tags
Commands
ansible-playbook playbook.yml --list-tasks --tags role_tag
Fix now
Apply tags at role level or to individual tasks
CI pipeline runs all tasks despite --tags+
Immediate action
Check if ansible.cfg has tags overrides
Commands
grep -i tags ansible.cfg
ansible-playbook --version
Fix now
Remove tags from ansible.cfg or override with --tags
Tagging Methods Comparison
ConstructInherits Parent Tags?Handlers Inherit?Overridable with apply?
TaskNo (unless inside block/role)N/AN/A
BlockYes, to all tasks insideNoN/A
RoleYes, to all tasks in roleNoYes, via role-level tags
Include/ImportYes, to all tasks in fileNoYes, via apply keyword
HandlerNo (must be tagged explicitly)N/AN/A

Key takeaways

1
Always tag handlers explicitly; they don't inherit tags from notifying tasks.
2
Use --list-tags and --list-tasks --tags to preview before running.
3
The always tag ensures tasks run even with --tags; never excludes them unless explicitly included.
4
Tag inheritance works with includes/imports; apply replaces, not adds.
5
In CI, map changed files to tags to run only relevant tasks.
6
Avoid dynamic tags in static imports; prefer static tags.
7
Document your tagging strategy in a README or tags.yml.
8
Use compound tags (e.g., deploy_app) for granular control.
9
Handlers are not executed in --check mode; use --list-tasks to verify.
10
Vault decryption occurs before tag filtering; you'll need the vault password regardless.
11
Typos in tag names are silent; always verify with --list-tags.
12
Combine --check with --tags for safe dry runs.

Common mistakes to avoid

6 patterns
×

Not tagging handlers

Symptom
Handlers don't run when using --tags
Fix
Add tags to handlers, e.g., tags: always
×

Using --skip-tags always

Symptom
Critical tasks skipped
Fix
Remove --skip-tags always or don't skip always
×

Typo in tag name

Symptom
Tasks not running, no error
Fix
Use --list-tags to verify spelling
×

Assuming tags on block apply to handlers

Symptom
Handler not running after notification
Fix
Tag handlers explicitly
×

Using apply: tags: to add instead of replace

Symptom
Tasks lose inherited tags
Fix
Include all needed tags in apply
×

Not using --list-tasks before run

Symptom
Unexpected tasks run or missing
Fix
Always preview with --list-tasks --tags
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between --tags and --skip-tags?
Q02SENIOR
How do you ensure a handler runs even when using --tags?
Q03SENIOR
What are the special tags always and never?
Q04SENIOR
How does tag inheritance work with import_tasks vs include_tasks?
Q05SENIOR
Can you use variables to set tags dynamically?
Q06SENIOR
How would you integrate Ansible tags with a CI pipeline to run only chan...
Q07JUNIOR
What does --list-tags do and why is it useful?
Q08SENIOR
Explain a scenario where using --skip-tags always could cause a producti...
Q01 of 08JUNIOR

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

ANSWER
--tags runs only tasks with specified tags; --skip-tags excludes tasks with specified tags. If a task has both a tag in --tags and a tag in --skip-tags, it is skipped because --skip-tags takes precedence.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
Can I use multiple tags in a single --tags flag?
02
Do tags work with ansible-pull?
03
What happens if I use --tags with a tag that doesn't exist?
04
Can I tag a play itself?
05
How do I run all tasks except those with a specific tag?
06
Do tags affect variable precedence?
07
Can I use tags with ansible-navigator?
08
Is there a way to see which tags are applied to a task?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
🔥

That's Ansible. Mark it forged?

7 min read · try the examples if you haven't

Previous
Ansible Galaxy and Collections
16 / 23 · Ansible
Next
Ansible AWS Automation