# How to build a Slack bot that manages files in Vercel Blob

**Author:** Ben Sabic

---

You can build a Slack bot that browses, reads, uploads, and deletes files in [Vercel Blob](https://vercel.com/storage/blob) by combining three SDKs. Chat SDK handles the Slack integration, AI SDK's `ToolLoopAgent` runs the agent loop, and Files SDK exposes your Blob store to the agent as a set of approval-gated tools. [Vercel Connect](https://vercel.com/connect) supplies the Slack credentials at runtime and forwards Slack events to your project. The result is a chat-first interface to your object storage, with read tools that run freely and write tools that prompt for approval by default.

You'll scaffold the bot with the `create-chat-sdk` CLI, which generates the Connect wiring for you, then create a Slack connector, connect a Blob store, and add an agent backed by Files SDK's `createFileTools` factory. From there, you'll configure per-tool approval gates and read-only mode to keep write operations safe in production.

> 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).

## Prerequisites

Before you begin, make sure you have:

- Node.js 20+ and a package manager (e.g., [pnpm](https://pnpm.io/))
  
- A Slack workspace where you can install an app
  
- Access to a Vercel team and project with Vercel Connect enabled
  
- Vercel CLI installed (`npm i -g vercel`)
  
- A [Vercel account](https://vercel.com/signup) with AI Gateway and Vercel Blob
  

## How it works

Each SDK has one job:

- Chat SDK receives Slack webhooks, normalizes them into events like `onNewMention` and `onSubscribedMessage`, and streams responses back through Slack's native streaming API.
  
- AI SDK provides `ToolLoopAgent`, which wraps a language model with tools and runs the loop: the model picks a tool, the SDK executes it, and the result feeds into the next step until the model finishes.
  
- Files SDK presents a `Files` interface over your Blob store and ships a `createFileTools` factory that turns it into ready-to-use AI SDK tools. The factory returns eight tools: four reads (`listFiles`, `getFileMetadata`, `downloadFile`, `getFileUrl`) that run freely and four writes (`uploadFile`, `deleteFile`, `copyFile`, `signUploadUrl`) that require approval by default.
  

Vercel Connect handles the Slack credentials, in both directions:

- Outbound: the `connectSlackAdapter` helper from `@vercel/connect/chat` supplies a `botToken` resolver that fetches a fresh, short-lived Slack token per API request via Connect's `getToken`.
  
- Inbound: Connect verifies each Slack event, then forwards it to a trigger destination you register on the connector. For this bot, that destination is your project at `/api/webhooks/slack`. The helper's `webhookVerifier` confirms each forwarded event carries a valid Vercel OIDC token.
  

Because OIDC verification replaces Slack's signed timestamp check, freshness relies on the short-lived token's expiry, and Connect has no built-in delivery de-duplication. Keep your webhook handlers idempotent.

Chat SDK accepts any `AsyncIterable<string>` as a message, so the agent's `fullStream` flows straight into `thread.post()` for real-time streaming in Slack.

## Steps

### 1\. Scaffold the project with create-chat-sdk

The `create-chat-sdk` CLI generates the Chat SDK skeleton with a single command. Pass the [Slack platform adapter](https://chat-sdk.dev/adapters/official/slack), [Redis state adapter](https://chat-sdk.dev/adapters/official/redis), and `--connect` flag to authenticate with Vercel Connect:

`pnpm create chat-sdk@latest my-files-bot --adapter slack redis --connect -y cd my-files-bot`

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

`npm create chat-sdk@latest -- my-files-bot --adapter slack redis --connect -y`

The CLI generates:

- The `Chat` configuration in `lib/bot.ts`. Because you passed `--connect`, it spreads the `connectSlackAdapter` helper into the Slack adapter and adds `@vercel/connect` to your dependencies.
  
- A dynamic webhook route that becomes `POST /api/webhooks/slack`.
  
- Starter handlers for mentions and thread replies, which you'll extend in step six.
  

The scaffolded app is webhook-only. Add the AI SDK and Files SDK packages yourself, since the agent is your code rather than part of the scaffold:

`pnpm add ai zod files-sdk @vercel/blob`

The `ai` package includes `ToolLoopAgent` and the [AI Gateway provider](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway). `zod` defines tool input schemas. `files-sdk` is the storage SDK, and `@vercel/blob` is the peer dependency its Vercel Blob adapter requires.

### 2\. Create a Slack connector with Vercel Connect

Vercel Connect creates and manages the Slack app for you. You don't register an app at [api.slack.com](http://api.slack.com), write a manifest, or copy any credentials.

Open the [Connect page](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fconnect) on your team's dashboard, then click Create Connector. Once you've done that:

1. Choose Slack as the provider.
   
2. Select your Slack workspace and name the app (e.g., `files-bot`). If you haven't connected a Slack workspace yet, connect and authorize one first, then return to the Connect page. Keep Triggers enabled so Slack events reach your project.
   
3. Open Advanced and set:
   
   - Bot Scopes: `chat:write`, `channels:history`, `channels:read`, `groups:history`, `im:history`, `mpim:history`, `reactions:write`, `users:read`, and `files:read`. The `files:read` scope lets the bot read files users add to Slack threads.
     
   - Trigger Event Types: `app_mention`, plus the message events for the surfaces the bot supports (`message.channels`, `message.groups`, `message.im`, `message.mpim`), so both new mentions and follow-up replies work.
     
4. Click Create Slack Connector, then Install to your Slack Workspace.
   
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 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:

`vercel connect create slack --name files-bot --triggers vercel connect attach slack/files-bot \ --project my-files-bot --environment production \ --triggers --trigger-path /api/webhooks/slack`

Because Connect manages the Slack app, Slack delivers events to Connect's intake URL (shown in the connector settings), not directly to your deployment. Connect verifies Slack events, then forwards them to the trigger destination you registered.

Finally, add the connector UID to your project's [environment variables](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fsettings%2Fenvironment-variables) so deployments can resolve it:

`SLACK_CONNECTOR=slack/files-bot`

Replace `slack/files-bot` with your connector UID from the Connect dashboard or `vercel connect list`.

### 3\. Create a Vercel Blob store

In your Vercel dashboard, open Storage, click Create Database, and create a new Blob store. Connect it to the project you'll be deploying to.

Vercel adds `BLOB_STORE_ID` to your project's environment variables, which the SDK pairs with the deployment's short-lived `VERCEL_OIDC_TOKEN` to authenticate via OIDC. No static Blob token to manage.

### 4\. Link your project and provision Redis

Your bot uses Redis for thread subscriptions and distributed locking.

Link your project, then provision [Upstash Redis](https://vercel.com/marketplace/upstash) with the Vercel CLI:

`vercel link vercel integration add upstash`

`vercel integration add` provisions a database, connects it to your project, and adds its connection variables to your environment. Other providers are also supported in the Vercel Marketplace, including [Redis](https://vercel.com/marketplace/redis).

To use the dashboard instead, create the database from your project's [Storage](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fstores) page.

Deployments also populate `VERCEL_OIDC_TOKEN` automatically. AI SDK uses it to authenticate requests to the AI Gateway. The Vercel Connect SDK `@vercel/connect` and Files SDK read it too, so there's no API key to generate or store.

### 5\. Configure Files SDK with the Vercel Blob adapter

Create `lib/files.ts`:

`import { Files } from "files-sdk"; import { vercelBlob } from "files-sdk/vercel-blob"; // The SDK authenticates via OIDC using VERCEL_OIDC_TOKEN and BLOB_STORE_ID, // both added automatically on Vercel deployments. export const files = new Files({ adapter: vercelBlob(), });`

The `vercelBlob` adapter defaults to `access: "public"`, which matches the most common Blob usage and lets the agent return CDN URLs from `getFileUrl`.

For private buckets, pass `vercelBlob({ access: "private" })`, which routes uploads through Vercel's private mode and reads through the API instead of a public URL. With private access, `getFileUrl` throws because no permanent URL exists; use `downloadFile` instead.

### 6\. Add the agent to the generated bot

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

`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 { connectSlackAdapter } from "@vercel/connect/chat"; import { createFileTools } from "files-sdk/ai-sdk"; import { files } from "./files"; const agent = new ToolLoopAgent({ model: "anthropic/claude-sonnet-4.6", instructions: "You are a file management assistant in a Slack workspace. " + "Use the file tools to help users browse, read, upload, and delete " + "files in their object storage. When a write operation is rejected, " + "explain what you were about to do and ask the user to confirm.", tools: createFileTools({ files }), }); export const bot = new Chat({ userName: "files-bot", adapters: { slack: createSlackAdapter({ ...connectSlackAdapter(process.env.SLACK_CONNECTOR!), }), }, 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) => { const { messages } = await thread.adapter.fetchMessages(thread.id, { limit: 20, }); const history = await toAiMessages(messages); const result = await agent.stream({ prompt: history }); await thread.post(result.fullStream); });`

`createFileTools({ files })` returns all eight file tools. Read tools run as soon as the agent calls them, while write tools are gated by AI SDK's tool approval flow.

The `connectSlackAdapter` spread is the Connect wiring the CLI generated.

It returns two fields:

- A `botToken` resolver. The adapter calls it on each Slack API request, and it fetches a fresh, short-lived token via Connect's `getToken`. The token's subject is pinned to `{ type: "app" }`, so the bot acts as the application itself, and the scopes you set on the connector determine what the token can do.
  
- A `webhookVerifier` that validates the Vercel OIDC token Connect attaches to each forwarded event. The default verifier matches the deployment's own project and environment (`projectId` defaults to `VERCEL_PROJECT_ID`, and `environment` to `VERCEL_TARGET_ENV`, then `VERCEL_ENV`), fails closed when those values are missing, and pins the issuer to `https://oidc.vercel.com`. It also replaces Slack's 5-minute timestamp check.
  

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

The event handlers work like this:

- `onNewMention` fires the first time someone @mentions the bot in a channel. `thread.subscribe()` opts the thread into future `onSubscribedMessage` events, so the bot keeps responding without further mentions.
  
- `onSubscribedMessage` fetches the 20 most recent thread messages and converts them with `toAiMessages` into the AI SDK `ModelMessage[]` shape, preserving roles, attachments, and chronological order, so the agent has conversation context.    - Both handlers post `result.fullStream` rather than `textStream` because it preserves paragraph breaks between tool-calling steps, which Slack renders cleanly.    You don't need to modify the generated webhook route. With trigger forwarding enabled, Connect POSTs verified Slack payloads to `/api/webhooks/slack`, the `webhookVerifier` runs before the adapter parses the body, and the route's `waitUntil` keeps your handlers running after the HTTP response returns, which is required on serverless platforms where the function would otherwise terminate early. ### Custom webhook verification (optional) 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: `import { connectSlackAdapter, createConnectWebhookVerifier, } from "@vercel/connect/chat"; createSlackAdapter({ ...connectSlackAdapter(process.env.SLACK_CONNECTOR!), webhookVerifier: createConnectWebhookVerifier({ environment: ["production", "preview"], }), });` Avoid hardcoding `environment: "production"` unless you only forward to production, since that would reject preview and development deployments. ### 7\. Test the agent 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. 1. Deploy a preview build to receive the Slack events:     `vercel` 2\. 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`. The default verifier only accepts tokens for the deployment's own environment, so the connector must forward to the environment you're testing.

3\. Invite the bot to a channel: `/invite @Files Bot`.

4\. @mention the bot and ask it to list files: "Show me what's in the bucket." The agent calls `listFiles` and streams the response back into the thread. To test a write operation end-to-end before building an approval flow, temporarily pass `requireApproval: false` to `createFileTools`, redeploy, and ask the bot to "Upload a file called test.txt with the contents 'hello world'."

### 8\. Deploy to production

Once you've tested the bot, deploy it to production:

`vercel --prod`

Confirm the production environment is linked to the connector and registered as a trigger destination at `/api/webhooks/slack`. Redis comes from the Upstash integration, Blob authentication from OIDC via the store connection, and Slack credentials from Connect at runtime, so `SLACK_CONNECTOR` is the only variable you add to the project yourself.

## Configuring approval and read-only mode

The default `createFileTools({ files })` gates every write tool with approval and leaves reads open. That's a reasonable default, but you'll often want to tune it.

### Granular approval

Pass an object to `requireApproval` to opt individual tools in or out:

`const tools = createFileTools({ files, requireApproval: { deleteFile: true, signUploadUrl: true, uploadFile: false, copyFile: false, }, });`

Unspecified entries default to `true`, so it's safe to opt in only the cases you trust. In the example above, the agent can upload and copy without prompting, but still needs approval for deletes and pre-signed upload URLs.

For a production-grade approval handler that pauses the workflow in Slack until a human clicks Approve or Deny, see [Human-in-the-Loop with Chat SDK and Workflow SDK](https://vercel.com/kb/guide/human-in-the-loop-with-chat-sdk-and-workflow-sdk). The same pattern wraps any write tool from `createFileTools`.

### Read-only mode

For a bot that should only browse and summarize files, pass `readOnly: true`:

`const tools = createFileTools({ files, readOnly: true }); // Returns only: listFiles, getFileMetadata, downloadFile, getFileUrl`

Read-only mode drops every write tool from the toolset, so approval configuration becomes irrelevant. This is useful when the bot's job is to find a file and hand the user a download URL rather than mutate the bucket.

### Tightening descriptions per tool

To scope a tool's behavior to your domain, use `overrides` to patch its description without touching the underlying implementation:

`const tools = createFileTools({ files, overrides: { listFiles: { description: "List files in the current Slack workspace's bucket", }, deleteFile: { title: "Remove file" }, }, });`

`execute`, `inputSchema`, and `outputSchema` are intentionally not overridable. Override descriptions to improve tool selection, override titles for clearer approval UIs, and let the SDK keep ownership of the I/O contract.

## Reading Slack uploads with toAiMessages

When users upload files to a Slack thread, `toAiMessages` automatically includes them in the AI SDK message stream. Images become `image` parts and supported text files (JSON, XML, YAML, plain text) become `file` parts, both with base64 data. Video and audio attachments are skipped, with a `console.warn` by default. This relies on the `files:read` scope you set on the connector in step two.

A user can drag a CSV into the thread and ask, "Upload this to reports/q4.csv," and the agent will see the file contents in its message history and can call `uploadFile` with that content. No extra wiring needed.

To customize how unsupported attachments are handled, pass `onUnsupportedAttachment`:

``const history = await toAiMessages(messages, { onUnsupportedAttachment: (attachment, message) => { logger.warn( `Skipped ${attachment.type} in message ${message.id}`, ); }, });``

PDFs and other unrecognized MIME types are silently skipped. If you need to handle them, fetch the raw attachment via `attachment.fetchData()` in your handler and route it directly to `files.upload()` outside the agent loop.

## Troubleshooting

### The bot doesn't respond to mentions

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 `app_mention` and the relevant message events. You can review all of this in the connector's settings. Verify deployment logs on `/api/webhooks/slack` for 401s before debugging token issues.

### Token requests fail or return unauthorized

Make sure the project is linked (`vercel link`) and that the connector is linked to it for the current environment. Confirm `SLACK_CONNECTOR` is set to the correct connector UID, the connector is installed in your workspace, and the scopes the bot uses are enabled on the connector.

### Webhook returns 401

- Confirm `connectSlackAdapter` is spread into `createSlackAdapter`, 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, every request is rejected.
  
- Confirm the request is coming from Connect (trigger destination configured), not from Slack 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.    ### Missing Authorization bearer token The event reached your app without Connect forwarding, meaning the trigger destination isn't registered or points to the wrong path. Fix the trigger path to `/api/webhooks/slack` and link the correct environment. ### Duplicate replies or duplicate writes OIDC verification replaces Slack's native signature and timestamp check, and Connect has no built-in delivery de-duplication. If a forwarded event is delivered more than once, your handlers run multiple times, which for this bot can mean a duplicate write against your Blob store. Keep webhook handlers idempotent, for example by tracking processed event IDs in Redis. ### Tool calls fail silently If the agent calls a tool but no result appears, check your server logs for thrown errors. Common causes include a Blob store not connected to the project, an invalid file key, or a `vercelBlob({ access: "private" })` adapter trying to call `getFileUrl`. AI SDK surfaces tool execution errors back to the model, which may attempt to recover; add explicit error handling in your tools if you need to control how the model sees the failure. ### Write operations are always rejected By default, write tools require approval. Until you build an approval handler that resolves these requests, every write call will be denied. For development, pass `requireApproval: false` to disable the gate, or `requireApproval: { deleteFile: true }` to leave only the most destructive operations gated. ### getFileUrl throws on private blobs `vercelBlob({ access: "private" })` has no permanent public URL, so `getFileUrl` (which wraps `url()`) throws an error. Use `downloadFile` to fetch private blob contents through the API instead. If you need both public and private blobs in the same bot, construct two `Files` instances with different adapters and route the agent to the right one through separate tools. ### Thread history grows too large For long-running threads, the conversation history can exceed the model's context window. Limit the number of messages passed to the agent (the example above uses `limit: 20`) or summarize older messages in a separate step. ## How to add Teams, Discord, or other platforms Chat SDK supports multiple platforms from a single codebase. The event handlers and agent logic you've already defined work identically across all of them, since the SDK normalizes messages, threads, and reactions into a consistent format. To add Microsoft Teams or another platform, register an additional adapter: `import { createSlackAdapter } from "@chat-adapter/slack"; import { createTeamsAdapter } from "@chat-adapter/teams"; import { connectSlackAdapter } from "@vercel/connect/chat"; export const bot = new Chat({ adapters: { slack: createSlackAdapter({ ...connectSlackAdapter(process.env.SLACK_CONNECTOR!), }), teams: createTeamsAdapter(), }, state, userName: "files-bot", });` The webhook route already uses a `:platform` parameter, so Teams webhooks are handled at `/api/webhooks/teams` with no additional routing code. Connect trigger forwarding currently supports Slack, GitHub, and Linear, so adapters for other platforms manage their own credentials and receive webhooks directly. Streaming behavior varies by platform. Slack uses its native streaming API for smooth real-time updates, while Teams, Discord, and Google Chat fall back to a post-then-edit pattern that throttles updates to avoid rate limits. You can adjust the update interval with the `streamingUpdateIntervalMs` option when creating your `Chat` instance. See the [Chat SDK adapter directory](https://chat-sdk.dev/adapters) for the full list of supported platforms.

## Related resources and next steps

- [Vercel Connect overview](https://vercel.com/docs/connect)
  
- [Vercel Connect quickstart guide](https://vercel.com/docs/connect/quickstart)
  
- [Vercel Connect SDK reference](https://vercel.com/docs/connect/ts-sdk-reference)
  
- [Chat SDK CLI reference](https://chat-sdk.dev/docs/create-chat-sdk)
  
- [Chat SDK Vercel Connect integration docs](https://chat-sdk.dev/docs/vercel-connect)
  
- [Chat SDK AI utilities overview](https://chat-sdk.dev/docs/ai)
  
- [Files SDK Vercel Blob adapter](https://files-sdk.dev/adapters/vercel-blob)
  
- [AI SDK agents documentation](https://ai-sdk.dev/docs/agents/building-agents)
  
- [Vercel Blob documentation](https://vercel.com/docs/storage/vercel-blob)
  
- [AI Gateway documentation](https://vercel.com/docs/ai-gateway)

---

[View full KB sitemap](/kb/sitemap.md)
