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.
69
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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)6fca5f8
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.