Ansible Handlers: Trigger Tasks on Change (Complete Guide)
By Luca Berton · Published 2026-04-03 · Category: troubleshooting
How to use Ansible handlers for event-driven task execution. Trigger handlers with notify, flush handlers, listen directive, multiple handlers.
Ansible handlers are special tasks that run only when triggered by a notify directive. They're perfect for actions like restarting services — you want them to run only if something actually changed.
Basic Handler Example
- name: Configure nginx
hosts: webservers
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
The handler restart nginx only runs if the template task reports "changed".
See also: Ansible loop_control: Control Loop Output, Labels & Pauses (Guide)
How Handlers Work
A task withnotify triggers the handler when the task status is changed
Handlers are queued, not run immediately
All handlers run once at the end of the play, in the order they're defined
Even if notified multiple times, a handler runs only once
Multiple Notifications
tasks:
- name: Update nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Update sites-enabled
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-enabled/mysite.conf
notify: restart nginx
# nginx restarts only ONCE even though notified twice
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
See also: Ansible Handlers: Trigger Service Restarts on Change (Guide)
Notify Multiple Handlers
tasks:
- name: Update SSL certificate
ansible.builtin.copy:
src: cert.pem
dest: /etc/ssl/certs/app.pem
notify:
- restart nginx
- restart app
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: restart app
ansible.builtin.service:
name: myapp
state: restarted
listen: Group Handlers
Use listen to trigger multiple handlers with a single notification:
tasks:
- name: Deploy new release
ansible.builtin.unarchive:
src: release.tar.gz
dest: /opt/app
notify: "deploy complete"
handlers:
- name: clear cache
ansible.builtin.command: /opt/app/clear-cache.sh
listen: "deploy complete"
- name: restart app
ansible.builtin.service:
name: myapp
state: restarted
listen: "deploy complete"
- name: notify slack
ansible.builtin.uri:
url: https://hooks.slack.com/services/xxx
method: POST
body_format: json
body:
text: "Deployment complete on {{ inventory_hostname }}"
listen: "deploy complete"
See also: Ansible Troubleshooting: Fix Jinja2 Syntax & Inventory Errors
flush_handlers: Run Handlers Mid-Play
tasks:
- name: Update config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: restart app
- name: Force handler execution now
ansible.builtin.meta: flush_handlers
- name: Verify app is running (needs restart to complete first)
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
Handler Execution Order
Handlers run in the order they're defined, not the order they're notified:
handlers:
- name: stop app # Runs first
ansible.builtin.service:
name: myapp
state: stopped
- name: clear cache # Runs second
ansible.builtin.command: rm -rf /tmp/cache
- name: start app # Runs third
ansible.builtin.service:
name: myapp
state: started
Handlers in Roles
roles/
nginx/
tasks/
main.yml
handlers/
main.yml # Handlers auto-loaded from here
templates/
nginx.conf.j2
# roles/nginx/handlers/main.yml
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Common Patterns
Reload vs Restart
tasks:
- name: Update main config (needs restart)
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Update site config (needs reload only)
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-enabled/mysite.conf
notify: reload nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Validate before restart
handlers:
- name: validate nginx config
ansible.builtin.command: nginx -t
listen: "restart nginx"
- name: restart nginx service
ansible.builtin.service:
name: nginx
state: restarted
listen: "restart nginx"
FAQ
Why doesn't my handler run?
The task must report changed status. If the task shows "ok" (no changes), the handler won't trigger. Check with -v for task status.
Can I force a handler to run?
Use changed_when: true on the notifying task, or call meta: flush_handlers followed by the handler task directly.
Do handlers run on failed plays?
No. If a play fails, pending handlers are skipped. Use --force-handlers or set force_handlers = True in ansible.cfg to override.
Can handlers notify other handlers?
Yes! A handler can include notify to chain handlers together.
Basic Handler
- hosts: webservers
become: true
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Multiple Notifiers
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- copy:
src: ssl.conf
dest: /etc/nginx/conf.d/ssl.conf
notify: restart nginx
# Handler runs ONCE even if notified multiple times
handlers:
- name: restart nginx
service: name=nginx state=restarted
Multiple Handlers
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- validate nginx config
- restart nginx
handlers:
- name: validate nginx config
command: nginx -t
- name: restart nginx
service: name=nginx state=restarted
listen (Handler Groups)
tasks:
- template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: restart app stack
handlers:
- name: restart app
service: name=myapp state=restarted
listen: restart app stack
- name: restart worker
service: name=myapp-worker state=restarted
listen: restart app stack
- name: clear cache
command: redis-cli FLUSHALL
listen: restart app stack
Flush Handlers Mid-Play
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
# Force handler to run NOW (before next task)
- meta: flush_handlers
- name: Verify nginx is running
uri:
url: http://localhost/health
status_code: 200
Handler in Roles
# roles/nginx/tasks/main.yml
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
# roles/nginx/handlers/main.yml
- name: restart nginx
service:
name: nginx
state: restarted
become: true
Handler Execution Order
Handlers run:
At the end of the play (after all tasks)
In the order defined in the handlers section
Once regardless of how many tasks notify them
Only if at least one task notified them and reported changed
Conditional Handlers
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
when: ansible_os_family == "Debian"
changed_when and Handlers
# Handler only fires when task reports changed
- command: /opt/scripts/check-config.sh
register: check
changed_when: "'updated' in check.stdout"
notify: reload config
FAQ
Why didn't my handler run?
The notifying task didn't reportchanged (was already in desired state)
The task failed before reaching the handler
Handler name doesn't match notify string (case-sensitive!)
Using --check mode (handlers don't run in check mode)
Can I force a handler to always run?
Use meta: flush_handlers or move the action to a regular task instead.
Handlers vs regular tasks?
Handlers are conditional — they only run when notified by a changed task. Use handlers for restarts/reloads that should only happen when configuration actually changes.
Basic Handler
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
become: true
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
become: true
Multiple Notifications
tasks:
- template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
notify:
- validate nginx
- restart nginx
handlers:
- name: validate nginx
command: nginx -t
become: true
- name: restart nginx
service: { name: nginx, state: restarted }
become: true
listen (Topic-Based)
tasks:
- template: { src: app.conf.j2, dest: /etc/myapp/config }
notify: app config changed
handlers:
# Multiple handlers respond to one topic
- name: restart app
service: { name: myapp, state: restarted }
listen: app config changed
become: true
- name: clear app cache
file: { path: /tmp/myapp-cache, state: absent }
listen: app config changed
flush_handlers
tasks:
- template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
notify: restart nginx
# Force handlers to run NOW (before continuing)
- meta: flush_handlers
- uri:
url: http://localhost/health
status_code: 200
retries: 5
delay: 2
Handler Ordering
# Handlers run in DEFINITION order, not notification order
handlers:
- name: reload systemd # Runs 1st (defined first)
systemd: { daemon_reload: true }
- name: restart app # Runs 2nd
service: { name: myapp, state: restarted }
- name: check health # Runs 3rd
uri: { url: "http://localhost/health" }
Handler in Roles
# roles/nginx/handlers/main.yml
- name: restart nginx
service: { name: nginx, state: restarted }
become: true
- name: reload nginx
service: { name: nginx, state: reloaded }
become: true
# roles/nginx/tasks/main.yml
- template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
notify: reload nginx
Conditional Handler
handlers:
- name: restart nginx
service: { name: nginx, state: restarted }
become: true
when: ansible_os_family == "Debian"
Handler with Block
tasks:
- block:
- template: { src: a.conf.j2, dest: /etc/a.conf }
- template: { src: b.conf.j2, dest: /etc/b.conf }
notify: restart app
# Handler notified if ANY task in block changes
Handlers Run Once
# Even if notify is called 5 times, handler runs ONCE
- template: { src: "{{ item }}.j2", dest: "/etc/conf.d/{{ item }}" }
loop: [a, b, c, d, e]
notify: restart app
# restart app runs ONCE at end, not 5 times
FAQ
When do handlers actually run?
At the end of the play (after all tasks), or when meta: flush_handlers is called.
What if the play fails before handlers run?
Notified handlers do NOT run if the play fails. Use --force-handlers or force_handlers: true in the play to override.
Can handlers notify other handlers?
Yes — a handler can include notify: to chain to another handler.
Related Articles
• Jinja2 filters in Ansible templates • skipping tasks with Ansible when • Ansible Inventory Guide • TLS for Nginx via Ansible • what Ansible roles are and how to use themCategory: troubleshooting