chat 4.31.0 → 4.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ # Build your own Slackbot with Vercel Connect
2
+
3
+ **Author:** Ben Sabic
4
+
5
+ ---
6
+
7
+ You can build an AI-powered Slack bot that responds to mentions, maintains conversation history, and calls tools autonomously, without storing a long-lived Slack bot token or signing 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](https://vercel.com/connect) issues a user-authorized Slack token at runtime and forwards Slack events to your project, keeping credentials scoped to the environments that need them.
8
+
9
+ You'll create a Slack connector using Vercel Connect, link it to your project, and request a scoped runtime token from your agent's code. Along the way, you'll enable streaming responses, tool calling, and more using Redis and the [Vercel AI Gateway](https://vercel.com/ai-gateway).
10
+
11
+ > **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](https://vercel.com/docs/release-phases/public-beta-agreement) and [Vercel Connect terms](https://vercel.com/docs/connect/legal).
12
+
13
+ ## Prerequisites
14
+
15
+ Before you begin, make sure you have:
16
+
17
+ * Node.js 20+ and a package manager (e.g., [pnpm](https://pnpm.io/))
18
+
19
+ * Access to a Vercel team and project with Vercel Connect enabled
20
+
21
+ * Vercel CLI installed (`npm i -g vercel`)
22
+
23
+ * A Slack workspace where you can install an app
24
+
25
+
26
+ ## How it works
27
+
28
+ Chat SDK is a unified TypeScript SDK for building chatbots across Slack, Teams, Discord, and other platforms. You register event handlers (like `onNewMention` and `onSubscribedMessage`), and the SDK routes incoming webhooks to them. The Slack adapter handles message parsing and Slack API interactions. The Redis state adapter tracks which threads your bot has subscribed to and manages distributed locking to handle concurrent messages.
29
+
30
+ 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.
31
+
32
+ Chat SDK accepts any `AsyncIterable<string>` as a message, so you can pass the agent's `fullStream` directly to `thread.post()` for real-time streaming in Slack.
33
+
34
+ Vercel Connect provides Slack credentials at runtime, so you don't have to manage a static bot token. You register a Slack connector once, link it to your project, then call `getToken` from your code for a short-lived, scoped Slack token. Connect also forwards inbound Slack events to a trigger destination you register on the connector. For this agent, that destination is your project at `/api/webhooks/slack`.
35
+
36
+ Connect handles authentication in both directions:
37
+
38
+ * For outbound calls to the Slack API, `getToken` from `@vercel/connect` gives your agent a short-lived Slack token.
39
+
40
+ * For inbound events, Connect verifies them with Slack and forwards them to your app. Your `webhookVerifier` then confirms each one came from Connect using `verifyVercelOidcToken` from `@vercel/oidc`.
41
+
42
+
43
+ ## Steps
44
+
45
+ ### 1\. Scaffold the project, install dependencies, and add the Vercel Plugin
46
+
47
+ Create a new [Next.js](https://nextjs.org/) app and add your dependencies:
48
+
49
+ `npx create-next-app@latest my-slack-agent --typescript --app cd my-slack-agent pnpm add chat @chat-adapter/slack @chat-adapter/state-redis ai zod @vercel/connect @vercel/oidc`
50
+
51
+ Here's what each package does:
52
+
53
+ * The `chat` package is the Chat SDK core.
54
+
55
+ * The `@chat-adapter/slack` and `@chat-adapter/state-redis` packages are the [Slack platform adapter](https://chat-sdk.dev/adapters/slack) and [Redis state adapter](https://chat-sdk.dev/adapters/redis), respectively.
56
+
57
+ * The `ai` package is the AI SDK, and includes the [AI Gateway provider](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway).
58
+
59
+ * `zod` is used to define tool input schemas.
60
+
61
+ * `@vercel/connect` is the Vercel Connect SDK, which provides `getToken`.
62
+
63
+ * `@vercel/oidc` verifies the Bearer OIDC token on Connect-forwarded webhooks (`verifyVercelOidcToken`).
64
+
65
+
66
+ * * *
67
+
68
+ The [Vercel Plugin](https://vercel.com/docs/agent-resources/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 Slackbot or to follow this guide.
69
+
70
+ `npx plugins add vercel/vercel-plugin`
71
+
72
+ ### 2\. Create a Slack connector with Vercel Connect
73
+
74
+ Vercel Connect creates and manages the Slack app for you. You don't register an app at [api.slack.com](https://api.slack.com), write a manifest, or copy any credentials. You create a connector in the Vercel dashboard, set its scopes and events there, and install it in your workspace.
75
+
76
+ Open the [Connect page](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fconnect) on your team's dashboard, then click Create Connector to start the Add Connection flow. Once you've done that:
77
+
78
+ 1. Choose Slack as the provider.
79
+
80
+ 2. Select your Slack workspace and name the app (e.g., `acme-slack`). If you haven't connected a Slack workspace yet, connect and authorize one first, then return to the [Connect page](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fconnect). Keep Triggers enabled so Slack events reach your project.
81
+
82
+ 3. Open Advanced and set:
83
+
84
+ * Bot Scopes the agent needs: `chat:write`, `channels:history`, `channels:read`, `groups:history`, `im:history`, `mpim:history`, `reactions:write`, and `users:read`.
85
+
86
+ * Trigger Event Types to forward: `app_mention`, plus the message events for the surfaces your agent supports (`message.channels`, `message.groups`, `message.im`, `message.mpim`), so both new mentions and follow-up replies work.
87
+
88
+ 4. Click Create Slack Connector, then Install to your Slack Workspace.
89
+
90
+ 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/slack`, the route you'll create in step seven.
91
+
92
+
93
+ Because Connect manages the Slack app, Slack delivers events to Connect's intake URL (shown in the connector settings), not directly to your deployment's `/api/webhooks/slack`. Connect verifies Slack, then forwards each event to the trigger destination you registered.
94
+
95
+ ### 3\. Provision Redis and pull environment variables
96
+
97
+ Your agent uses Redis for thread subscriptions and distributed locking. Provision [Upstash Redis](https://vercel.com/marketplace/upstash) and connect it to your project with the Vercel CLI:
98
+
99
+ `vercel link vercel integration add upstash`
100
+
101
+ `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.
102
+
103
+ To use the dashboard instead, open the [Storage](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fstores) page for your project, click Create Database, and follow the flow to add Upstash Redis. Then sync the variables locally:
104
+
105
+ `vercel env pull`
106
+
107
+ `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. 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 your code calls `getToken`.
108
+
109
+ You should see `REDIS_URL` (from Upstash) and `VERCEL_OIDC_TOKEN` (for AI Gateway and `@vercel/connect`). You should not add `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET`.
110
+
111
+ ### 4\. Define your agent's tools
112
+
113
+ Create `lib/tools.ts` with the tools your agent can call. This example defines a weather tool and a docs tool, but you can add any tools your use case requires:
114
+
115
+ ``import { tool } from "ai"; import { z } from "zod"; export const tools = { getWeather: tool({ description: "Get the current weather for a location", inputSchema: z.object({ location: z.string().describe("City name, e.g. San Francisco"), }), execute: async ({ location }) => { // Replace with a real weather API call const response = await fetch( `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(location)}` ); const data = await response.json(); return { location, temperature: data.current.temp_f, condition: data.current.condition.text, }; }, }), searchDocs: tool({ description: "Search the company documentation for a topic", inputSchema: z.object({ query: z.string().describe("The search query"), }), execute: async ({ query }) => { // Replace with your actual search implementation return { results: [`Result for: ${query}`] }; }, }), };``
116
+
117
+ 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. If a tool calls another provider that Vercel Connect supports (e.g., GitHub), it can request a scoped token for itself.
118
+
119
+ ### 5\. Create the Connect webhook verifier
120
+
121
+ Before the bot can trust an incoming event, it needs to verify the OIDC Bearer token that Connect attaches to each forwarded webhook. Create `lib/slack-connect-webhook-verifier.ts`:
122
+
123
+ `import { verifyVercelOidcToken } from "@vercel/oidc"; const BEARER_TOKEN_PATTERN = /^Bearer\s+(.+)$/i; export async function verifySlackConnectWebhook( request: Request ): Promise<true> { const token = request.headers .get("authorization") ?.match(BEARER_TOKEN_PATTERN)?.[1] ?.trim(); if (!token) { throw new Error("Missing Authorization bearer token"); } await verifyVercelOidcToken(token); return true; }`
124
+
125
+ `verifyVercelOidcToken` checks the JWT against Vercel's JWKS and, by default, matches `project_id` and `environment` to `VERCEL_PROJECT_ID` and `VERCEL_ENV` on the deployment. Returning `true` tells the adapter the body is trusted; throwing or returning a falsy value yields a `401`. When you use `webhookVerifier`, the adapter doesn't run Slack's 5-minute timestamp check, since Connect is your freshness boundary.
126
+
127
+ For more details, see the [OIDC API reference](https://vercel.com/docs/oidc/api) and [Connect triggers](https://vercel.com/docs/connect/concepts/triggers).
128
+
129
+ ### 6\. Create the agent and bot
130
+
131
+ Create `lib/bot.ts` with a `ToolLoopAgent` and a `Chat` instance.
132
+
133
+ Instead of reading a long-lived `SLACK_BOT_TOKEN`, the Slack adapter fetches a short-lived token with Vercel Connect:
134
+
135
+ `import { Chat } from "chat"; import { toAiMessages } from "chat/ai"; import { createSlackAdapter } from "@chat-adapter/slack"; import { createRedisState } from "@chat-adapter/state-redis"; import { ToolLoopAgent } from "ai"; import { getToken } from "@vercel/connect"; import { tools } from "./tools"; import { verifySlackConnectWebhook } from "./slack-connect-webhook-verifier"; const agent = new ToolLoopAgent({ model: "anthropic/claude-opus-4.8", instructions: "You are a helpful AI assistant in a Slack workspace. " + "Answer questions clearly and use your tools when you need " + "real-time data. Keep responses concise and well-formatted for chat.", tools, }); export const bot = new Chat({ userName: "ai-agent", adapters: { slack: createSlackAdapter({ // Outbound: short-lived Slack token from Connect botToken: () => getToken("slack/acme-slack", { subject: { type: "app" } }), // Inbound: verify Connect-forwarded OIDC Bearer token webhookVerifier: verifySlackConnectWebhook, }), }, 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); });`
136
+
137
+ The `botToken` option accepts a function that returns a token, and the adapter calls it on each Slack API request. That makes it a natural fit for Vercel Connect's short-lived tokens, since `getToken` returns a fresh token for the connector's installation each time. Request the token with subject `app` so the agent acts as the application itself; the bot scopes you set on the connector determine what the token can do.
138
+
139
+ Replace `slack/acme-slack` with your connector UID from the Connect dashboard or `vercel connect list`.
140
+
141
+ Pass `webhookVerifier` whenever Slack events arrive via Connect triggers. Omit `signingSecret` and don't set `SLACK_SIGNING_SECRET`. If both are present, `webhookVerifier` wins, but leaving `SLACK_SIGNING_SECRET` unset avoids mixing direct-Slack and Connect modes.
142
+
143
+ When someone tags the bot, `onNewMention` fires. The handler subscribes to the thread (to track future messages in that thread) and streams the agent's response.
144
+
145
+ For follow-up messages, `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.
146
+
147
+ Using `fullStream` is preferred over `textStream` because it preserves paragraph breaks between tool-calling steps. Chat SDK auto-detects the stream type and handles Slack's native streaming API for real-time updates.
148
+
149
+ ### 7\. Wire up the webhook route
150
+
151
+ Create the API route at `app/api/webhooks/[platform]/route.ts`:
152
+
153
+ ``import { after } from "next/server"; import { bot } from "@/lib/bot"; type Platform = keyof typeof bot.webhooks; export async function POST( request: Request, context: RouteContext<"/api/webhooks/[platform]"> ) { const { platform } = await context.params; const handler = bot.webhooks[platform as Platform]; if (!handler) { return new Response(`Unknown platform: ${platform}`, { status: 404 }); } return handler(request, { waitUntil: (task) => after(() => task), }); }``
154
+
155
+ This creates a `POST /api/webhooks/slack` endpoint. The `waitUntil` option ensures your event handlers finish processing after the HTTP response is sent, which is required on serverless platforms where the function would otherwise terminate early.
156
+
157
+ With trigger forwarding enabled, Connect POSTs verified Slack payloads to this route. Verification happens in `webhookVerifier` before the adapter parses the body. The route itself stays unchanged. Only the adapter config gains `webhookVerifier`.
158
+
159
+ ### 8\. Test the agent
160
+
161
+ Slack 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 Slack POSTs unless you add a separate direct-webhook path with `SLACK_SIGNING_SECRET`.
162
+
163
+ 1. Deploy a preview build to receive the Slack events:
164
+
165
+
166
+ `vercel`
167
+
168
+ 1. In the [connector's settings](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fconnect?service=slack), make sure that deployment's environment is linked and registered as the trigger destination at `/api/webhooks/slack`.
169
+
170
+ 2. Invite the bot to a channel (`/invite @AI Agent`).
171
+
172
+ 3. Tag the bot and ask it, "What's the weather in San Francisco?". You should see a streaming response appear in the thread.
173
+
174
+
175
+ ### 9\. Deploy to Production
176
+
177
+ Once you've tested your agent, deploy it to production:
178
+
179
+ `vercel --prod`
180
+
181
+ Your Slack AI agent is now live and will respond to mentions in your workspace.
182
+
183
+ ## Troubleshooting
184
+
185
+ ### Bot doesn't respond to mentions
186
+
187
+ Check that your Slack connector has trigger forwarding enabled and that your project is registered as a trigger destination with the correct path (`/api/webhooks/slack`). Confirm the connector is installed in your workspace and that its Trigger Event Types include the events your agent needs (`app_mention` and the relevant message events). You can review all of this in the connector's settings. Verify production/preview deployment logs on `/api/webhooks/slack` for 401s before debugging `getToken`.
188
+
189
+ ### Token requests fail or return unauthorized
190
+
191
+ Make sure the project is linked (`vercel link`) and that the connector is linked to it for the current environment. Confirm the connector is installed in your workspace and that the bot scopes the agent uses are enabled on the connector. You can check the connector's link, environments, and scopes in its settings.
192
+
193
+ ### Webhook returns 401 / Invalid signature
194
+
195
+ * Confirm `webhookVerifier` is set and imports `verifyVercelOidcToken`.
196
+
197
+ * Confirm OIDC Federation is enabled on the project.
198
+
199
+ * Remove `SLACK_SIGNING_SECRET` from the project if set (can force the wrong verification path in some setups).
200
+
201
+ * Confirm the request is coming from Connect (trigger destination configured), not from Slack hitting your app directly.
202
+
203
+
204
+ ### Missing Authorization bearer token
205
+
206
+ * The event reached your app without Connect forwarding (wrong Events URL on the Slack side, or trigger destination not registered).
207
+
208
+ * Fix the trigger path to `/api/webhooks/slack` and link the correct environment.
209
+
210
+
211
+ ### Streaming appears choppy or delayed
212
+
213
+ Chat SDK uses Slack's native streaming API for smooth updates. If you're seeing issues, check that your Redis connection is stable, as the SDK uses distributed locks to manage concurrent messages.
214
+
215
+ ### Tool calls fail silently
216
+
217
+ 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.
218
+
219
+ ### Thread history grows too large
220
+
221
+ 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.
222
+
223
+ ## Related resources
224
+
225
+ * [Vercel Connect overview](https://vercel.com/docs/connect)
226
+
227
+ * [Vercel Connect quickstart guide](https://vercel.com/docs/connect/quickstart)
228
+
229
+ * [Chat SDK streaming](https://chat-sdk.dev/docs/streaming)
230
+
231
+ * [Chat SDK actions](https://chat-sdk.dev/docs/actions) and [cards](https://chat-sdk.dev/docs/cards)
232
+
233
+ * [AI SDK agent documentation](https://ai-sdk.dev/docs/agents/building-agents)
234
+
235
+ * [AI Gateway documentation](https://vercel.com/docs/ai-gateway)
236
+
237
+ ---
238
+
239
+ [View full KB sitemap](/kb/sitemap.md)
@@ -56,7 +56,7 @@ The `chat` package is the Chat SDK core. The `@chat-adapter/discord` and `@chat-
56
56
 
57
57
  Then set up the Interactions endpoint:
58
58
 
59
- 1. In **General Information**, set the **Interactions Endpoint URL** to [`https://your-domain.com/api/webhooks/discord`](https://your-domain.com/api/webhooks/discord)
59
+ 1. In **General Information**, set the **Interactions Endpoint URL** to `https://your-domain.com/api/webhooks/discord`
60
60
 
61
61
  2. Discord will send a PING to verify the endpoint. You'll need to deploy first or use a tunnel
62
62
 
@@ -128,7 +128,7 @@ The Gateway listener connects to Discord's WebSocket, receives messages, and for
128
128
 
129
129
  3. Expose your server with a tunnel (e.g. `ngrok http 3000`)
130
130
 
131
- 4. Update the **Interactions Endpoint URL** in your Discord app settings to your tunnel URL (e.g. [`https://abc123.ngrok.io/api/webhooks/discord`](https://abc123.ngrok.io/api/webhooks/discord))
131
+ 4. Update the **Interactions Endpoint URL** in your Discord app settings to your tunnel URL (e.g. `https://abc123.ngrok.io/api/webhooks/discord`)
132
132
 
133
133
  5. @mention the bot in your Discord server. It should respond with a support card
134
134
 
@@ -0,0 +1,306 @@
1
+ # Build a daily digest bot with Chat SDK and Workflow SDK
2
+
3
+ **Author:** Anshuman Bhardwaj
4
+
5
+ ---
6
+
7
+ Build a daily digest bot that gathers GitHub activity, summarizes it with a model, and posts to Slack on a schedule without running it inline in a cron handler, where a single slow source or a failing channel can take down the whole run. A cron route starts a single durable workflow that fans out a step per channel, so each fetches, summarizes, and posts on its own, and retries independently.
8
+
9
+ This guide builds the bot with [Chat SDK](https://chat-sdk.dev) and [Workflow SDK](https://workflow-sdk.dev). Every credential is brokered at runtime: Slack and GitHub authenticate through [Vercel Connect](https://vercel.com/connect), and the model authenticates with your project's [OpenID Connect (OIDC) token](https://vercel.com/docs/oidc). There are no API keys or client secrets to set.
10
+
11
+ Deploy the template now, or read on for a deeper look at how it all works.
12
+
13
+ ## Quick start with an AI coding agent
14
+
15
+ If you are working with an AI coding agent, hand it the project and this prompt:
16
+
17
+ ### Vercel Plugin
18
+
19
+ Turn your agent into a Vercel expert with this [plugin](https://vercel.com/docs/agent-resources/vercel-plugin). It gives your coding agent current knowledge of the Vercel products this template uses, including Vercel Connect, Vercel Workflows, Vercel Cron, AI Gateway, and Chat SDK. The plugin is optional; it is not required to use this template or for this guide.
20
+
21
+ `npx plugins add vercel/vercel-plugin`
22
+
23
+ ## Prerequisites
24
+
25
+ Before you begin, make sure you have:
26
+
27
+ * Node.js 20+
28
+
29
+ * [pnpm](https://pnpm.io/) (or npm/yarn)
30
+
31
+ * A Vercel team and project with Vercel Connect enabled, plus permission to create connectors and link them to projects
32
+
33
+ * The [Vercel CLI](https://vercel.com/docs/cli) is installed
34
+
35
+ * A Slack workspace where you can install apps
36
+
37
+ * A GitHub account where you can install apps
38
+
39
+
40
+ ## Create the project
41
+
42
+ Create a new [Next.js](https://nextjs.org/docs) app with `create-next-app` :
43
+
44
+ `pnpm create next-app@latest scheduled-digest-bot --yes cd scheduled-digest-bot`
45
+
46
+ Then install the Chat SDK, Vercel Connect, and Workflow SDK packages:
47
+
48
+ `pnpm add chat @chat-adapter/slack @chat-adapter/state-redis ai workflow @vercel/connect zod`
49
+
50
+ Wrap your Next.js config in `withWorkflow`:
51
+
52
+ `import { withWorkflow } from "workflow/next"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { serverExternalPackages: [ "@chat-adapter/slack", "@chat-adapter/state-redis", "@redis/client", "@slack/socket-mode", "@slack/web-api", "redis", ], }; export default withWorkflow(nextConfig);`
53
+
54
+ ## Configure credentials
55
+
56
+ Your agent uses Redis for thread subscriptions and distributed locking. Provision [Upstash Redis](https://vercel.com/marketplace/upstash) and connect it to your project with the Vercel CLI:
57
+
58
+ `vercel link vercel integration add upstash`
59
+
60
+ `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.
61
+
62
+ Use the [Vercel CLI](https://vercel.com/docs/cli) to link the project and pull [environment variables](https://vercel.com/docs/environment-variables):
63
+
64
+ `vercel env pull`
65
+
66
+ AI SDK uses `VERCEL_OIDC_TOKEN` to authenticate with the Vercel AI Gateway with [OIDC authentication](https://vercel.com/docs/ai-gateway/authentication-and-byok/authentication#oidc-token).
67
+
68
+ `VERCEL_OIDC_TOKEN=...`
69
+
70
+ ### Create and link the Slack connector
71
+
72
+ Create the Slack connector in Vercel Connect before you wire the bot locally. Vercel Connect creates and manages the Slack app, so you do not need to create a Slack app at [`api.slack.com`](http://api.slack.com) or copy a long-lived Slack bot token.
73
+
74
+ 1. Open the Connect page in your Vercel team dashboard.
75
+
76
+ 2. Choose **Create Connector**.
77
+
78
+ 3. Select **Slack** as the provider.
79
+
80
+ 4. Select the Slack workspace and name the connector, for example `digest-bot`.
81
+
82
+ 5. Keep triggers enabled if this project should receive Slack events.
83
+
84
+ 6. Keep the default scopes selected.
85
+
86
+ 7. Create the connector and install it in the Slack workspace.
87
+
88
+ 8. In the connector settings, link it to the Vercel project and select the environments where it should be available.
89
+
90
+
91
+ Copy the Slack connector id and store it in `.env.local` file as `CONNECTOR_SLACK`, for example:
92
+
93
+ `CONNECTOR_SLACK=slack/digest-bot`
94
+
95
+ Also, add the following environment variables:
96
+
97
+ `CRON_SECRET="replace-with-a-long-random-string" # e.g. "openssl rand --base64 32" DIGEST_CHANNEL_ID="slack:SLACK_CHANNEL_ID"`
98
+
99
+ ### Create and link the GitHub connector
100
+
101
+ Create the GitHub connector in Vercel Connect and install it on the repositories you want included in the digest.
102
+
103
+ 1. Open the Connect page in your Vercel team dashboard.
104
+
105
+ 2. Choose **Create Connector**.
106
+
107
+ 3. Select **GitHub** as the provider.
108
+
109
+ 4. Select the GitHub account or organization to connect.
110
+
111
+ 5. Install the connector on all repositories the digest should read, or select a smaller repository allowlist.
112
+
113
+ 6. Create the connector.
114
+
115
+ 7. In the connector settings, link it to the Vercel project and select the environments where it should be available.
116
+
117
+
118
+ Copy the GitHub connector id and store it in `.env.local` file as `CONNECTOR_GITHUB`, for example:
119
+
120
+ `CONNECTOR_GITHUB=github/digest-github`
121
+
122
+ The digest requests an app-scoped token from Vercel Connect:
123
+
124
+ `import { getToken } from "@vercel/connect"; export async function getGitHubToken() { const connector = process.env.CONNECTOR_GITHUB; if (!connector) { throw new Error("CONNECTOR_GITHUB is required."); } return getToken(connector, { subject: { type: "app" } }); }`
125
+
126
+ The connector installation determines which repositories the token can access. If a repository is missing from the digest, check the connector installation and project/environment link before changing code.
127
+
128
+ ## Create the Chat SDK bot
129
+
130
+ `lib/bot.ts` centralizes the Chat SDK instance. It requests short-lived Slack tokens from Vercel Connect and configures Redis state so proactive posts and webhook handling share the same bot.
131
+
132
+ `import { createSlackAdapter } from "@chat-adapter/slack"; import { createRedisState } from "@chat-adapter/state-redis"; import { getToken } from "@vercel/connect"; import { Chat } from "chat"; let bot: Chat | null = null; function getSlackBotToken() { const connector = process.env.CONNECTOR_SLACK; if (!connector) { throw new Error( "CONNECTOR_SLACK is required. Use the Vercel Connect connector id, such as slack/acme-slack.", ); } return getToken(connector, { subject: { type: "app" } }); } function verifyConnectForwardedSlackRequest() { return true; } export function getBot() { if (!bot) { bot = new Chat({ userName: process.env.BOT_USER_NAME ?? "digest-bot", adapters: { slack: createSlackAdapter({ botToken: getSlackBotToken, webhookVerifier: verifyConnectForwardedSlackRequest, }), }, state: createRedisState(), dedupeTtlMs: 600_000, }).registerSingleton(); } return bot; }`
133
+
134
+ ## Define digest schemas and types
135
+
136
+ `lib/digest/types.ts` keeps the workflow input, source contract, and model output schema in one place. The same schema validates the cron input and constrains the AI SDK response.
137
+
138
+ `import { z } from "zod"; export const DigestConfigSchema = z.object({ tone: z.enum(["terse", "detailed"]).default("terse"), maxSections: z.number().int().positive().max(8).default(4), include: z .array(z.string()) .default(["github-repositories", "github-issues"]), }); export const DigestInputSchema = z.object({ channelId: z.string().min(1), lookbackHours: z.number().int().positive().max(168).default(24), detailsUrl: z.url().optional(), config: DigestConfigSchema.default({ tone: "terse", maxSections: 4, include: ["github-repositories", "github-issues"], }), }); export const DigestChannelIdSchema = z.string().min(1); export type DigestConfig = z.infer<typeof DigestConfigSchema>; export type DigestInput = z.infer<typeof DigestInputSchema>; export type GatherActivity = (input: DigestInput) => Promise<unknown>; export const DigestSchema = z.object({ headline: z.string().min(1), sections: z .array( z.object({ label: z.string().min(1), body: z.string().min(1), }), ) .min(1), }); export type Digest = z.infer<typeof DigestSchema>;`
139
+
140
+ ## Enroll the digest channel
141
+
142
+ `lib/digest/enrollment.ts` turns the single `DIGEST_CHANNEL_ID` environment variable into the workflow input. Keep schedule-independent choices, like lookback window and tone, in code so the environment stays small.
143
+
144
+ `import { DigestChannelIdSchema, type DigestConfig, type DigestInput, } from "./types"; const DIGEST_LOOKBACK_HOURS = 24; const DIGEST_DETAILS_URL = "https://vercel.com"; const DIGEST_CONFIG: DigestConfig = { tone: "terse", maxSections: 4, include: ["github-repositories", "github-issues"], }; export async function loadDigestChannel(): Promise<DigestInput | null> { const raw = process.env.DIGEST_CHANNEL_ID; if (!raw) { return null; } const channelId = DigestChannelIdSchema.parse(raw); return { channelId, lookbackHours: DIGEST_LOOKBACK_HOURS, detailsUrl: DIGEST_DETAILS_URL, config: DIGEST_CONFIG, }; }`
145
+
146
+ ## Start the workflow from cron
147
+
148
+ Keep the cron route thin. It should verify the secret `CRON_SECRET`), load the single configured channel, start the workflow, and return the run id.
149
+
150
+ ``import { start } from "workflow/api"; import { loadDigestChannel } from "@/lib/digest/enrollment"; import { runDailyDigest } from "@/lib/digest/workflow"; export async function GET(request: Request) { const auth = request.headers.get("authorization"); if (!process.env.CRON_SECRET || auth !== `Bearer ${process.env.CRON_SECRET}`) { return new Response("Unauthorized", { status: 401 }); } const channel = await loadDigestChannel(); if (!channel) { return Response.json({ started: false, reason: "DIGEST_CHANNEL_ID is not set" }); } const run = await start(runDailyDigest, [channel]); return Response.json({ started: true, runId: run.runId }); }``
151
+
152
+ ### Scheduling the cron route
153
+
154
+ `vercel.json` tells Vercel Cron to call the digest route every day. The route still checks `CRON_SECRET`, so only authorized cron requests can start a workflow.
155
+
156
+ `{ "crons": [ { "path": "/api/cron/digest", "schedule": "0 8 * * *" } ] }`
157
+
158
+ Add the `CRON_SECRET` environment variable to your Vercel project before deploying the application.
159
+
160
+ ## Create GitHub digest workflow
161
+
162
+ Use one workflow to orchestrate the work, but keep expensive or failure-prone operations in separate steps. This makes the workflow retries and run inspection more useful.
163
+
164
+ `import { fetchGitHubActivity, generateDigest, postDigest } from "./step"; import type { DigestInput } from "./types"; export async function runDailyDigest(channel: DigestInput) { "use workflow"; try { const activity = await fetchGitHubActivity(channel); const digest = await generateDigest(channel, activity); await postDigest(channel, digest); return { posted: 1, failed: 0, channelId: channel.channelId }; } catch (error) { return { posted: 0, failed: 1, channelId: channel.channelId, error: error instanceof Error ? error.message : String(error), }; } }`
165
+
166
+ `import { generateText, Output } from "ai"; import { postDigestCard } from "./card"; import { buildPrompt } from "./prompt"; import { gatherProjectActivity } from "./sources"; import { DigestSchema, type Digest, type DigestInput } from "./types"; export async function fetchGitHubActivity(input: DigestInput) { "use step"; return gatherProjectActivity(input); } export async function generateDigest(input: DigestInput, activity: unknown) { "use step"; const { output } = await generateText({ model: process.env.DIGEST_MODEL ?? "anthropic/claude-haiku-4.5", output: Output.object({ schema: DigestSchema, name: "daily_digest", description: "A concise channel digest with headline and sections.", }), prompt: buildPrompt(activity, input.config), }); return output; } export async function postDigest(input: DigestInput, digest: Digest) { "use step"; return postDigestCard(input, digest); }`
167
+
168
+ ### Fetch GitHub issues with Search API
169
+
170
+ `lib/digest/sources.ts` owns the GitHub data contract. It gets a token from Vercel Connect, discovers connector-visible repositories, counts open issues, and returns a precomputed activity payload for the model.
171
+
172
+ Use the REST repository endpoints to discover connector-visible repositories. Then use batched GitHub GraphQL queries for issue counts and recent issue nodes.
173
+
174
+ > Avoid the GitHub Search API for per-repository issue counts. Its quota is much lower, and a scheduled digest can exhaust it quickly when it searches once per repository.
175
+
176
+ Here’s the API request flow:
177
+
178
+ * Fetch up to a bounded number of active repositories.
179
+
180
+ * Count public and private repositories in code.
181
+
182
+ * Query GraphQL in batches for `issues(states: OPEN) { totalCount }`.
183
+
184
+ * Fetch recent issue nodes only for the repositories shown in the digest. Keep recent issue nodes capped, for example 10 per repository.
185
+
186
+
187
+ The digest payload should give the model precomputed totals, not ask it to infer them. Build the payload in a complete helper function:
188
+
189
+ `type RepositorySummary = { name: string; visibility: "public" | "private"; }; type RecentIssue = { repository: string; title: string; url: string; createdAt: string; }; type IssueReport = { repository: RepositorySummary; openIssueCount: number; recentlyOpenedIssues: RecentIssue[]; }; function buildDigestActivity( repositories: RepositorySummary[], issueReports: IssueReport[], maxProcessed: number, ) { const publicRepositories = repositories.filter( (repository) => repository.visibility === "public", ); const privateRepositories = repositories.filter( (repository) => repository.visibility === "private", ); const recentlyOpened = issueReports .flatMap((report) => report.recentlyOpenedIssues) .sort((left, right) => right.createdAt.localeCompare(left.createdAt)); const openInPublicRepositories = sumOpenIssues(issueReports, "public"); const openInPrivateRepositories = sumOpenIssues(issueReports, "private"); return { repositories: { total: repositories.length, public: publicRepositories.length, private: privateRepositories.length, processed: Math.min(repositories.length, maxProcessed), maxProcessed, }, issues: { totalOpen: openInPublicRepositories + openInPrivateRepositories, openInPublicRepositories, openInPrivateRepositories, recentlyOpenedInLookback: recentlyOpened.length, recentlyOpened, byRepository: issueReports.map((report) => ({ repository: report.repository.name, visibility: report.repository.visibility, openIssueCount: report.openIssueCount, recentlyOpenedInLookback: report.recentlyOpenedIssues.length, })), }, }; } function sumOpenIssues( issueReports: IssueReport[], visibility: RepositorySummary["visibility"], ) { return issueReports .filter((report) => report.repository.visibility === visibility) .reduce((sum, report) => sum + report.openIssueCount, 0); }`
190
+
191
+ This keeps the prompt focused on writing the digest rather than doing arithmetic over raw issue lists. It also makes the posted summary easier to verify when you inspect a workflow run.
192
+
193
+ The source file should also throw `RetryableError` for transient GitHub failures and `FatalError` for bad configuration, so workflow retries only the failures that can recover.
194
+
195
+ ### Build the digest prompt
196
+
197
+ `lib/digest/prompt.ts` turns the structured GitHub activity into model instructions. Keep totals and source data in the activity object, then use the prompt only to control tone and output priorities.
198
+
199
+ ``import type { DigestConfig } from "./types"; export function buildPrompt(activity: unknown, config: DigestConfig) { return [ `Write a ${config.tone} daily GitHub issues digest.`, `Return at most ${config.maxSections} sections.`, `Only include these source areas: ${config.include.join(", ")}.`, "Include the total number of public repositories and private repositories.", "Include the total number of open issues in public repositories and private repositories.", "If repository totals may be limited by the fetch cap, call that out briefly.", "Highlight newly opened issues, affected repositories, owners, labels, and notable themes.", "If there are no recently opened issues, still summarize the repository and open issue totals.", "Use clear labels and concise bodies.", "Return a headline and sections that match the requested schema.", JSON.stringify(activity, null, 2), ].join("\n\n"); }``
200
+
201
+ ## Post a Chat SDK Card
202
+
203
+ Create the Slack message card in the `lib/digest/card.tsx` :
204
+
205
+ ``import { Actions, Card, CardText, LinkButton, Section } from "chat"; import { getBot } from "@/lib/bot"; import type { Digest, DigestInput } from "./types"; export async function postDigestCard(input: DigestInput, digest: Digest) { const channel = getBot().channel(input.channelId); await channel.post( Card({ title: digest.headline, children: [ ...digest.sections.map((section) => Section([CardText(`**${section.label}** ${section.body}`)]), ), Actions([ LinkButton({ url: input.detailsUrl ?? "https://vercel.com", label: "View details", }), ]), ], }), ); return { posted: true as const, channelId: input.channelId }; }``
206
+
207
+ ## Run the application locally
208
+
209
+ To trigger the cron route, start the app:
210
+
211
+ `pnpm dev`
212
+
213
+ In another terminal, run the following curl command to trigger the cron job:
214
+
215
+ `curl -H "Authorization: Bearer $CRON_SECRET" \ http://localhost:3000/api/cron/digest`
216
+
217
+ To inspect workflow runs include the steps timeline and retries during development, run the following command:
218
+
219
+ `pnpm exec workflow web`
220
+
221
+ You now have a daily digest bot with Chat SDK, Workflow SDK, and Vercel Connect. These primitives are extensible for more comprehensible use cases like a PR review bot or an incident watchlist to help you navigate public reports on GitHub.
222
+
223
+ ## Troubleshooting
224
+
225
+ Each item below lists a symptom, its cause, and the fix.
226
+
227
+ ### The cron route returns unauthorized
228
+
229
+ Make sure `CRON_SECRET` is set in the environment where the cron job runs, such as production. Vercel Cron sends `Authorization: Bearer $CRON_SECRET`, and the route fails closed when the variable is missing or the header does not match.
230
+
231
+ Symptom: A Vercel Cron run or manual request gets `401 Unauthorized`.
232
+
233
+ Cause: `CRON_SECRET` is missing from the environment where the route runs, or the request does not include `Authorization: Bearer $CRON_SECRET`.
234
+
235
+ Fix: Set `CRON_SECRET` in production and any preview environment where the cron route should run. For local tests, pull the environment with `vercel env pull` or set the variable in the shell before calling the route.
236
+
237
+ ### The Slack digest does not post
238
+
239
+ Symptom: The workflow runs, but no message appears in Slack.
240
+
241
+ Cause: The Slack app is not in the configured channel, `DIGEST_CHANNEL_ID` is not in Chat SDK channel id format, or `CONNECTOR_SLACK` is missing from the environment.
242
+
243
+ Fix: Invite the Slack app to the channel, set `DIGEST_CHANNEL_ID` to a value such as `slack:C123ABC`, and confirm the Slack connector is linked to the Vercel project environment.
244
+
245
+ ### GitHub repositories are missing
246
+
247
+ Symptom: The digest includes fewer repositories than expected.
248
+
249
+ Cause: The GitHub connector is installed on a limited repository allowlist, or the connector is not linked to the environment running the workflow.
250
+
251
+ Fix: Update the GitHub connector installation to include the repositories you want, then confirm `CONNECTOR_GITHUB` is available in the same Vercel environment as the workflow.
252
+
253
+ ### GitHub API rate limit reached
254
+
255
+ Check that issue counts use GraphQL batching, not GitHub Search API calls. Search API limits are easier to hit and should not be used once per repository.
256
+
257
+ Symptom: The `fetchGitHubActivity` step fails with a rate limit error.
258
+
259
+ Cause: GitHub rate limits can still apply to connector tokens, especially if the workflow queries too many repositories or uses the Search API for counts.
260
+
261
+ Fix: Keep repository fetching bounded, use GraphQL batching for issue counts, and avoid per-repository GitHub Search API calls. The template marks rate limits as `RetryableError` so workflow can retry after a delay.
262
+
263
+ ### Slack signing secret error
264
+
265
+ When Slack events are routed through Vercel Connect, Connect verifies the event before forwarding it. The Slack adapter still expects a `webhookVerifier`, so provide one that delegates trust to Connect for that route. If you receive events directly from Slack, use Slack's signing secret instead.
266
+
267
+ Symptom: The Slack webhook route fails with a signing secret error.
268
+
269
+ Cause: The Slack adapter requires a webhook verifier, but Vercel Connect has already verified Connect-forwarded Slack events before they reach the app.
270
+
271
+ Fix: For Connect-forwarded Slack events, use a verifier that delegates trust to Connect for that route. If events come directly from Slack, configure verification with Slack's signing secret instead.
272
+
273
+ ### Invalid JSX element: must be a Card element
274
+
275
+ Use the Chat SDK function-call Card API in workflow steps instead of JSX if the generated workflow route does not recognize the JSX Card shape at runtime.
276
+
277
+ Symptom: The Slack post step fails with `Invalid JSX element: must be a Card element`.
278
+
279
+ Cause: The workflow route may not recognize a JSX Card shape at runtime.
280
+
281
+ Fix: Use the Chat SDK function-call Card API in `lib/digest/card.tsx`, as shown above.
282
+
283
+ ### Local Connect token requests fail
284
+
285
+ Symptom: GitHub or Slack token requests work in production but fail locally.
286
+
287
+ Cause: The local `VERCEL_OIDC_TOKEN` written by `vercel env pull` has expired, or the local project is linked to the wrong Vercel project.
288
+
289
+ Fix: Run `vercel link` to confirm the project, then run `vercel env pull` again.
290
+
291
+ ## Related resources
292
+
293
+ * [Workflow SDK documentation](https://workflow-sdk.dev)
294
+
295
+ * [Chat SDK cards](https://chat-sdk.dev/docs/cards)
296
+
297
+
298
+ * [Vercel Connect](https://vercel.com/docs/connect) and [Vercel Connect CLI](https://vercel.com/docs/cli/connect)
299
+
300
+ * [Vercel AI Gateway](https://vercel.com/docs/ai-gateway)
301
+
302
+ * [Vercel OIDC](https://vercel.com/docs/oidc)
303
+
304
+ ---
305
+
306
+ [View full KB sitemap](/kb/sitemap.md)
@@ -45,7 +45,7 @@ Select your workspace and paste the following manifest:
45
45
 
46
46
  `display_information: name: My Bot description: A bot built with Chat SDK features: bot_user: display_name: My Bot always_online: true oauth_config: scopes: bot: - app_mentions:read - channels:history - channels:read - chat:write - groups:history - groups:read - im:history - im:read - mpim:history - mpim:read - reactions:read - reactions:write - users:read settings: event_subscriptions: request_url: https://your-domain.com/api/webhooks/slack bot_events: - app_mention - message.channels - message.groups - message.im - message.mpim interactivity: is_enabled: true request_url: https://your-domain.com/api/webhooks/slack org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false`
47
47
 
48
- Replace [`https://your-domain.com/api/webhooks/slack`](https://your-domain.com/api/webhooks/slack) with your deployed webhook URL, then click **Create**.
48
+ Replace `https://your-domain.com/api/webhooks/slack` with your deployed webhook URL, then click **Create**.
49
49
 
50
50
  After creating the app:
51
51
 
@@ -43,8 +43,6 @@ Before continuing, make sure you have a React app with Liveblocks Comments up an
43
43
 
44
44
  Install the Liveblocks adapter, Chat SDK, AI SDK, and other related packages:
45
45
 
46
- `npm install @liveblocks/chat-sdk-adapter @liveblocks/node chat @chat-adapter/state-redis ai zod`
47
-
48
46
  The `chat` package is the Chat SDK core. `@liveblocks/chat-sdk-adapter` is the Liveblocks platform adapter. `ai` is the AI SDK, which includes `ToolLoopAgent`. `zod` is used to define tool input schemas. `@chat-adapter/state-redis` is the [Redis state adapter,](https://chat-sdk.dev/adapters/official/redis) which handles thread subscriptions and distributed locking.
49
47
 
50
48
  ### 3\. Create a Liveblocks project
@@ -45,7 +45,7 @@ The `chat` package is the Chat SDK core. The `@chat-adapter/github` and `@chat-a
45
45
 
46
46
  1. Go to your repository **Settings**, then **Webhooks**, then **Add webhook**
47
47
 
48
- 2. Set **Payload URL** to [`https://your-domain.com/api/webhooks/github`](https://your-domain.com/api/webhooks/github)
48
+ 2. Set **Payload URL** to `https://your-domain.com/api/webhooks/github`
49
49
 
50
50
  3. Set **Content type** to `application/json`
51
51