Ansible Callback Plugins: Customize Output, Logging, and Notifications Complete Guide
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible callback plugins. Learn to customize playbook output with built-in callbacks like json, yaml, timer, and profile_tasks.
Callback plugins control what happens during playbook execution — output formatting, timing, logging, notifications. Ansible ships with 20+ built-in callbacks and you can write custom ones for Slack alerts, database logging, or metrics collection. Here's how to use them all.
How Callback Plugins Work
Callbacks hook into Ansible events:
• v2_playbook_on_start — playbook begins
• v2_playbook_on_task_start — each task begins
• v2_runner_on_ok — task succeeds
• v2_runner_on_failed — task fails
• v2_runner_on_skipped — task skipped
• v2_playbook_on_stats — final summary
There are two types: • stdout callback — one active at a time, controls terminal output • notification callback — any number active simultaneously, for side effects (logging, alerts)
See also: Ansible Output to File: Save Playbook Results and Logs (Complete Guide)
Using Built-in Callbacks
Change Output Format
# ansible.cfg
[defaults]
# Choose one stdout callback:
stdout_callback = yaml # Human-readable YAML output
# stdout_callback = json # Machine-parseable JSON
# stdout_callback = debug # Full output with stdout/stderr
# stdout_callback = dense # Minimal one-line-per-task
# stdout_callback = minimal # Bare minimum
Enable Notification Callbacks
# ansible.cfg
[defaults]
# Enable multiple notification callbacks
callbacks_enabled = timer, profile_tasks, profile_roles
timer — Total Execution Time
callbacks_enabled = timer
Output:
PLAY RECAP *****
Playbook run took 0 days, 0 hours, 2 minutes, 34 seconds
profile_tasks — Per-Task Timing
callbacks_enabled = profile_tasks
[callback_profile_tasks]
task_output_limit = 20
sort_order = descending
Output:
Wednesday 13 April 2026 10:30:00 +0000 (0:00:45.123) 0:02:34.567 ***
===============================================================================
Install packages ----------------------------------------- 45.12s
Deploy application --------------------------------------- 32.45s
Configure nginx ------------------------------------------ 18.67s
Gather facts --------------------------------------------- 12.34s
This is essential for identifying slow tasks.
profile_roles — Per-Role Timing
callbacks_enabled = profile_roles
Output:
===============================================================================
nginx ---------------------------------------------------- 52.34s
common --------------------------------------------------- 34.12s
app_deploy ----------------------------------------------- 28.67s
json — Machine-Readable Output
# Use as stdout callback
ANSIBLE_STDOUT_CALLBACK=json ansible-playbook site.yml
# Or pipe to jq
ansible-playbook site.yml --stdout-callback json | jq '.plays[].tasks[].hosts'
yaml — Readable Structured Output
ANSIBLE_STDOUT_CALLBACK=yaml ansible-playbook site.yml
Difference from default:
# Default callback shows:
ok: [server1] => {"changed": false, "msg": "Hello"}
# YAML callback shows:
ok: [server1] =>
msg: Hello
changed: false
debug — Full stdout/stderr
ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook site.yml
Shows complete stdout and stderr for every task — useful when debugging command/shell modules.
log_plays — File Logging
callbacks_enabled = log_plays
[callback_log_plays]
log_folder = /var/log/ansible/
Creates per-host log files in the specified directory.
mail — Email on Failure
callbacks_enabled = mail
[callback_mail]
to = ops-team@example.com
sender = ansible@example.com
smtphost = smtp.example.com
smtpport = 587
junit — JUnit XML Reports
callbacks_enabled = junit
[callback_junit]
output_dir = /tmp/ansible-junit/
Integrates with Jenkins, GitLab CI, and other CI systems that parse JUnit XML.
All Built-in Stdout Callbacks
| Callback | Description |
|----------|-------------|
| default | Standard Ansible output |
| yaml | YAML-formatted results |
| json | Full JSON output |
| debug | Shows stdout/stderr for all tasks |
| dense | One line per task |
| minimal | Bare minimum output |
| null | No output at all |
| oneline | Condensed single-line per host |
| selective | Only shows tasks tagged print_action |
| unixy | Unix-style condensed output |
| counter_enabled | Adds task counters |
See also: Simplify Ansible Output with the community.general.dense Callback Plugin
Writing a Custom Callback Plugin
Basic Structure
# callback_plugins/my_callback.py
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""Custom callback plugin."""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification' # or 'stdout'
CALLBACK_NAME = 'my_callback'
def v2_playbook_on_start(self, playbook):
self._display.display("Playbook started: %s" % playbook._file_name)
def v2_runner_on_ok(self, result):
host = result._host.get_name()
task = result._task.get_name()
self._display.display("✅ %s: %s" % (host, task))
def v2_runner_on_failed(self, result, ignore_errors=False):
host = result._host.get_name()
task = result._task.get_name()
if not ignore_errors:
self._display.display("❌ %s: %s" % (host, task), color='red')
def v2_runner_on_skipped(self, result):
host = result._host.get_name()
task = result._task.get_name()
self._display.display("⏭️ %s: %s" % (host, task))
def v2_playbook_on_stats(self, stats):
hosts = sorted(stats.processed.keys())
for host in hosts:
summary = stats.summarize(host)
self._display.display(
"%s: ok=%d changed=%d failed=%d skipped=%d" % (
host,
summary['ok'],
summary['changed'],
summary['failures'],
summary['skipped']
)
)
Slack Notification Callback
# callback_plugins/slack_notify.py
import json
import os
from urllib.request import Request, urlopen
from ansible.plugins.callback import CallbackBase
DOCUMENTATION = '''
name: slack_notify
type: notification
short_description: Send Slack notifications on playbook events
description:
- Sends a Slack message when a playbook completes or fails
requirements:
- SLACK_WEBHOOK_URL environment variable
'''
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'slack_notify'
def __init__(self):
super().__init__()
self.webhook_url = os.environ.get('SLACK_WEBHOOK_URL')
self.playbook_name = ''
self.failures = []
def _send_slack(self, message, color='good'):
if not self.webhook_url:
return
payload = {
'attachments': [{
'color': color,
'text': message,
'footer': 'Ansible Automation'
}]
}
req = Request(
self.webhook_url,
data=json.dumps(payload).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
urlopen(req)
def v2_playbook_on_start(self, playbook):
self.playbook_name = playbook._file_name
def v2_runner_on_failed(self, result, ignore_errors=False):
if not ignore_errors:
self.failures.append({
'host': result._host.get_name(),
'task': result._task.get_name(),
'msg': result._result.get('msg', 'Unknown error')
})
def v2_playbook_on_stats(self, stats):
hosts = sorted(stats.processed.keys())
total_ok = sum(stats.summarize(h)['ok'] for h in hosts)
total_changed = sum(stats.summarize(h)['changed'] for h in hosts)
total_failed = sum(stats.summarize(h)['failures'] for h in hosts)
if total_failed > 0:
msg = "🔴 *%s* failed on %d host(s)\n" % (
self.playbook_name, total_failed
)
for f in self.failures[:5]:
msg += "• `%s` on %s: %s\n" % (f['task'], f['host'], f['msg'])
self._send_slack(msg, color='danger')
else:
msg = "✅ *%s* completed: %d ok, %d changed across %d hosts" % (
self.playbook_name, total_ok, total_changed, len(hosts)
)
self._send_slack(msg, color='good')
Database Logging Callback
# callback_plugins/db_logger.py
import json
import sqlite3
import time
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'db_logger'
def __init__(self):
super().__init__()
self.db = sqlite3.connect('/var/log/ansible/runs.db')
self.db.execute('''
CREATE TABLE IF NOT EXISTS runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
playbook TEXT,
host TEXT,
task TEXT,
status TEXT,
result TEXT,
timestamp REAL
)
''')
self.db.commit()
def _log(self, host, task, status, result):
self.db.execute(
'INSERT INTO runs (playbook, host, task, status, result, timestamp) '
'VALUES (?, ?, ?, ?, ?, ?)',
(self.playbook_name, host, task, status,
json.dumps(result), time.time())
)
self.db.commit()
def v2_playbook_on_start(self, playbook):
self.playbook_name = playbook._file_name
def v2_runner_on_ok(self, result):
self._log(
result._host.get_name(),
result._task.get_name(),
'ok',
{'changed': result._result.get('changed', False)}
)
def v2_runner_on_failed(self, result, ignore_errors=False):
self._log(
result._host.get_name(),
result._task.get_name(),
'failed' if not ignore_errors else 'failed_ignored',
{'msg': result._result.get('msg', '')}
)
Configuration Options
Where to Put Callback Plugins
# ansible.cfg
[defaults]
# Local project directory
callback_plugins = ./callback_plugins
# Or system-wide
callback_plugins = /usr/share/ansible/plugins/callback
Environment Variables
# Override stdout callback
export ANSIBLE_STDOUT_CALLBACK=yaml
# Enable notification callbacks
export ANSIBLE_CALLBACKS_ENABLED=timer,profile_tasks
# Set callback-specific options
export ANSIBLE_CALLBACK_PROFILE_TASKS_TASK_OUTPUT_LIMIT=30
Per-Playbook Callback Selection
# One-off callback for a single run
ansible-playbook site.yml -e 'ANSIBLE_STDOUT_CALLBACK=json'
# Or use environment
ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook site.yml
See also: Simplifying Ansible Output with the community.general.unixy Callback Plugin
Practical Examples
CI/CD Pipeline with JUnit + Timer
# ansible.cfg for CI
[defaults]
stdout_callback = yaml
callbacks_enabled = timer, profile_tasks, junit
[callback_junit]
output_dir = {{ lookup('env', 'CI_PROJECT_DIR') }}/test-results/
[callback_profile_tasks]
task_output_limit = 30
sort_order = descending
Production Deployment with Slack + Logging
# ansible.cfg for production
[defaults]
stdout_callback = default
callbacks_enabled = timer, profile_tasks, log_plays, slack_notify
[callback_log_plays]
log_folder = /var/log/ansible/hosts/
Performance Profiling
# Find your slowest tasks
ANSIBLE_CALLBACKS_ENABLED=profile_tasks,profile_roles \
ansible-playbook site.yml
# Output shows timing per task and per role
# Use this to identify optimization targets
Available Callback Events
| Event | When It Fires |
|-------|---------------|
| v2_playbook_on_start | Playbook execution begins |
| v2_playbook_on_play_start | Each play begins |
| v2_playbook_on_task_start | Each task begins |
| v2_runner_on_ok | Task succeeds on a host |
| v2_runner_on_failed | Task fails on a host |
| v2_runner_on_skipped | Task skipped on a host |
| v2_runner_on_unreachable | Host unreachable |
| v2_runner_retry | Task being retried |
| v2_playbook_on_handler_task_start | Handler fires |
| v2_playbook_on_stats | Playbook complete (final stats) |
| v2_on_file_diff | Diff output available |
| v2_runner_item_on_ok | Loop item succeeds |
| v2_runner_item_on_failed | Loop item fails |
FAQ
How do I see which callbacks are available?
Run ansible-doc -t callback -l to list all installed callback plugins. Use ansible-doc -t callback for details on a specific one.
Can I use multiple stdout callbacks?
No — only one stdout callback is active at a time. But you can enable unlimited notification callbacks alongside it. If you need multiple output formats, use json stdout callback and post-process the output.
Why aren't my custom callbacks loading?
Check: (1) the file is in a directory listed in callback_plugins in ansible.cfg, (2) for notification callbacks, it's listed in callbacks_enabled, (3) the class is named CallbackModule, (4) CALLBACK_TYPE is set correctly.
How do I pass configuration to custom callbacks?
Use environment variables or the DOCUMENTATION string with options: to define configurable parameters that users set in ansible.cfg.
Do callbacks slow down playbook execution?
Notification callbacks run synchronously in the main thread. Heavy operations (HTTP calls, database writes) add latency per task. For high-frequency callbacks, use async I/O or batch writes.
Conclusion
Callback plugins are Ansible's extension point for output, logging, and notifications. Start with profile_tasks and timer for visibility, add junit for CI integration, then build custom callbacks for Slack, PagerDuty, or metrics as your automation matures. The event-driven model makes it straightforward to hook into any playbook lifecycle stage.
Related Articles
• Ansible Performance Tuning • Ansible debug and assert Module Guide • Ansible CI/CD Pipeline Integration • Ansible Check Mode and Diff ModeCategory: installation