.agents/skills/notification-platform/SKILL.md
Sentry's NotificationPlatform is a provider-based system for sending notifications across Email, Slack, Discord, and MS Teams. You define data + template, register it, and the platform handles rendering and delivery per provider.
| Concept | Role | Location |
|---|---|---|
NotificationData | Protocol. Frozen dataclass carrying the payload for a single notification. Must declare a source class variable. | types.py |
NotificationTemplate | Abstract class. Converts NotificationData into a NotificationRenderedTemplate. Registered per NotificationSource. | types.py |
NotificationRenderedTemplate | Dataclass. Provider-agnostic output: subject, body blocks, actions, chart, footer, optional email paths. | types.py |
NotificationProvider | Protocol. Knows how to validate a target, pick a renderer, and send the final renderable (Email, Slack, etc.). | provider.py |
NotificationRenderer | Protocol. Converts a NotificationRenderedTemplate into a provider-specific renderable (HTML email, Slack blocks, etc.). | renderer.py |
NotificationTarget | Protocol. Identifies the recipient: email address, channel ID, or DM user ID. Two concrete classes: GenericNotificationTarget (email) and IntegrationNotificationTarget (Slack/Discord/MSTeams). | target.py |
NotificationService | Entry point. Orchestrates lookup, rendering, and delivery. Provides has_access(), notify_target(), notify_async(), notify_sync(). | service.py |
All paths below are relative to src/sentry/notifications/platform/.
| I want to... | Go to |
|---|---|
| Add a new notification (most common) | Steps 2-5 |
| Add a custom renderer for an existing provider | Step 6 |
| Add an entirely new provider | Step 7 |
After any operation, continue to Step 8 (Test) and Step 9 (Verify).
Every notification needs a unique NotificationSource enum value and must be mapped to a NotificationCategory. A NotificationSource should represent the domain or feature that a given notification belongs to.
For examples, load
src/sentry/notifications/platform/types.py.
File: types.py
class NotificationSource(StrEnum):
# MY_CATEGORY
MY_NEW_SOURCE = "my-new-source"
NOTIFICATION_SOURCE_MAP under the matching category key:NOTIFICATION_SOURCE_MAP[NotificationCategory.MY_CATEGORY].append(
NotificationSource.MY_NEW_SOURCE
)
If no existing NotificationCategory fits, add a new one to the NotificationCategory enum first, then create its entry in NOTIFICATION_SOURCE_MAP.
All NotificationCategory options are defined in the src/sentry/notifications/platform/types.py file.
The data class is a frozen dataclass implementing the NotificationData protocol. It carries everything the template needs to render.
File: templates/<your_notification>.py (new file)
from dataclasses import dataclass
from sentry.notifications.platform.types import NotificationData, NotificationSource
@dataclass(frozen=True)
class MyNotificationData(NotificationData):
source = NotificationSource.MY_NEW_SOURCE # class variable, not a field
title: str
detail_url: str
Rules:
source is a class variable (no type annotation), not a dataclass fieldfrozen=True for serialization safetyrender() methodFor full examples (DataExportSuccess, DataExportFailure), load
references/data-and-templates.md.
The template converts your data into a provider-agnostic NotificationRenderedTemplate.
Same file as Step 3: templates/<your_notification>.py
from sentry.notifications.platform.registry import template_registry
from sentry.notifications.platform.types import (
NotificationCategory,
NotificationRenderedAction,
NotificationRenderedTemplate,
NotificationTemplate,
ParagraphBlock,
PlainTextBlock,
)
@template_registry.register(MyNotificationData.source)
class MyNotificationTemplate(NotificationTemplate[MyNotificationData]):
category = NotificationCategory.MY_CATEGORY
example_data = MyNotificationData(
title="Example title",
detail_url="https://example.com",
)
def render(self, data: MyNotificationData) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject=data.title,
body=[
ParagraphBlock(blocks=[PlainTextBlock(text="Something happened.")])
],
actions=[
NotificationRenderedAction(label="View Details", link=data.detail_url)
],
)
Available body block types:
Refer to src/sentry/notifications/platform/types.py for the latest available block types.
Register the import in templates/__init__.py:
from .my_notification import MyNotificationTemplate
This import is required so the @template_registry.register decorator executes at startup (via sentry/notifications/apps.py).
For the full rendered template field reference and more examples, load
references/data-and-templates.md.
The platform uses a tiered rollout system. Each notification source must be added to the appropriate rollout option before it will be delivered.
Rollout options are configured externally in sentry-options-automator (not this repo). The option keys are:
| Rollout stage | Option key |
|---|---|
| Internal testing | notifications.platform-rollout.internal-testing |
| Sentry orgs | notifications.platform-rollout.is-sentry |
| Early adopter | notifications.platform-rollout.early-adopter |
| General access | notifications.platform-rollout.general-access |
Each option is a Dict mapping source string to rollout rate (0.0-1.0). Example:
{"my-new-source": 1.0}
These options are registered in src/sentry/options/defaults.py (already done for the four stages above).
from sentry.notifications.platform.service import NotificationService
from sentry.notifications.platform.target import GenericNotificationTarget
from sentry.notifications.platform.types import (
NotificationProviderKey,
NotificationTargetResourceType,
)
data = MyNotificationData(title="Export ready", detail_url="https://...")
# Guard with rollout check
if NotificationService.has_access(organization, data.source):
service = NotificationService(data=data)
target = GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id=user.email,
)
service.notify_async(targets=[target])
For target types, async/sync decisions, and strategy patterns, load
references/targets-and-sending.md.
Custom renderers bypass the default template-to-renderable conversion for a specific provider + category combination. Use when the default block-based rendering is too limiting (e.g., interactive Slack buttons, rich card layouts).
When to use:
How it works: Override get_renderer() on the provider to return your custom renderer class for the relevant category:
# In the provider class
@classmethod
def get_renderer(
cls, *, data: NotificationData, category: NotificationCategory
) -> type[NotificationRenderer[MyRenderable]]:
if category == NotificationCategory.MY_CATEGORY:
return MyCustomRenderer
return cls.default_renderer
File placement: {provider}/renderers/{name}.py (e.g., slack/renderers/seer.py)
For architecture details and the full Seer Slack renderer example, load
references/custom-renderers.md.
Adding a new provider requires implementing the NotificationProvider protocol, a default NotificationRenderer, and registering both. This should only be done when onboarding a new integration provider.
High-level steps:
{provider_name}/provider.py with provider + default renderer classes@provider_registry.register(NotificationProviderKey.MY_PROVIDER)NotificationProviderKey.MY_PROVIDER to the NotificationProviderKey enum in types.pysentry/notifications/apps.pyis_available()For the full provider scaffold and protocol requirements, load
references/provider-template.md.
Test directory: tests/sentry/notifications/platform/
class TestMyNotificationTemplate:
def test_render(self):
data = MyNotificationData(title="Test", detail_url="https://example.com")
template = MyNotificationTemplate()
rendered = template.render(data)
assert rendered.subject == "Test"
assert len(rendered.body) == 1
assert len(rendered.actions) == 1
assert rendered.actions[0].link == "https://example.com"
def test_render_example(self):
template = MyNotificationTemplate()
rendered = template.render_example()
assert rendered.subject # Verify example_data produces valid output
from unittest.mock import patch
from sentry.notifications.platform.service import NotificationService
class TestMyNotificationService:
@patch("sentry.notifications.platform.email.provider.EmailNotificationProvider.send")
def test_notify_target(self, mock_send):
data = MyNotificationData(title="Test", detail_url="https://example.com")
service = NotificationService(data=data)
target = GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id="[email protected]",
)
service.notify_target(target=target)
assert mock_send.called
If you added a custom renderer, test that the provider dispatches to it:
def test_get_renderer_returns_custom():
data = MySpecialData(source=NotificationSource.MY_SOURCE, ...)
renderer = MyProvider.get_renderer(data=data, category=NotificationCategory.MY_CATEGORY)
assert renderer is MyCustomRenderer
Pre-flight checklist before submitting:
NotificationSource enum value added to types.pyNOTIFICATION_SOURCE_MAP under correct category@dataclass(frozen=True) with source as class variable@template_registry.register(DataClass.source)templates/__init__.pyexample_data on template produces valid output via render_example()sentry-options-automator)NotificationService.has_access()pytest -svv --reuse-db tests/sentry/notifications/platform/