---
title: Triage form submissions with Chat SDK
description: Build a Slack bot that triages form submissions with interactive cards. Forward, edit, or mark as spam without leaving Slack. Built with Chat SDK, Hono, and Resend.
url: "https://vercel.com/kb/guide/triage-form-submissions-with-chat-sdk"
published: 2026-04-02
last_updated: 2026-07-29
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Build a Slack bot that triages form submissions where your team already works. When someone submits a form on your website, the bot posts an interactive card to a Slack channel. A reviewer clicks a button to forward the submission via email, edit it before forwarding, or mark it as spam. The whole workflow happens inside Slack, with no separate dashboard or inbox to monitor.

The bot is built with [Chat SDK](https://chat-sdk.dev/), the unified TypeScript SDK for building chatbots that work across Slack, Microsoft Teams, Discord, and other platforms from a single codebase. You write your logic once, and Chat SDK handles the platform-specific details, such as Block Kit on Slack or Adaptive Cards on Teams.

Deploy the template now, or read on for a deeper look at how it all works.

**Chat SDK Form Bot Template**

Triage form submissions from Slack. Forward, edit, or mark as spam without leaving the channel.

[Deploy Template](https://vercel.link/chat-sdk-form-bot-template)

## Quick start with an AI coding agent

If you're working with an AI coding agent like Claude Code or Cursor, you can clone the template and hand off implementation with this prompt:

```plaintext
I want to build a form triage Slack bot using Chat SDK.

Clone the template repo at https://github.com/vercel-labs/chat-sdk-form-bot, install dependencies with pnpm, and walk me through setting up the environment variables in .env.local.

I need a Slack app, Redis (Upstash), and Resend configured.

After setup, help me deploy it to Vercel and test it with a curl POST to the /api/form endpoint.

When searching for information, check for applicable skill(s) first and review local documentation.
```

### Vercel Plugin

Turn your agent into a Vercel expert with this [plugin](https://vercel.com/docs/agent-resources/vercel-plugin); the [Chat SDK skill](https://skills.sh/vercel/chat/chat-sdk) is included. This plugin is optional; it’s not required to use Chat SDK or for this guide.

```bash
npx plugins add vercel/vercel-plugin
```

## Setup and deployment

### What you need before deploying

You'll need accounts with three services:

- **Slack** for the bot itself. Create a new app at [api.slack.com/apps](https://api.slack.com/apps).
  
- **Redis** for temporary submission storage. Any Redis provider works. [Upstash](https://vercel.com/marketplace/upstash) supports serverless deployments and has a free tier.
  
- **Resend** to send forwarded submissions via email. Sign up at [resend.com](https://resend.com/) and verify a sending domain.
  

### Configure your Slack app

1. Create a new Slack app from a manifest at [api.slack.com/apps](https://api.slack.com/apps). Use the [slack-manifest.json](https://github.com/vercel-labs/chat-sdk-form-bot/blob/main/slack-manifest.json) file included in the template repo. Replace the two `https://example.com` URLs with your production domain (e.g. `https://your-app.vercel.app/api/webhooks/slack`).
   
2. Install the app in your workspace and copy the **Bot User OAuth Token**.
   
3. Copy the **Signing Secret** from the **Basic Information** page.
   

### Environment variables

The template needs these environment variables:

```bash
# Slack
SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=...
SLACK_CHANNEL_ID=C...

# Redis
REDIS_URL=redis://...

# Resend
RESEND_API_KEY=re_...
RESEND_FROM_ADDRESS=bot@yourdomain.com
RESEND_FROM_NAME=Formbot                # Optional, defaults to "Formbot"

# Forwarding
FORWARD_EMAIL=team@yourdomain.com       # Where approved submissions are sent
FORWARD_ENDPOINT=                       # Optional: webhook URL for downstream systems
```

`SLACK_CHANNEL_ID` is the channel where submission cards will appear. You can find it by right-clicking a channel in Slack and selecting "View channel details" (the ID is at the bottom of the modal).

`FORWARD_ENDPOINT` is optional. If set, the bot will also POST the form data as JSON to that URL when a submission is forwarded. This is useful for piping approved submissions into a CRM, database, or another service.

### Deploy to Vercel

[Deploy the bot with one click](https://vercel.com/new/clone?repository-url=https://github.com/vercel-labs/chat-sdk-form-bot&env=SLACK_BOT_TOKEN,SLACK_SIGNING_SECRET,SLACK_CHANNEL_ID,REDIS_URL,RESEND_API_KEY,RESEND_FROM_ADDRESS,FORWARD_EMAIL), or clone the repo and deploy manually:

```bash
git clone https://github.com/vercel-labs/chat-sdk-form-bot.git
cd chat-sdk-form-bot
pnpm install
vercel
```

After deploying, update your Slack app's interactivity request URL to point to your production domain: `https://<your-vercel-domain>/api/webhooks/slack`.

### Test the form endpoint

Send a test submission:

```bash
curl -X POST https://<your-domain>/api/form \
  -H "Content-Type: application/json" \
  -d '{"Name": "Jane Doe", "Email": "jane@example.com", "Message": "Hello!"}'
```

A card should appear in your configured Slack channel within a few seconds, and you should see a JSON response in your terminal:

```json
{"status":"received","submissionId":"a1b2c3d4-..."}
```

### Local development

For local development, the template includes a Node.js server entrypoint:

```bash
pnpm dev
```

This starts a local server at `http://localhost:3000`. To receive Slack webhooks locally, use [ngrok](https://ngrok.com/) to create a public tunnel:

```bash
ngrok http 3000
```

Then update your Slack app's request URL to the ngrok URL (e.g. `https://abc123.ngrok-free.dev/api/webhooks/slack`).

## How the form triage bot works

The bot has three moving parts, including an HTTP endpoint that receives form data, a Redis store that temporarily holds submissions, and a Chat SDK bot that manages Slack interactions.

1. An external service (your website, a form provider, a webhook) sends a `POST` request to `/api/form` with JSON data
   
2. The bot generates a unique ID, stores the submission in Redis with a 7-day TTL, and posts an interactive card to a Slack channel
   
3. A reviewer sees the card and takes one of three actions:
   
   - **Forward Submission** sends a styled HTML email via [Resend](https://vercel.com/marketplace/resend) to a configured recipient, then updates the card to show who forwarded it
     
   - **Edit & Forward** opens a modal where the reviewer can modify fields before forwarding
     
   - **Mark as Spam** updates the card and deletes the submission from Redis
     

Once an action is taken, the card updates in place. The reviewer sees the result immediately in the same channel, without a confirmation page or context switch.

## Code walkthrough

The entire bot is about 200 lines across six files.

### Receiving submissions

The HTTP layer uses [Hono](https://vercel.com/docs/frameworks/backend/hono), a lightweight web framework. The form endpoint accepts any JSON body, assigns it a UUID, and hands it off to the bot:

```typescript
import { Hono } from "hono";
import { cors } from "hono/cors";
import { bot, postFormCard } from "./bot.js";

const app = new Hono();

app.use("/api/form", cors());

app.post("/api/form", (c) =>
  c.req.json().then(async (formData: Record<string, unknown>) => {
    const submissionId = crypto.randomUUID();
    await Promise.resolve(postFormCard(formData, submissionId));
    return c.json({ status: "received", submissionId });
  })
);

app.post("/api/webhooks/:platform", (c) => {
  const platform = c.req.param("platform");
  if (platform !== "slack") {
    return c.json({ error: `Unknown platform: ${platform}` }, 404);
  }
  return bot.webhooks.slack(c.req.raw);
});

export default app;
```

You can submit directly from a browser-based form on any origin. The webhook route at `/api/webhooks/:platform` handles Slack's interaction payloads (button clicks and modal submissions). The `:platform` parameter is already set up for adding other platforms later.

### Building the bot

The bot itself is a Chat SDK instance with a Slack adapter and Redis-backed state:

```typescript
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";

export const bot = new Chat({
  adapters: {
    slack: createSlackAdapter(),
  },
  state: createRedisState(),
  userName: "form-bot",
});
```

That's the entire bot setup. Chat SDK handles Slack's signature verification, payload parsing, and response formatting. The `state` object is a Redis adapter that Chat SDK uses for its internal state management, and the same Redis connection is reused to store form submissions.

### Interactive cards

Cards are built using Chat SDK's component functions. These are platform-agnostic: on Slack, they render as Block Kit, on Teams, they'd render as Adaptive Cards. Here's the card that appears when a new submission arrives:

```typescript
import { Actions, Button, Card, Fields, Field, Divider } from "chat";

export const newSubmissionCard = (
  formData: Record<string, unknown>,
  submissionId: string
) =>
  Card({
    children: [
      Fields(
        Object.entries(formData).map(([key, value]) =>
          Field({ label: key, value: String(value) })
        )
      ),
      Divider(),
      Actions([
        Button({
          id: "forward",
          label: "Forward Submission",
          style: "primary",
          value: submissionId,
        }),
        Button({
          id: "edit",
          label: "Edit & Forward",
          style: "primary",
          value: submissionId,
        }),
      ]),
      Actions([
        Button({
          id: "spam",
          label: "Mark as Spam",
          style: "danger",
          value: submissionId,
        }),
      ]),
    ],
    title: "New Form Submission",
  });
```

The form data is dynamic. Whatever keys and values are in the JSON body become fields on the card. A submission with `{"Name": "Jane", "Email": "jane@example.com"}` produces a card with two fields. A submission with ten fields produces a card with ten fields. No schema changes needed.

### Handling actions

When a reviewer clicks a button, Chat SDK routes the event to the right handler based on the action ID:

```typescript
bot.onAction(["forward", "spam", "edit"], async (event) => {
  const submissionId = event.value;
  if (!submissionId || !event.thread) return;

  const formData = await getSubmission(submissionId);
  if (!formData) return;

  if (event.actionId === "edit") {
    await event.openModal(editSubmissionModal(formData, submissionId));
  } else {
    const handler = event.actionId === "forward" ? handleForward : handleSpam;
    await handler(event, formData, submissionId, event.thread.id);
  }
});
```

The forward handler sends the email, optionally POSTs to a webhook endpoint, updates the Slack card, and cleans up Redis, all in parallel:

```typescript
const handleForward = async (event, formData, submissionId, threadId) => {
  const slack = bot.getAdapter("slack");

  await Promise.all([
    forwardToEmail(formData),
    forwardToEndpoint(formData),
    slack.editMessage(
      threadId,
      event.messageId,
      forwardedCard(formData, event.user.fullName)
    ),
    deleteSubmission(submissionId),
  ]);
};
```

After forwarding, the card is replaced with a read-only version showing who forwarded it and where. The submission is deleted from Redis since it's no longer needed.

## How to add Teams, Discord, or other platforms

Chat SDK supports multiple platforms from a single codebase. The cards, fields, and buttons you've already defined render natively on each platform, including Block Kit on Slack, Adaptive Cards on Teams, and Google Chat Cards.

To add Microsoft Teams or another platform, register an additional adapter:

```typescript
import { createSlackAdapter } from "@chat-adapter/slack";
import { createTeamsAdapter } from "@chat-adapter/teams";

export const bot = new Chat({
  adapters: {
    slack: createSlackAdapter(),
    teams: createTeamsAdapter(),
  },
  state,
  userName: "form-bot",
});
```

The existing webhook route in `src/index.ts` already uses a `:platform` parameter, so Teams webhooks would be handled at `/api/webhooks/teams` with no additional routing code.

You could also post to multiple platforms at once. For example, you might post form submissions to both a Slack channel and a Teams channel by calling `bot.channel()` with different platform prefixes:

```typescript
const slackChannel = bot.channel(`slack:${process.env.SLACK_CHANNEL_ID}`);
const teamsChannel = bot.channel(`teams:${process.env.TEAMS_CHANNEL_ID}`);

await Promise.all([
  slackChannel.post(newSubmissionCard(formData, submissionId)),
  teamsChannel.post(newSubmissionCard(formData, submissionId)),
]);
```

Modals are currently Slack-only, so the Edit & Forward button only works on Slack. On other platforms, you'd want to either hide that button or replace it with a different editing flow.

See the [Chat SDK adapter directory](https://chat-sdk.dev/adapters) for the full list of supported platforms.

Build a bot that lets you deploy projects from Slack, with a built-in approval flow.

[Run and track deploys from Slack](/kb/guide/run-and-track-deploys-from-slack)

## Related resources

- [Chat SDK Form Bot template](https://github.com/vercel-labs/chat-sdk-form-bot)
  
- [Chat SDK documentation](https://chat-sdk.dev/docs)
  
- [Chat SDK GitHub](https://github.com/vercel/chat)
  
- [The Complete Guide to Chat SDK](https://vercel.com/kb/guide/the-complete-guide-to-chat-sdk)
  
- [Resend documentation](https://resend.com/docs/api-reference/emails/send-email)
  
- [Hono documentation](https://hono.dev/docs/getting-started/vercel)
  

## FAQ

### 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](https://chat-sdk.dev/docs/usage) 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](https://vercel.com/kb/guide/how-to-build-a-slack-bot-with-next-js-and-redis), [Nuxt](https://vercel.com/kb/guide/create-a-discord-support-bot-with-nuxt-and-redis), [Hono](https://vercel.com/kb/guide/ship-a-github-code-review-bot-with-hono-and-redis), 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](https://chat-sdk.dev/adapters), but there is no requirement to deploy on Vercel to use it.

## More Chat SDK guides

- [Build AI agents with AI Gateway and AI SDK](/kb/guide/ai-gateway-and-ai-sdk): Build AI agents on Vercel with AI Gateway and AI SDK, then make them reliable, capable, and durable with Sandbox, Chat SDK, Vercel Connect, and Workflow.
- [Using Chat SDK and eve together](/kb/guide/chat-sdk-and-eve): Understand how eve and Chat SDK divide responsibilities, when to use eve's first-class channels versus the Chat SDK channel, and how to bridge any Chat SDK adapter into an eve agent
- [Build Claude Managed Agents with Chat SDK](/kb/guide/claude-managed-agents-chat-sdk): Ship a Slack research bot built on Claude Managed Agents and Chat SDK. One persistent session per thread, streamed briefs with sources, and no Slack credentials to manage.