
Notification Platform
- 24 installs
- 44.5k repo stars
- Updated August 5, 2026
- getsentry/sentry
notification-platform is an agent skill that Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", ".
About
Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", "notification platform", "send notification", "notification template", "notification renderer", "notification provider", "NotificationPlatform", "notify user", "send email notification", "send slack notification". --- name: notification-platform description: Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", "notification platform", "send notification", "notification template", "notification renderer", "notification provider", "NotificationPlatform", "notify user", "send email notification", "send slack notification". --- # NotificationPlatform Guide 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. ## Glossary | Concept | Role | Location | | ------------------------------ | --------------------------------------------------------------------.
- NotificationPlatform Guide
- Add the enum value under the appropriate category comment:
- Add it to `NOTIFICATION_SOURCE_MAP` under the matching category key:
- `source` is a **class variable** (no type annotation), not a dataclass field
- Use `frozen=True` for serialization safety
Notification Platform by the numbers
- 24 all-time installs (skills.sh)
- Ranked #1,397 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
notification-platform capabilities & compatibility
- Capabilities
- notificationplatform guide · add the enum value under the appropriate categor · add it to `notification_source_map` under the ma · `source` is a **class variable** (no type annota · use `frozen=true` for serialization safety
- Use cases
- documentation
What notification-platform says it does
--- name: notification-platform description: Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform.
--- # NotificationPlatform Guide 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.
Frozen dataclass carrying the payload for a single notification.
npx skills add https://github.com/getsentry/sentry --skill notification-platformAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24 |
|---|---|
| repo stars | ★ 44.5k |
| Last updated | August 5, 2026 |
| Repository | getsentry/sentry ↗ |
What problem does notification-platform solve for developers using this skill?
Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", "notification platform", "send notificati
Who is it for?
Developers who need notification-platform patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", "notification platform", "send notificati
What you get
Actionable workflows and conventions from SKILL.md for notification-platform.
Files
NotificationPlatform Guide
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.
Glossary
| 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/.
Step 1: Determine Your Operation
| 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).
Step 2: Define the Notification Source
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
1. Add the enum value under the appropriate category comment:
class NotificationSource(StrEnum):
# MY_CATEGORY
MY_NEW_SOURCE = "my-new-source"2. Add it to 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.
Step 3: Create the Notification Data
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: strRules:
sourceis a class variable (no type annotation), not a dataclass field- Use
frozen=Truefor serialization safety - Only include fields needed by the template's
render()method - Avoid Django model instances; use primitive types or simple dataclasses for async serialization
For full examples (DataExportSuccess, DataExportFailure), load references/data-and-templates.md.Step 4: Create the Notification Template
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 MyNotificationTemplateThis 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.Step 5: Register Rollout and Send
Rollout registration
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).
Sending pattern
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.Step 6: Add a Custom Renderer
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:
- The notification needs provider-specific interactive elements (buttons with action IDs, rich text blocks)
- The rendered output structure differs significantly from subject + body + actions
- You need to render different data types differently within the same provider
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_rendererFile 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.Step 7: Add a New Provider
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:
1. Create {provider_name}/provider.py with provider + default renderer classes 2. Register with @provider_registry.register(NotificationProviderKey.MY_PROVIDER) 3. Add NotificationProviderKey.MY_PROVIDER to the NotificationProviderKey enum in types.py 4. Import the provider in sentry/notifications/apps.py 5. Gate availability behind a feature flag in is_available()
For the full provider scaffold and protocol requirements, load references/provider-template.md.Step 8: Test
Test directory: tests/sentry/notifications/platform/
Template test
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 outputService integration test
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="user@example.com",
)
service.notify_target(target=target)
assert mock_send.calledCustom renderer test
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 MyCustomRendererStep 9: Verify
Pre-flight checklist before submitting:
- [ ]
NotificationSourceenum value added totypes.py - [ ] Source added to
NOTIFICATION_SOURCE_MAPunder correct category - [ ] Data class is
@dataclass(frozen=True)withsourceas class variable - [ ] Template registered with
@template_registry.register(DataClass.source) - [ ] Template imported in
templates/__init__.py - [ ]
example_dataon template produces valid output viarender_example() - [ ] Rollout option value configured (or ticket filed for
sentry-options-automator) - [ ] Sending code guarded with
NotificationService.has_access() - [ ] Tests pass:
pytest -svv --reuse-db tests/sentry/notifications/platform/ - [ ] Pre-commit passes on all modified files
Custom Renderers — Full Reference
Architecture
The default flow is: NotificationData → NotificationTemplate.render() → NotificationRenderedTemplate → NotificationRenderer.render() → provider-specific renderable.
Custom renderers replace the last step. The provider's get_renderer() method dispatches to a custom renderer class based on category or data type, bypassing the default block-to-renderable conversion.
Template.render(data) → NotificationRenderedTemplate
↓
Provider.get_renderer(data, category)
├── default → DefaultRenderer.render(data, rendered_template)
└── custom → CustomRenderer.render(data, rendered_template)The custom renderer still receives the rendered_template, but is free to ignore it and render directly from data.
When to Use
Use a custom renderer when:
- You need interactive elements (e.g., Slack buttons with action IDs)
- The output structure differs significantly from the standard subject/body/actions layout
- Different data types within the same category need completely different renderings
- You need provider-specific features (rich text blocks, adaptive cards, embeds)
Do NOT use a custom renderer when:
- The default block types (
ParagraphBlock,CodeBlock,PlainTextBlock,BoldTextBlock,CodeTextBlock) are sufficient - You only need to tweak styling — the default renderers establish common styles that the majority of notifications should abide by.
File Placement
Custom renderers live at: {provider}/renderers/{name}.py
Example: slack/renderers/seer.py
Concrete Example: SeerSlackRenderer
File: src/sentry/notifications/platform/slack/renderers/seer.py
This renderer handles three different Seer notification data types with completely different Slack outputs:
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.notifications.platform.types import (
NotificationData,
NotificationRenderedTemplate,
)
class SeerSlackRenderer(NotificationRenderer[SlackRenderable]):
@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
) -> SlackRenderable:
if isinstance(data, SeerAutofixTrigger):
# Renders a single action button
return SlackRenderable(
blocks=[ActionsBlock(elements=[autofix_button])],
text="Seer Autofix Trigger",
)
elif isinstance(data, SeerAutofixError):
# Renders error sections
return SlackRenderable(
blocks=[
SectionBlock(text=data.error_title),
SectionBlock(text=MarkdownTextObject(text=f">{data.error_message}")),
],
text=f"Seer stumbled: {data.error_title}",
)
elif isinstance(data, SeerAutofixUpdate):
# Complex rendering: heading, summary, steps list, code changes, PR buttons
# ... (see full source for details)
pass
else:
raise ValueError(f"SeerSlackRenderer does not support {data.__class__.__name__}")Provider-Side Registration
The provider dispatches to the custom renderer by overriding get_renderer():
File: src/sentry/notifications/platform/slack/provider.py
from sentry.notifications.platform.slack.renderers.seer import SeerSlackRenderer
@provider_registry.register(NotificationProviderKey.SLACK)
class SlackNotificationProvider(NotificationProvider[SlackRenderable]):
key = NotificationProviderKey.SLACK
default_renderer = SlackRenderer # default for all categories
@classmethod
def get_renderer(
cls, *, data: NotificationData, category: NotificationCategory
) -> type[NotificationRenderer[SlackRenderable]]:
if category == NotificationCategory.SEER:
return SeerSlackRenderer
return cls.default_rendererCreating Your Own Custom Renderer
1. Create {provider}/renderers/{name}.py 2. Implement the NotificationRenderer protocol:
from sentry.notifications.platform.renderer import NotificationRenderer
class MyCustomRenderer(NotificationRenderer[ProviderRenderable]):
provider_key = NotificationProviderKey.MY_PROVIDER
@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
) -> ProviderRenderable:
# Build provider-specific output from data
# rendered_template is available but can be ignored
...3. Update the provider's get_renderer() to return your renderer for the relevant category 4. If using hide_from_debugger = True on the template, the debugger won't try to render the standard template output
Templates with hide_from_debugger
When a template only makes sense with a custom renderer (e.g., SeerAutofixUpdateTemplate), set hide_from_debugger = True. The render() method can return a minimal NotificationRenderedTemplate since the custom renderer will ignore it anyway:
@template_registry.register(SeerAutofixUpdate.source)
class SeerAutofixUpdateTemplate(NotificationTemplate[SeerAutofixUpdate]):
category = NotificationCategory.SEER
hide_from_debugger = True
example_data = SeerAutofixUpdate(...)
def render(self, data: SeerAutofixUpdate) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject="Seer Autofix Update",
body=[ParagraphBlock(blocks=[PlainTextBlock(text="Update")])],
)Data Classes and Templates — Full Reference
Complete Example: DataExportSuccess
File: src/sentry/notifications/platform/templates/data_export.py
from dataclasses import dataclass
from datetime import datetime
from django.utils import timezone
from sentry.notifications.platform.registry import template_registry
from sentry.notifications.platform.types import (
NotificationCategory,
NotificationData,
NotificationRenderedAction,
NotificationRenderedTemplate,
NotificationSource,
NotificationTemplate,
ParagraphBlock,
PlainTextBlock,
)
def format_date(date: datetime) -> str:
return date.strftime("%I:%M %p on %B %d, %Y (%Z)")
@dataclass(frozen=True)
class DataExportSuccess(NotificationData):
source = NotificationSource.DATA_EXPORT_SUCCESS
export_url: str
expiration_date: datetime
@template_registry.register(DataExportSuccess.source)
class DataExportSuccessTemplate(NotificationTemplate[DataExportSuccess]):
category = NotificationCategory.DATA_EXPORT
example_data = DataExportSuccess(
export_url="https://example.com/export",
expiration_date=timezone.now(),
)
def render(self, data: DataExportSuccess) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject="Your data is ready.",
body=[
ParagraphBlock(
blocks=[
PlainTextBlock(
text="See, that wasn't so bad. We're all done assembling your download."
)
],
)
],
actions=[NotificationRenderedAction(label="Take Me There", link=data.export_url)],
footer=f"This download file expires at {format_date(data.expiration_date)}.",
)Complete Example: DataExportFailure (with CodeBlock)
@dataclass(frozen=True)
class DataExportFailure(NotificationData):
source = NotificationSource.DATA_EXPORT_FAILURE
error_message: str
error_payload: dict[str, Any]
creation_date: datetime
@template_registry.register(DataExportFailure.source)
class DataExportFailureTemplate(NotificationTemplate[DataExportFailure]):
category = NotificationCategory.DATA_EXPORT
example_data = DataExportFailure(
error_message="An error occurred while exporting your data.",
error_payload={"export_type": "Issues-by-Tag", "project": [1234567890], "key": "user"},
creation_date=timezone.now(),
)
def render(self, data: DataExportFailure) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject="We couldn't export your data.",
body=[
ParagraphBlock(
blocks=[
PlainTextBlock(
text=f"The data export you created at {format_date(data.creation_date)} didn't work."
)
]
),
ParagraphBlock(
blocks=[
PlainTextBlock(text="It looks like there was an error: "),
CodeTextBlock(text=data.error_message),
]
),
CodeBlock(blocks=[PlainTextBlock(text=orjson.dumps(data.error_payload).decode())]),
],
actions=[
NotificationRenderedAction(label="Documentation", link="https://docs.sentry.io/"),
],
)NotificationCategory to NotificationSource Mapping
For a complete list ofNotificationCategorytoNotificationSourcemappings, loadsrc/sentry/notifications/platform/types.py.
NotificationRenderedTemplate Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
subject | str | Yes | Title/subject line. No formatting — displayed as-is. |
body | list[NotificationBodyFormattingBlock] | Yes | Main content using block types below. |
actions | list[NotificationRenderedAction] | No | Buttons/links. Each has label (str) and link (str). |
chart | NotificationRenderedImage | No | Image with url and alt_text fields. |
footer | str | No | Extra text after actions. No formatting. |
email_html_path | str | No | Custom Django HTML template path. Data class passed as context. Default: sentry/emails/platform/default.html. |
email_text_path | str | No | Custom Django text template path. Data class passed as context. |
Notes on Special Template Fields
hide_from_debugger
Set hide_from_debugger = True on templates that only use custom renderers and bypass NotificationRenderedTemplate rendering. These won't appear in the internal debugger at sentry.io/debug/notifications. Example: SeerAutofixUpdateTemplate.
render_example()
The default implementation calls self.render(data=self.example_data). Override only if the example rendering needs special behavior. The example_data class variable must produce a valid rendered template — the debugger uses this to preview notifications.
get_data_class()
Returns the NotificationData subclass for this template by inspecting example_data.__class__. Used internally for deserialization in async tasks. You do not need to override this.
New Provider — Full Reference
Provider Protocol Requirements
| Attribute/Method | Type | Description |
|---|---|---|
key | NotificationProviderKey | Unique enum value for this provider |
default_renderer | type[NotificationRenderer[RenderableT]] | Default renderer class |
target_class | type[NotificationTarget] | Target class this provider accepts |
target_resource_types | list[NotificationTargetResourceType] | Supported resource types |
validate_target(target) | classmethod | Validates target is correct type for provider |
get_renderer(data, category) | classmethod | Returns renderer class (default or custom) |
is_available(organization) | classmethod | Whether provider is enabled |
send(target, renderable) | classmethod | Delivers the rendered notification, typically by instantiating an IntegrationInstallation class of the matching provider type, and invoking its dispatch method |
Provider Scaffold
Based on the Discord provider pattern (src/sentry/notifications/platform/discord/provider.py):
````python from __future__ import annotations
from typing import TYPE_CHECKING
from sentry.notifications.platform.provider import NotificationProvider, NotificationProviderError from sentry.notifications.platform.registry import provider_registry from sentry.notifications.platform.renderer import NotificationRenderer from sentry.notifications.platform.target import ( IntegrationNotificationTarget, PreparedIntegrationNotificationTarget, ) from sentry.notifications.platform.types import ( NotificationBodyFormattingBlock, NotificationBodyFormattingBlockType, NotificationBodyTextBlock, NotificationBodyTextBlockType, NotificationData, NotificationProviderKey, NotificationRenderedTemplate, NotificationTarget, NotificationTargetResourceType, ) from sentry.organizations.services.organization.model import RpcOrganizationSummary
Define the renderable type for this provider
type MyRenderable = dict # Replace with actual type
class MyDefaultRenderer(NotificationRenderer[MyRenderable]): provider_key = NotificationProviderKey.MY_PROVIDER
@classmethod def render[DataT: NotificationData]( cls, *, data: DataT, rendered_template: NotificationRenderedTemplate ) -> MyRenderable:
Convert rendered_template blocks into provider-specific format
body = cls.render_body_blocks(rendered_template.body)
Build and return provider-specific renderable
return {"subject": rendered_template.subject, "body": body}
@classmethod def render_body_blocks(cls, body: list[NotificationBodyFormattingBlock]) -> str: parts = [] for block in body: if block.type == NotificationBodyFormattingBlockType.PARAGRAPH: parts.append(cls.render_text_blocks(block.blocks)) elif block.type == NotificationBodyFormattingBlockType.CODE_BLOCK: parts.append(f"``{cls.render_text_blocks(block.blocks)}``") return "\n".join(parts)
@classmethod def render_text_blocks(cls, blocks: list[NotificationBodyTextBlock]) -> str: texts = [] for block in blocks: if block.type == NotificationBodyTextBlockType.PLAIN_TEXT: texts.append(block.text) elif block.type == NotificationBodyTextBlockType.BOLD_TEXT: texts.append(f"{block.text}") elif block.type == NotificationBodyTextBlockType.CODE: texts.append(f"{block.text}") return " ".join(texts)
@provider_registry.register(NotificationProviderKey.MY_PROVIDER) class MyNotificationProvider(NotificationProvider[MyRenderable]): key = NotificationProviderKey.MY_PROVIDER default_renderer = MyDefaultRenderer target_class = IntegrationNotificationTarget # or GenericNotificationTarget target_resource_types = [ NotificationTargetResourceType.CHANNEL, NotificationTargetResourceType.DIRECT_MESSAGE, ]
@classmethod def is_available(cls, *, organization: RpcOrganizationSummary | None = None) -> bool:
Gate behind a feature flag until ready
return False
@classmethod def send(cls, *, target: NotificationTarget, renderable: MyRenderable) -> None: if not isinstance(target, cls.target_class): raise NotificationProviderError( f"Target '{target.__class__.__name__}' is not valid for {cls.__name__}" )
Deliver the renderable via your provider's API
... ````
Registration Steps
1. Add provider key enum
File: src/sentry/notifications/platform/types.py
class NotificationProviderKey(StrEnum):
# ... existing keys ...
MY_PROVIDER = "my_provider"2. Import in apps.py
File: src/sentry/notifications/apps.py
class Config(AppConfig):
name = "sentry.notifications"
def ready(self) -> None:
# Register providers
import sentry.notifications.platform.discord.provider
import sentry.notifications.platform.email.provider
import sentry.notifications.platform.msteams.provider
import sentry.notifications.platform.my_provider.provider # Add this
import sentry.notifications.platform.slack.provider
# Register templates
import sentry.notifications.platform.templates3. Feature flag gating
Use is_available() to gate behind a feature flag:
@classmethod
def is_available(cls, *, organization: RpcOrganizationSummary | None = None) -> bool:
if organization is None:
return False
from sentry import features
return features.has("organizations:my-provider-notifications", organization)The provider_registry.get_available(organization) method filters providers by is_available(), so unavailable providers won't be used for multi-provider sends.
File Structure
src/sentry/notifications/platform/
└── my_provider/
├── __init__.py
├── provider.py # Provider + default renderer
└── renderers/ # Optional custom renderers
└── __init__.pyTargets and Sending — Full Reference
Target Types
GenericNotificationTarget (for Email)
Used when no integration is needed. The resource_id is the recipient's email address.
from sentry.notifications.platform.target import GenericNotificationTarget
from sentry.notifications.platform.types import (
NotificationProviderKey,
NotificationTargetResourceType,
)
target = GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id="user@example.com",
)Real example (from data export sending):
target = GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id=user.email,
)IntegrationNotificationTarget (for Slack, Discord, MS Teams)
Used when sending through an integration. Requires integration_id and organization_id in addition to the base target fields.
from sentry.notifications.platform.target import IntegrationNotificationTarget
target = IntegrationNotificationTarget(
provider_key=NotificationProviderKey.SLACK,
resource_type=NotificationTargetResourceType.CHANNEL,
resource_id="C01ABC23DEF", # Slack channel ID
integration_id=integration.id,
organization_id=organization.id,
)For direct messages, use NotificationTargetResourceType.DIRECT_MESSAGE and the user's provider-specific ID as resource_id.
Target Resource Types by Provider
| Provider | Supported resource types |
|---|---|
EMAIL | |
| Slack | CHANNEL, DIRECT_MESSAGE |
| Discord | CHANNEL, DIRECT_MESSAGE |
| MS Teams | CHANNEL, DIRECT_MESSAGE |
Sending: async vs sync vs notify_target
| Method | Behavior | Use when |
|---|---|---|
notify_async(targets=[...]) | Sends via Celery task. Fire-and-forget. | Default choice. Most notifications. |
notify_sync(targets=[...]) | Sends synchronously. Returns dict[ProviderKey, list[str]] of errors. | You need to report errors back to the caller. |
notify_target(target=...) | Sends one target synchronously. Ignores notification settings. | Low-level. Called internally by the other methods. |
All three methods require the service to be initialized with data:
service = NotificationService(data=my_data)Using a Strategy
Instead of constructing targets manually, you can implement NotificationStrategy:
from sentry.notifications.platform.types import NotificationStrategy, NotificationTarget
class MyNotificationStrategy(NotificationStrategy):
def __init__(self, organization, project):
self.organization = organization
self.project = project
def get_targets(self) -> list[NotificationTarget]:
# Query for relevant users/channels and build targets
return [
GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id=member.email,
)
for member in self.get_relevant_members()
]Then pass it to the service:
service.notify_async(strategy=MyNotificationStrategy(org, project))You must provide either strategy or targets, not both. Strategies should be used whenever a notification is targeting multiple recipients, or must do complex lookup logic to construct a valid target.
Rollout Setup
How rollout works
NotificationService.has_access(organization, source) delegates to NotificationRolloutService, which:
1. Checks feature flags in priority order (internal-testing > is-sentry > early-adopter > general-access) 2. Looks up the rollout rate for the source in the matched option 3. Rolls a random number against the rate
Feature flag hierarchy
| Priority | Feature flag | Option key |
|---|---|---|
| 1 (highest) | organizations:notification-platform.internal-testing | notifications.platform-rollout.internal-testing |
| 2 | organizations:notification-platform.is-sentry | notifications.platform-rollout.is-sentry |
| 3 | organizations:notification-platform.early-adopter | notifications.platform-rollout.early-adopter |
| 4 (lowest) | organizations:notification-platform.general-access | notifications.platform-rollout.general-access |
Option registration
The four rollout options are already registered in src/sentry/options/defaults.py:
register(
"notifications.platform-rollout.internal-testing",
type=Dict,
default={},
flags=FLAG_AUTOMATOR_MODIFIABLE,
)
# ... same pattern for is-sentry, early-adopter, general-accessConfiguring rollout rates
Rollout rates are configured in sentry-options-automator (separate repo). Each option is a dict mapping source string to float (0.0-1.0):
{
"data-export-success": 1.0, # 100% rollout
"my-new-source": 0.5, # 50% rollout
"experimental-feature": 0.01, # 1% rollout
}Standard sending pattern with rollout guard
from sentry.notifications.platform.service import NotificationService
data = MyNotificationData(...)
if NotificationService.has_access(organization, data.source):
service = NotificationService(data=data)
service.notify_async(targets=[target])Always guard with has_access() before sending. This ensures rollout controls are respected.
Related skills
FAQ
What does notification-platform do?
Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", "notification platform", "send notification", "notification t
When should I use notification-platform?
Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to "add notification", "new notification", "notification platform", "send notification", "notification t
Is notification-platform safe to install?
Review the Security Audits panel on this page before installing in production.