AnsibleXI · PlaybooksPlaybooks
Reading the recap, and the exit code
What you'll learn
- Define every field of the PLAY RECAP and what it says about that host
- Map a run outcome to the exit code ansible-playbook returns
- Explain why a CI job testing only for exit code 2 can pass a failed run
- Recognise why a controller-side task cannot detect an unreachable host
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
The PLAY RECAP is the only summary of a run that Ansible produces on
its own, and the exit code is the only part of it a machine can read.
Between them they answer “what just happened to my estate”, and almost
every automation pipeline that has ever gone green on a failed run got
one of the two wrong.
The seven fields
$ ansible-playbook -i inv2.ini vocab.ymlPLAY RECAP *********************************************************************
localhost : ok=6 changed=1 unreachable=0 failed=0 skipped=1 rescued=1 ignored=1One line per host that the run attempted. Read it left to right:
| Field | Counts | What it tells you about that host |
|---|---|---|
ok | Tasks that ran and did not fail | Includes both “changed something” and “checked and it was already correct”. changed is a subset of it in the sense that a changed task is also a successful one, but the two counters are reported separately. |
changed | Tasks that reported a state change | The audit answer to “what did this run alter”. Its accuracy is the subject of Part XII. |
unreachable | Tasks that could not connect | An infrastructure fact, not a playbook fact. The host was not talked to. |
failed | Tasks that failed and were not rescued or ignored | The playbook ran and the host said no. |
skipped | Tasks whose when: was false, or which lacked check-mode support in a --check run | Two very different causes, one counter. |
rescued | Tasks that failed inside a block and were handled by its rescue | The failure happened and was dealt with. Not a clean run. |
ignored | Tasks that failed under ignore_errors: true | The failure happened and was discarded. |
Two of those deserve emphasis.
skipped conflates two causes. A task skipped by a false when:
and a task skipped because the module does not support check mode
produce identical output. In a --check run, skipped is partly a
measure of how much the prediction did not cover.
rescued and ignored are not zero-cost. A recap reporting
failed=0 rescued=3 describes a run in which three tasks failed. The
run recovered; the estate may not have. Treating failed=0 as “clean”
is how a degraded run gets recorded as a success.
unreachable is not failed
These are different questions about different systems, and conflating them sends you to debug the wrong thing.
failed=1— Ansible connected, ran a module, and the module reported failure. The playbook, the host’s state, or the module’s arguments are at fault.unreachable=1— Ansible could not establish a usable connection. SSH refused, the host is down, DNS did not resolve, the key was rejected, or the Python interpreter could not be started. Nothing in your playbook is implicated.
$ ansible-playbook -i inv2.ini ec-unreach.ymlfatal: [web01.example.com]: UNREACHABLE! => {"changed": false, "msg": "Task failed: Failed to connect to the host via ssh: ssh: connect to host 192.0.2.11 port 22: Connection timed out", "unreachable": true}
PLAY RECAP *********************************************************************
web01.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0The pre-flight check that proves nothing
Here is a reachability check that a lot of people have written:
- name: Confirm the fleet is reachable
hosts: all
gather_facts: false
tasks:
- name: Confirm the host answers
ansible.builtin.debug:
msg: 'reached {{ inventory_hostname }}'
Run against the same unreachable host as above, on the same inventory:
$ ansible-playbook -i inv2.ini preflight-debug.ymlTASK [Confirm the host answers] ************************************************
ok: [web01.example.com] => {
"msg": "reached web01.example.com"
}
PLAY RECAP *********************************************************************
web01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0ok=1. unreachable=0. Exit code 0. A completely green run against a
host that does not exist.
The exit code
The recap is for humans. Cron, CI and the wrapper script read the exit code, and it has one property that catches people out.
| Run outcome | Exit code |
|---|---|
| Success | 0 |
| Task failure only | 2 |
| Unreachable host only | 4 |
| Task failure and unreachable host | 4 |
All four rows verified by execution on ansible-core 2.21.3. The
exit-code table is not published in the upstream CLI documentation
or the ansible-playbook(1) manual page; these values come from
observed behaviour and from the installed source, and the version they
were established against is stated deliberately.
The last row is the one that matters. Unreachable takes precedence:
$ ansible-playbook -i inv2.ini ec-both.yml; echo EXIT=$?fatal: [localhost]: FAILED! => {"changed": false, "msg": "deliberate failure"}
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
web01.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
EXIT=4ansible-playbook -i inventory.ini site.yml
rc=$?
if [ "$rc" -ne 0 ]; then
echo "ansible-playbook exited $rc"
exit "$rc"
fiPreserving the exact code on the way out is worth the extra word: 4
tells whoever reads the job log that at least one host was unreachable,
which is an infrastructure page rather than a playbook bug.
Knowledge check
Knowledge check · 4 questions
Q1. A run has twelve hosts that failed a task and three that were unreachable. What exit code does ansible-playbook return?
Q2. A play whose only task is ansible.builtin.debug is run against a host that is powered off. What does the recap report for that host?
Q3. A recap reads: ok=9 changed=2 unreachable=0 failed=0 skipped=3 rescued=2 ignored=1. Which statements are true? Select all that apply.
Q4. A host that appears in the inventory but not in the PLAY RECAP was reachable and simply had no work to do.
Passing score: 75%. Answers are checked in this browser.