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
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>;
}
Event type (e.g. "session.needs_input", "ci.failing", "merge.ready")
Priority level: "urgent", "action", "warning", or "info"
Session that generated this event
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;
}
Button/link label (e.g. "View PR", "Approve", "Kill Session")
URL to open (for simple links)
API endpoint to call when action is clicked (for interactive buttons)
NotifyContext
export interface NotifyContext {
sessionId?: SessionId;
projectId?: string;
prUrl?: string;
channel?: string;
}
PR URL to include in message
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