Tasks and Module Invocation
A task is the atomic unit of work in a play: a name:, a module, and the module's arguments. The module does the actual work — ansible.builtin.apt installs a package, ansible.builtin.template renders a config file — and the name: is the label that shows up in run output. An unnamed task costs you legibility you will miss the moment something fails in a long run.
Tasks are where a playbook stops describing structure and starts doing work. This topic pins down the three parts of a task, why the fully-qualified module name is the form to write from day one, what the name: discipline buys, and how to read a module's changed-versus-ok result as the real signal of whether anything happened.
The Three Parts of a Task
Every task is three things: a name: that labels it for humans, a module that names the action, and the module's arguments. The canonical shape is a - name: Install nginx line followed by the module key and its inputs indented beneath it. The module key plus its arguments is where the work lives; the name is purely for the person reading the output.
tasks: - name: Install nginx ansible.builtin.apt: name: nginx state: present - name: Render larkspur.conf ansible.builtin.template: src: larkspur.conf.j2 dest: /etc/nginx/sites-available/larkspur.conf
Read each task as those three parts. The first labels itself "Install nginx," calls ansible.builtin.apt, and passes it a package name and desired state. The second renders a template to a path. Strip the names and the modules still run identically — but the output becomes far harder to read.
FQCN vs Short Names
The fully-qualified collection name, like ansible.builtin.apt, names the collection and the module explicitly. The short name apt still resolves for builtin modules, so a beginner's playbook works without the prefix. But once your project installs collections, a community module can carry the same short name as a builtin and shadow it — and you run a different module than you meant, with no warning.
Writing the FQCN from day one removes that ambiguity for good. ansible.builtin.apt and community.postgresql.postgresql_db each name exactly one module, and a collection upgrade can never silently swap which one runs. It is a few extra characters per task that buys you a guarantee about what executes.
The name: Discipline
Give every task a descriptive name: that states intent — "Install nginx," "Render larkspur.conf," "Create the larkspur database" — because that exact string is what ansible-playbook prints as each task runs. It is also what you grep the log for when a run fails at task thirty of forty. The name is the handle you reach for every time you read the output, which is often.
A good name describes what the task is trying to achieve, not which module it calls. "Render larkspur.conf" tells you the intent at a glance; the reader does not need the module name repeated, because the output already shows it. The intent is the part only you can supply, and it is the part that makes a forty-task run readable.
What Unnamed Tasks Cost You
An unnamed task prints as TASK [ansible.builtin.template] with no hint which file or which host it touched. In a run with four template tasks, you see the same line four times and cannot tell them apart. When one of them fails, finding which file it was writing becomes a guessing game against the playbook source — at exactly the moment you least want to be guessing.
The name is free legibility you pay for once and read many times. Writing "Render larkspur.conf" takes a second when you author the task and saves minutes every time you read a run that includes it. Leaving it off saves nothing and costs you the one signal that makes failures fast to locate.
Module Arguments and Return Values
Arguments are the module's inputs — name, state, src, dest — and a module rejects any argument it does not accept. That strictness is a feature: a typo'd stat: where you meant state: stops the run with a clear error instead of silently doing the wrong thing. The module's argument list is a contract, and breaking it fails loudly.
Modules also return structured data: changed, failed, and module-specific keys. You can capture that return with register: and branch on it in a later task, which is the bridge to the control-flow chapter. The most important return to read now is changed versus ok: ok means the module checked and state already matched, while changed means it actually acted. Conflating the two misreads what a run did.
- Leaving tasks unnamed and then reading
TASK [ansible.builtin.copy]four times in the output with no way to tell which file each one wrote. - Using bare short module names like
aptorservicein a project that also installs collections, where a community module can shadow the builtin and you run the wrong one silently. - Writing a
name:that restates the module —name: apt— instead of the intent —name: Install nginx— wasting the one field that makes output readable. - Passing a module an argument it does not accept and reading the hard failure as a bug — modules reject unknown args, so a typo'd
stat:instead ofstate:stops the run by design. - Assuming a task that printed
okdid something —okmeans state already matched; onlychangedmeans the module acted, and conflating them misreads the run.
- Write a clear, intent-stating
name:on every task, since that string is your primary signal in run output and the thing you grep logs for. - Use FQCN —
ansible.builtin.apt,community.postgresql.postgresql_db— everywhere from the start, so module resolution is unambiguous and collection upgrades never silently change which module runs. - Let the module do the work — prefer
ansible.builtin.apt,template, andserviceovercommandandshell— so idempotency andchangedreporting stay honest. - Read the
changed-versus-okdistinction in the recap as the real signal of whether a run did anything, not just the presence of green text. - Name a task by its goal, not its module, so the output reads as a list of intents rather than a list of module calls.
changed reporting
register captures a module's return for later control flow
Knowledge Check
What are the three parts of an Ansible task?
- A
name:that labels it, the module that performs the action, and the module's arguments - A host pattern that selects targets, a tag for filtering, and a handler to notify on change
- A variable declaration, a
when:condition, and a loop over an item list - The enclosing play, the inventory group it targets, and the exit code it returns
Why is the FQCN ansible.builtin.apt the form to write once a project installs collections?
- It names exactly one module, so a community module sharing the short name
aptcan never silently shadow the builtin you meant - Short names like
aptstop resolving entirely the moment any extra collection is installed, so every task that uses the bare module name errors out at parse time until you rewrite it as a fully qualified name - The fully qualified form runs faster, because Ansible skips the module-resolution lookup step
- Only modules written as an FQCN can be given a
name:label that shows in the output
What does an unnamed task cost you in a forty-task run?
- The output labels the line by module only, so identical calls blur together and a failure leaves you guessing which file or host it touched
- The task is skipped entirely during the run and reported as skipped in the recap, because Ansible refuses to execute any task that has no
name:label and treats the missing name as an explicit opt-out - The task loses its idempotency and reports
changedon every single run, even when nothing was altered - The whole run fails with a YAML syntax error before any task in the play executes
A task reports ok rather than changed. What does that mean?
- The module checked the host and current state already matched, so it did nothing — only
changedmeans it acted - The module successfully modified the host's state, which is exactly the outcome that
okreports - The task was skipped by a
when:condition that evaluated to false on this host, so the module never ran and Ansible logged the result asok - The module hit an error, but Ansible suppressed it and let the run continue
You got correct