---
title: "Chat bot"
description: "Build a GitHub bot that classifies, labels and comments on an issue when someone mentions it, and extend it to Slack, with Chat SDK and Vercel Workflow."
canonical_url: "https://github-tools.com/guides/chat-bot"
---
# Chat bot

> Build a GitHub bot that classifies, labels and comments on an issue when someone mentions it, and extend it to Slack, with Chat SDK and Vercel Workflow.

To build an AI bot that answers on GitHub or Slack, receive mentions with [Chat SDK](https://github-tools.com/integrations/chat-sdk), and run a `createGithubAgent` turn inside a [Vercel Workflow](https://github-tools.com/integrations/workflow) step for each message. This guide builds an issue-triage bot: mention it on an issue and it classifies the issue, applies a label and comments with a next step.

## What you'll build

Comment `@triage-bot triage this` on an issue. The bot adds an `eyes` reaction, classifies the issue as a bug, a feature or a question, applies the matching label, and posts a comment with a suggested next step. Replies in the same thread continue the conversation, and each turn runs as a durable workflow step.

The bot is the [pull request review bot](https://github-tools.com/guides/review-pull-requests) with a different workflow and preset. If you built that one, you change two files.

<chat-bot-thread>



</chat-bot-thread>

## Before you start

- A copy of [`examples/pr-review-agent`](https://github.com/vercel-labs/github-tools/tree/main/examples/pr-review-agent), a Nitro server built with Vite, or the bot from [Review pull requests](https://github-tools.com/guides/review-pull-requests).
- A GitHub connector in [Vercel Connect](https://github-tools.com/auth#create-a-github-connector), installed on the repositories the bot triages, with its token pulled locally.
- A GitHub account for the bot, separate from the account you comment with.

## 1. Install the packages

<code-group>

```bash [pnpm]
pnpm add @github-tools/sdk @vercel/connect chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
```

```bash [npm]
npm install @github-tools/sdk @vercel/connect chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
```

```bash [yarn]
yarn add @github-tools/sdk @vercel/connect chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
```

```bash [bun]
bun add @github-tools/sdk @vercel/connect chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
```

</code-group>

The Nitro and Vite config come with the example. The files you touch:

```text
server/
├── lib/
│   ├── agent.ts                    # Chat SDK bot: import triageWorkflow instead of reviewWorkflow
│   └── github.ts                   # token and permission check: preset 'issue-triage'
├── routes/webhooks/github.post.ts  # webhook endpoint, unchanged
└── workflows/
    └── triage.ts                   # replaces review.ts
```

## 2. Scope the token to the issue's repository

`lib/github.ts` mints a token limited to the repository the webhook came from, and only lets people who can write to that repository drive the bot, since every call acts as the GitHub App. Set its preset to `issue-triage`, so the token gets that preset's permissions:

```ts [server/lib/github.ts]
export function repositoryToken(owner: string, repo: string) {
  return connectGithubToken(CONNECTOR, { preset: 'issue-triage', params: { repositories: [`${owner}/${repo}`] } })
}
```

## 3. Write the triage workflow

The workflow runs one agent turn for the mention, then one per follow-up message:

```ts [server/workflows/triage.ts]
import { createGithubAgent } from '@github-tools/sdk'
import { createHook, getWorkflowMetadata } from 'workflow'
import type { ChatTurnPayload, GitHubContext } from '../lib/agent'
import { repositoryToken } from '../lib/github'

async function runTriageTurn(prompt: string, ctx: GitHubContext) {
  'use step'
  const agent = createGithubAgent({
    model: 'anthropic/claude-opus-5.5',
    token: repositoryToken(ctx.owner, ctx.repo),
    preset: 'issue-triage',
    requireApproval: { addLabels: false, addIssueComment: false },
    context: { owner: ctx.owner, repo: ctx.repo, issueNumber: ctx.issueNumber },
    additionalInstructions: `
      Classify the issue as bug, feature, or question.
      Apply the matching label with addLabels, then post one comment with the
      classification and a one-line suggested next step. Do not close anything.
    `,
  })
  const { text } = await agent.generate({ prompt })
  return text
}

export async function triageWorkflow(prompt: string, ctx: GitHubContext) {
  'use workflow'
  using hook = createHook<ChatTurnPayload>({ token: getWorkflowMetadata().workflowRunId })

  await runTriageTurn(prompt, ctx)
  for await (const event of hook) {
    await runTriageTurn(event.text, ctx)
  }
}
```

Labels and comments run without asking. Every other write in the `issue-triage` [preset](https://github-tools.com/presets), such as `closeIssue`, still needs [approval](https://github-tools.com/approval). Nothing answers approvals in this bot, so those calls end the turn without running. `context` points every tool at the mentioned issue ([Context](https://github-tools.com/context)).

In `lib/agent.ts`, import `triageWorkflow` from `../workflows/triage` and pass it to `start()` in `onNewMention` in place of `reviewWorkflow`.

## Run it

Set `.env`, next to the `VERCEL_OIDC_TOKEN` that `vercel env pull` wrote:

```bash [.env]
GITHUB_CONNECT_CONNECTOR=github/my-connector
GITHUB_WEBHOOK_SECRET=a-random-secret
GITHUB_AGENT_USERNAME=triage-bot
GITHUB_TOKEN=github_pat_xxxxxxxxxxxx
```

`GITHUB_TOKEN` is for the GitHub adapter, which reacts and reads threads as the bot account. The agent's tools use the connector.

In the repository's **Settings** > **Webhooks**, point a webhook at `/webhooks/github` with content type `application/json`, the same secret, and the **Issue comments** event. Then start the server:

<code-group>

```bash [pnpm]
pnpm dev
```

```bash [npm]
npm run dev
```

```bash [yarn]
yarn dev
```

```bash [bun]
bun run dev
```

</code-group>

Expose port 3000 with a tunnel such as `cloudflared tunnel --url http://localhost:3000`, update the webhook URL, and comment `@triage-bot triage this` on an issue from your own account. The label and the comment appear on the issue.

## Answer on Slack too

Chat SDK runs the same handlers for every adapter you register. Add `@chat-adapter/slack` to `adapters` in `lib/agent.ts` with `createSlackAdapter()`, which reads `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET`, and add a `server/routes/webhooks/slack.post.ts` route that hands requests to `agent.webhooks.slack`.

Two parts of the GitHub bot depend on GitHub webhooks, so change them for Slack:

- **The target.** A Slack message carries no repository or issue number. Map each channel to a repository, or let the model ask, and set `context` from that.
- **The trust check.** `canWrite` checks a GitHub username. For Slack, check the Slack user against your own list of who may drive the bot, and keep the token scoped to the repositories that channel may touch.

[Chat SDK](https://github-tools.com/integrations/chat-sdk#add-slack-or-discord) covers adapters and production state.

## FAQ

<accordion>
<accordion-item label="Why doesn't the bot answer my mention?">

Chat SDK ignores messages from the bot's own account to prevent loops, so comment from a different account. The bot also ignores people without write access to the repository.

</accordion-item>

<accordion-item label="Can the bot close duplicates?">

Not as written: `closeIssue` asks for approval, and nothing in a GitHub thread answers it. Set `closeIssue: false` in `requireApproval` only if you accept the bot closing issues on its own, and tell it when to in `additionalInstructions`.

</accordion-item>

<accordion-item label="Does the bot use only existing labels?">

Yes. `getIssueContext` returns the repository's label names, and the `issue-triage` system prompt tells the model to pick from them. `createLabel` asks for approval, which nothing answers in this bot, so it can't add new labels to the repository.

</accordion-item>

<accordion-item label="What happens to conversations when the server restarts?">

`createMemoryState()` keeps thread state in memory, so the run IDs are lost. In production, use a persistent state adapter such as `@chat-adapter/state-redis` or `@chat-adapter/state-pg`.

</accordion-item>
</accordion>


## Sitemap

See the full [sitemap](https://github-tools.com/sitemap.md) for all pages.
