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 Guide • Ansible Ignore Errors Complete Guide • Ansible Check Mode (Dry Run) Complete Guide • Ansible Handlers: Trigger Actions on ChangeCategory: installation