posthog/sending-notifications
How to send real-time in-app notifications from PostHog backend code. Use when integrating notifications into a new feature, wiring up a notification source (alerts, comments, approvals, pipelines, issues), or choosing the right target type and priority for a notification.
npx skills add https://github.com/PostHog/posthog --skill sending-notifications
You're adding notification support to a PostHog feature — for example, notifying a user when they're mentioned in a comment, when an alert fires, or when an approval is requested.
All notification creation goes through a single function. Import from the facade, not from internal modules:
from products.notifications.backend.facade.api import (
create_notification,
NotificationData,
NotificationType,
Priority,
TargetType,
)
Build a NotificationData and call create_notification:
event = create_notification(
NotificationData(
team_id=team.id,
notification_type=NotificationType.ALERT_FIRING,
priority=Priority.CRITICAL,
title="Event ingestion latency > 30s",
body="Events are queuing up. Ingestion pipeline is degraded.",
target_type=TargetType.USER,
target_id=str(user.id),
resource_type="dashboard",
resource_id="42",
source_url="/dashboard/42",
)
)
Returns a NotificationEvent on success, or None if the feature flag is disabled, no recipients were resolved, or the team doesn't exist. Safe to call in any context.
Required:
| Field | Type | Description |
| ------------------- | ------------------ | -------------------------------------------------------------------------- |
| team_id | int | Team context — used to look up the organization and check the feature flag |
| notification_type | NotificationType | Determines the icon in the UI |
| title | str | Notification headline (~100 chars recommended) |
| body | str | Longer description shown on expand. Can be empty string |
| target_type | TargetType | Who receives this: user, team, organization, or role |
| target_id | str | ID of the target (user ID, team ID, org UUID, or role UUID as string) |
Optional:
| Field | Type | Default | Description |
| --------------- | ---------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| resource_type | NotificationResourceType \| None | None | Access-controlled types (e.g. "dashboard") auto-filter recipients without viewer access |
| resource_id | str | "" | ID of the resource for linking |
| source_url | str | "" | Relative URL path (e.g. /dashboard/42), shown as link icon in UI |
| priority | Priority | NORMAL | normal = popover only; critical = popover + persistent toast |
| archivable | bool | False | Opt in to a per-recipient "archive" (dismiss) action that moves the notification to the recipient's Archived tab. When False, recipients can only mark it read/unread (the default pattern) |
| resolver | RecipientsResolver \| None | None | Custom recipient resolver. Default handles user/team/org/role targeting |
| Type | When to use |
| -------------------- | ------------------------------------------------ |
| comment_mention | User was @mentioned in a comment or discussion |
| alert_firing | A monitoring alert threshold was breached |
| approval_requested | A change requires the user's approval |
| approval_resolved | An approval the user requested has been resolved |
| pipeline_failure | A data pipeline or batch export failed |
| issue_assigned | An error tracking issue was assigned to the user |
Be very careful with critical. It triggers a persistent toast popup that overlays the user's screen and must be manually dismissed. This is intentionally intrusive — reserve it for genuine emergencies like outages, security alerts, or SLA breaches. Overusing critical will train users to ignore notifications entirely. When in doubt, use normal.
| Target | target_id value | Recipients |
| -------------- | ----------------- | -------------------------------------- |
| user | User ID | Just that user |
| team | Team ID | All members of the team's organization |
| organization | Organization ID | All organization members |
| role | Role ID | All users with that RBAC role |
When resource_type matches an access-controlled resource (dashboard, feature_flag, experiment, etc.), recipients without viewer access are automatically excluded. For notification-only types (pipeline, approval, comment), no AC filtering is applied.
Django (create_notification)
→ Postgres (NotificationEvent row)
→ Kafka (notification_events topic, on transaction commit)
→ Go livestream service (Kafka consumer)
→ Redis SPUBLISH (sharded pub/sub, keyed by org ID)
→ SSE (/notifications endpoint)
→ Browser (popover + optional toast)
Kafka publish happens on transaction.on_commit — won't fire if the transaction rolls back.
products/notifications/backend/facade/enums.pyfrontend/src/lib/components/NotificationsMenu/notificationToasts.tsx (NOTIFICATION_TYPE_ICONS) — the single icon source, read by getNotificationIcon, which only NotificationRow calls; the side panel gets the icon by rendering that rowfrontend/src/lib/components/NotificationsMenu/NotificationRow.tsx (REALTIME_NOTIFICATION_TYPE_META) — drives the per-type notification preferences UIpython manage.py makemigrations notificationsMock the feature flag in tests:
from unittest.mock import patch
with patch("posthoganalytics.feature_enabled", side_effect=lambda flag, *a, **kw: flag == "real-time-notifications"):
event = create_notification(data)
Take posthog/sending-notifications from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.