Skip to main content

Overview

The Notifier interface is the primary interface between the orchestrator and the human. Humans walk away after spawning agents; notifications bring them back when their judgment is needed. Core principle: Push, not pull. The human never polls. Plugin Slot: notifier
Default Plugin: desktop

Interface Definition

export interface Notifier {
  readonly name: string;
  
  notify(event: OrchestratorEvent): Promise<void>;
  notifyWithActions?(event: OrchestratorEvent, actions: NotifyAction[]): Promise<void>;
  post?(message: string, context?: NotifyContext): Promise<string | null>;
}

Methods

name
string
required
Plugin name identifier (e.g. "desktop", "slack", "webhook").
notify
(event: OrchestratorEvent) => Promise<void>
required
Push a notification to the human.Parameters:
  • event - Orchestrator event with type, priority, session ID, message, data
notifyWithActions
(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>
Optional: Push a notification with actionable buttons/links.Parameters:
  • event - Orchestrator event
  • actions - Array of actions with labels and URLs/callbacks
post
(message: string, context?: NotifyContext) => Promise<string | null>
Optional: Post a message to a channel (for team-visible notifiers like Slack).Parameters:
  • message - Message text
  • context - Optional context (session ID, project ID, PR URL, channel)
Returns: Message ID/URL if posted, null otherwise

OrchestratorEvent

export interface OrchestratorEvent {
  id: string;
  type: EventType;
  priority: EventPriority;
  sessionId: SessionId;
  projectId: string;
  timestamp: Date;
  message: string;
  data: Record<string, unknown>;
}
id
string
required
Unique event identifier
type
EventType
required
Event type (e.g. "session.needs_input", "ci.failing", "merge.ready")
priority
EventPriority
required
Priority level: "urgent", "action", "warning", or "info"
sessionId
SessionId
required
Session that generated this event
projectId
string
required
Project identifier
timestamp
Date
required
When event occurred
message
string
required
Human-readable message
data
Record<string, unknown>
required
Event-specific data (PR URL, error details, etc.)

EventType

export type EventType =
  // Session lifecycle
  | "session.spawned" | "session.working" | "session.exited"
  | "session.killed" | "session.stuck" | "session.needs_input"
  | "session.errored"
  // PR lifecycle
  | "pr.created" | "pr.updated" | "pr.merged" | "pr.closed"
  // CI
  | "ci.passing" | "ci.failing" | "ci.fix_sent" | "ci.fix_failed"
  // Reviews
  | "review.pending" | "review.approved" | "review.changes_requested"
  | "review.comments_sent" | "review.comments_unresolved"
  // Automated reviews
  | "automated_review.found" | "automated_review.fix_sent"
  // Merge
  | "merge.ready" | "merge.conflicts" | "merge.completed"
  // Reactions
  | "reaction.triggered" | "reaction.escalated"
  // Summary
  | "summary.all_complete";

EventPriority

export type EventPriority = "urgent" | "action" | "warning" | "info";
  • urgent - Immediate attention required (session stuck, CI failing repeatedly)
  • action - Human decision needed (merge ready, changes requested)
  • warning - Something to be aware of (session exited, CI fix sent)
  • info - FYI only (session spawned, PR created)

NotifyAction

export interface NotifyAction {
  label: string;
  url?: string;
  callbackEndpoint?: string;
}
label
string
required
Button/link label (e.g. "View PR", "Approve", "Kill Session")
url
string
URL to open (for simple links)
callbackEndpoint
string
API endpoint to call when action is clicked (for interactive buttons)

NotifyContext

export interface NotifyContext {
  sessionId?: SessionId;
  projectId?: string;
  prUrl?: string;
  channel?: string;
}
sessionId
SessionId
Session ID for context
projectId
string
Project ID for context
prUrl
string
PR URL to include in message
channel
string
Channel/thread to post to (Slack, Discord)

Usage Examples

Implementing a Notifier Plugin

import type { Notifier, OrchestratorEvent, NotifyAction } from "@composio/ao-core";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export function create(): Notifier {
  return {
    name: "desktop",
    
    async notify(event: OrchestratorEvent): Promise<void> {
      const title = `[${event.priority.toUpperCase()}] ${event.type}`;
      const body = `${event.message}\n\nSession: ${event.sessionId}`;
      
      // macOS notification
      await execFileAsync("osascript", [
        "-e",
        `display notification "${body}" with title "${title}"`
      ], { timeout: 5_000 });
    },
    
    async notifyWithActions(event: OrchestratorEvent, actions: NotifyAction[]): Promise<void> {
      // For desktop, open URLs in browser
      await this.notify(event);
      
      if (actions.length > 0 && actions[0].url) {
        await execFileAsync("open", [actions[0].url], { timeout: 5_000 });
      }
    }
  };
}

Slack Notifier Example

import type { Notifier, OrchestratorEvent, NotifyAction, NotifyContext } from "@composio/ao-core";

export function create(config: { webhookUrl: string; channel?: string }): Notifier {
  return {
    name: "slack",
    
    async notify(event: OrchestratorEvent): Promise<void> {
      const color = {
        urgent: "danger",
        action: "warning",
        warning: "warning",
        info: "good"
      }[event.priority];
      
      await fetch(config.webhookUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          channel: config.channel,
          attachments: [{
            color,
            title: event.type,
            text: event.message,
            fields: [
              { title: "Session", value: event.sessionId, short: true },
              { title: "Project", value: event.projectId, short: true }
            ],
            footer: "Agent Orchestrator",
            ts: Math.floor(event.timestamp.getTime() / 1000)
          }]
        })
      });
    },
    
    async notifyWithActions(event: OrchestratorEvent, actions: NotifyAction[]): Promise<void> {
      await fetch(config.webhookUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          channel: config.channel,
          text: event.message,
          attachments: [{
            callback_id: event.id,
            actions: actions.map(a => ({
              type: "button",
              text: a.label,
              url: a.url,
              value: a.callbackEndpoint
            }))
          }]
        })
      });
    },
    
    async post(message: string, context?: NotifyContext): Promise<string | null> {
      const response = await fetch(config.webhookUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          channel: context?.channel || config.channel,
          text: message,
          context: context ? {
            session: context.sessionId,
            project: context.projectId,
            pr: context.prUrl
          } : undefined
        })
      });
      
      return response.ok ? "posted" : null;
    }
  };
}

Using Notifiers in Lifecycle Manager

import type { Notifier, OrchestratorEvent } from "@composio/ao-core";

// Get notifiers for this priority level
const priority = "urgent";
const notifierNames = config.notificationRouting[priority]; // ["desktop", "slack"]
const notifiers = notifierNames.map(name => registry.get("notifier", name));

// Create event
const event: OrchestratorEvent = {
  id: crypto.randomUUID(),
  type: "session.needs_input",
  priority: "urgent",
  sessionId: session.id,
  projectId: session.projectId,
  timestamp: new Date(),
  message: `Session ${session.id} is waiting for your input`,
  data: {
    workspacePath: session.workspacePath,
    branch: session.branch
  }
};

// Notify all configured notifiers
await Promise.all(notifiers.map(n => n.notify(event)));

// Or with actions
const actions = [
  { label: "Open Session", url: `http://localhost:3000/sessions/${session.id}` },
  { label: "Kill", callbackEndpoint: `/api/sessions/${session.id}/kill` }
];

for (const notifier of notifiers) {
  if (notifier.notifyWithActions) {
    await notifier.notifyWithActions(event, actions);
  } else {
    await notifier.notify(event);
  }
}

Implementation Notes

Notification Routing

Notifications are routed by priority in agent-orchestrator.yaml:
notificationRouting:
  urgent: [desktop, slack]      # Immediate attention
  action: [desktop]             # Human decision needed
  warning: [slack]              # FYI for team
  info: []                      # No notifications

Rate Limiting

Notifiers should implement rate limiting to avoid spam:
  • Group similar events (multiple CI failures)
  • Debounce rapid events (agent activity)
  • Respect platform limits (Slack API rate limits)

Error Handling

Notifiers should fail gracefully:
  • Log errors but don’t throw (notification failure shouldn’t crash orchestrator)
  • Retry with backoff for transient failures
  • Fall back to simpler notifiers (desktop if Slack fails)

Built-in Plugins

  • desktop - macOS/Linux desktop notifications (default)
  • slack - Slack webhook/API integration
  • composio - Composio notification API
  • webhook - Generic webhook POST
Future plugins could support Discord, Email, SMS, PagerDuty, etc.

See Also

Build docs developers (and LLMs) love