---
name: notification-platform
source: https://app.decimal.ai/s/notification-platform@2/SKILL.md
source_sha256: 292cfabce3f4
---

# 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 (str or text blocks), body (sections containing text blocks), actions, chart, footer (str or text blocks), 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:

```python
class NotificationSource(StrEnum):
    # MY_CATEGORY
    MY_NEW_SOURCE = "my-new-source"
```

2. Add it to `NOTIFICATION_SOURCE_MAP` under the matching category key:

```python
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)

```python
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 field
- Use `frozen=True` for 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`

```python
from sentry.notifications.platform.registry import template_registry
from sentry.notifications.platform.types import (
    NotificationCategory,
    NotificationRenderedAction,
    NotificationRenderedTemplate,
    NotificationTemplate,
    ParagraphSection,
    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=[
                ParagraphSection(blocks=[PlainTextBlock(text="Something happened.")])
            ],
            actions=[
                NotificationRenderedAction(label="View Details", link=data.detail_url)
            ],
        )
```

**Available types:**

A notification is composed of **sections** and **text blocks**. Text blocks can appear in sections (for the body), and also directly in the `subject` and `footer` fields (as `list[NotificationTextBlock]` instead of plain `str`).

Sections (portions of a message, used in `body`):

| Section             | Description                                       |
| ------------------- | ------------------------------------------------- |
| `ParagraphSection`  | A block of text with a line break before.         |
| `CodeSection`       | A code block with a line break before.            |
| `BlockQuoteSection` | A quoted block of text, rendered as a blockquote. |

Text blocks (parts of text — used inside sections, and also in `subject` and `footer`):

| Block             | Description                               |
| ----------------- | ----------------------------------------- |
| `PlainTextBlock`  | Unformatted text.                         |
| `BoldTextBlock`   | Bold text.                                |
| `ItalicTextBlock` | Italic text.                              |
| `CodeTextBlock`   | Inline code.                              |
| `LinkTextBlock`   | A hyperlink with `text` and `url` fields. |

Refer to `src/sentry/notifications/platform/types.py` for the full definitions.

**Register the import** in `templates/__init__.py`:

```python
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`.

## 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:

```python
{"my-new-source": 1.0}
```

These options are registered in `src/sentry/options/defaults.py` (already done for the four stages above).

### Sending pattern

```python
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 section/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:

```python
# 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`.

## 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

```python
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
```

### Service integration test

```python
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.called
```

### Custom renderer test

If you added a custom renderer, test that the provider dispatches to it:

```python
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
```

## Step 9: Verify

Pre-flight checklist before submitting:

- [ ] `NotificationSource` enum value added to `types.py`
- [ ] Source added to `NOTIFICATION_SOURCE_MAP` under correct category
- [ ] Data class is `@dataclass(frozen=True)` with `source` as class variable
- [ ] Template registered with `@template_registry.register(DataClass.source)`
- [ ] Template imported in `templates/__init__.py`
- [ ] `example_data` on template produces valid output via `render_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