Examples

Example: an issue triage bot with Chat SDK

A durable GitHub bot that triages @mentioned issues, classifies them, and labels them, built with Chat SDK, Vercel Workflow, the issue-triage preset, and Vercel Connect.

Build an issue triage bot with Chat SDK

@mention the bot on an issue and it classifies it (bug, feature, or question), applies a matching label, and posts a short triage note. Same shape as the PR review bot example, different preset and a different job.

Files

server/lib/agent.ts
import { Chat, emoji, type Message, type Thread } from 'chat'
import { createGitHubAdapter } from '@chat-adapter/github'
import { createMemoryState } from '@chat-adapter/state-memory'
import { start } from 'workflow/api'
import { triageWorkflow } from '../workflows/triage'

const adapters = { github: createGitHubAdapter() }

export const agent = new Chat<typeof adapters>({
  userName: process.env.GITHUB_AGENT_USERNAME || 'triage-bot',
  adapters,
  state: createMemoryState(),
}).registerSingleton()

agent.onNewMention(async (thread: Thread, message: Message) => {
  const sent = thread.createSentMessageFromMessage(message)
  await sent.addReaction(emoji.eyes)
  await start(triageWorkflow, [message.text])
})
server/workflows/triage.ts
import { createGithubAgent } from '@github-tools/sdk'
import { connectGithubToken } from '@github-tools/sdk/connect'

async function runTriageTurn(prompt: string) {
  'use step'
  const agent = createGithubAgent({
    model: 'anthropic/claude-sonnet-4.6',
    token: connectGithubToken('github/my-connector', { preset: 'issue-triage' }),
    preset: 'issue-triage',
    requireApproval: false,
    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) {
  'use workflow'
  await runTriageTurn(prompt)
}
server/routes/webhooks/github.post.ts
import { defineHandler } from 'nitro/h3'
import { agent } from '../../lib/agent'

export default defineHandler(async (event) => {
  const handler = agent.webhooks.github
  if (!handler) {
    return new Response('GitHub adapter not configured', { status: 404 })
  }
  return handler(event.req, {
    waitUntil: (task: Promise<unknown>) => event.waitUntil(task),
  })
})
.env
# Vercel Connect OIDC token, required by the "github/my-connector" connector.
# vercel link && vercel env pull
VERCEL_OIDC_TOKEN=

# Webhook secret (must match your GitHub webhook config)
GITHUB_WEBHOOK_SECRET=

# Bot account username, for @mention detection
GITHUB_AGENT_USERNAME=triage-bot

Install

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

Set up the connector and webhook

Create a GitHub connector

Create a github/my-connector connector installed on the repositories this bot triages. See Vercel Connect for the full checklist.

Pull a local OIDC token

Terminal
vercel link
vercel env pull

Configure the GitHub webhook

In Settings → Webhooks, point a webhook at /webhooks/github with content type application/json, the same secret as GITHUB_WEBHOOK_SECRET, and the Issue comments event.

Use a different GitHub account for the bot than the one commenting, Chat SDK filters self-messages to prevent loops. Set GITHUB_AGENT_USERNAME to that account.

Run it

Terminal
pnpm dev

Comment @triage-bot triage this on an issue. The bot reacts, labels the issue, and posts its classification, no approval prompt, since requireApproval: false and this job never closes or deletes anything.

Go further

  • Set requireApproval: { closeIssue: true } and let the bot close issues once the author confirms it's resolved, instead of leaving that to a human every time
  • Reuse the same agent.ts shape with @chat-adapter/slack to triage from a Slack channel instead of GitHub comments, see Beyond GitHub
  • Compare with the PR review bot to see the same architecture applied to a different preset and prompt