Notifiers
A notifier delivers messages from the engine to the owner: today approval requests, later "stuck" pings and results. It is a small interface you implement or install.
ts
/*
* Copyright (c) 2026.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/
export type Notification = {
kind: 'approval' | 'stuck' | 'result' | 'info';
agent: string;
runId: string;
title: string;
body: string;
/** Signed token resuming the run (kind 'approval'). */
resumeToken?: string;
expiresAt?: Date;
};
export interface INotifier {
readonly name: string;
/**
* Delivers a notification. `idempotencyKey` is stable per logical
* notification (`runId:approvalId` for approvals), so transports can dedupe.
*/
notify(n: Notification, opts: { idempotencyKey: string }): Promise<void>;
}Notification field | Description |
|---|---|
kind | approval, stuck, result or info. The core engine sends approval |
agent, runId | Who is asking |
title, body | Human-readable text. For approvals the body shows the tool name and its input as JSON |
resumeToken | The signed token for engine.resume() (approvals only) |
expiresAt | When the approval times out (approvals only) |
Registering notifiers
Register notifiers by name on the engine and reference one from an agent with notify:
ts
const triage = defineAgent({
name: 'triage',
model,
instructions: 'Triage the issue.',
});
const digest = defineAgent({
name: 'digest',
model,
instructions: 'Summarize the day.',
notify: 'console',
});
const engine = new RunEngine({
store: new MemoryStore(),
agents: [triage, digest],
notifiers: { console: new ConsoleNotifier() },
resumeSecret: 'change-me',
maxConcurrentRuns: 8,
claimTtlMs: 30_000,
});The engine rejects an agent whose notify names no registered notifier.
Writing a notifier
ts
export class ConsoleNotifier implements INotifier {
readonly name = 'console';
async notify(n: Notification, opts: { idempotencyKey: string }): Promise<void> {
console.log(`[${n.kind}] ${n.title} (${opts.idempotencyKey})`);
console.log(n.body);
if (n.resumeToken) console.log(`resume token: ${n.resumeToken}`);
}
}A notifier that talks to a remote service:
ts
export class WebhookNotifier implements INotifier {
readonly name = 'webhook';
constructor(protected readonly url: string) {}
async notify(n: Notification, opts: { idempotencyKey: string }): Promise<void> {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': opts.idempotencyKey,
},
body: JSON.stringify(n),
});
// throw on failure: the engine retries the approval notification on the next poll
if (!response.ok) throw new Error(`Notification failed with ${response.status}.`);
}
}Two rules:
- Throw on failure. Never swallow a failed send. The engine records
waiting.notifiedAtonly afternotify()resolves, and retries every poll until it does. - Respect
idempotencyKey. It is stable per logical notification (<runId>:<approvalId>for approvals). If the engine crashed after a successful send but before recording it, the retry carries the same key; pass it to the transport (anIdempotency-Keyheader, a hidden marker) so the owner gets one message.
A store lock per approval keeps replicas from sending the same notification at the same time.
Planned notifiers Planned
| Package | Delivers to |
|---|---|
@chamberu/notify-openclaw | OpenClaw chats via POST /hooks/agent with an Idempotency-Key. See OpenClaw |
@chamberu/notify-github | A comment on the run's issue or pull request, deduplicated by a hidden marker |