Skip to main content
Back to Blog
Reference
2026-05-11

Robot Framework Listeners Complete Reference

Build Robot Framework listeners for event-driven test extensions. Listener API v2 and v3, hooks, custom reporters, integration with Slack, Jira, and CI tools.

Robot Framework Listeners Complete Reference

Robot Framework listeners are the framework's plugin mechanism - they let you respond to test execution events in real time without modifying tests themselves. Whether you want to post failure notifications to Slack, create Jira tickets for flaky tests, stream metrics to Datadog, or build custom reporters, listeners are the right tool. They observe events like start_test, end_test, log_message, start_keyword, end_keyword, and so on, and execute your Python code in response. Combined with the rich attribute dictionaries Robot provides, listeners can build sophisticated integrations with very little code.

This complete reference covers the listener API in depth - both the older v2 API and the modern v3 API, the event lifecycle, custom logging patterns, integration with monitoring and ticketing systems, and packaging listeners for distribution. Examples cover a Slack notifier, a flaky test detector, a TestRail uploader, and a Datadog metrics emitter. By the end you'll be ready to extend Robot Framework execution with your own listeners that fit your team's workflow.

Key Takeaways

  • Listeners are Python classes registered via --listener flag
  • v2 API uses dict attributes; v3 API uses ExecutionResult model objects
  • Events fire at every level: suite, test, keyword, log message
  • Listeners can modify behavior, not just observe (v3 only)
  • Multiple listeners can run together
  • Library listeners attach to specific libraries
  • Common use: notifications, reporting, metrics

Basic Listener

# listeners/SimpleListener.py

class SimpleListener:
    ROBOT_LISTENER_API_VERSION = 2

    def start_suite(self, name, attributes):
        print(f'Suite started: {name}')

    def end_suite(self, name, attributes):
        print(f'Suite ended: {name} - {attributes["status"]}')

    def start_test(self, name, attributes):
        print(f'Test started: {name}')

    def end_test(self, name, attributes):
        status = attributes['status']
        print(f'Test ended: {name} - {status}')

Use it:

robot --listener listeners/SimpleListener.py tests/

Listener API Versions

VersionStatusAPI Style
v2StableDict attributes
v3StableModel objects with mutation

For most use cases v2 is enough. Use v3 when you need to modify test results.

Available Events

class FullListener:
    ROBOT_LISTENER_API_VERSION = 2

    def start_suite(self, name, attrs): pass
    def end_suite(self, name, attrs): pass
    def start_test(self, name, attrs): pass
    def end_test(self, name, attrs): pass
    def start_keyword(self, name, attrs): pass
    def end_keyword(self, name, attrs): pass
    def log_message(self, message): pass
    def message(self, message): pass
    def library_import(self, name, attrs): pass
    def resource_import(self, name, attrs): pass
    def variables_import(self, name, attrs): pass
    def output_file(self, path): pass
    def log_file(self, path): pass
    def report_file(self, path): pass
    def xunit_file(self, path): pass
    def debug_file(self, path): pass
    def close(self): pass

Attributes Available

The attrs dict for end_test contains:

{
    'id': 's1-t1',
    'longname': 'Suite.Test',
    'doc': 'Test documentation',
    'tags': ['smoke', 'auth'],
    'starttime': '20260512 10:00:00.000',
    'endtime': '20260512 10:00:05.500',
    'elapsedtime': 5500,
    'status': 'PASS',  # or FAIL or SKIP
    'message': '',  # error message if FAIL
    'template': '',
    'critical': 'yes',
}

Slack Notifier

# listeners/SlackNotifier.py
import requests
import os

class SlackNotifier:
    ROBOT_LISTENER_API_VERSION = 2

    def __init__(self):
        self.webhook = os.environ['SLACK_WEBHOOK']
        self.failed_tests = []

    def end_test(self, name, attrs):
        if attrs['status'] == 'FAIL':
            self.failed_tests.append({
                'name': name,
                'message': attrs['message'],
                'tags': attrs['tags'],
            })

    def close(self):
        if not self.failed_tests:
            return
        text = f'Robot run had {len(self.failed_tests)} failures:\n'
        for t in self.failed_tests:
            text += f'- *{t["name"]}*: {t["message"]}\n'
        requests.post(self.webhook, json={'text': text})
SLACK_WEBHOOK=https://hooks.slack.com/... robot --listener listeners/SlackNotifier.py tests/

Flaky Test Detector

# listeners/FlakyDetector.py
import json
import os
from collections import defaultdict

class FlakyDetector:
    ROBOT_LISTENER_API_VERSION = 2
    STATE_FILE = 'flaky_state.json'

    def __init__(self):
        self.results = {}

    def end_test(self, name, attrs):
        self.results[name] = attrs['status']

    def close(self):
        # Load previous run
        prev = {}
        if os.path.exists(self.STATE_FILE):
            with open(self.STATE_FILE) as f:
                prev = json.load(f)

        # Find tests that flipped state
        flips = []
        for name, status in self.results.items():
            if name in prev and prev[name] != status:
                flips.append((name, prev[name], status))

        # Update state
        with open(self.STATE_FILE, 'w') as f:
            json.dump(self.results, f, indent=2)

        # Report
        if flips:
            print('FLAKY TESTS DETECTED:')
            for name, old, new in flips:
                print(f'  {name}: {old} -> {new}')

TestRail Uploader

# listeners/TestRailUploader.py
import requests
import os

class TestRailUploader:
    ROBOT_LISTENER_API_VERSION = 2

    def __init__(self):
        self.url = os.environ['TESTRAIL_URL']
        self.auth = (os.environ['TESTRAIL_USER'], os.environ['TESTRAIL_KEY'])
        self.run_id = os.environ['TESTRAIL_RUN_ID']
        self.results = []

    def end_test(self, name, attrs):
        case_id = self._extract_case_id(attrs['tags'])
        if not case_id:
            return
        status_id = 1 if attrs['status'] == 'PASS' else 5
        self.results.append({
            'case_id': case_id,
            'status_id': status_id,
            'comment': attrs['message'],
        })

    def _extract_case_id(self, tags):
        for t in tags:
            if t.startswith('TC-'):
                return int(t[3:])
        return None

    def close(self):
        if not self.results:
            return
        requests.post(
            f'{self.url}/index.php?/api/v2/add_results_for_cases/{self.run_id}',
            auth=self.auth,
            json={'results': self.results},
        )

Datadog Metrics

# listeners/DatadogMetrics.py
from datadog import statsd
import os

class DatadogMetrics:
    ROBOT_LISTENER_API_VERSION = 2

    def __init__(self):
        os.environ.setdefault('STATSD_HOST', 'localhost')

    def end_test(self, name, attrs):
        tags = [f'tag:{t}' for t in attrs['tags']] + [
            f'status:{attrs["status"].lower()}',
        ]
        statsd.increment('robot.test.executions', tags=tags)
        statsd.histogram('robot.test.duration', attrs['elapsedtime'], tags=tags)

    def end_suite(self, name, attrs):
        statsd.gauge('robot.suite.duration', attrs['elapsedtime'], tags=[f'suite:{name}'])

Library Listener

Attach a listener to a library so it activates only when the library is imported:

class MyLibrary:
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
    ROBOT_LIBRARY_LISTENER = None

    def __init__(self):
        self.ROBOT_LIBRARY_LISTENER = self

    def start_test(self, name, attrs):
        # Auto-setup before each test
        self._connect()

    def end_test(self, name, attrs):
        # Auto-teardown
        self._disconnect()

Listener v3 - Modifying Results

# listeners/v3_modifier.py

class TestModifier:
    ROBOT_LISTENER_API_VERSION = 3

    def end_test(self, data, result):
        # Modify test result
        if 'retry' in result.tags and result.failed:
            result.message += '\n[Marked for retry]'
            result.status = 'SKIP'

This can be useful for soft-failing certain tests in CI.

CI Integration

name: Robot With Listeners
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install robotframework requests datadog
      - run: robot --listener listeners/SlackNotifier.py --listener listeners/DatadogMetrics.py tests/
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}

Multiple Listeners

You can use --listener multiple times:

robot --listener Slack --listener TestRail --listener Datadog tests/

Each runs independently.

Listener Comparison

ListenerPurposePerformance
Slack notifierFailure alertsLow overhead
TestRail uploaderSync with TestRailNetwork calls at end
Flaky detectorTrack state changesDisk write at end
Datadog metricsReal-time monitoringStatsD UDP
HTML beautifierCustom reportsDisk write at end

Listener Best Practices

PracticeWhy
Use close() for batch operationsAvoid per-test network calls
Catch exceptions silentlyDon't break test runs
Make listeners idempotentReruns shouldn't break
Log to stderr separatelyDon't pollute Robot logs
Version your listenersTrack behavior changes

Real Suite Example

robot \
  --listener listeners/SlackNotifier.py \
  --listener listeners/TestRailUploader.py \
  --listener listeners/DatadogMetrics.py \
  --listener listeners/FlakyDetector.py \
  --outputdir results \
  tests/
# listeners/CompoundListener.py
class CompoundListener:
    """Combines multiple listener behaviors into one file."""
    ROBOT_LISTENER_API_VERSION = 2

    def __init__(self):
        self.start_time = None
        self.results = []

    def start_suite(self, name, attrs):
        print(f'Starting: {name}')

    def end_test(self, name, attrs):
        self.results.append((name, attrs['status'], attrs['elapsedtime']))

    def close(self):
        passed = sum(1 for r in self.results if r[1] == 'PASS')
        failed = sum(1 for r in self.results if r[1] == 'FAIL')
        avg_duration = sum(r[2] for r in self.results) / max(len(self.results), 1)
        print(f'Summary: {passed} passed, {failed} failed, avg {avg_duration}ms per test')

Anti-Patterns

Anti-PatternBetter
Network call per testBuffer in close()
Listener raises uncaught exceptionTry/except around handler
Mutating attrs dict in v2Use v3 model objects
Long blocking operationsSpawn background thread
Hardcoded credentialsEnv vars

Packaging As PyPI Package

robotframework_company_listeners/
  pyproject.toml
  src/
    company_listeners/
      __init__.py
      slack.py
      testrail.py
[project]
name = "robotframework-company-listeners"
version = "1.0.0"

Then teams install with pip and reference by name:

pip install robotframework-company-listeners
robot --listener company_listeners.slack tests/

Debugging Listeners

When a listener doesn't appear to be running:

  1. Verify the path: robot --listener /full/path/Listener.py tests/
  2. Check ROBOT_LISTENER_API_VERSION is set
  3. Add print statements to confirm methods are called
  4. Run with --loglevel DEBUG to see listener registration

Conclusion

Robot Framework listeners are the most powerful extension point in the framework. With a few lines of Python, you can integrate Robot into Slack, Jira, TestRail, Datadog, Prometheus, custom dashboards, and anything else your organization uses. They run alongside your tests without modifying them, so existing suites can be enhanced overnight. The combination of simple v2 API and powerful v3 API covers everything from one-off notifications to complex result modification.

Start with a Slack notifier - it's the highest-value listener you can write in an hour. Then layer in metrics, ticketing, and flake detection as needed. Within a sprint your CI runs will produce far more actionable signal. Explore the skills directory or read the CI/CD testing pipeline guide for adjacent topics.

Robot Framework Listeners Complete Reference | QASkills.sh