Custom channels
defineChannel is the escape hatch for any transport the package doesn’t ship. A fetch-based channel needs no SDK:
import { defineChannel } from "@warlock.js/notifications";
type DiscordPayload = { content: string };
export const discordChannel = () => defineChannel<DiscordPayload>({ name: "discord", route: (notifiable) => notifiable.get("discord_webhook") as string, async send({ payload, route }) { const res = await fetch(route as string, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`Discord send failed: ${res.status}`); }, });
// Teach the registry — now notify.discord(...) and `defineNotification`'s// `discord:` renderer are typed.declare module "@warlock.js/notifications" { interface NotificationChannels { discord: DiscordPayload; }}Register it:
channels: { // …built-ins… discord: discordChannel(),}The route contract
Section titled “The route contract”route() returns | Meaning |
|---|---|
string | The address (URL, email, chat id) |
{ id } | A storage id (database-style channels) |
undefined | Unroutable for this recipient → UnresolvableRouteError |
(no route) | Falls back to { id: notifiable.id } |
Throw inside send to fail; the dispatcher isolates it as a failed event and continues with sibling channels.
SDK-backed channels — lazy-load the SDK
Section titled “SDK-backed channels — lazy-load the SDK”A channel that wraps a heavy SDK (Slack, Twilio, FCM) should lazily import() it so apps that don’t use it never pay the cost, and a missing package surfaces as a clear install message. Follow the lazy-import pattern used by cascade/cache drivers.
import type { WebClient } from "@slack/web-api";let Slack: typeof import("@slack/web-api");let loaded: boolean | null = null;async function loadSlack() { try { Slack = await import("@slack/web-api"); loaded = true; } catch { loaded = false; } }loadSlack();
export const slackChannel = (config: { token: string }) => defineChannel<{ text: string }>({ name: "slack", route: (n) => n.get("slack_channel"), async send({ payload, route }) { await loadSlack(); if (!loaded) throw new Error("Install @slack/web-api to use the slack channel"); await new Slack.WebClient(config.token).chat.postMessage({ channel: route as string, text: payload.text }); }, });WhatsApp, Telegram, Slack, and push will ship as first-class channels via
@warlock.js/bridges— the same provider, reused by AI agents and notifications alike.
- Channels & the registry — how the typing works.
- Ad-hoc sends —
notify.<yourChannel>after registration.