AnsibleXII · Idempotency and Change ReportingIdempotency and change reporting
How Ansible decides ok versus changed
What you'll learn
- Describe the read, compare, act, report cycle a module performs
- Define each result state precisely and say what it asserts about the host
- Distinguish results that establish a fact from results that establish nothing
- Read a mixed recap and say what happened to the estate
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
changed is the most consequential word Ansible produces. It fires
handlers, it populates the audit trail, it is the number a drift
detector watches, and it is the thing a CI gate asserts.
It is also produced entirely by the module, using logic the module author wrote. Understanding where it comes from is what lets you judge whether to believe it.
The decision, step by step
A well-written module executes four steps on the managed node:
- Read. Determine the current state of the resource. For
ansible.builtin.userthat is agetpwnamlookup; foransible.builtin.copyit is a checksum of the destination; foransible.builtin.serviceit is a query to the service manager. - Compare. Diff the current state against the declared parameters,
attribute by attribute. Only the attributes you declared are
compared —
ansible.builtin.filewith nomodedoes not care what the mode is. - Act. If and only if something differs, perform the minimal change that removes the difference. Not “reapply everything” — change the one attribute that was wrong.
- Report. Return
changed: trueif step 3 did anything,falseotherwise.
Two things follow directly.
A module reports changed for the resource, not for the run. A
user task that corrects only the shell reports changed: true
identically to one that created the account from nothing. The recap
counts tasks that changed something; it does not weigh them.
Only declared attributes participate. This is why a task can report
ok on a host whose configuration is visibly wrong: the wrong thing was
an attribute nobody declared. Idempotency is relative to the
declaration, and a thin declaration converges to a thin state.
The full result vocabulary
Seven states. Five are per-task results, and two only appear in the recap.
ok
The task ran to completion and reported no change. For a declarative module this is a positive assertion: the module read the resource, compared it, and found it already matching.
For ansible.builtin.debug, ansible.builtin.assert and
ansible.builtin.set_fact, ok means only that the task did its job —
those modules do not change host state and never report changed on
their own.
changed
The module performed an action that altered the resource. Or — and this
is the whole reason Part XII exists — the module believes it did, or
an author’s changed_when said so.
changed triggers notify. That single fact is why an inaccurate one
is not cosmetic.
failed
The task ran and reported failure: a package that does not exist, a service that would not start, a template that could not be rendered, a command that exited non-zero. The connection worked. The host or the declaration is at fault.
A failed host is removed from the play and runs nothing further.
skipped
The task was not executed. Three distinct causes produce this one word:
- A
when:condition evaluated false. - The task’s tags did not match a
--tagsor--skip-tagsselection. - The run is in check mode and the module does not support it.
None of them tells you anything about the host’s state. A skipped
result is the absence of information.
unreachable
Ansible could not establish a usable connection. This is a statement about the network, the host, SSH or the remote interpreter — never about your playbook.
Critically, a task that never connects cannot produce this result. A
play built from debug reports unreachable=0 for a host that is
switched off.
rescued (recap only)
A task failed inside a block and the block’s rescue section handled
it. The failure was real; recovery happened. failed does not count it.
ignored (recap only)
A task failed under ignore_errors: true. The failure was real and was
discarded. failed does not count it either.
All seven in one run
$ ansible-playbook -i inv2.ini vocab.ymlTASK [A module that inspected nothing and reports changed anyway] **************
changed: [localhost]
TASK [The same command, told the truth about itself] ***************************
ok: [localhost]
TASK [The same command, judged on its own evidence] ****************************
ok: [localhost]
TASK [A task whose condition is false] *****************************************
skipping: [localhost]
TASK [A non-zero exit that is not a failure] ***********************************
ok: [localhost]
TASK [A real failure that was swallowed] ***************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": ["/bin/false"], "msg": "The command exited with a non-zero return code.", "rc": 1}
...ignoring
TASK [Fail inside the block] ***************************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": ["/bin/false"], "msg": "The command exited with a non-zero return code.", "rc": 1}
TASK [Recover] *****************************************************************
ok: [localhost] => {
"msg": "recovered"
}
PLAY RECAP *********************************************************************
localhost : ok=6 changed=1 unreachable=0 failed=0 skipped=1 rescued=1 ignored=1Read that recap and describe the run out loud. failed=0 — and three
tasks failed. Two fatal: lines are printed in full, one followed by
...ignoring, and neither contributes to the failure count.
Which results establish a fact
This is the distinction that makes the vocabulary operationally useful:
| Result | Does it tell you something about the host? |
|---|---|
ok from a declarative module | Yes — the resource matches the declaration |
changed from a declarative module | Yes — it did not match, and now does |
ok from debug / set_fact | No — nothing on the host was inspected |
changed from command / shell | No — the module cannot know what it did |
skipped | No — nothing was inspected |
failed | Yes — the host or declaration is wrong, in a stated way |
unreachable | Yes, about the infrastructure — nothing about the host’s config |
Half the table is “no”. A recap full of ok from a play built out of
debug and command tasks with changed_when: false is a green run
that has verified nothing whatsoever, and it looks exactly like a green
run that verified everything.
Knowledge check
Knowledge check · 4 questions
Q1. A copy task declares owner, group and mode. The destination file has the right content but the wrong mode. What does the task report?
Q2. Which result establishes a positive fact about the configuration of the host?
Q3. Which causes produce a skipped result? Select all that apply.
Q4. changed_when is evaluated on the controller after the module returns, and it replaces the module result rather than being combined with it.
Passing score: 75%. Answers are checked in this browser.