Introduction

Ansible handlers are special tasks that run at the end of a play when notified by other tasks using the notify directive. A handler only executes when the notifying task finishes in the changed state. When a task correctly modifies the system but reports ok instead of changed, the handler is never triggered - the service isn't restarted, the configuration isn't reloaded, and the changes don't take effect.

This issue commonly occurs with: - Shell and command modules that don't have change detection built-in - Custom scripts that modify state but don't communicate changes - Conditional logic in changed_when that incorrectly evaluates to false - Modules that check state but don't actually change anything

Symptoms

The playbook runs successfully but the handler is never triggered:

``` TASK [Deploy application configuration] ******** ok: [web-server-01]

RUNNING HANDLER [Restart nginx] ************ skipping: [web-server-01]

PLAY RECAP ***************** web-server-01 : ok=3 changed=0 unreachable=0 failed=0 ```

When checking verbose output:

```bash $ ansible-playbook deploy.yml -vv TASK [Deploy application configuration] ******** task path: /home/user/deploy.yml:15 ok: [web-server-01] => {"changed": false, "stdout": "Configuration applied", "stderr": ""} # Handler is skipped because changed=false

# The configuration file was actually modified on the target $ ssh web-server-01 "cat /etc/nginx/conf.d/app.conf" upstream backend { server 10.0.1.50:8080; # New backend - but nginx not reloaded } ```

Debug output shows the task result:

yaml
- name: Debug task output
  debug:
    var: deploy_result
  # Output shows:
  # deploy_result.changed: false
  # deploy_result.stdout: "Updated 3 records"
  # But records were actually changed!

Checking handler notification state:

```bash $ ansible-playbook site.yml --check --diff TASK [Update configuration] ************ --- before: /etc/app/config.yml +++ after: /etc/app/config.yml @@ -1,3 +1,3 @@ -setting: old_value +setting: new_value

changed: [server-01]

# But without --check, the task reports ok: TASK [Update configuration] ************ ok: [server-01] ```

Common Causes

**1. Command/Shell Modules Default to changed_when: false**

Unlike file-based modules, command and shell don't automatically detect changes:

yaml
- name: Update configuration
  command: /usr/local/bin/update_config.sh
  # This ALWAYS reports changed=false unless you define changed_when

**2. Incorrect changed_when Condition**

The condition logic doesn't match the actual output:

yaml
- name: Enable feature
  command: appctl enable feature-x
  register: result
  changed_when: "'enabled' in result.stdout"
  # But stdout says "Feature feature-x is now enabled"
  # The substring 'enabled' is present, but...
  # If output is "Feature already enabled", this also matches incorrectly

3. Idempotent Commands Always Report Same Output

Commands that check state before making changes may not change anything on subsequent runs:

yaml
- name: Create database
  command: createdb myapp
  register: result
  changed_when: result.rc == 0
  # But createdb returns 0 even if database already exists
  # Need to check stderr or stdout for "already exists"

4. Module-Specific Change Detection Issues

Some modules have quirks in change detection:

yaml
- name: Set sysctl value
  sysctl:
    name: net.core.somaxconn
    value: "65535"
  # This may report ok if value was already set via other means
  # Even though sysctl.conf wasn't updated

5. Conditional Logic Errors

Complex conditions in changed_when that evaluate unexpectedly:

yaml
- name: Deploy application
  command: /opt/deploy.sh
  register: deploy
  changed_when:
    - deploy.stdout | search('Deployment complete')
    - deploy.rc == 0
  # Jinja2 search filter returns None if not found, not False
  # This causes the condition to evaluate unexpectedly

Step-by-Step Fix

Step 1: Diagnose Why the Task Reports `ok`

Run the playbook with high verbosity to see task output:

```bash # See detailed task output ansible-playbook site.yml -vvv --limit server-01 2>&1 | grep -A20 "TASK [.*]"

# Or use debug task to inspect the register variable - name: Debug register output debug: var: task_result verbosity: 1 ```

Check the actual change on the target:

bash
# Before running playbook
ssh server-01 "cat /etc/app/config.yml"
ansible-playbook site.yml --check --diff  # See what would change
ansible-playbook site.yml                  # Run actual playbook
ssh server-01 "cat /etc/app/config.yml"    # Verify change happened

Step 2: Fix Change Detection for Command/Shell Tasks

Define proper changed_when logic based on command output:

```yaml - name: Update application configuration shell: | set -o pipefail /usr/local/bin/update_config.sh 2>&1 | tee /tmp/update.log register: config_update changed_when: - config_update.rc == 0 - "'No changes needed' not in config_update.stdout" failed_when: config_update.rc != 0 notify: Restart application

  • name: Create database (if not exists)
  • command: createdb myapp
  • register: db_create
  • changed_when: "'already exists' not in db_create.stderr"
  • failed_when:
  • - db_create.rc != 0
  • - "'already exists' not in db_create.stderr"
  • notify: Initialize database
  • `

For complex change detection, use a custom filter:

yaml
- name: Deploy and check for changes
  shell: /opt/deploy.sh
  register: deploy_result
  changed_when: deploy_result.stdout | regex_search('Deployed (\\d+) files') | default('0') | int > 0
  notify: Restart application

Step 3: Use State-Aware Modules Instead of Commands

Replace shell commands with proper Ansible modules:

```yaml # Instead of: - name: Create user command: useradd -m -s /bin/bash appuser register: user_create changed_when: "'already exists' not in user_create.stderr"

# Use: - name: Create user user: name: appuser shell: /bin/bash create_home: yes state: present notify: Restart application ```

Replace file manipulation commands:

```yaml # Instead of: - name: Update config file shell: | sed -i 's/old_value/new_value/g' /etc/app/config.yml register: sed_result changed_when: true # Always reports changed!

# Use: - name: Update config file lineinfile: path: /etc/app/config.yml regexp: '^setting=' line: 'setting=new_value' backrefs: yes notify: Restart application ```

Step 4: Implement Robust Change Detection Patterns

Create a reusable pattern for detecting changes:

```yaml - name: Run deployment script with change detection block: - name: Get pre-deployment state command: md5sum /opt/app/app.jar register: pre_state changed_when: false failed_when: false

  • name: Run deployment
  • command: /opt/deploy.sh
  • register: deploy
  • name: Get post-deployment state
  • command: md5sum /opt/app/app.jar
  • register: post_state
  • changed_when: false
  • failed_when: false
  • name: Set changed fact
  • set_fact:
  • deployment_changed: "{{ pre_state.stdout != post_state.stdout }}"
  • name: Notify handler based on actual change
  • debug:
  • msg: "Deployment changed application files"
  • changed_when: deployment_changed
  • notify: Restart application
  • when: deployment_changed
  • `

Step 5: Debug Handler Notification

Add debugging to understand handler behavior:

```yaml - name: Update configuration template: src: app.conf.j2 dest: /etc/app/app.conf register: config_result notify: Restart nginx

  • name: Debug handler notification
  • debug:
  • msg: |
  • Task changed: {{ config_result.changed }}
  • Handler will be notified: {{ config_result.changed }}
  • Task stdout: {{ config_result.stdout | default('N/A') }}
  • Task stderr: {{ config_result.stderr | default('N/A') }}
  • name: Force handler execution for debugging
  • meta: flush_handlers
  • `

Step 6: Use `changed_when` with Failed Detection

Handle both success and failure conditions:

```yaml - name: Run database migration command: python manage.py migrate register: migration changed_when: - migration.rc == 0 - "'No migrations to apply' not in migration.stdout" failed_when: migration.rc != 0 notify: - Clear cache - Restart application

  • name: Install package with yum
  • command: yum install -y mypackage
  • register: yum_install
  • changed_when: yum_install.stdout is search('Installed:')
  • failed_when:
  • - yum_install.rc != 0
  • - yum_install.stderr is not search('Nothing to do')
  • notify: Start service
  • `

Step 7: Create a Handler Debug Playbook

Create a standalone playbook to debug handler issues:

```yaml # debug_handlers.yml - name: Debug handler notification hosts: "{{ target | default('all') }}" gather_facts: false vars: test_task: "Update configuration"

tasks: - name: {{ test_task }} command: echo "Test output" register: test_result

  • name: Show task result
  • debug:
  • msg:
  • - "Task name: {{ test_task }}"
  • - "Changed: {{ test_result.changed }}"
  • - "RC: {{ test_result.rc }}"
  • - "Stdout: {{ test_result.stdout }}"
  • - "Stderr: {{ test_result.stderr }}"
  • name: Set changed status explicitly
  • command: echo "Forcing change detection"
  • changed_when: true
  • notify: Test handler
  • when: force_change | default(false)

handlers: - name: Test handler debug: msg: "Handler was triggered!" ```

Verification

Test that handlers are triggered correctly:

```bash # Run with verbose output to see handler execution ansible-playbook site.yml -v

# Expected output: TASK [Update configuration] ************ changed: [server-01]

RUNNING HANDLER [Restart nginx] ************ changed: [server-01]

# Verify the service was restarted ansible server-01 -m shell -a "systemctl status nginx | grep 'Active:'" # Should show recently restarted timestamp

# Test idempotency - handler should not trigger on second run ansible-playbook site.yml -v # Expected: handler skipped because task reports ok (no change)

# Use check mode to preview changes ansible-playbook site.yml --check --diff # Shows what would change without making changes ```

Verify change detection logic:

```bash # Test specific task with debug ansible-playbook site.yml --tags "config" -vv

# Check the registered variable value ansible server-01 -m debug -a "msg={{ hostvars[inventory_hostname]['task_result'] }}" ```

  • [ansible-handler-not-triggered-notify-missing](/articles/ansible-handler-not-triggered-notify-missing)
  • [ansible-idempotency-issues-shell-commands](/articles/ansible-idempotency-issues-shell-commands)
  • [ansible-service-restart-not-applied](/articles/ansible-service-restart-not-applied)
  • [WordPress troubleshooting: Ansible Artifact Download Uses an Old Mi](ansible-artifact-download-uses-an-old-mirror-after-proxy-change)
  • [WordPress troubleshooting: Ansible Audit Trail Misses Events Under ](ansible-audit-trail-misses-events-under-burst-load)
  • [WordPress troubleshooting: Ansible Background Worker Gets Stuck in ](ansible-background-worker-stuck-in-a-retry-loop)
  • [WordPress troubleshooting: Ansible Backup Completes but Restore Fai](ansible-backup-completes-but-restore-fails-checksum-validation)
  • [WordPress troubleshooting: Ansible Batch Importer Duplicates Rows A](ansible-batch-importer-duplicates-rows-after-a-retry)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "WordPress troubleshooting: Ansible Handler Not Notified Because the", "description": "Learn how to fix Ansible Handler Not Notified Because the Task Never Reports Changed. Professional WordPress troubleshooting solutions with step-by-step guidance. WP error fix, WordPress optimization, WP security, WordPress performance.", "url": "https://www.fixwikihub.com/ansible-handler-not-notified-task-changed-never-triggers", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-01-22T15:36:08.049Z", "dateModified": "2026-01-22T15:36:08.049Z" } </script>