Skip to main content
RunBook Academy

AnsibleXXVIII · Plugins, Lookups and FiltersPlugins, lookups and filters

Callback plugins and run output

Intermediate⏱ ~19 minansible-playbookansible-docansible-config

What you'll learn

  • Change a run report without touching a playbook, using configuration alone
  • Distinguish the single stdout callback from the many notification callbacks
  • Produce machine-readable run artefacts for CI and for an audit trail
  • Recognise which shipped callbacks carry a removal date, and what to use instead

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.

Everything ansible-playbook prints — the PLAY banners, the TASK headers, the JSON blobs, the recap — is produced by a callback plugin. None of it is built into the engine, and all of it is configurable without touching a playbook.

That last clause is the operational point. Changing what a run reports is a configuration change, reversible, with no diff in any repository your change process governs.

The one-stdout rule

Read-only / Safewhat ships
$ ansible-doc -t callback -l
ansible.builtin.default default Ansible screen output
ansible.builtin.junit   write playbook output to a JUnit file
ansible.builtin.minimal minimal Ansible screen output
ansible.builtin.oneline oneline Ansible screen output
ansible.builtin.tree    Save host events to files

Five, and they divide into two kinds:

One stdout callback, set by DEFAULT_STDOUT_CALLBACK. Its documentation states the rule directly: you can only have one at a time. default, minimal and oneline are stdout callbacks and choosing one replaces the others.

Any number of notification callbacks, listed in CALLBACKS_ENABLED. These do not own the screen; they write files, post to services, collect timings. junit and tree are these.

Read-only / Safeansible.cfg
[defaults]
stdout_callback = default
callback_result_format = yaml
callbacks_enabled = ansible.posix.profile_tasks, ansible.builtin.junit

Or by environment variable, which is how you do it for one run without changing anything:

Read-only / Safeone run only
ANSIBLE_STDOUT_CALLBACK=minimal ansible-playbook -i inventory site.yml

The single highest-value change: result_format: yaml

The default output for structured data is JSON:

Read-only / Safethe default
$ ansible-playbook -i inv.ini small.yml
TASK [Report a structure] ******************************************************
ok: [web01.example.com] => {
  "msg": {
      "ports": [
          80,
          443
      ],
      "service": "nginx"
  }
}

The same run with one environment variable:

Read-only / Saferesult_format yaml
$ ANSIBLE_CALLBACK_RESULT_FORMAT=yaml ansible-playbook -i inv.ini small.yml
TASK [Report a structure] ******************************************************
ok: [web01.example.com] =>
  msg:
      ports:
      - 80
      - 443
      service: nginx

Ten lines become six, and the saving grows with the size of the structure. On a --diff run, or anything printing a registered result, this is the difference between readable output and scrolling.

There is a companion option, pretty_resultsANSIBLE_CALLBACK_FORMAT_PRETTY — and its documentation carries a warning worth quoting, because it is the sort of thing that costs an hour:

When set to true and used with the yaml result format, this option will modify module responses in an attempt to produce a more human friendly output at the expense of correctness, and should not be relied upon to aid in writing variable manipulations or conditionals. For correctness, set this option to false or set result_format to json.

Machine-readable artefacts

junit — a test report for CI

Read-only / SafeJUnit XML from an ordinary run
$ ANSIBLE_CALLBACKS_ENABLED=ansible.builtin.junit JUNIT_OUTPUT_DIR=./out ansible-playbook -i inv.ini small.yml
Read-only / Safewhat it wrote
$ ls ./out && head -6 ./out/*.xml
small-1786486982.0603883.xml
<?xml version="1.0" ?>
<testsuites disabled="0" errors="0" failures="0" tests="2" time="0.0187306404113769531250">
<testsuite disabled="0" errors="0" failures="0" name="small" skipped="0" tests="2" time="0.0187306404113769531250">
	<testcase classname="/home/opsuser/project/small.yml:5" name="[web01.example.com] Small run: Report a structure msg={'service': 'nginx', 'ports': [80, 443]}" time="0.0086882114410400390625">
		<system-out>{
  "changed": false,

Every CI system on earth renders JUnit XML, so an Ansible run becomes a test report with per-task results and timings, for free, from an environment variable.

Note what is inside <system-out>: the full task result.

tree — one file per host

Writes each host’s results as JSON into a directory, which is the shape you want for per-host audit artefacts.

Read-only / Safeper-host files
$ ANSIBLE_CALLBACKS_ENABLED=ansible.builtin.tree ANSIBLE_CALLBACK_TREE_DIR=./tree ansible-playbook -i inv.ini small.yml && ls ./tree
web01.example.com
web02.example.com

profile_tasks — where the time went

ansible.posix.profile_tasks prints a per-task duration and a summary of the slowest tasks. It is the first thing to reach for when a run that used to take eleven minutes takes forty.

It lives in the ansible.posix collection, so it is not present on a bare ansible-core controller — one more instance of Part XXVII’s theme, that the tool people assume is built in is a pinned dependency.

Read-only / Safeenable it for one run
ANSIBLE_CALLBACKS_ENABLED=ansible.posix.profile_tasks \
ansible-playbook -i inventory site.yml

Part XXXIV covers performance work properly. The relevant discipline here is the general one: measure before tuning, and a callback is how you measure without changing the thing you are measuring.

Knowledge check

Knowledge check · 4 questions

  1. Q1. You want per-task timings and a JUnit report from the same run. How many stdout callbacks and how many notification callbacks does that need?

  2. Q2. A task result printed with result_format: yaml shows enabled: true, but a conditional written against it behaves unexpectedly. What is the likely explanation?

  3. Q3. Which are true about enabling the junit callback in CI? Select all that apply.

  4. Q4. ansible.builtin.tree and ansible.builtin.oneline both carry deprecation notices in 2.21 naming ansible-core 2.23 as their removal version.

Passing score: 75%. Answers are checked in this browser.