Skip to content
Learn Agentic AI9 min read0 views

AI-Powered Notifications: Intelligent Alert Prioritization and Delivery

Build an AI notification system that scores alerts by importance, selects the right delivery channel, bundles related notifications, and learns from user engagement patterns.

The Notification Overload Problem

SaaS products generate an enormous volume of notifications: task assignments, status changes, comments, system alerts, billing reminders, and feature announcements. When everything is treated as equally important, users either enable all notifications and get overwhelmed, or disable them and miss critical alerts.

AI-powered notifications solve this by scoring each notification for importance, choosing the right delivery channel, and bundling related alerts into digestible summaries.

Notification Scoring Engine

The scoring engine assigns an importance score to each notification based on the event type, the user's relationship to the event, and historical engagement patterns.

from dataclasses import dataclass
from enum import Enum
from datetime import datetime

class NotificationChannel(str, Enum):
    IN_APP = "in_app"
    EMAIL = "email"
    PUSH = "push"
    SMS = "sms"
    SLACK = "slack"

@dataclass
class Notification:
    id: str
    user_id: str
    tenant_id: str
    event_type: str       # e.g., "task_assigned", "comment_mention", "deal_closed"
    title: str
    body: str
    entity_type: str
    entity_id: str
    actor_id: str | None  # Who triggered the event
    created_at: datetime
    metadata: dict

class NotificationScorer:
    # Base importance scores by event type
    BASE_SCORES = {
        "task_assigned": 0.8,
        "task_due_soon": 0.9,
        "task_overdue": 1.0,
        "comment_mention": 0.85,
        "comment_reply": 0.6,
        "deal_closed": 0.7,
        "deal_stage_changed": 0.5,
        "system_maintenance": 0.4,
        "feature_announcement": 0.2,
        "weekly_digest": 0.3,
    }

    def __init__(self, db):
        self.db = db

    async def score(self, notification: Notification) -> float:
        base = self.BASE_SCORES.get(notification.event_type, 0.5)

        # Boost if the actor is someone the user frequently interacts with
        relationship_boost = await self.get_relationship_boost(
            notification.user_id, notification.actor_id
        )

        # Boost if the entity is something the user recently worked on
        recency_boost = await self.get_recency_boost(
            notification.user_id, notification.entity_type,
            notification.entity_id
        )

        # Penalize if the user typically ignores this event type
        engagement_factor = await self.get_engagement_factor(
            notification.user_id, notification.event_type
        )

        score = (base + relationship_boost + recency_boost) * engagement_factor
        return min(max(score, 0.0), 1.0)  # Clamp to [0, 1]

    async def get_relationship_boost(self, user_id: str,
                                      actor_id: str | None) -> float:
        if not actor_id:
            return 0.0
        interaction_count = await self.db.fetchval("""
            SELECT COUNT(*) FROM user_interactions
            WHERE user_id = $1 AND other_user_id = $2
              AND created_at > NOW() - INTERVAL '30 days';
        """, user_id, actor_id)
        if interaction_count > 20:
            return 0.15
        if interaction_count > 5:
            return 0.08
        return 0.0

    async def get_recency_boost(self, user_id: str, entity_type: str,
                                 entity_id: str) -> float:
        last_access = await self.db.fetchval("""
            SELECT MAX(accessed_at) FROM user_activity
            WHERE user_id = $1 AND entity_type = $2 AND entity_id = $3;
        """, user_id, entity_type, entity_id)
        if not last_access:
            return 0.0
        hours_since = (datetime.utcnow() - last_access).total_seconds() / 3600
        if hours_since < 1:
            return 0.15
        if hours_since < 24:
            return 0.08
        return 0.0

    async def get_engagement_factor(self, user_id: str,
                                     event_type: str) -> float:
        stats = await self.db.fetchrow("""
            SELECT COUNT(*) as total,
                   COUNT(*) FILTER (WHERE read_at IS NOT NULL) as read_count
            FROM notifications
            WHERE user_id = $1 AND event_type = $2
              AND created_at > NOW() - INTERVAL '90 days';
        """, user_id, event_type)
        if not stats or stats["total"] == 0:
            return 1.0  # No history, use default
        read_rate = stats["read_count"] / stats["total"]
        return 0.3 + (0.7 * read_rate)  # Floor at 0.3 to never fully suppress

Channel Selection

The delivery channel depends on the notification score and the user's current availability.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

class ChannelSelector:
    def __init__(self, db):
        self.db = db

    async def select_channels(self, notification: Notification,
                               score: float) -> list[NotificationChannel]:
        prefs = await self.get_user_preferences(notification.user_id)
        channels = []

        # Always deliver in-app
        channels.append(NotificationChannel.IN_APP)

        # Critical notifications: push + email
        if score >= 0.9:
            if prefs.get("push_enabled", True):
                channels.append(NotificationChannel.PUSH)
            if prefs.get("email_enabled", True):
                channels.append(NotificationChannel.EMAIL)

        # Important notifications: push or email based on preference
        elif score >= 0.7:
            preferred = prefs.get("preferred_channel", "push")
            if preferred == "push" and prefs.get("push_enabled", True):
                channels.append(NotificationChannel.PUSH)
            elif prefs.get("email_enabled", True):
                channels.append(NotificationChannel.EMAIL)

        # Medium notifications: check if user is active in-app
        elif score >= 0.4:
            is_online = await self.is_user_online(notification.user_id)
            if not is_online and prefs.get("email_enabled", True):
                channels.append(NotificationChannel.EMAIL)

        # Low-importance: in-app only (already added)
        return channels

    async def get_user_preferences(self, user_id: str) -> dict:
        row = await self.db.fetchrow(
            "SELECT preferences FROM notification_settings WHERE user_id = $1",
            user_id
        )
        return row["preferences"] if row else {}

    async def is_user_online(self, user_id: str) -> bool:
        last_seen = await self.db.fetchval(
            "SELECT last_seen_at FROM user_presence WHERE user_id = $1",
            user_id
        )
        if not last_seen:
            return False
        return (datetime.utcnow() - last_seen).total_seconds() < 300

Notification Bundling

Group related notifications into a single digest to reduce volume.

from collections import defaultdict

class NotificationBundler:
    def __init__(self, bundle_window_seconds: int = 300):
        self.window = bundle_window_seconds
        self.pending: dict[str, list[Notification]] = defaultdict(list)

    def add(self, notification: Notification):
        key = f"{notification.user_id}:{notification.entity_type}"
        self.pending[key].append(notification)

    async def flush(self) -> list[dict]:
        bundles = []
        for key, notifications in self.pending.items():
            if len(notifications) == 1:
                bundles.append({
                    "type": "single",
                    "notification": notifications[0],
                })
            else:
                bundles.append({
                    "type": "bundle",
                    "summary": self.create_summary(notifications),
                    "count": len(notifications),
                    "notifications": notifications,
                })
        self.pending.clear()
        return bundles

    def create_summary(self, notifications: list[Notification]) -> str:
        event_types = set(n.event_type for n in notifications)
        entity_type = notifications[0].entity_type

        if len(event_types) == 1:
            return (f"{len(notifications)} new {notifications[0].event_type} "
                    f"events on {entity_type} records")
        return (f"{len(notifications)} updates on {entity_type} records "
                f"({', '.join(event_types)})")

The Complete Notification Pipeline

from fastapi import FastAPI

app = FastAPI()

async def process_notification(notification: Notification,
                                scorer: NotificationScorer,
                                channel_selector: ChannelSelector,
                                bundler: NotificationBundler):
    score = await scorer.score(notification)
    channels = await channel_selector.select_channels(notification, score)

    notification.metadata["score"] = score
    notification.metadata["channels"] = [c.value for c in channels]

    # High-priority: deliver immediately
    if score >= 0.8:
        for channel in channels:
            await deliver(notification, channel)
    else:
        # Lower priority: add to bundler for digest delivery
        bundler.add(notification)

FAQ

How do I let users override the AI prioritization?

Provide a notification settings page where users can pin specific event types as "always high priority" or "always mute." These overrides take precedence over AI scoring. Store overrides as explicit rules that the scorer checks before running its scoring logic.

What if a critical notification gets scored too low?

Define a set of event types that bypass scoring entirely — system outages, security alerts, billing failures, and account lockouts should always be treated as maximum priority. Maintain this list in configuration, not in AI logic, so it cannot be affected by model behavior.

How do I measure whether the AI notification system is working?

Track three key metrics: notification read rate (should increase after implementing AI scoring), time-to-action (how quickly users respond to actionable notifications), and unsubscribe rate (should decrease). Compare these metrics to the pre-AI baseline over a 30-day window.


#AINotifications #AlertPrioritization #SaaS #IntelligentDelivery #Python #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.