AnsibleXI · PlaybooksPlaybooks
Execution order inside a play
What you'll learn
- State the order in which the four task sections of a play execute
- Identify the three points at which notified handlers are flushed
- Use --list-tasks to read the resolved execution order before running
- Explain why the linear strategy makes a halfway failure interpretable
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
You can only interpret a partial failure if you know what had already run. “The play stopped at task 14 of 22” is only useful information when you can say which fourteen tasks those were — and you can, because Ansible’s execution order inside a play is fully determined before the first connection is made.
This lesson establishes that order precisely, including the part almost everybody gets wrong: where handlers actually run.
The four sections, in order
A play executes its task sections in a fixed sequence:
pre_tasksrolestaskspost_tasks
Fact gathering, if enabled, precedes all four. The sequence does not
depend on the order you write the keys in the YAML — writing tasks:
above pre_tasks: in the file changes nothing about when they run,
which is a good reason to write them in execution order so the file
reads the way it behaves.
The design intent is a bracket: pre_tasks prepares (drain from the
load balancer, take a snapshot, verify a precondition), the middle does
the work, and post_tasks restores (health check, return to rotation,
notify a channel).
$ ansible-playbook -i inventory.ini order.yml --list-tasksplaybook: order.yml
play #1 (web): Deploy the web tier TAGS: []
tasks:
Take the host out of the load balancer pool TAGS: []
webserver : Install the web server package TAGS: []
webserver : Deploy the vhost configuration TAGS: []
Apply the site-specific tuning TAGS: []
Return the host to the load balancer pool TAGS: []The playbook that produced it declares pre_tasks, then roles, then
tasks, then post_tasks. The output flattens all four into one
ordered list, which is exactly the list the strategy plugin will walk.
Role tasks carry a rolename : prefix, which is how you tell borrowed
work from local work at a glance.
Where handlers actually flush
The widely repeated version of this rule is that notified handlers are flushed after each section. That is not what happens, and the difference matters when you are trying to work out why a restart happened later than you expected.
Here is the experiment. Four tasks, one in each section, each notifying a distinct handler:
- name: Where handlers actually flush
hosts: web01.example.com
gather_facts: false
pre_tasks:
- name: P1 pre_task, notifies H-pre
ansible.builtin.debug: { msg: p1 }
changed_when: true
notify: H-pre
roles:
- webserver # its single task notifies H-role
tasks:
- name: T1 task, notifies H-task
ansible.builtin.debug: { msg: t1 }
changed_when: true
notify: H-task
post_tasks:
- name: Z1 post_task, notifies H-post
ansible.builtin.debug: { msg: z1 }
changed_when: true
notify: H-post
And here is the order it produced:
$ ansible-playbook -i inventory.ini flush.ymlTASK [P1 pre_task, notifies H-pre] *********************************************
RUNNING HANDLER [H-pre] ********************************************************
TASK [webserver : R1 role task, notifies H-role] *******************************
TASK [T1 task, notifies H-task] ************************************************
RUNNING HANDLER [webserver : H-role] *******************************************
RUNNING HANDLER [H-task] *******************************************************
TASK [Z1 post_task, notifies H-post] *******************************************
RUNNING HANDLER [H-post] *******************************************************
PLAY RECAP *********************************************************************
web01.example.com : ok=8 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Read the third and fourth lines carefully. H-role was notified during
the roles section, and it did not run at the end of that section.
It ran after T1, alongside the handler notified from tasks.
So there are three flush points, not four:
| After | Handlers flushed |
|---|---|
pre_tasks | Those notified during pre_tasks |
tasks | Those notified during roles or tasks |
post_tasks | Those notified during post_tasks |
roles and tasks are one execution block as far as handlers are
concerned. That is not a quirk to memorise for its own sake — it is the
answer to “why did the role’s service reload happen after my own tasks
had already edited the config?”
You can force a flush wherever you need one:
- name: Flush pending handlers before the health check
ansible.builtin.meta: flush_handlers
That is the correct tool when a post_tasks health check must run
against a service that has already been reloaded. Without it, the
reload happens after post_tasks and the health check tests the old
process.
Handlers run in the order they are defined
The second surprise. A handler’s position in the run is decided by where
it appears in the handlers section, not by when it was notified.
handlers:
- name: Reload the proxy
ansible.builtin.debug: { msg: 'runs first, always' }
- name: Restart the application
ansible.builtin.debug: { msg: 'runs second, always' }
If a task notifies Restart the application at task 2 and another
notifies Reload the proxy at task 9, the proxy reload still runs
first, because it is defined first. Notification order is discarded:
$ ansible-playbook -i inv2.ini horder.ymlTASK [Task 2 notifies Restart the application] *********************************
TASK [Task 9 notifies Reload the proxy] ****************************************
TASK [Task 10 notifies Reload the proxy again] *********************************
RUNNING HANDLER [Reload the proxy] *********************************************
RUNNING HANDLER [Restart the application] **************************************
PLAY RECAP *********************************************************************
localhost : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Three changed tasks, two handler runs, ok=5 — the proxy reload was
notified twice and executed once.
The operational consequence: if your restart sequence has a required order — database, then application, then proxy — that order must be expressed in the definition order of the handlers section. It cannot be expressed by the order of the notifying tasks, and a reviewer who assumes otherwise will approve a broken restart sequence.
A handler notified more than once in a run still runs once. That is deliberate and useful: five tasks editing five fragments of one config file can all notify the same reload without producing five reloads.
Across hosts: the linear strategy
Inside a play, order is per-section. Across hosts, order is governed by
the strategy, and the default is linear.
Linear means: every targeted host runs task 1, then every host runs task
2, and so on. Ansible does not let host A get to task 5 while host B is
still on task 2. The width of that lockstep is forks — with
forks = 5 and 40 hosts, task 1 is executed in eight batches of five,
and only when all 40 have finished task 1 does anything start task 2.
Two properties follow, and both are why linear is the default:
- A halfway failure is interpretable. If the play stopped during task 14, then every host that is still in the play completed tasks 1–13. You are recovering from one known state, not from 40 different ones.
- Ordering guarantees hold across hosts. “All the web servers have
the new config before any of them is restarted” is true under
linearand false underfree.
Host order within a task follows inventory order.
Knowledge check
Knowledge check · 4 questions
Q1. A task inside a role notifies a handler. A later task in the tasks: section of the same play edits the same file. When does the handler run?
Q2. Task 2 notifies Restart the application; task 9 notifies Reload the proxy. The handlers section defines Reload the proxy first. Which handler runs first?
Q3. Which statements about the default linear strategy are true? Select all that apply.
Q4. A host that fails at task 4 of a 20-task play still runs the post_tasks section of that play.
Passing score: 75%. Answers are checked in this browser.