channel docs

channel / reference

ChannelMessage

The canonical message crossing any channel. It mirrors the channel_messages entity; provider payloads live in raw and provider ids in externalId. Every channel — web, email, whatsapp, slack, custom — flows through this one shape.

Type

@ekairos/channel
export type ChannelMessage = {
  id: string;
  channel: ChannelKind;
  direction: ChannelDirection;
  role?: "user" | "assistant" | "system";
  text?: string;
  parts?: unknown[];
  status?: ChannelMessageStatus;
  externalId?: string;
  participant?: string;
  raw?: unknown;
  createdAt: string;
  updatedAt?: string;
  contextId?: string;
  itemId?: string;
};
proptypedescription
id *stringStable message id (UUID in InstantDB deployments).
channel *ChannelKindWhich channel the message belongs to: "web", "email", "whatsapp", or any custom kind.
direction *ChannelDirection"inbound" (from the counterpart to your system) or "outbound" (from your system out).
role"user" | "assistant" | "system"Conversational role, when the message participates in an agent thread.
textstringPlain-text body.
partsunknown[]Structured parts (rich content, attachments, tool results).
statusChannelMessageStatusDelivery lifecycle status; see the union below.
externalIdstringProvider-side id (Twilio SID, email message-id, platform timestamp).
participantstringResolved identity of the counterpart (phone, email address, user id).
rawunknownOriginal provider payload, kept verbatim.
createdAt *stringISO timestamp; the timeline sort key.
updatedAtstringISO timestamp of the last update (e.g. a status transition).
contextIdstringLinks into the agent context, when attached — mirrors the channel_messagesContext link.
itemIdstringAnchors the message to one context item, when attached — mirrors the channel_messagesItem link. Anchored messages render inside their event in the thread timeline.

ChannelKind

open union + constants
export const WEB_CHANNEL = "web";
export const EMAIL_CHANNEL = "email";
export const WHATSAPP_CHANNEL = "whatsapp";

/** Open union: known channels get literal types, custom channels are allowed. */
export type ChannelKind = "web" | "email" | "whatsapp" | (string & {});

The (string & {}) trick keeps the union open: the three known kinds autocomplete, and any other string (a custom "sms" or "push" channel) still typechecks. Prefer the exported constants over string literals when referring to the built-in kinds.

ChannelDirection

type
export type ChannelDirection = "inbound" | "outbound";
proptypedescription
"inbound"ChannelDirectionReceived from the counterpart (a webhook delivery, an incoming email).
"outbound"ChannelDirectionSent by your system (an agent reply, a broadcast).

ChannelMessageStatus

open union
export type ChannelMessageStatus =
  | "pending"
  | "sending"
  | "sent"
  | "delivered"
  | "read"
  | "failed"
  | (string & {});
proptypedescription
"pending"statusCreated, not yet handed to a provider.
"sending"statusIn flight to the provider.
"sent"statusAccepted by the provider.
"delivered"statusConfirmed delivered to the counterpart.
"read"statusRead receipt received.
"failed"statusDelivery failed; details usually in raw.
(string & {})statusOpen for provider-specific statuses that do not map to the six above.

createChannelMessage

signature
export function createChannelMessage(
  input: Omit<ChannelMessage, "id" | "createdAt"> & { id?: string; createdAt?: string },
): ChannelMessage

Builds a complete ChannelMessage from a partial input, filling in the two defaults:

proptypedescription
idstringDefaults to crypto.randomUUID() (with a timestamp-random fallback when the Web Crypto API is unavailable).
createdAtstringDefaults to new Date().toISOString().
usage
import { createChannelMessage, WHATSAPP_CHANNEL } from "@ekairos/channel";

const message = createChannelMessage({
  channel: WHATSAPP_CHANNEL,
  direction: "inbound",
  role: "user",
  text: "Hola, ¿tienen stock del modelo XR-200?",
  participant: "+5491155550123",
  externalId: "SMa4c9f2e8d7b64f1aa0c3",
  contextId: "ctx_01HZX4Q8",
});
// → { id: "9f4b...", createdAt: "2026-06-10T14:03:21.000Z", ...input }

ChannelMessageStore

interface
export interface ChannelMessageStore {
  saveChannelMessage(message: ChannelMessage): Promise<ChannelMessage>;
  getChannelMessages(params: { contextId: string }): Promise<ChannelMessage[]>;
}
proptypedescription
saveChannelMessage *(message: ChannelMessage) => Promise<ChannelMessage>Persists one canonical message (and its context/item links, when contextId/itemId are set). Returns the persisted record.
getChannelMessages *(params: { contextId: string }) => Promise<ChannelMessage[]>Reads every message attached to a context.

Who implements it:

proptypedescription
Instant-backed storeinternal to the runtimeInstantDB deployments back the interface with the channel_messages entity. You get it as channels.store from createChannels — you never construct it yourself.
MemoryAgentStore@ekairos/agentIn-memory implementation for local/embedded runtimes (Electron, CLIs, tests). It implements ChannelMessageStore alongside the context and thread stores, so the same agent code runs without InstantDB.

Real-world records

inbound whatsapp message
{
  id: "6c1f6a3e-2d44-4d8a-9f0b-7a3c5e91d2b8",
  channel: "whatsapp",
  direction: "inbound",
  role: "user",
  text: "Hola, ¿tienen stock del modelo XR-200?",
  status: "delivered",
  externalId: "SMa4c9f2e8d7b64f1aa0c3",
  participant: "+5491155550123",
  raw: { MessageSid: "SMa4c9f2e8d7b64f1aa0c3", From: "whatsapp:+5491155550123", Body: "..." },
  createdAt: "2026-06-10T14:03:21.000Z",
  contextId: "ctx_01HZX4Q8"
}
outbound email message
{
  id: "b8e2c7d1-90af-4a36-8c52-f41d0e6a9b77",
  channel: "email",
  direction: "outbound",
  role: "assistant",
  text: "Yes — the XR-200 is in stock. I attached the quote you asked for.",
  status: "sent",
  externalId: "re_8GqkPzVx3JmN2cTd",
  participant: "purchasing@acme-industries.com",
  raw: { id: "re_8GqkPzVx3JmN2cTd", to: ["purchasing@acme-industries.com"] },
  createdAt: "2026-06-10T14:05:02.000Z",
  updatedAt: "2026-06-10T14:05:04.000Z",
  contextId: "ctx_01HZX4Q8",
  itemId: "itm_01HZX4RM"
}
Note itemId on the outbound record: the reply is anchored to the agent item that produced it, so the timeline renders it inside that event instead of as a standalone entry.

Next

database…