Back to Copilotkit

Slack

showcase/shell-docs/src/content/docs/frontends/slack.mdx

1.61.113.2 KB
Original Source

This guide takes you from zero to a Slack bot you can @-mention in a channel, then adds an interactive button card. You write handlers in TypeScript, Copilot Runtime hosts the agent, and rich messages are JSX that the adapter renders to Block Kit (Slack's message-UI format). No public URL needed.

Prerequisites

  • Node.js 20+
  • A Slack workspace where you can install apps
  • An OpenAI API key (or Anthropic/Google — any model supported by Model Selection)

Getting started

<Steps> <Step> ### Create the Slack app from a manifest
    The manifest declares everything the bot needs — scopes, events, Socket Mode, a `/agent` slash command, and the **assistant pane** ("Agents & AI Apps") — in one shot.

    1. Open [api.slack.com/apps?new_app=1](https://api.slack.com/apps?new_app=1) and choose **From a manifest**.
    2. Pick your workspace.
    3. Switch the editor to the **YAML** tab (it defaults to JSON) and paste the contents of [`examples/slack/slack-app-manifest.yaml`](https://github.com/CopilotKit/CopilotKit/blob/main/examples/slack/slack-app-manifest.yaml).
    4. Review and create the app.

    <Callout type="info" title="The assistant pane is on by default">
      The manifest's `assistant_view` block (plus the `assistant:write` scope and the `assistant_thread_started` / `assistant_thread_context_changed` events) turns on Slack's AI assistant pane: opening it greets the user with tappable prompt chips, replies stream natively with live "is thinking…" status, and each pane conversation is its own thread. To run a plain channel bot without the pane, drop the `assistant_view` block and the `assistant:*` scope/events, and pass `assistant: false` to `slack(...)`.
    </Callout>
</Step>
<Step>
    ### Install the app and copy both tokens

    The bot needs two tokens:

    1. **Bot token (`xoxb-…`)** — under **OAuth & Permissions**, click **Install to Workspace** and approve. Copy the **Bot User OAuth Token** that appears after the install.
    2. **App-level token (`xapp-…`)** — under **Basic Information → App-Level Tokens**, click **Generate Token and Scopes**, name it anything, add the `connections:write` scope, and generate. Copy the token.

    <Callout type="info" title="No public URL needed">
      Socket Mode opens an outbound WebSocket to Slack — no public URL, no ngrok, works from your laptop.
    </Callout>
</Step>
<Step>
    ### Scaffold the project

    ```bash
    mkdir my-slack-bot && cd my-slack-bot
    npm init -y && npm pkg set type=module
    ```

    Install the bot packages, plus `@copilotkit/runtime` for Copilot Runtime and `tsx` to run TypeScript directly:

    <Tabs groupId="package-manager" items={['npm', 'pnpm', 'yarn']}>
        <Tab value="npm">
            ```bash
            npm install @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-slack @copilotkit/runtime
            npm install -D tsx typescript @types/node
            ```
        </Tab>
        <Tab value="pnpm">
            ```bash
            pnpm add @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-slack @copilotkit/runtime
            pnpm add -D tsx typescript @types/node
            ```
        </Tab>
        <Tab value="yarn">
            ```bash
            yarn add @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-slack @copilotkit/runtime
            yarn add -D tsx typescript @types/node
            ```
        </Tab>
    </Tabs>

    Then create a `tsconfig.json` that points the JSX factory at `@copilotkit/bot-ui` — this is what makes `<Message>` / `<Button>` statically type-checked bot UI instead of React:

    ```json title="tsconfig.json"
    {
      "compilerOptions": {
        "target": "es2022",
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "strict": true,
        "skipLibCheck": true,
        "noEmit": true,
        "types": ["node"],
        "jsx": "react-jsx",
        "jsxImportSource": "@copilotkit/bot-ui"
      },
      "include": ["bot.tsx"]
    }
    ```

    <Callout type="info" title="ESM only">
      The bot packages are ESM-only — `"type": "module"` (set above) is required.
    </Callout>
</Step>
<Step>
    ### Set up Copilot Runtime and the bot

    The smallest working bot is `createBot` + the Slack adapter + one `onMention` handler that runs the agent. For the quickstart, Copilot Runtime runs in the same process as the Slack bot, serves a `BuiltInAgent` on a local port, and the bot connects to that runtime URL:

    ```tsx title="bot.tsx"
    import { createServer } from "node:http";
    import { createBot } from "@copilotkit/bot";
    import { slack, SanitizingHttpAgent } from "@copilotkit/bot-slack"; // [!code highlight]
    import { BuiltInAgent, CopilotRuntime } from "@copilotkit/runtime/v2";
    import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";

    const agentId = "assistant";
    const runtimePort = Number(process.env.RUNTIME_PORT ?? 8200);
    const runtimeUrl = `http://localhost:${runtimePort}/api/copilotkit`;

    // Copilot Runtime: hosts the assistant agent.
    const runtime = new CopilotRuntime({
      agents: {
        [agentId]: new BuiltInAgent({
          model: process.env.OPENAI_MODEL ?? "openai:gpt-5-mini",
          prompt: "You are a helpful Slack assistant. Keep replies short.",
        }),
      },
    });
    createServer(
      createCopilotNodeListener({
        runtime,
        basePath: "/api/copilotkit",
        cors: true,
      }),
    ).listen(runtimePort, () => {
      console.log(`Copilot Runtime listening at ${runtimeUrl}`);
    });

    // The bot: the Slack adapter + one handler.
    const bot = createBot({
      adapters: [
        // [!code highlight:4]
        slack({
          botToken: process.env.SLACK_BOT_TOKEN!, // xoxb-…
          appToken: process.env.SLACK_APP_TOKEN!, // xapp-…
        }),
      ],
      // One agent connection per Slack conversation.
      agent: (threadId) => {
        const agent = new SanitizingHttpAgent({
          url: `${runtimeUrl}/agent/${encodeURIComponent(agentId)}/run`,
        });
        agent.threadId = threadId;
        return agent;
      },
    });

    bot.onMention(async ({ thread }) => {
      await thread.runAgent(); // [!code highlight]
    });

    await bot.start();
    console.log("⚡ Bot connected over Socket Mode");
    ```

    `thread.runAgent()` streams the agent's reply into the Slack thread, editing the message in place as tokens arrive.
</Step>
<Step>
    ### Run it

    ```bash
    export SLACK_BOT_TOKEN=xoxb-…
    export SLACK_APP_TOKEN=xapp-…
    export OPENAI_API_KEY=sk-…

    npx tsx bot.tsx
    ```

    You should see `Copilot Runtime listening at http://localhost:8200/api/copilotkit` and `⚡ Bot connected over Socket Mode` in the terminal. Now **invite the bot** to a channel and mention it — the bot's name comes from the manifest (if autocomplete can't find it, check **App Home** → *Default username*):

    ```
    /invite @YourBot
    @YourBot what can you do?
    ```

    <Accordions className="mb-4">
        <Accordion title="Troubleshooting">
            - **Mentioning the bot does nothing** — `app_mention` only fires in channels the bot is a **member** of: `/invite` it first. Also check the process is running and both tokens are set.
            - **`@`-autocomplete doesn't find the bot** — search by the bot user's *Default username* (visible under **App Home**), not the app's display name. Identity changes only propagate when you **reinstall** the app; in stubborn cases a full uninstall → reinstall is needed, which **rotates the `xoxb-` token** — update your env when it does.
            - **`bot.start()` fails with an auth error** — the `xoxb-` token is wrong or was rotated by a reinstall; copy the current one from OAuth & Permissions.
            - **The bot connects but never replies** — confirm `OPENAI_API_KEY` is set and `http://localhost:8200/api/copilotkit/info` returns the `assistant` agent.
            - **Slash command does nothing** — the command isn't declared in the Slack app config (see the last step), or the process isn't running.
        </Accordion>
    </Accordions>
</Step>
<Step>
    ### Post interactive UI

    Replies don't have to be text. Messages are authored as JSX from the [`@copilotkit/bot-ui` vocabulary](/reference/bot/components/Message) — including buttons with **inline `onClick` handlers**. Replace the `onMention` handler from the previous step:

    ```tsx title="bot.tsx"
    import { Message, Header, Section, Actions, Button } from "@copilotkit/bot-ui"; // [!code highlight]

    bot.onMention(async ({ thread, message }) => {
      if (message.text.toLowerCase().includes("deploy")) {
        await thread.post(
          <Message accent="#27AE60">
            <Header>Deploy v1.4.2</Header>
            <Section>Ship **v1.4.2** to production?</Section>
            <Actions>
              <Button
                style="primary"
                onClick={async ({ thread }) => {
                  await thread.post("🚀 Shipping!");
                }}
              >
                Ship it
              </Button>
              <Button
                onClick={async ({ thread }) => {
                  await thread.post("Standing down.");
                }}
              >
                Cancel
              </Button>
            </Actions>
          </Message>,
        );
        return;
      }
      await thread.runAgent();
    });
    ```

    Mention the bot with "deploy" in the message and click the buttons. Your handler code never leaves your process — Slack only sees an opaque action id.

    <Callout type="warn" title="Buttons expire on restart">
      The default action store is **in-memory**: after a process restart, clicks on old buttons are acknowledged but ignored. For buttons that survive restarts, plug a durable store (Redis, a database) into `createBot({ actionStore })` — see the [ActionStore contract](/reference/bot/types/ActionStore).
    </Callout>
</Step>
<Step>
    ### Add a slash command

    The manifest already declares `/agent` — register its handler above `bot.start()`. Slash-command text never appears in the channel, so pass it to the agent explicitly with `prompt`:

    ```tsx title="bot.tsx"
    bot.onCommand("agent", async ({ thread, text }) => {
      await thread.runAgent({ prompt: text }); // [!code highlight]
    });
    ```

    <Callout type="warn" title="Declare commands in the Slack app config">
      Slack silently drops undeclared commands — declare new ones in the manifest's `slash_commands` (or **Slash Commands** in the app settings) first.
    </Callout>
</Step>
</Steps>

Split the bot and the agent

The bot and its agent talk over AG-UI through Copilot Runtime, so they don't have to share a process. The production shape is two services joined by a URL: move the CopilotRuntime block into its own process (or use a deployed Copilot Runtime) and point the bot at it via env, exactly how the full on-call triage example is wired:

tsx
const runtimeUrl = process.env.COPILOT_RUNTIME_URL!;

agent: (threadId) => {
  const agent = new SanitizingHttpAgent({
    url: `${runtimeUrl}/agent/assistant/run`, // [!code highlight]
  });
  agent.threadId = threadId;
  return agent;
},

SanitizingHttpAgent is an HttpAgent that tolerates the event streams real agent backends emit — use it over the stock HttpAgent when connecting a Slack bot to Copilot Runtime.

Known limitations (v1)

  • Single workspace — one bot token; no OAuth/multi-workspace install flow
  • In-memory action store by default — inline button handlers expire on restart unless you provide a durable ActionStore
  • No modals or reactions — the adapter doesn't open modals or add reactions yet
  • Replies only — the bot answers turns it's part of (mentions, its threads, DMs); it doesn't post proactively

Next steps