Build GitHub bots with Chat SDK
Chat SDK is a multi-platform agent framework: one agent definition, adapters for GitHub, Slack, and Discord. Pair it with @github-tools/sdk and Vercel Workflow and you get a durable platform bot — @mention it on a PR, it reviews the changes and answers follow-ups, surviving crashes and timeouts along the way.
Build a durable GitHub PR review bot
How it works
When someone @mentions the bot on a PR, the GitHub adapter receives the webhook, the agent reacts, and a durable workflow runs the review — reading files, commits, and diffs with GitHub tools — then posts the result as a comment. Follow-up messages resume the same workflow via a hook.
@my-agent review this PR
│
▼
Chat SDK receives webhook
│
▼
Reaction added ──► starts durable workflow
│
▼
Agent reads PR (files, diffs, commits)
│
▼
Posts structured review comment
│
▼
Listens for follow-up messages
The complete implementation is about 60 lines across two files, in examples/pr-review-agent/.
Install
pnpm add @github-tools/sdk chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
npm install @github-tools/sdk chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
yarn add @github-tools/sdk chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
bun add @github-tools/sdk chat @chat-adapter/github @chat-adapter/state-memory workflow ai zod
The agent
Chat SDK handles webhooks, thread state, and @mention detection. onNewMention starts a durable workflow; onSubscribedMessage resumes it:
import { Chat, emoji, type Message, type Thread } from 'chat'
import { createGitHubAdapter } from '@chat-adapter/github'
import { createMemoryState } from '@chat-adapter/state-memory'
import { resumeHook, start } from 'workflow/api'
import { reviewWorkflow } from '../workflows/review'
export interface ThreadState { runId?: string }
export type ChatTurnPayload = { text: string }
const adapters = { github: createGitHubAdapter() }
export const agent = new Chat<typeof adapters, ThreadState>({
userName: process.env.GITHUB_AGENT_USERNAME || 'pr-review-agent',
adapters,
state: createMemoryState(),
}).registerSingleton()
agent.onNewMention(async (thread: Thread<ThreadState>, message: Message) => {
const sent = thread.createSentMessageFromMessage(message)
await sent.addReaction(emoji.eyes)
await thread.subscribe()
const run = await start(reviewWorkflow, [message.text])
await thread.setState({ runId: run.runId })
})
agent.onSubscribedMessage(async (thread: Thread<ThreadState>, message: Message) => {
const state = await thread.state
if (!state?.runId) return
await resumeHook<ChatTurnPayload>(state.runId, { text: message.text })
})
The workflow
The review itself is a "use workflow" function. Each turn runs createGithubAgent with the code-review preset inside a "use step" — durable, retryable, observable. A createHook keeps the workflow alive for follow-up messages:
import { createHook, getWorkflowMetadata } from 'workflow'
import { createGithubAgent } from '@github-tools/sdk'
import type { ChatTurnPayload } from '../lib/agent'
async function runAgentTurn(prompt: string) {
'use step'
const agent = createGithubAgent({
model: 'anthropic/claude-sonnet-4.6',
preset: 'code-review',
requireApproval: false,
additionalInstructions: 'Always post your review as a comment on the PR.',
})
const { text } = await agent.generate({ prompt })
return text
}
export async function reviewWorkflow(prompt: string) {
'use workflow'
const { workflowRunId } = getWorkflowMetadata()
using hook = createHook<ChatTurnPayload>({ token: workflowRunId })
await runAgentTurn(prompt)
for await (const event of hook) {
await runAgentTurn(event.text)
}
}
The webhook route
One route hands GitHub webhooks to the adapter:
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),
})
})
Configure the webhook in your repository settings (Settings → Webhooks) with the route URL, a shared secret (GITHUB_WEBHOOK_SECRET), and the Issue comments and Pull request review comments events.
GITHUB_AGENT_USERNAME to the bot account's username.Add observability with evlog
The example wires evlog into the agent turn for token usage, tool calls, cost, and timing:
import { createLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
async function runAgentTurn(prompt: string) {
'use step'
const log = createLogger()
const ai = createAILogger(log, { toolInputs: { maxLength: 500 } })
const agent = createGithubAgent({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
preset: 'code-review',
requireApproval: false,
})
const { text } = await agent.generate({ prompt })
log.emit()
return text
}
Beyond GitHub
Chat SDK adapters share the same agent definition — swap or add @chat-adapter/slack or @chat-adapter/discord and the same GitHub-tools-powered agent answers in Slack channels or Discord threads.