Channel

Reference for createChannel options, managed providers, handlers, tools, storage, and runtime lifecycle.


createChannel(options) returns the Channel you declare on CopilotRuntime({ channels }).

channel.ts
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";

const channel = createChannel({
  name: "support",
  identifyUser: "platform",
  agent: makeAgent,
});

Use the project-unique Intelligence Channel Code. The managed runtime declares both Slack and Teams for this Channel and keeps each delivery provider-scoped.

createChannel options

OptionTypeDescription
namestringProject-unique Intelligence Code. Required for managed Channels; lowercase kebab-case, 3–64 characters, and not channels.
identifyUser"platform" | ChannelIdentifyUserRequired identity policy. Returns one application user or null for each provider event.
showToolStatusbooleanControls managed Slack tool-call progress. It is hidden by default; set true to show it.
replyContinuationReplyContinuationOptionsManaged Slack long-reply limits: messageByteLimit, maxMessages, and truncationMarker.
agentAbstractAgent | (threadId: string) => AbstractAgentAgent instance or factory. Factory preferred; every turn clones the configured result.
sanitizeAgentEventsbooleanDefaults to true; repairs a known malformed AG-UI parent-message field in HTTP event streams.
toolsChannelTool[]Typed tools forwarded to the agent.
contextContextEntry[]Stable { description, value } context sent on each run.
componentsChannelComponent[]Named JSX components whose callbacks can be reconstructed from stored snapshots.
commandsChannelCommand[]Declared commands for providers that support them.
storeStoreConfigState schema, persistence, turn concurrency (parallel default), dedup, and optional transcript bridge.
adaptersPlatformAdapter[]Low-level direct transports. They may coexist with the managed Intelligence adapter on this Channel.

One Channel may receive managed Slack and Teams deliveries. Names must be unique inside one CopilotRuntime.

For long-reply defaults, event sanitization, and agent clone requirements, see the detailed createChannel options.

StoreConfig

OptionTypeDescription
stateStandardSchemaTypes thread.state() and validates every setState(value).
adapterStateStorePersistence implementation for SDK state. Without it, the current managed realtime path falls back to process memory.
transcriptsTranscriptsConfigSDK-owned cross-platform transcript configuration keyed by the application user from top-level identifyUser.
actionRetentionMsnumberDurable callback and one-use HITL continuation retention. Default: seven days.
concurrency"parallel" | "serial" | "drop"How overlapping turns on the same conversation are handled. Default: "parallel". See StoreConfig.
onLockConflict"drop" | "force" | callbackDeprecated. Prefer concurrency. Maps to drop/parallel when static.
lockTtlnumberConversation lock TTL in milliseconds (drop / legacy paths). Default: 60_000.
dedupTtlnumberInbound event deduplication window in milliseconds. Default: 300_000.

StateStore contains kv, list, lock, dedup, and queue facets. Remote implementations must preserve JSON-serializable values.

StateStore contract

A production adapter implements this asynchronous contract. Locks, deduplication, and queue operations must remain atomic when multiple runner instances share the same backend.

state-store.ts
interface StateStore {
  kv: {
    get<T>(key: string): Promise<T | undefined>;
    set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
    consume<T>(key: string): Promise<T | undefined>;
    delete(key: string): Promise<void>;
  };
  list: {
    append<T>(
      key: string,
      value: T,
      options?: { maxLen?: number; ttlMs?: number },
    ): Promise<number>;
    range<T>(key: string, start?: number, stop?: number): Promise<T[]>;
    trim(key: string, maxLen: number): Promise<void>;
    delete(key: string): Promise<void>;
  };
  lock: {
    acquire(
      key: string,
      options?: { ttlMs?: number },
    ): Promise<{ token: string } | null>;
    release(key: string, token: string): Promise<void>;
  };
  dedup: {
    // true means the key was already recorded inside the TTL.
    seen(key: string, ttlMs: number): Promise<boolean>;
  };
  queue: {
    enqueue<T>(
      key: string,
      value: T,
      options?: {
        maxSize?: number;
        onFull?: "drop-oldest" | "drop-newest";
      },
    ): Promise<number>;
    dequeue<T>(key: string): Promise<T | undefined>;
    depth(key: string): Promise<number>;
  };
}

All values must round-trip through JSON. A distributed implementation must release a lock only when the supplied token still owns it.

Properties

PropertyTypeDescription
namestring | undefinedDeclared Intelligence Code.
showToolStatusboolean | undefinedManaged Slack tool-call visibility preference from createChannel().
replyContinuationReplyContinuationOptions | undefinedManaged Slack continuation preference from createChannel().
adaptersreadonly PlatformAdapter[]Attached transport snapshot. Managed Channels start empty and receive the Intelligence adapter at activation.
commandNamesstring[]Normalized names declared through commands or onCommand.
transcriptsTranscriptsOptional SDK transcript API. It is not the managed provider-history store.

The transcripts getter is only available after Channel activation and throws if it is read before the runtime listener starts the Channel.

Message handlers

channel.ts
channel.onMessage(async ({ thread, message }) => {
  await thread.runAgent({
    prompt: message.contentParts?.length
      ? [
          ...(message.text
            ? [{ type: "text" as const, text: message.text }]
            : []),
          ...message.contentParts,
        ]
      : message.text,
  });
});
MethodHandler inputNotes
onMessage(handler){ thread, message }Use this for managed Slack and Teams turns.
onMention(handler){ thread, message }A bot mention selects this handler when registered; otherwise it falls back to onMessage. Other content selects onMessage.
onWelcome(handler){ thread, user, actor, platform }Runs once for a supported provider installation or conversation activation. No handler means no welcome output.
onThreadStarted(handler){ thread, user, actor }Fires for provider surfaces that report a conversation-open event.

message.platform is the native provider ("slack" or "teams"). thread.platform carries that same native provider on a managed Channel.

Interaction and interrupt handlers

MethodPurposeManaged Slack/Teams
onInteraction(id, handler)Handle a specific opaque action id.Supported for delivered actions. JSX callbacks are usually simpler.
onInterrupt(eventName, handler)Post UI for an agent interrupt; later call thread.resume(value).Use eventName: "on_interrupt" for the current managed renderer.
onReaction(emoji?, handler)Handle delivered reaction events.Inbound support is provider-dependent.
onCommand(command)Register a typed or free-text command.Provider-dependent.
onModalSubmit(callbackId, handler)Handle a modal form submission.Not delivered by the managed realtime path.
onModalClose(callbackId, handler)Handle a modal dismissal.Not delivered by the managed realtime path.

See the Slack interactive messages guide, the Teams interactive messages guide, and the JSX callback reference.

Register a tool

Pass tools in createChannel({ tools }), or add one before activation:

channel.ts
channel.tool(getIncident);

See Tools and context for Slack or Tools and context for Teams.

Runtime lifecycle

A Channel has no public start() method. Node and Express listeners own a long-running process lifetime, so creating either listener starts the managed Channel automatically. On those mounts, ready() is optional: await it when startup must observe the initial managed status before the rest of the process continues.

Hono and generic Fetch mounts are request-oriented and remain lazy. Call and await ready() during an explicit long-running startup path before expecting managed traffic; creating one of those handlers alone does not open the Realtime Gateway connection.

Runtime mountStart behaviorRole of ready()
Node listenerStarts when the listener is createdOptional readiness wait and initial status check
Express listenerStarts when the listener is createdOptional readiness wait and initial status check
Hono mountLazyExplicitly await before expecting managed traffic
Generic Fetch mountLazyExplicitly await before expecting managed traffic

Install shutdown handlers before waiting for readiness so a slow or incomplete provider setup cannot prevent graceful teardown:

channel.ts
const listener = createCopilotNodeListener({ runtime });

const channels = listener.channels;
if (!channels) throw new Error("Channels were not configured.");

const shutdown = async () => {
  await channels.stop();
  process.exit(0);
};

process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);

await channels.ready({ timeoutMs: 30_000 });
const status = channels.status();
if (status.overall !== "online") {
  throw new Error(`Channel not online: ${JSON.stringify(status)}`);
}

ready() observes the first online or setup_required outcome; it does not initiate Node or Express activation. Inspect status() before accepting managed traffic. A configured direct adapter still starts when managed setup is incomplete, while the Channel remains setup_required until its managed providers are configured. Observe later drops and reconnects through status().

StatusMeaning
connectingActivation is in progress, or a lazy mount has not started the Channel yet.
onlineThe managed session is healthy and can receive delivery invitations.
setup_requiredThe runtime declaration exists but provider setup is incomplete.
reconnectingThe gateway connection dropped and is retrying.
errorActivation or bounded reconnect failed.
stoppedThe listener was torn down.