channel docs

channel / reference

createChannels

Boots the multichannel runtime: every configured platform feeds the same inbound pipeline (canonical channel_messages on InstantDB → agent reaction → reply through the platform), and exposes one webhook handler per platform. Delivery internals are an implementation detail of the package.

Signature

@ekairos/channel
export async function createChannels(
  options: CreateChannelsOptions,
): Promise<ChannelsRuntime>

Call it once at boot (a module-level await in a server file is fine). It lazily loads the platform runtime, connects every configured platform to the same store and reaction pipeline, and returns a ChannelsRuntime with one webhook handler per platform.

CreateChannelsOptions

proptypedescription
db *anyInstantDB admin client with the channel domain schema pushed. The runtime persists canonical messages and its internal delivery state (channel_state, channel_locks, ...) through this client.
userName *stringBot identity across platforms — the name the agent posts under.
platforms *ChannelPlatformsConfigOne entry per channel to enable. The shape of each entry is that platform's credentials/config (bot tokens, signing secrets); it is passed through to the underlying delivery runtime.
resolveContextId *(params: { channel: ChannelKind; threadKey: string }) => Promise<string>Maps a platform conversation to an agent context. Called with the channel kind and the stable per-platform threadKey; return the contextId the conversation belongs to (typically by ensuring an agent thread keyed on `${channel}:${threadKey}`).
react *(inbound: ChannelInbound) => Promise<string | null | undefined | void>Reacts to an inbound message (typically: thread.react on the agent domain). Return text to auto-reply, or use inbound.reply for streaming / multi-part replies and return null.

ChannelPlatformsConfig

type
export type ChannelPlatformsConfig = Partial<
  Record<"slack" | "teams" | "gchat" | "discord" | "telegram", Record<string, unknown>>
> &
  Record<string, Record<string, unknown> | undefined>;

The five known platform keys get autocompletion; the index signature keeps the map open for additional platforms. Each value is an opaque Record<string, unknown> of platform credentials, forwarded verbatim to the delivery runtime.

ChannelInbound

The object handed to your react callback for every inbound message.

proptypedescription
channel *ChannelKindWhich channel the message arrived on ("slack", "telegram", ...). Open union — custom kinds are allowed.
threadKey *stringStable per-platform conversation key. Use it to key agent threads so the same platform conversation always maps to the same thread.
contextId *stringThe agent context this conversation maps to — the value your resolveContextId returned.
message *ChannelMessageThe persisted canonical inbound message. Already written to channel_messages and linked to the context before react runs.
reply *(text: string) => Promise<void>Posts a reply on the same platform thread and persists it as an outbound channel_messages record. Call it any number of times for multi-part replies.

ChannelsRuntime

proptypedescription
platforms *string[]Platforms that were enabled, in the order they were configured.
webhooks *Record<string, (request: Request, options?: Record<string, unknown>) => Promise<Response>>Webhook handlers to mount per platform — one fetch-style handler keyed by platform name (e.g. mount under app/api/channels/[platform]).
store *ChannelMessageStoreCanonical message store, for app-side writes (broadcasts, outbound messages you trigger yourself, etc).
dispose *() => Promise<void>Tears the runtime down — disconnects platforms and releases internal resources.

Complete example

lib/channels.ts
import { createChannels } from "@ekairos/channel/platforms";
import { db } from "@/lib/db"; // InstantDB admin client
import { ensureThread, reactOnThread } from "@/lib/agent";

export const channels = await createChannels({
  db,
  userName: "ekairos",
  platforms: {
    slack: {
      botToken: process.env.SLACK_BOT_TOKEN!,
      signingSecret: process.env.SLACK_SIGNING_SECRET!,
    },
    telegram: { botToken: process.env.TELEGRAM_BOT_TOKEN! },
  },
  resolveContextId: async ({ channel, threadKey }) => {
    const thread = await ensureThread({ key: `${channel}:${threadKey}` });
    return thread.contextId;
  },
  react: async (inbound) => {
    const reaction = await reactOnThread(inbound.contextId, inbound.message);
    return reaction.text;
  },
});
app/api/channels/[platform]/route.ts
import { channels } from "@/lib/channels";

export async function POST(
  request: Request,
  { params }: { params: Promise<{ platform: string }> },
) {
  const { platform } = await params;
  const handler = channels.webhooks[platform];
  if (!handler) {
    return new Response("unknown platform", { status: 404 });
  }
  return handler(request);
}

Reply semantics

The return value of react drives the auto-reply:

proptypedescription
stringreturn valueAuto-reply: the runtime posts the text back on the same platform thread and persists it as an outbound message. One return, one reply.
null | undefined | voidreturn valueNo auto-reply. Use this together with inbound.reply when you want streaming or multi-part replies — call reply(text) once per part as your reaction progresses, then return null.
multi-part reply
react: async (inbound) => {
  await inbound.reply("On it — checking the order...");
  const result = await lookupOrder(inbound.message.text ?? "");
  await inbound.reply(`Order ${result.id} ships ${result.eta}.`);
  return null; // replies already posted; no auto-reply
},
Every reply call persists its own outbound channel_messages record, so multi-part replies show up as individual messages in the timeline.

Next

database…