Proactive Notification and Alert Systems
Agents that surface important information without being asked.
Proactive Notification and Alert Systems is a free AI Agents lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Agents learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Proactive vs Reactive Agents
Most agents are reactive — they respond to requests. A proactive agent monitors conditions and reaches out to the user when something noteworthy happens, without being asked. This is more like having a personal assistant.
Background Polling Loop
The simplest proactive pattern: a background loop that runs a check every N minutes and sends an alert if a condition is met. Use a thread or an async loop to avoid blocking.
import asyncio
import httpx
from datetime import datetime
async def poll_price(ticker: str, alert_threshold: float) -> None:
async with httpx.AsyncClient() as client:
response = await client.get(
f'https://api.finance.example.com/quote/{ticker}',
headers={'Authorization': 'Bearer your-api-key'}
)
data = response.json()
price = float(data.get('price', 0))
if price < alert_threshold:
await send_push_notification(
title=f'{ticker} Price Alert',
message=f'{ticker} is now ${price:.2f}, below your threshold of ${alert_threshold:.2f}'
)
async def price_alert_loop(ticker: str, threshold: float, interval_seconds: int = 300):
print(f'Monitoring {ticker} every {interval_seconds}s, alert below ${threshold}')
while True:
try:
await poll_price(ticker, threshold)
except Exception as e:
print(f'Polling error: {e}')
await asyncio.sleep(interval_seconds)
print('Price alert loop defined')Threshold-Based Alerts
Threshold alerts fire when a metric crosses a defined boundary. Good thresholds are: absolute values (price < $100), percentage changes (dropped > 5%), or comparisons to historical averages.
from dataclasses import dataclass
from typing import Optional, Callable
@dataclass
class AlertRule:
name: str
check_fn: Callable # Returns a float value
condition: str # 'lt', 'gt', 'lte', 'gte'
threshold: float
cooldown_minutes: int = 60 # Don't re-alert for this long
last_alerted: Optional[float] = None
def evaluate_rule(rule: AlertRule) -> Optional[str]:
import time
# Respect cooldown
if rule.last_alerted:
elapsed = time.time() - rule.last_alerted
if elapsed < rule.cooldown_minutes * 60:
return None
current_value = rule.check_fn()
triggered = (
(rule.condition == 'lt' and current_value < rule.threshold) or
(rule.condition == 'gt' and current_value > rule.threshold) or
(rule.condition == 'lte' and current_value <= rule.threshold) or
(rule.condition == 'gte' and current_value >= rule.threshold)
)
if triggered:
rule.last_alerted = time.time()
return f'Alert: {rule.name} = {current_value:.2f} ({rule.condition} {rule.threshold})'
return None
# Example rule
rule = AlertRule(
name='CPU Usage',
check_fn=lambda: 85.0, # In reality: psutil.cpu_percent()
condition='gt',
threshold=80.0,
cooldown_minutes=30
)
print(evaluate_rule(rule))New Email Matching Criteria
A proactive email agent monitors the inbox and alerts when messages matching specific criteria arrive: from a VIP sender, containing keywords, or with high importance flags.
from dataclasses import dataclass
from typing import List
import re
@dataclass
class EmailAlertCriteria:
from_domains: List[str] = None
from_emails: List[str] = None
subject_keywords: List[str] = None
body_keywords: List[str] = None
min_importance: str = None # 'high', 'medium'
def matches_criteria(email: dict, criteria: EmailAlertCriteria) -> bool:
sender = email.get('from', '').lower()
subject = email.get('subject', '').lower()
body = email.get('body', '').lower()
if criteria.from_domains:
domain_match = any(domain.lower() in sender for domain in criteria.from_domains)
if not domain_match:
return False
if criteria.from_emails:
email_match = any(e.lower() in sender for e in criteria.from_emails)
if not email_match:
return False
if criteria.subject_keywords:
keyword_match = any(kw.lower() in subject for kw in criteria.subject_keywords)
if not keyword_match:
return False
return True
criteria = EmailAlertCriteria(
from_domains=['@important-client.com', '@boss.company.com'],
subject_keywords=['urgent', 'action required', 'ASAP']
)
email = {'from': 'john@important-client.com', 'subject': 'Urgent: Contract Issue'}
print('Matches:', matches_criteria(email, criteria))Pushover Notifications
Pushover delivers push notifications to iOS and Android. It has a simple REST API and free tier. Ideal for personal agents that need to alert a single user.
import httpx
import os
PUSHOVER_TOKEN = os.environ.get('PUSHOVER_APP_TOKEN', 'your-app-token')
PUSHOVER_USER = os.environ.get('PUSHOVER_USER_KEY', 'your-user-key')
async def send_pushover(title: str, message: str, priority: int = 0, url: str = None) -> bool:
'''
priority: -2 (lowest) to 2 (emergency with acknowledgment)
0 = normal, 1 = high priority (bypass quiet hours)
'''
async with httpx.AsyncClient() as client:
data = {
'token': PUSHOVER_TOKEN,
'user': PUSHOVER_USER,
'title': title,
'message': message,
'priority': priority
}
if url:
data['url'] = url
response = await client.post('https://api.pushover.net/1/messages.json', data=data)
result = response.json()
if result.get('status') == 1:
print(f'Push sent: {title}')
return True
else:
print(f'Push failed: {result.get("errors")}')
return False
async def send_push_notification(title: str, message: str):
await send_pushover(title, message)Slack DM Notifications
Send proactive alerts as Slack direct messages. Unlike channel messages, DMs ensure the user is notified even if they are not in a specific channel.
from slack_sdk import WebClient
import os
slack_client = WebClient(token=os.environ.get('SLACK_BOT_TOKEN', 'xoxb-...'))
def send_slack_dm(user_id: str, title: str, message: str, urgency: str = 'normal') -> bool:
# Build blocks for rich formatting
blocks = [
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': f'*{title}*\n{message}'
}
}
]
if urgency == 'high':
# Add urgent emoji prefix
blocks[0]['text']['text'] = '🚨 ' + blocks[0]['text']['text']
try:
response = slack_client.chat_postMessage(
channel=user_id, # DM: channel = user_id
text=f'{title}: {message}', # Fallback text
blocks=blocks
)
return True
except Exception as e:
print(f'Slack DM failed: {e}')
return False
print('Slack DM function defined')
print('Usage: send_slack_dm("U0123ABCD", "Price Alert", "AAPL dropped below $150")')Email Notification
Email is reliable for non-urgent alerts. Use Python's smtplib or SendGrid API for programmatic email sending from your agent.
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import os
def send_email_alert(to_email: str, subject: str, html_body: str) -> bool:
smtp_host = os.environ.get('SMTP_HOST', 'smtp.gmail.com')
smtp_port = int(os.environ.get('SMTP_PORT', '587'))
smtp_user = os.environ.get('SMTP_USER', '')
smtp_pass = os.environ.get('SMTP_PASS', '')
try:
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = smtp_user
msg['To'] = to_email
html_part = MIMEText(html_body, 'html')
msg.attach(html_part)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.sendmail(smtp_user, to_email, msg.as_string())
print(f'Email sent to {to_email}: {subject}')
return True
except Exception as e:
print(f'Email failed: {e}')
return False
html = '<p>Your agent detected a price drop: <strong>AAPL is now $147.50</strong></p>'
print('Email alert function defined')Multi-Channel Alert Router
Route alerts to the right channel based on urgency. Critical alerts go to push notifications; informational alerts go to email; daily summaries go to Slack DM.
import asyncio
from enum import Enum
class AlertLevel(Enum):
INFO = 'info' # Email/Daily summary
WARNING = 'warning' # Slack DM
CRITICAL = 'critical' # Push notification immediately
async def route_alert(title: str, message: str, level: AlertLevel, user_config: dict):
user_id = user_config.get('user_id')
email = user_config.get('email')
push_enabled = user_config.get('push_enabled', True)
if level == AlertLevel.CRITICAL and push_enabled:
success = await send_pushover(title, message, priority=1)
if not success:
# Fallback to Slack DM
slack_user = user_config.get('slack_user_id')
if slack_user:
send_slack_dm(slack_user, title, message, urgency='high')
elif level == AlertLevel.WARNING:
slack_user = user_config.get('slack_user_id')
if slack_user:
send_slack_dm(slack_user, title, message)
else: # INFO
if email:
send_email_alert(email, title, f'<p>{message}</p>')
user_config = {
'user_id': 'user-42',
'email': 'user@example.com',
'slack_user_id': 'U0123ABCD',
'push_enabled': True
}
asyncio.run(route_alert('Price Alert', 'AAPL dropped to $147', AlertLevel.WARNING, user_config))Alert Deduplication and Cooldowns
Without deduplication, a condition that persists for hours will send hundreds of alerts. Cooldowns prevent re-alerting for the same condition within a specified window.
import time
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def should_send_alert(alert_key: str, cooldown_seconds: int = 3600) -> bool:
redis_key = f'alert:cooldown:{alert_key}'
if r.exists(redis_key):
ttl = r.ttl(redis_key)
print(f'Alert {alert_key} on cooldown. {ttl}s remaining')
return False
return True
def record_alert_sent(alert_key: str, cooldown_seconds: int = 3600):
redis_key = f'alert:cooldown:{alert_key}'
r.setex(redis_key, cooldown_seconds, '1')
def maybe_send_alert(alert_type: str, metric_value: float, user_id: str, cooldown_hours: int = 1):
# Create unique key per alert type per user
alert_key = f'{user_id}:{alert_type}'
if not should_send_alert(alert_key, cooldown_seconds=cooldown_hours * 3600):
return False
# Send the alert
print(f'Sending alert: {alert_type} = {metric_value} for user {user_id}')
record_alert_sent(alert_key, cooldown_seconds=cooldown_hours * 3600)
return True
maybe_send_alert('price_drop_AAPL', 147.50, 'user-42')
maybe_send_alert('price_drop_AAPL', 146.00, 'user-42') # Blocked by cooldownUser Preference-Based Filtering
Not every user wants every alert. Store user preferences for which alert types they want, at what thresholds, and via which channels. The agent checks preferences before sending.
from dataclasses import dataclass, field
from typing import Dict, List
@dataclass
class UserAlertPreferences:
user_id: str
enabled_channels: List[str] = field(default_factory=lambda: ['push'])
alert_rules: Dict[str, dict] = field(default_factory=dict)
quiet_hours_start: int = 22 # 10pm
quiet_hours_end: int = 8 # 8am
def is_quiet_hours(prefs: UserAlertPreferences) -> bool:
from datetime import datetime
hour = datetime.now().hour
start = prefs.quiet_hours_start
end = prefs.quiet_hours_end
if start > end: # Spans midnight
return hour >= start or hour < end
return start <= hour < end
def should_alert_user(prefs: UserAlertPreferences, alert_type: str, value: float) -> bool:
rule = prefs.alert_rules.get(alert_type)
if not rule:
return False # User hasn't set up this alert type
threshold = rule.get('threshold')
condition = rule.get('condition', 'lt')
urgent = rule.get('urgent', False)
if is_quiet_hours(prefs) and not urgent:
print(f'Suppressing non-urgent alert during quiet hours')
return False
return (
(condition == 'lt' and value < threshold) or
(condition == 'gt' and value > threshold)
)
prefs = UserAlertPreferences(
user_id='user-42',
alert_rules={'price_drop': {'threshold': 150.0, 'condition': 'lt'}}
)
print('Should alert:', should_alert_user(prefs, 'price_drop', 147.50))Aggregating Alerts into Digests
Instead of sending individual alerts for each event, accumulate them and send a digest. This reduces notification fatigue for users who have many monitored conditions.
import redis
import json
from datetime import datetime
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def add_to_digest(user_id: str, alert: dict):
key = f'digest:{user_id}:{datetime.now().strftime("%Y%m%d")}'
r.rpush(key, json.dumps(alert))
r.expire(key, 86400 * 2) # Keep for 2 days
def send_and_clear_digest(user_id: str) -> int:
key = f'digest:{user_id}:{datetime.now().strftime("%Y%m%d")}'
raw_alerts = r.lrange(key, 0, -1)
if not raw_alerts:
print(f'No alerts for user {user_id} today')
return 0
alerts = [json.loads(a) for a in raw_alerts]
digest_text = f'Daily Digest ({len(alerts)} alerts):\n'
digest_text += '\n'.join([f'- {a["title"]}: {a["message"]}' for a in alerts])
# Send as single notification
print(f'Sending digest to {user_id}:\n{digest_text}')
# send_email_alert(user_email, 'Daily Alert Digest', digest_text)
r.delete(key)
return len(alerts)
add_to_digest('user-42', {'title': 'AAPL Alert', 'message': 'Price at $147.50'})
add_to_digest('user-42', {'title': 'GOOG Alert', 'message': 'Price at $175.00'})
send_and_clear_digest('user-42')Knowledge Check: Proactive Notifications
Test your understanding of proactive notification and alert systems.
Proactive Notifications Summary
Proactive agents use background polling loops to monitor conditions and send alerts via push (Pushover), Slack DM, or email. Key design elements: threshold-based rules with cooldowns to prevent fatigue, user preference filtering for personalization, multi-channel routing based on urgency, quiet hours support, and daily digests for low-priority accumulated alerts.
Frequently asked questions
Is the “Proactive Notification and Alert Systems” lesson free?
Yes — the full text of “Proactive Notification and Alert Systems” is free to read here on the web, and the AI Agents course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Agents course, upgrade to CoddyKit PRO.
What will I learn in “Proactive Notification and Alert Systems”?
Agents that surface important information without being asked. You practise AI Agents with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Agents?
No prior experience is required. AI Agents on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Proactive Notification and Alert Systems” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Agents lesson?
Yes. Every AI Agents lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Always-On Agent Design Patterns
- Proactive Notification and Alert Systems
- Context Persistence Across Sessions
- Building a Daily Briefing Agent