Skip to main content
Polpo can notify you about anything that happens — task failures, SLA warnings, mission completions. It subscribes to internal events, matches them against configurable rules, and dispatches notifications to external channels.

Architecture

The notification system has three layers:
  1. Channels — adapters that deliver messages to external services (Slack, Telegram, Email, Webhook)
  2. Rules — event pattern + optional condition + target channels
  3. Router — glue that listens to orchestrator events, evaluates rules, and dispatches
Notification routing: orchestrator events flow through NotificationRouter to Slack, Email, Telegram channels based on rules

Configuration

Notifications are configured under settings.notifications in your Polpo config:
{
  "settings": {
    "notifications": {
      "channels": {
        "slack-alerts": {
          "type": "slack",
          "webhookUrl": "https://hooks.slack.com/services/T00/B00/xxx"
        },
        "ops-email": {
          "type": "email",
          "provider": "smtp",
          "host": "smtp.example.com",
          "port": 587,
          "from": "polpo@example.com",
          "to": ["ops@example.com"]
        },
        "telegram-oncall": {
          "type": "telegram",
          "botToken": "${TELEGRAM_BOT_TOKEN}",
          "chatId": "-1001234567890"
        },
        "webhook-pagerduty": {
          "type": "webhook",
          "url": "https://events.pagerduty.com/v2/enqueue",
          "headers": {
            "Authorization": "Token token=${PD_TOKEN}"
          }
        }
      },
      "rules": [
        {
          "id": "failed-tasks",
          "name": "Notify on task failure",
          "events": ["task:transition"],
          "condition": { "field": "to", "op": "==", "value": "failed" },
          "channels": ["slack-alerts"],
          "severity": "critical"
        },
        {
          "id": "sla-warnings",
          "name": "SLA approaching deadline",
          "events": ["sla:warning", "sla:violated"],
          "channels": ["ops-email", "slack-alerts"],
          "severity": "warning",
          "cooldownMs": 300000
        }
      ]
    }
  }
}

Channel Adapters

Polpo supports four notification channels. Each has its own configuration page with setup instructions, payload formats, and attachment behavior.
ChannelRequired FieldsDescription
slackwebhookUrlPosts to a Slack incoming webhook
telegrambotToken, chatIdSends via Telegram Bot API
whatsapp(none required)Personal WhatsApp via Baileys (unofficial)
emailprovider, to, host/port or apiKeyEmail delivery via Resend, SendGrid, or SMTP
webhookurlGeneric HTTP POST with JSON payload
API keys, tokens, and URLs support ${ENV_VAR} syntax for environment variable references.

Event Pattern Matching

Rules use glob-style patterns to match events:
PatternMatches
task:createdExactly task:created
task:*task:created, task:transition, task:timeout, etc.
sla:**All SLA events at any depth
*Every event
The router resolves glob patterns against all known event names at startup and subscribes to each matching concrete event.

Condition DSL

Rules can include an optional JSON condition that filters on the event payload. Conditions are pure data — no eval(), no string parsing.

Simple comparison

{ "field": "to", "op": "==", "value": "failed" }

Supported operators

OperatorDescription
==Loose equality
!=Loose inequality
>, >=, <, <=Numeric comparison
includesString contains or array includes
not_includesNegation of includes
existsField is not null/undefined
not_existsField is null/undefined

Logical combinators

{
  "and": [
    { "field": "to", "op": "==", "value": "failed" },
    { "field": "task.retries", "op": ">=", "value": 3 }
  ]
}
{
  "or": [
    { "field": "severity", "op": "==", "value": "critical" },
    { "field": "level", "op": ">=", "value": 3 }
  ]
}
{
  "not": { "field": "status", "op": "==", "value": "done" }
}
Fields use dot-path resolution on the event data (e.g. task.status, result.assessment.globalScore).

Cooldown / Throttling

Each rule can set cooldownMs to prevent notification storms. If a rule fires and its cooldown has not elapsed since the last dispatch, the notification is silently dropped.
{
  "id": "task-failure-alert",
  "name": "Task failures",
  "events": ["task:transition"],
  "condition": { "field": "to", "op": "==", "value": "failed" },
  "channels": ["slack-alerts"],
  "cooldownMs": 60000
}

Template System

The router generates default titles and bodies for each event type using built-in templates with severity emoji prefixes. You can override this per-rule with a template field using {{variable}} interpolation:
{
  "id": "custom-alert",
  "name": "Custom task alert",
  "events": ["task:transition"],
  "channels": ["slack-alerts"],
  "template": "Task {{taskId}} moved to {{to}} (agent: {{agentName}})"
}
Template variables are resolved from the event payload using dot-path access.

Dynamic Rule Registration

Rules can be added programmatically at runtime via addRule(). This is used internally by:
  • SLAMonitor — registers rules for warningChannels and violationChannels
  • QualityController — registers rules for per-gate notifyChannels
When a rule is added after the router has started, the router automatically subscribes to any new event patterns.

Events

The notification system emits its own events:
EventPayloadDescription
notification:sent{ ruleId, channel, event }Notification delivered successfully
notification:failed{ ruleId, channel, error }Channel delivery failed
Use testChannels() on the NotificationRouter to verify all configured channels are reachable before starting Polpo.

Scoped Notification Rules

Global rules (configured in settings.notifications.rules) apply to every event by default. You can override or extend these rules at the mission and task level using scoped notification rules.

Scope hierarchy

Notification rule scope hierarchy: global → mission-level → task-level
The most specific scope wins: if a task has scoped rules, those are used instead of global rules when processing events for that task. Similarly, mission-level rules override global rules for all events within that mission’s execution.

Replacement vs. inheritance

By default, scoped rules replace the global rules for matching events. If you set inherit: true, the scoped rules are added on top of the parent scope instead.
{
  "notifications": {
    "rules": [
      {
        "id": "task-fail-slack",
        "name": "Notify Slack on task failure",
        "events": ["task:transition"],
        "condition": { "field": "to", "op": "==", "value": "failed" },
        "channels": ["slack-alerts"],
        "severity": "critical"
      }
    ],
    "inherit": false
  }
}
With inherit: false (the default), only the rules in this scope are used — global rules are ignored for events matching this scope.
{
  "notifications": {
    "rules": [
      {
        "id": "extra-telegram",
        "name": "Also ping Telegram",
        "events": ["task:transition"],
        "channels": ["telegram-oncall"],
        "severity": "warning"
      }
    ],
    "inherit": true
  }
}
With inherit: true, the scoped rules are added on top of global rules. Both the global Slack notification and this Telegram notification would fire.

Setting scoped rules via API

When creating tasks or missions via the REST API, include a notifications field:
# Task with scoped notification rules
curl -X POST http://localhost:3000/api/v1/projects/my-project/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Critical migration",
    "description": "Run database migration",
    "assignTo": "db-agent",
    "notifications": {
      "rules": [{
        "id": "migration-alert",
        "name": "Migration status",
        "events": ["task:transition"],
        "channels": ["telegram-oncall", "slack-alerts"],
        "severity": "critical"
      }],
      "inherit": false
    }
  }'
# Mission with scoped notification rules
curl -X POST http://localhost:3000/api/v1/projects/my-project/missions \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Release v2.0",
    "data": "...",
    "notifications": {
      "rules": [{
        "id": "release-updates",
        "name": "Release progress",
        "events": ["task:*", "mission:*"],
        "channels": ["slack-releases"],
        "severity": "info"
      }],
      "inherit": true
    }
  }'

Scope resolution logic

When an event fires, the router resolves which rules to use:
  1. If the event is associated with a task that has notifications, use those rules (respecting inherit).
  2. Otherwise, if the event is associated with a mission that has notifications, use those rules (respecting inherit).
  3. Otherwise, use global rules from settings.notifications.rules.

Send Direct API

The POST /notifications/send endpoint lets you send a one-off notification to any configured channel, bypassing rules entirely. This is useful for manual alerts, testing, or custom integrations.

Request

curl -X POST http://localhost:3000/api/v1/projects/my-project/notifications/send \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "telegram-oncall",
    "title": "Deployment Complete",
    "body": "Version 2.0.1 deployed to production",
    "severity": "info",
    "delayMs": 5000
  }'
FieldTypeRequiredDescription
channelstringYesChannel ID from your configuration
titlestringYesNotification title
bodystringYesNotification body (HTML supported for Telegram)
severitystringNoinfo, warning, or critical (default: info)
delayMsnumberNoDelay in milliseconds before sending (in-memory timer)

Response

{
  "ok": true,
  "data": {
    "id": "notif_abc123",
    "scheduledAt": "2025-01-15T10:30:00.000Z",
    "firesAt": "2025-01-15T10:30:05.000Z"
  }
}
When delayMs is set, the notification is scheduled with an in-memory setTimeout. The firesAt field indicates when the notification will actually be dispatched. Note that delayed notifications are not persisted — if the server restarts, scheduled notifications are lost.
Delayed notifications use in-memory timers and are not persisted. They will be lost if the server restarts before they fire.

REST API Endpoints

MethodPathDescription
GET/notificationsList notification history (supports ?limit, ?status, ?channel, ?rule query params)
GET/notifications/statsGet aggregate stats (total, sent, failed)
POST/notifications/sendSend a direct notification to a channel

React SDK

The @polpo-ai/react provides the useNotifications hook for accessing notification data in the UI:
import { useNotifications } from "@polpo-ai/react";

function NotificationsPanel() {
  const { notifications, stats, sendNotification, refetch, loading } = useNotifications({
    limit: 100,
    status: "sent",
  });

  const handleSend = async () => {
    await sendNotification({
      channel: "telegram-oncall",
      title: "Manual Alert",
      body: "Something needs attention",
      severity: "warning",
    });
  };

  return (
    <div>
      <p>Total: {stats?.total} | Sent: {stats?.sent} | Failed: {stats?.failed}</p>
      {notifications.map(n => (
        <div key={n.id}>{n.title}{n.status}</div>
      ))}
      <button onClick={handleSend}>Send Alert</button>
    </div>
  );
}