Skip to content
Docs

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

Build a Slack bot using Chat SDK, AI SDK, and Files SDK that can list, read, upload, and delete files in Vercel Blob through tool calls. Vercel Connect supplies runtime Slack tokens and forwards triggers.

Ben SabicContent Engineer

You can build a Slack bot that browses, reads, uploads, and deletes files in Vercel 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 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.

Copy link to headingPrerequisites

Before you begin, make sure you have:

  • Node.js 20+ and a package manager (e.g., pnpm)
  • 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 with AI Gateway and Vercel Blob

Copy link to headingHow 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.

Copy link to headingSteps

Copy link to heading1. 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, Redis state adapter, and --connect flag to authenticate with Vercel Connect:

Terminal
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:

Terminal
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:

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

The ai package includes ToolLoopAgent and the AI Gateway provider. zod defines tool input schemas. files-sdk is the storage SDK, and @vercel/blob is the peer dependency its Vercel Blob adapter requires.

Copy link to heading2. 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, write a manifest, or copy any credentials.

Open the Connect page 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:

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

Copy link to heading3. 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.

Your bot uses Redis for thread subscriptions and distributed locking.

Link your project, then provision Upstash Redis 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.

To use the dashboard instead, create the database from your project's Storage 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.

Copy link to heading5. Configure Files SDK with the Vercel Blob adapter

Create lib/files.ts:

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.

Copy link to heading6. 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:

lib/bot.ts
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.

Copy link to headingCustom 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.

Copy link to heading7. 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:
Terminal
vercel

2. In the connector's settings, 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'."

Copy link to heading8. Deploy to production

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

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

Copy link to headingConfiguring 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.

Copy link to headingGranular 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. The same pattern wraps any write tool from createFileTools.

Copy link to headingRead-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.

Copy link to headingTightening 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.

Copy link to headingReading 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.

Copy link to headingTroubleshooting

Copy link to headingThe 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.

Copy link to headingToken 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.

Copy link to headingWebhook 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.

Copy link to headingMissing 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.

Copy link to headingDuplicate 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.

Copy link to headingTool 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.

Copy link to headingWrite 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.

Copy link to headinggetFileUrl 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.

Copy link to headingThread 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.

Copy link to headingHow 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:

lib/bot.ts
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 for the full list of supported platforms.

Build a software factory with Vercel Connect

Ship a software factory in minutes, with GitHub and Linear connectors provisioned for you from the first deployment.

Learn more

Copy link to headingFAQ

Is Chat SDK free?

Yes, Chat SDK is free and open source. You can install it from npm, use it in commercial projects, and contribute to its development without any licensing cost. The official platform adapters published under @chat-adapter/* are also free to use. You will still need to pay for any third-party services your bot relies on, such as your chat platform's API tier, your AI model provider, and your Redis or Postgres host.

Can I use any number of adapters?

Yes, you can register as many adapters as you need on a single Chat instance. Add each adapter to the adapters object when you create your bot, and the same handlers will run for events from every connected platform. This is the main reason to use Chat SDK: you write your bot logic once and it works across Slack, Teams, Discord, Linear, and any other platform you add later, without changing the rest of your code.

What frameworks does Chat SDK support?

Chat SDK supports nearly any JavaScript or TypeScript framework, so you can add it to your existing codebase without changing your stack. It works with Next.js, Nuxt, Hono, SvelteKit, Express, and other server frameworks that can handle HTTP requests. Each platform adapter exposes a webhook handler (such as bot.webhooks.slack) that you wire into your framework's routing, whether that is a Next.js route handler, a Hono route, a Nuxt server route, or something else.

Does Chat SDK only work on Vercel?

No, Chat SDK works anywhere you can run a Node.js or TypeScript server. It is framework-agnostic and runs on Next.js, Nuxt, Hono, and other frameworks, so you can deploy it to Vercel, AWS, Netlify, Cloudflare Workers, your own infrastructure, or any other host. Vercel maintains the SDK and the official adapters, but there is no requirement to deploy on Vercel to use it.

Related documentation

More Chat SDK guides