Skip to content
Docs

Build a GitHub agent with Vercel Connect

Learn how to build a GitHub agent with Chat SDK and AI SDK. Vercel Connect supplies runtime GitHub tokens and forwards triggers, so you never store a long-lived token.

16 min read
Last updated July 7, 2026

You can build an AI-powered GitHub agent that responds to @-mentions in pull request and issue comment threads, maintains conversation history, and calls tools autonomously, without storing a GitHub App private key or webhook secret in your environment. Chat SDK handles the platform integration (e.g., webhooks and message formatting) and AI SDK's ToolLoopAgent runs the reasoning loop that lets your agent call tools and act on results. Vercel Connect issues a GitHub installation access token at runtime and forwards GitHub events to your project, keeping credentials scoped to the environments that need them.

You'll scaffold the bot with the create-chat-sdk CLI, which generates the Connect wiring for you, then create a GitHub connector, link it to your project, and add an agent with tools. Along the way, you'll enable streaming responses, tool calling, and more using Redis and the Vercel AI Gateway.

Vercel Connect is in beta and available on all plans. Features and behavior, including available connectors and trigger forwarding, may change before general availability. Usage is subject to the Beta Agreement and Vercel Connect terms.

Before you begin, make sure you have:

  • Node.js 20+ and a package manager (e.g., pnpm)
  • Access to a Vercel team and project with Vercel Connect enabled
  • Vercel CLI installed (npm i -g vercel)
  • A GitHub account or organization where you can install an app

Chat SDK is a unified TypeScript SDK for building chatbots across GitHub, Slack, Linear, and other platforms. You register event handlers (like onNewMention and onSubscribedMessage), and the SDK routes incoming webhooks to them. The GitHub adapter treats issues and pull requests as threads and comments as messages, and handles GitHub API interactions through Octokit. The Redis state adapter tracks which threads your bot has subscribed to and manages distributed locking to handle concurrent messages.

The create-chat-sdk CLI scaffolds a minimal, webhook-only Next.js app:

  • Your Chat configuration in src/lib/bot.ts
  • The dynamic webhook route at /api/webhooks/[platform]
  • .env.example file
  • The adapter dependencies

Passing --connect adds the Vercel Connect authentication pieces.

AI SDK's ToolLoopAgent wraps a language model with tools and runs an autonomous loop: the model generates text or calls a tool, the SDK executes the tool, feeds the result back, and repeats until the model finishes. When you pass a model string like "anthropic/claude-opus-4.8", and host your application on Vercel, the AI SDK will route the request through the AI Gateway automatically. Chat SDK accepts any AsyncIterable<string> as a message, so you can pass the agent's fullStream directly to thread.post(). On GitHub, streaming is buffered: the SDK posts a comment and updates it as content arrives, rather than rendering token by token.

Vercel Connect provides GitHub credentials at runtime, so you don't have to manage a GitHub App private key or perform the JWT exchange yourself. You register a GitHub connector once, link it to your project, and the generated bot spreads the connectGitHubAdapter helper from @vercel/connect/chat into the GitHub adapter. Connect also forwards inbound GitHub events to a trigger destination you register on the connector. For this agent, that destination is your project at /api/webhooks/github.

The helper wires authentication in both directions:

  • For outbound calls to the GitHub API, it supplies an installationToken resolver that fetches a fresh installation access token per request via Connect's getToken. This is the token that a GitHub App would normally mint through its private-key JWT exchange; the adapter uses it directly, skipping that exchange.
  • For inbound events, Connect verifies them with GitHub and forwards them to your app. The helper's webhookVerifier then confirms each forwarded event carries a valid Vercel OIDC token, replacing GitHub's native webhook signature check.

Because OIDC verification replaces GitHub's signed webhook check, request freshness relies on the short-lived OIDC token's expiry, and there is no built-in delivery de-duplication. Keep your webhook handlers idempotent.

The create-chat-sdk CLI generates the entire Chat SDK skeleton with a single command. Pass the GitHub platform adapter, the Redis state adapter, and the --connect flag to authenticate with Vercel Connect:

Terminal
pnpm create chat-sdk@latest my-github-agent --adapter github redis --connect -y
cd my-github-agent

If you use npm, add the -- separator so npm forwards the flags to the CLI instead of consuming them itself:

Terminal
npm create chat-sdk@latest -- my-github-agent --adapter github redis --connect -y

The CLI generates:

  • src/lib/bot.ts with the Chat configuration. Because you passed --connect, it spreads the connectGitHubAdapter helper from @vercel/connect/chat into the GitHub adapter factory, and @vercel/connect is added to your dependencies.
  • src/app/api/webhooks/[platform]/route.ts, the dynamic webhook route that becomes your POST /api/webhooks/github endpoint.
  • .env.example, which lists a GITHUB_CONNECTOR variable for your connector UID in place of GITHUB_APP_ID, GITHUB_PRIVATE_KEY, and GITHUB_WEBHOOK_SECRET.
  • Starter handlers for mentions and subscribed thread replies, which you'll extend with an agent in step five.

The scaffolded app is webhook-only, with no pages or client UI. You'll add the AI SDK and Zod yourself, since the agent loop is your code rather than part of the scaffold:

Terminal
pnpm add ai zod

The ai package is the AI SDK, and includes the AI Gateway provider. zod is used to define tool input schemas.


The Vercel Plugin turns your AI coding agent (e.g., OpenAI Codex, Claude Code, or Cursor) into a Vercel expert. It adds skills, slash commands, and current knowledge of the tools this template uses, including Vercel Connect, Chat SDK, and AI SDK.

The plugin is optional; it isn't required to build your GitHub agent or to follow this guide. Note that create-chat-sdk detects when it's run by a coding agent and runs non-interactively, so agents should pass adapters with --adapter as shown above.

Terminal
npx plugins add vercel/vercel-plugin

Vercel Connect creates and manages the GitHub App for you. You don't register an app in GitHub's developer settings, generate a private key, or copy any credentials. You create a connector in the Vercel dashboard, set its permissions and events there, and install it on your repositories.

Open the Connect page on your team's dashboard, then click Create Connector to start the Add Connection flow. Once you've done that:

  1. Choose GitHub as the provider.
  2. Select your GitHub account or organization and name the app (e.g., acme-github). If you haven't connected a GitHub account yet, connect and authorize one first, then return to the Connect page. Keep Triggers enabled so GitHub events reach your project.
  3. Open Advanced and set:
    • Permissions the agent needs: Issues (Read & write), Pull requests (Read & write), and Metadata (Read-only).
    • Trigger Event Types to forward: issue_comment and pull_request_review_comment, so mentions in both issue threads and PR review threads reach your agent.
  4. Click Create GitHub Connector, then Install to your GitHub account or organization, choosing which repositories the agent can access.
  5. In the connector's settings, link it to your project, select the environments it applies to (e.g., production), and register your project as a trigger destination with the path /api/webhooks/github, the route the CLI generated in step one.

You can also do this from the CLI. Create the connector with trigger forwarding enabled, then attach your project and register the trigger destination:

Terminal
vercel connect create github --name acme-github --triggers
vercel connect attach github/acme-github \
--project my-github-agent --environment production \
--triggers --trigger-path /api/webhooks/github

Because Connect manages the GitHub App, GitHub delivers events to Connect's intake URL (shown in the connector settings), not directly to your deployment's /api/webhooks/github. Connect verifies GitHub, then forwards each event to the trigger destination you registered.

Copy the generated .env.example file and add your connector UID:

Terminal
cp .env.example .env.local
.env.local
GITHUB_CONNECTOR=github/acme-github

Replace github/acme-github with your connector UID from the Connect dashboard or vercel connect list. Add the same variable to your Vercel project's environment variables so deployments can resolve the connector.

You also need one more value: your app's bot user ID. GitHub Apps comment as a your-app-name[bot] user, and the adapter uses this numeric ID to recognize its own comments. In Connect mode, the adapter only holds an installation token, so it can't look this up itself (the /app lookup requires the App's JWT). Without it, the bot can't tell its own comments apart from user comments and will reply to itself in a loop.

Fetch the ID from GitHub's public API:

Terminal
curl -s 'https://api.github.com/users/acme-github%5Bbot%5D'

Replace acme-github with your app's name. The %5Bbot%5D is the URL-encoded [bot] suffix.

Copy the id field from the response into your .env.local so the adapter can auto-detect it:

.env.local
GITHUB_BOT_USER_ID=1234567890123

Add this to your Vercel project's environment variables as well.

This matters on serverless in particular, where each webhook can hit a fresh instance, so the bot can't rely on learning its own ID in memory from the first comment it posts.

Your agent uses Redis for thread subscriptions and distributed locking. Provision Upstash Redis and connect it to your project with the Vercel CLI:

Terminal
vercel link
vercel integration add upstash

Other providers are also supported in the Vercel Marketplace, including Redis.

vercel integration add installs the Upstash integration if it isn't already, provisions a database, connects it to your project, and pulls its connection environment variables into .env.local. Follow the prompts to pick the Redis product and a plan.

To use the dashboard instead, open the Storage page for your project, click Create Database, and follow the flow to add Upstash Redis. Then sync the variables locally:

Terminal
vercel env pull

vercel env pull also adds a VERCEL_OIDC_TOKEN, which AI SDK uses to authenticate requests to the AI Gateway, so there's no API key to generate or store. @vercel/connect reads the same token automatically, both locally and on deployments.

The OIDC token expires after 12 hours, so re-run vercel env pull to refresh it, or start the dev server with vercel dev to refresh it automatically. Linking the project also lets Vercel Connect resolve the connector when the adapter requests a token.

You should see REDIS_URL (from Upstash), VERCEL_OIDC_TOKEN (for AI Gateway and @vercel/connect), and the GITHUB_CONNECTOR and GITHUB_BOT_USER_ID values you set earlier. You should not add GITHUB_TOKEN, GITHUB_APP_ID, GITHUB_PRIVATE_KEY, or GITHUB_WEBHOOK_SECRET.

Create src/lib/tools.ts with the tools your agent can call. Since this agent lives in issue and PR threads, this example gives it tools to answer the questions that come up there: what the latest release of a repository is and what's current for an npm dependency. You can add any tools your use case requires:

src/lib/tools.ts
import { tool } from "ai";
import { z } from "zod";
export const tools = {
getLatestRelease: tool({
description:
"Get the latest published release for a GitHub repository, including its tag and release notes",
inputSchema: z.object({
owner: z.string().describe("Repository owner, e.g. vercel"),
repo: z.string().describe("Repository name, e.g. next.js"),
}),
execute: async ({ owner, repo }) => {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
{ headers: { Accept: "application/vnd.github+json" } }
);
if (!response.ok) {
return { error: `No releases found for ${owner}/${repo}` };
}
const release = await response.json();
return {
tag: release.tag_name,
name: release.name,
publishedAt: release.published_at,
url: release.html_url,
notes: release.body?.slice(0, 2000),
};
},
}),
getPackageInfo: tool({
description:
"Look up an npm package's latest version, description, and license",
inputSchema: z.object({
name: z.string().describe("Package name, e.g. ai or @vercel/connect"),
}),
execute: async ({ name }) => {
const response = await fetch(
`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`
);
if (!response.ok) {
return { error: `Package ${name} not found on npm` };
}
const pkg = await response.json();
return {
name: pkg.name,
version: pkg.version,
description: pkg.description,
license: pkg.license,
};
},
}),
};

Each tool has a description (which tells the model when to use it), an inputSchema (a Zod schema that the model fills in), and an execute function that runs when the tool is called. Both of these tools call public, unauthenticated endpoints, so they work as-is. If a tool needs to call the GitHub API with the agent's own permissions (e.g., reading a private repository's contents), it can request a scoped token for itself via Connect's getToken.

The CLI already generated src/lib/bot.ts with the Connect wiring and starter handlers. Update it to create a ToolLoopAgent and stream its responses from the mention and thread handlers:

src/lib/bot.ts
import { Chat } from "chat";
import { toAiMessages } from "chat/ai";
import { createGitHubAdapter } from "@chat-adapter/github";
import { createRedisState } from "@chat-adapter/state-redis";
import { ToolLoopAgent } from "ai";
import { connectGitHubAdapter } from "@vercel/connect/chat";
import { tools } from "./tools";
const agent = new ToolLoopAgent({
model: "anthropic/claude-opus-4.8",
instructions:
"You are a helpful AI assistant in GitHub issue and pull request " +
"threads. Answer questions clearly and use your tools when you need " +
"release or package data. Keep responses concise and formatted in " +
"GitHub-flavored Markdown.",
tools,
});
export const bot = new Chat({
userName: "acme-github[bot]",
adapters: {
github: createGitHubAdapter({
...connectGitHubAdapter(process.env.GITHUB_CONNECTOR!),
userName: "acme-github[bot]",
}),
},
state: createRedisState(),
});
// Handle first-time mentions
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
const result = await agent.stream({ prompt: message.text });
await thread.post(result.fullStream);
});
// Handle follow-up messages in subscribed threads
bot.onSubscribedMessage(async (thread, message) => {
const allMessages = [];
for await (const msg of thread.allMessages) {
allMessages.push(msg);
}
const history = await toAiMessages(allMessages);
const result = await agent.stream({ messages: history });
await thread.post(result.fullStream);
});

Replace acme-github[bot] with your app's bot username. The adapter uses userName to detect @-mentions in comments, so it must match the app's actual bot login. The GITHUB_BOT_USER_ID you set in step two handles the other half of self-identification: recognizing the bot's own comments by their numeric user ID.

The connectGitHubAdapter spread is the part the CLI generated for you.

It returns two fields:

  • An installationToken resolver in function form. The adapter calls it on each GitHub API request, and it resolves a fresh installation access token via Connect's getToken, skipping the GitHub App JWT exchange entirely. The token's subject is pinned to { type: "app" }, so the agent acts as the application itself, and the permissions you set on the connector determine what the token can do.
  • A webhookVerifier that validates the Vercel OIDC token Connect attaches to each trigger-forwarded event. The default verifier matches the deployment's project and environment automatically (projectId defaults to VERCEL_PROJECT_ID, and environment to VERCEL_TARGET_ENV, then VERCEL_ENV), so production, preview, and development each accept only their own tokens. Verification fails closed: if those values are absent, every request is rejected, and the issuer is pinned to https://oidc.vercel.com.

Omit webhookSecret and don't set GITHUB_WEBHOOK_SECRET. When webhookVerifier is set, it takes precedence over the signing secret, and the OIDC check is the freshness boundary. Leaving GITHUB_WEBHOOK_SECRET unset avoids mixing direct-GitHub and Connect modes.

The helper also accepts optional getToken parameters as a second argument (everything except subject), such as installationId, scopes, or validityBufferMs, if you need to target a specific installation or tune token requests.

When someone tags the bot in an issue or PR comment, onNewMention fires. The handler subscribes to the thread (to track future comments in that issue or PR) and streams the agent's response.

For follow-up comments, onSubscribedMessage retrieves the full thread history using thread.allMessages, converts it to the AI SDK message format with toAiMessages, and passes it to the agent so it has complete conversation context.

Using fullStream is preferred over textStream because it preserves paragraph breaks between tool-calling steps. Chat SDK auto-detects the stream type; on GitHub, streaming is buffered, so the SDK posts a comment and edits it as the agent produces content rather than rendering token-by-token.

The CLI also generated the webhook route at src/app/api/webhooks/[platform]/route.ts, which exposes POST /api/webhooks/github. It routes each request to the matching adapter's handler and uses waitUntil so your event handlers finish processing after the HTTP response is sent, which is required on serverless platforms where the function would otherwise terminate early. You don't need to modify it. With trigger forwarding enabled, Connect POSTs verified GitHub payloads to this route, and the helper's webhookVerifier runs before the adapter parses the body.

The default verifier accepts only the deployment's own environment. If you forward events to more than one environment, build a verifier with createConnectWebhookVerifier and override the field:

src/lib/bot.ts
import {
connectGitHubAdapter,
createConnectWebhookVerifier,
} from "@vercel/connect/chat";
createGitHubAdapter({
...connectGitHubAdapter(process.env.GITHUB_CONNECTOR!),
userName: "acme-github[bot]",
webhookVerifier: createConnectWebhookVerifier({
environment: ["production", "preview"],
}),
});

Avoid hardcoding environment: "production" unless you only forward to production, since that would reject preview and development deployments.

GitHub sends events to Connect, which forwards them to a deployed Vercel project rather than to your machine. You test the full round trip against a preview or development deployment, with no local tunnel to spin up. Your app rejects direct GitHub POSTs unless you add a separate direct-webhook path with GITHUB_WEBHOOK_SECRET.

  1. Deploy a preview build to receive the GitHub events:
Terminal
vercel
  1. In the connector's settings, make sure that deployment's environment is linked and registered as the trigger destination at /api/webhooks/github. The default verifier only accepts tokens for the deployment's own environment, so the connector must forward to the environment you're testing.
  2. Open an issue or pull request in a repository where the app is installed.
  3. Tag the bot in a comment and ask it, "@acme-github what's the latest release of vercel/next.js?". You should see a comment appear and fill in as the agent responds.

Once you've tested your agent, deploy it to production:

Terminal
vercel --prod

Your GitHub AI agent is now live and will respond to mentions across your repositories.

Check that your GitHub connector has trigger forwarding enabled and that your project is registered as a trigger destination with the correct path (/api/webhooks/github). Confirm the app is installed on the repository you're testing in and that its Trigger Event Types include issue_comment and pull_request_review_comment. Also verify that userName in your adapter config matches the app's actual bot login (including the [bot] suffix), since the adapter uses it for mention detection. You can review the connector configuration in its settings. Verify production/preview deployment logs on /api/webhooks/github for 401s before debugging token issues.

In Connect mode, the adapter only holds an installation token, so it can't auto-detect its own bot user ID (the /app lookup requires the App's JWT). Without that ID, the adapter can't recognize its own comments and treats them as new messages. Set GITHUB_BOT_USER_ID to your app's numeric bot user ID, which you can fetch with curl -s 'https://api.github.com/users/your-app%5Bbot%5D'. This must be set as an environment variable (or passed as botUserId in the adapter config) rather than learned at runtime, because on serverless each webhook can hit a fresh instance with no memory of earlier comments.

Make sure the project is linked (vercel link) and that the connector is linked to it for the current environment. Confirm GITHUB_CONNECTOR is set to the correct connector UID in the environment, the app is installed on the target repositories, and the permissions the agent uses are enabled on the connector. You can check the connector's link, environments, and permissions in its settings. For local development, run vercel env pull to refresh VERCEL_OIDC_TOKEN, since it expires after 12 hours.

  • Confirm connectGitHubAdapter is spread into createGitHubAdapter, so the helper's webhookVerifier is active.
  • Confirm OIDC Federation is enabled on the project. The verifier fails closed, so if VERCEL_PROJECT_ID or the other required environment variables are missing, then every request is rejected.
  • Remove GITHUB_WEBHOOK_SECRET from the project if set (it can force the wrong verification path in some setups).
  • Confirm the request is coming from Connect (trigger destination configured), not from GitHub hitting your app directly.
  • If you forward to multiple environments, the default verifier rejects tokens from environments other than the deployment's own. Override it with createConnectWebhookVerifier({ environment: [...] }) as shown above.
  • The event reached your app without Connect forwarding (either a GitHub webhook pointing directly to your deployment or a trigger destination not registered).
  • Fix the trigger path to /api/webhooks/github and link the correct environment.

OIDC verification replaces GitHub's native webhook signature check, and Connect has no built-in delivery de-duplication. If a forwarded event is delivered more than once, your handlers run multiple times. Keep webhook handlers idempotent, for example by tracking processed event IDs in Redis.

This is expected. GitHub doesn't support token-level streaming, so Chat SDK buffers the agent's output and updates the comment as content arrives. You'll see the comment appear and grow in chunks rather than character by character.

If the agent calls a tool but no result appears, check for errors in your tool's execute function. AI SDK surfaces tool execution errors back to the model, which may attempt to recover. Add error handling in your tools and check your server logs for details. For the tools in this guide, also check for GitHub API rate limiting, since unauthenticated requests to api.github.com are limited to 60 per hour per IP.

For long-running threads, the conversation history can exceed the model's context window. Consider limiting the number of messages you pass to the agent by slicing the history array or by using a summarization step for older messages.

Was this helpful?

supported.