Back to Copilotkit

createBot

showcase/shell-docs/src/content/reference/bot/functions/createBot.mdx

1.61.111.8 KB
Original Source

Overview

createBot is the entry point of @copilotkit/bot. It wires one or more platform adapters to an AG-UI agent and returns a Bot — the surface for registering turn handlers, interaction handlers, interrupt handlers, slash commands, and tools, plus start() / stop() lifecycle control.

For a complete walkthrough, see the Slack quickstart.

Signature

ts
import { createBot } from "@copilotkit/bot";

function createBot<TStateSchema extends StandardSchemaV1 | undefined = undefined>(
  opts: CreateBotOptions<TStateSchema>
): Bot<ThreadStateOf<TStateSchema>>;

createBot is generic over the per-thread state schema. Pass a Standard Schema as store.state and the returned Bot's handler callbacks receive a thread narrowed to StatefulThread<YourStateType>thread.state() and thread.setState() are fully typed at the call site.

Parameters

<PropertyReference name="opts" type="CreateBotOptions" required> Bot configuration. <PropertyReference name="adapters" type="PlatformAdapter[]" required> The platform adapters to run, e.g. [`slack(...)`](/reference/bot/slack). `start()` brings every adapter up; `stop()` brings them down. </PropertyReference> <PropertyReference name="agent" type="AbstractAgent | ((threadId: string) => AbstractAgent)"> The AG-UI agent behind [`thread.runAgent()`](/reference/bot/classes/Thread) — a single instance or a per-conversation factory receiving the conversation's thread id. Optional: when omitted, calling `thread.runAgent()` throws. </PropertyReference> <PropertyReference name="actionStore" type="ActionStore" default="new InMemoryActionStore()"> **Deprecated** — pass `store.adapter` instead. Where snapshots of inline JSX handlers are persisted. When both `actionStore` and `store.adapter` are supplied, `store.adapter` wins for action storage. See [ActionStore](/reference/bot/types/ActionStore) for background. </PropertyReference> <PropertyReference name="store" type="StoreConfig"> Persistence, per-thread state schema, transcript storage, and lock/dedup tuning grouped under one option. All fields are optional.
<PropertyReference name="adapter" type="StateStore" default="new MemoryStore()">
  The pluggable persistence backend — the [StateStore](/reference/bot/types/StateStore) that backs action snapshots, per-thread state, transcripts, turn locks, and dedup. Defaults to an in-memory `MemoryStore` that is lost on restart. Pass a `createRedisStore(…)` or `createPostgresStore(…)` for durability across restarts and processes.
</PropertyReference>

<PropertyReference name="state" type="StandardSchemaV1">
  A [Standard Schema](https://standardschema.dev/) (Zod, Valibot, ArkType, …) that describes the per-thread state shape. When set, `thread.setState()` validates at runtime and throws on a mismatch, and `thread.state()` is typed to the schema's output. The inferred type flows up to the `Bot`'s handler callbacks — you do not need explicit type annotations.
</PropertyReference>

<PropertyReference name="identity" type="(ctx: { adapter: string; author: PlatformUser; message: IncomingMessage }) => string | null | Promise<string | null>">
  Resolve a stable, cross-platform identity key for the current user — typically an email address returned from a directory lookup. Return `null` to opt out for a given turn (transcripts are skipped). Must be configured together with `transcripts`; configuring one without the other throws at startup.
</PropertyReference>

<PropertyReference name="transcripts" type="TranscriptsConfig">
  Cross-platform transcript storage. Must be configured together with `identity`.

  <PropertyReference name="retention" type="string | number">
    How long to keep transcript entries. Accepts a human-readable duration string (`"7d"`, `"2h30m"`) or milliseconds. Omit to keep entries indefinitely.
  </PropertyReference>

  <PropertyReference name="maxPerUser" type="number">
    Maximum transcript entries to retain per user. When the list exceeds this length, the oldest entries are dropped. Omit for no cap.
  </PropertyReference>
</PropertyReference>

<PropertyReference name="onLockConflict" type='"drop" | "force" | ((conversationKey: string, message: IncomingMessage) => "drop" | "force" | Promise<"drop" | "force">)' default='"drop"'>
  What to do when a new turn arrives while a prior turn in the same conversation is still processing. `"drop"` silently discards the overlapping turn (default). `"force"` lets it proceed without waiting for the lock — both turns run concurrently. A function receives the conversation key and the incoming message and returns either decision.
</PropertyReference>

<PropertyReference name="lockTtl" type="number" default="60000">
  TTL in milliseconds for the per-conversation turn lock. If a handler crashes without releasing the lock, it auto-expires after this window so subsequent turns are not permanently blocked.
</PropertyReference>

<PropertyReference name="dedupTtl" type="number" default="300000">
  TTL in milliseconds for the inbound-event dedup window. Events with the same `eventId` delivered within this window are deduplicated and discarded; events outside the window are treated as new.
</PropertyReference>
</PropertyReference> <PropertyReference name="tools" type="BotTool[]"> Tools forwarded to the agent as frontend tools on every run. See [defineBotTool](/reference/bot/functions/defineBotTool). </PropertyReference> <PropertyReference name="context" type="ContextEntry[]"> Knowledge folded into the agent's context on each run. A `ContextEntry` is `{ description: string; value: string }`. </PropertyReference> <PropertyReference name="commands" type="BotCommand[]"> Slash commands to register up front — equivalent to calling `bot.onCommand` per command. See [defineBotCommand](/reference/bot/functions/defineBotCommand). </PropertyReference> </PropertyReference>

Return Value

<PropertyReference name="Bot" type="Bot"> The bot's registration and lifecycle surface. <PropertyReference name="onMention" type="(h: BotHandler) => void"> Register a turn handler. `BotHandler` receives `{ thread, message }` — a [`Thread`](/reference/bot/classes/Thread) and the incoming message (`{ text, user, ref, platform }`). On Slack one mention handler covers @-mentions, replies in threads the bot owns, and DMs. </PropertyReference> <PropertyReference name="onMessage" type="(h: BotHandler) => void"> Register a turn handler that fires only when **no** mention handler is registered — routing is mention-preferred (see Behavior). </PropertyReference> <PropertyReference name="onInteraction" type="<TValue>(id: string, h: (ctx: InteractionContext<TValue>) => void | Promise<void>) => void"> Escape-hatch handler for a known action `id`, bypassing the action registry. `ctx.action.value` is typed as `TValue`. Most bots don't need this — inline `onClick` handlers on [`Button`](/reference/bot/components/Button) are bound automatically. </PropertyReference> <PropertyReference name="onInterrupt" type="<TPayload>(eventName: string, h: (args: { payload: TPayload; thread: Thread }) => void | Promise<void>) => void"> Handle a captured agent interrupt (a LangGraph-style `on_interrupt` custom event). The handler typically posts a picker and resumes the run with [`thread.resume(value)`](/reference/bot/classes/Thread) when the user answers. </PropertyReference> <PropertyReference name="onCommand" type="(command: BotCommand) => void"> Register a slash command — a full [`BotCommand`](/reference/bot/functions/defineBotCommand), or the shorthand overload `onCommand(name, handler)` for a free-text command. Commands delivered by the platform but not registered here are ignored. </PropertyReference> <PropertyReference name="tool" type="(t: BotTool) => void"> Register a tool (alternative to `opts.tools`). Must be called before `start()` — the tool descriptors handed to the agent are computed at startup. </PropertyReference> <PropertyReference name="start" type="() => Promise<void>"> Bring all adapters up. Also forwards declared commands to adapters that implement `registerCommands` (e.g. a Discord-style application-command API); adapters without it — including Slack — are skipped. </PropertyReference> <PropertyReference name="stop" type="() => Promise<void>"> Bring all adapters down. </PropertyReference> </PropertyReference>

Usage

Minimal

tsx
import { createBot } from "@copilotkit/bot";
import { slack, defaultSlackTools, defaultSlackContext } from "@copilotkit/bot-slack";

const bot = createBot({
  adapters: [
    slack({
      botToken: process.env.SLACK_BOT_TOKEN!,
      appToken: process.env.SLACK_APP_TOKEN!,
    }),
  ],
  agent: (threadId) => makeAgent(threadId),
  tools: [...defaultSlackTools, ...appTools],
  context: [...defaultSlackContext, ...appContext],
});

bot.onMention(async ({ thread }) => {
  await thread.runAgent();
});

await bot.start();

Durable persistence + typed per-thread state

tsx
import { createBot } from "@copilotkit/bot";
import { createRedisStore } from "@copilotkit/bot-store-redis";
import { z } from "zod";

const WorkflowState = z.object({
  step: z.enum(["idle", "awaiting-approval", "done"]),
  lastUpdated: z.number(),
});

const bot = createBot({
  adapters: [slack({ botToken, appToken })],
  agent: (threadId) => makeAgent(threadId),
  store: {
    adapter: createRedisStore({ url: process.env.REDIS_URL }),
    state: WorkflowState,
  },
});

// `thread` is narrowed to StatefulThread<{ step: ...; lastUpdated: number }>
bot.onMention(async ({ thread }) => {
  const current = await thread.state();
  await thread.setState({ step: "awaiting-approval", lastUpdated: Date.now() });
  await thread.runAgent();
});

Cross-platform transcripts

tsx
const bot = createBot({
  adapters: [slackAdapter, discordAdapter],
  agent: (threadId) => makeAgent(threadId),
  store: {
    adapter: createRedisStore({ url: process.env.REDIS_URL }),
    identity: async ({ author }) => lookupEmailForUser(author.id),
    transcripts: { retention: "30d", maxPerUser: 500 },
  },
});

bot.onMention(async ({ thread }) => {
  // Injects prior cross-platform history, appends the user turn,
  // runs the agent, and captures the assistant reply — all in one call.
  await thread.runAgent({ transcript: true });
});

For a detailed guide on persistence and transcripts, see Persistence and Transcripts.

Behavior

  • Mention-preferred routing — there is no per-turn "kind": when any onMention handler is registered, all turns route to the mention handlers; otherwise onMessage handlers fire. Registering identical handlers on both never double-fires.
  • Expired actions are swallowed — a click whose snapshot is gone (e.g. after a restart with the in-memory store) raises ActionExpiredError internally; createBot swallows it, so the click is acked but ignored and no message is posted.
  • Commands are matched case-insensitively and without the leading slash.
  • No agent, no runAgent — omitting agent is fine for bots that only post UI, but thread.runAgent() will throw.