Skip to main content
RunBook Academy

AnsibleXI · PlaybooksPlaybooks

Execution order inside a play

Intermediate⏱ ~16 minansible-playbook

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

Not yet marked complete on this device.

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:

  1. pre_tasks
  2. roles
  3. tasks
  4. post_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).

Read-only / Safethe resolved order, without connecting to anything
$ ansible-playbook -i inventory.ini order.yml --list-tasks
playbook: 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:

Read-only / Safehandler flush points, measured
$ ansible-playbook -i inventory.ini flush.yml
TASK [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=0

Read 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:

AfterHandlers flushed
pre_tasksThose notified during pre_tasks
tasksThose notified during roles or tasks
post_tasksThose 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:

Read-only / Safenotification order is not execution order
$ ansible-playbook -i inv2.ini horder.yml
TASK [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=0

Three 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 linear and false under free.

Host order within a task follows inventory order.

Knowledge check

Knowledge check · 4 questions

  1. 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?

  2. 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?

  3. Q3. Which statements about the default linear strategy are true? Select all that apply.

  4. 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.