← all cheat sheets
FUNDAMENTALS · MECHANISM WALKTHROUGH

Ansible Changed vs OK
How Idempotency Actually Works

the same task shows "changed" once and "ok" forever after — here's exactly how a module decides which.
TASK RUNS MODULE CHECKS CURRENT STATE COMPARE TO DESIRED DECISION CHANGED OR OK RETURN JSON HANDLER NOTIFIED?
01 The Full Sequence — Worked Example
1

The Task Declares Desired State, Not an Action

An Ansible task like - apt: name=nginx state=present doesn't say "install nginx" — it says "nginx should be present." That distinction is the entire reason idempotency is possible.

Declare
2

Module Inspects Current State First

Before making any change, a well-written module queries the actual current state on the host — e.g. the apt module checks the local package database for whether nginx is already installed at the requested version.

Inspect
3

Compare Current State to Desired State

The module diffs what it found against what the task asked for. This comparison logic is written into each module individually — there's no generic engine doing this centrally; it's why some third-party modules are less reliably idempotent than core ones.

Compare
4

Checkpoint — The Decision Point

If current state already matches desired state, the module does nothing further and reports changed: false. If it doesn't match, the module performs the minimum action needed to converge, then reports changed: true. This single branch is the whole idempotency mechanism.

Checkpoint
5

First Run — State Doesn't Match

nginx isn't installed. The apt module runs the actual installation, then returns {"changed": true, ...}. Ansible prints the task line in yellow in the terminal.

Changed
6

Second Run — State Already Matches

Run the exact same playbook again. This time apt finds nginx already at the requested state, does nothing, and returns {"changed": false, ...}. Ansible prints the task line in green as "ok."

OK
7

Handlers Only Fire on "Changed"

If the task has notify: restart nginx, that handler only gets queued when changed is true. On the second run, since nothing changed, the handler is never queued — nginx is not needlessly restarted.

Notify Gate
8

Non-Idempotent Modules — shell/command

The shell and command modules have no way to inspect "current state" for an arbitrary command — they always report changed: true on every run, whether or not the command actually changed anything, unless you explicitly override it.

Exception
9

Overriding with changed_when

For shell/command tasks, you can tell Ansible how to interpret the result yourself: changed_when: "'installed' in result.stdout". This restores idempotent-looking output even though the module itself can't detect it natively.

changed_when
10

--check Mode Uses the Same Comparison, Without Step 2's Action

Running with --check triggers the exact same "compare current to desired" logic, but the module stops right after the decision in step 4 and reports what it would have done, without doing it — that's the entire mechanism behind a dry run.

Dry Run
02 How to Explain This in an Interview
03 Follow-Up / Gotcha Questions
Q Is "changed: false" the same as "the task succeeded"?
A Not quite — a task can succeed and report changed: false (already correct), or succeed and report changed: true (fixed something). Failure is a completely separate field (failed: true) from changed.
Q Why is the shell module considered "dangerous" for idempotency?
A Because it always reports changed: true regardless of actual effect, playbooks built heavily on shell tasks will always show noisy "changed" output and can trigger handlers unnecessarily on every run.
Q Does --check actually connect and run anything on the managed node?
A Yes — it still connects and runs the module's inspection logic (step 2-4), it just skips the action in step 5/6. shell/command tasks are typically skipped entirely in check mode since they can't safely predict their own effect.
Q Can a task be idempotent but still slow on every run?
A Yes — idempotency is about the end state converging, not about avoiding work. A module might re-check a large state (e.g. re-scan package lists) every single run even when nothing needs to change.
Q If a handler is notified by three separate tasks, does it run three times?
A No — Ansible deduplicates by handler name. It runs exactly once at the end of the play, regardless of how many tasks notified it, as long as at least one reported changed.
Q What does failed_when do differently from changed_when?
A failed_when overrides whether the task counts as a failure (based on return code by default); changed_when overrides only the changed flag. They're independent and can both be set on the same task.
04 Quick-Fire Glossary
TermMeaning
IdempotentRunning the same operation repeatedly produces the same end state, with no side effects after the first successful run
changedJSON field a module returns indicating whether it altered the host's state
changed_whenTask-level override that lets you define your own logic for the changed flag
failed_whenTask-level override for what counts as failure, independent of changed
--checkDry-run flag — reports what would change without applying it
--diffShows the actual before/after content difference for file-based modules
shell / commandModules that run arbitrary commands and can't natively detect idempotency
HandlerA task triggered only when notified by a changed:true task, run once at play end
registerCaptures a task's full return JSON into a variable for later use
Desired StateWhat the task declares should be true, as opposed to a literal sequence of commands