AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible changed_when & failed_when: Control Task Status (Guide)

By Luca Berton · Published 2024-01-01 · Category: installation

Complete guide to Ansible changed_when and failed_when. Override task status, suppress false changes, define custom failure conditions, and write idempotent.

By default, Ansible determines if a task "changed" or "failed" based on the module's return code. But many modules — especially command, shell, and uri — always report "changed" even when nothing actually changed. The changed_when and failed_when directives let you override this behavior with custom conditions.

changed_when

Suppress False "Changed" Status

The most common use — mark a read-only command as never changed:

- name: Check disk usage (read-only, never changes anything)
  ansible.builtin.command: df -h
  register: disk_usage
  changed_when: false

- name: Get current git branch ansible.builtin.command: git branch --show-current args: chdir: /opt/myapp register: git_branch changed_when: false

Without changed_when: false, these tasks always show yellow "changed" in output, even though they're read-only.

Conditional Changed Based on Output

- name: Run database migration
  ansible.builtin.command: /opt/myapp/manage.py migrate --noinput
  register: migrate_result
  changed_when: "'No migrations to apply' not in migrate_result.stdout"

- name: Update apt cache ansible.builtin.command: apt-get update register: apt_result changed_when: "'packages can be upgraded' in apt_result.stdout"

Changed Based on Return Code

- name: Check if reboot is required
  ansible.builtin.stat:
    path: /var/run/reboot-required
  register: reboot_file

- name: Run system update ansible.builtin.command: apt-get dist-upgrade -y register: upgrade_result changed_when: upgrade_result.rc == 0 and 'upgraded' in upgrade_result.stdout

Multiple Conditions

- name: Sync files
  ansible.builtin.command: rsync -avz /src/ /dest/
  register: rsync_result
  changed_when:
    - rsync_result.rc == 0
    - "'Number of files transferred: 0' not in rsync_result.stdout"

Multiple conditions use AND logic (all must be true).

Always Changed

- name: Send notification (always report as changed)
  ansible.builtin.uri:
    url: "{{ webhook_url }}"
    method: POST
    body: '{"msg": "Deploy completed"}'
    body_format: json
  changed_when: true

See also: Ansible block, rescue, always: Error Handling Complete Guide (2026)

failed_when

Custom Failure Conditions

Override when a task should be considered failed:

- name: Check application health
  ansible.builtin.uri:
    url: http://localhost:8080/health
    return_content: true
  register: health
  failed_when: "'healthy' not in health.content"

- name: Run validation script ansible.builtin.command: /opt/validate.sh register: validate failed_when: - validate.rc != 0 - "'WARNING' not in validate.stderr" # Fails unless rc is 0 OR stderr contains WARNING

Ignore Specific Return Codes

- name: Check if service exists (rc=4 means not found, that's OK)
  ansible.builtin.command: systemctl status myapp
  register: service_status
  failed_when: service_status.rc not in [0, 3, 4]
  # 0=running, 3=stopped, 4=not found — all acceptable
  changed_when: false

Never Fail

- name: Try to reach external service (don't fail if down)
  ansible.builtin.uri:
    url: https://external-api.example.com/status
    timeout: 5
  register: api_check
  failed_when: false
  # Alternative: ignore_errors: true

Fail on Specific Output

- name: Run database backup
  ansible.builtin.command: /usr/local/bin/db_backup.sh
  register: backup
  failed_when: >
    'ERROR' in backup.stderr or
    'FATAL' in backup.stderr or
    backup.rc != 0

- name: Check SSL certificate expiry ansible.builtin.command: > openssl x509 -in /etc/ssl/certs/app.crt -noout -enddate register: cert_check changed_when: false failed_when: "'notAfter' not in cert_check.stdout"

Combine changed_when and failed_when

- name: Run application test suite
  ansible.builtin.command: /opt/myapp/run_tests.sh
  register: test_result
  changed_when: false  # Tests don't change anything
  failed_when: test_result.rc != 0  # Fail if tests fail

- name: Apply database schema ansible.builtin.command: /opt/myapp/schema.sh apply register: schema_result changed_when: "'Applied' in schema_result.stdout" failed_when: "'ERROR' in schema_result.stderr"

See also: Ansible retries & until: Retry Failed Tasks Automatically (Guide)

Real-World Patterns

Idempotent Shell Commands

- name: Add line to crontab (only if not present)
  ansible.builtin.shell: |
    crontab -l 2>/dev/null | grep -q 'backup.sh' && echo 'EXISTS' || \
    (crontab -l 2>/dev/null; echo '0 2 * * * /opt/backup.sh') | crontab -
  register: cron_result
  changed_when: "'EXISTS' not in cron_result.stdout"

API Calls

- name: Create DNS record (idempotent)
  ansible.builtin.uri:
    url: "https://api.dns.example.com/records"
    method: POST
    body:
      name: "{{ hostname }}"
      type: A
      value: "{{ ansible_default_ipv4.address }}"
    body_format: json
    headers:
      Authorization: "Bearer {{ dns_token }}"
    status_code: [200, 201, 409]  # 409 = already exists
  register: dns_result
  changed_when: dns_result.status == 201
  failed_when: dns_result.status not in [200, 201, 409]

Package Management Scripts

- name: Install npm packages
  ansible.builtin.command:
    cmd: npm install --production
    chdir: /opt/myapp
  register: npm_result
  changed_when: "'added' in npm_result.stdout or 'updated' in npm_result.stdout"
  failed_when: npm_result.rc != 0

Configuration Checks

- name: Validate nginx config
  ansible.builtin.command: nginx -t
  register: nginx_test
  changed_when: false
  failed_when: nginx_test.rc != 0

- name: Check if config needs update ansible.builtin.command: diff /etc/app/config.yml /tmp/new_config.yml register: config_diff changed_when: config_diff.rc == 1 # diff returns 1 when files differ failed_when: config_diff.rc > 1 # rc > 1 means error

changed_when vs Handlers

changed_when controls whether handlers are triggered:

- name: Update application config
  ansible.builtin.command: /opt/myapp/update_config.sh
  register: config_result
  changed_when: "'updated' in config_result.stdout"
  notify: restart myapp

handlers: - name: restart myapp ansible.builtin.service: name: myapp state: restarted

The handler only runs if changed_when evaluates to true.

See also: Ansible Playbook Structure: Anatomy, Best Practices & Examples (2026)

changed_when/failed_when vs ignore_errors

| Feature | changed_when: false | failed_when: false | ignore_errors: true | |---------|----------------------|---------------------|----------------------| | Task shows "changed" | No | Yes (if module reports) | Yes | | Task shows "failed" | Possible | No | Yes (but ignored) | | Play continues | Yes | Yes | Yes | | Handlers triggered | No | Depends on changed | No (failed tasks don't trigger) | | ansible_failed_result | Not set | Not set | Set | | Use when | Read-only commands | Custom success criteria | You want to handle failure later |

ansible-lint Integration

The no-changed-when rule in ansible-lint flags command/shell tasks without changed_when:

# ❌ ansible-lint warning: no-changed-when
- name: Check version
  ansible.builtin.command: python --version

# ✅ No warning - name: Check version ansible.builtin.command: python --version changed_when: false

FAQ

What does changed_when false mean in Ansible?

changed_when: false tells Ansible to never mark a task as "changed" regardless of the module's return. Use it for read-only commands like command: df -h or command: git status that don't modify the system but always return "changed" by default.

What is the difference between changed_when and failed_when?

changed_when controls whether Ansible considers a task to have made changes (affects handler triggering and output color). failed_when controls whether Ansible considers a task to have failed (affects play continuation). Both accept Jinja2 expressions.

Can I use both changed_when and failed_when on the same task?

Yes, they're independent directives. You can set both on a single task: changed_when determines if the task reports as changed, while failed_when determines if it reports as failed.

How do I make a shell command idempotent?

Register the output and use changed_when with a condition that checks whether the command actually modified something: changed_when: "'already exists' not in result.stdout".

What is the difference between failed_when false and ignore_errors?

failed_when: false means the task never reports as failed — it's treated as a success. ignore_errors: true means the task can still fail, but the play continues anyway. The distinction matters for rescue blocks and the ansible_failed_result variable.

Conclusion

changed_when: false — Essential for read-only commands (command, shell, uri) • changed_when: condition — Make commands idempotent based on output • failed_when: condition — Define custom failure criteria • Always register output when using conditional status directives • ansible-lint will warn about missing changed_when on command/shell tasks

These two directives are key to writing production-quality, idempotent Ansible playbooks.

Related Articles

Ansible block, rescue, always: Error Handling GuideAnsible Ignore Errors Complete GuideAnsible Check Mode (Dry Run) Complete GuideAnsible Handlers: Trigger Actions on Change

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home