chat 4.26.0 → 4.28.1

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.
Files changed (50) hide show
  1. package/dist/{chunk-OPV5U4WG.js → chunk-V25FKIIL.js} +44 -1
  2. package/dist/index.d.ts +485 -33
  3. package/dist/index.js +862 -135
  4. package/dist/{jsx-runtime-DxATbnrP.d.ts → jsx-runtime-DxGwoLu2.d.ts} +49 -5
  5. package/dist/jsx-runtime.d.ts +1 -1
  6. package/dist/jsx-runtime.js +1 -1
  7. package/docs/actions.mdx +52 -1
  8. package/docs/adapters.mdx +43 -37
  9. package/docs/api/cards.mdx +4 -0
  10. package/docs/api/chat.mdx +172 -6
  11. package/docs/api/index.mdx +2 -0
  12. package/docs/api/markdown.mdx +28 -5
  13. package/docs/api/message.mdx +58 -1
  14. package/docs/api/meta.json +2 -0
  15. package/docs/api/modals.mdx +50 -0
  16. package/docs/api/postable-message.mdx +55 -1
  17. package/docs/api/thread.mdx +33 -3
  18. package/docs/api/transcripts.mdx +220 -0
  19. package/docs/cards.mdx +6 -0
  20. package/docs/concurrency.mdx +4 -0
  21. package/docs/contributing/building.mdx +73 -1
  22. package/docs/contributing/publishing.mdx +33 -0
  23. package/docs/conversation-history.mdx +137 -0
  24. package/docs/direct-messages.mdx +13 -4
  25. package/docs/ephemeral-messages.mdx +1 -1
  26. package/docs/error-handling.mdx +15 -3
  27. package/docs/files.mdx +2 -1
  28. package/docs/getting-started.mdx +1 -11
  29. package/docs/index.mdx +7 -5
  30. package/docs/meta.json +14 -5
  31. package/docs/modals.mdx +97 -1
  32. package/docs/posting-messages.mdx +7 -3
  33. package/docs/streaming.mdx +74 -18
  34. package/docs/subject.mdx +53 -0
  35. package/docs/threads-messages-channels.mdx +43 -0
  36. package/docs/usage.mdx +11 -2
  37. package/package.json +3 -2
  38. package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +180 -0
  39. package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +134 -0
  40. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +220 -0
  41. package/resources/guides/run-and-track-deploys-from-slack.md +270 -0
  42. package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +147 -0
  43. package/resources/guides/triage-form-submissions-with-chat-sdk.md +178 -0
  44. package/resources/templates.json +19 -0
  45. package/docs/guides/code-review-hono.mdx +0 -241
  46. package/docs/guides/discord-nuxt.mdx +0 -227
  47. package/docs/guides/durable-chat-sessions-nextjs.mdx +0 -337
  48. package/docs/guides/meta.json +0 -10
  49. package/docs/guides/scheduled-posts-neon.mdx +0 -447
  50. package/docs/guides/slack-nextjs.mdx +0 -234
@@ -1,447 +0,0 @@
1
- ---
2
- title: Schedule Slack posts with Next.js, Workflow, and Neon
3
- description: This guide walks through scheduling Slack channel posts with Chat SDK, persisting schedules in Neon, and using Workflow sleep timers for durable delivery.
4
- type: guide
5
- prerequisites: []
6
- related:
7
- - /docs/guides/slack-nextjs
8
- - /docs/slash-commands
9
- - /docs/state/postgres
10
- - /docs/api/channel
11
- - /docs/api/chat
12
- ---
13
-
14
- Chat SDK already supports native scheduled messages on Slack with `thread.schedule()` and `channel.schedule()`. That is useful for simple Slack-only cases.
15
-
16
- This guide uses a different pattern: Workflow owns the timer with `sleep()`, and Neon stores the schedule as your source of truth. That gives you a more durable scheduling system you can extend with cancellation, rescheduling, approvals, and cross-platform delivery later.
17
-
18
- The example uses Slack slash commands to schedule top-level channel posts.
19
-
20
- ## Prerequisites
21
-
22
- - Node.js 18+
23
- - [pnpm](https://pnpm.io) (or npm/yarn)
24
- - A Next.js App Router project
25
- - A Slack workspace where you can install apps
26
- - A Neon Postgres database
27
-
28
- <Callout type="info">
29
- If you still need Slack app setup and the webhook route, start with [Slack bot with Next.js and Redis](/docs/guides/slack-nextjs), then come back here and swap the state adapter to PostgreSQL.
30
- </Callout>
31
-
32
- ## Install the dependencies
33
-
34
- Install Chat SDK, the Slack adapter, the PostgreSQL state adapter, Workflow, and `pg`:
35
-
36
- ```sh title="Terminal"
37
- pnpm add chat @chat-adapter/slack @chat-adapter/state-pg workflow pg
38
- ```
39
-
40
- ## Create a Neon database
41
-
42
- Create a Neon project, copy its **pooled** Postgres connection string, and store it as `POSTGRES_URL`.
43
-
44
- `createPostgresState()` already supports `POSTGRES_URL` and `DATABASE_URL`, so the same Neon database can back both Chat SDK state and your app's schedule table.
45
-
46
- ## Configure environment variables
47
-
48
- Create a `.env.local` file:
49
-
50
- ```bash title=".env.local"
51
- SLACK_BOT_TOKEN=xoxb-your-bot-token
52
- SLACK_SIGNING_SECRET=your-signing-secret
53
- POSTGRES_URL=postgresql://user:password@your-neon-host-pooler.neon.tech/neondb?sslmode=require
54
- ```
55
-
56
- ## Enable Workflow in Next.js
57
-
58
- Wrap your Next.js config with `withWorkflow()` so `"use workflow"` and `"use step"` directives are compiled:
59
-
60
- ```typescript title="next.config.ts" lineNumbers
61
- import { withWorkflow } from "workflow/next";
62
- import type { NextConfig } from "next";
63
-
64
- const nextConfig: NextConfig = {
65
- // ...your existing config
66
- };
67
-
68
- export default withWorkflow(nextConfig);
69
- ```
70
-
71
- <Callout type="info">
72
- If your app uses `proxy.ts`, exclude `.well-known/workflow/` from the matcher so Workflow's internal routes are not intercepted.
73
- </Callout>
74
-
75
- ## Add slash commands to Slack
76
-
77
- Add two slash commands to your Slack app and point both at your webhook route:
78
-
79
- ```yaml title="slack-manifest.yml"
80
- features:
81
- slash_commands:
82
- - command: /remind
83
- url: https://your-domain.com/api/webhooks/slack
84
- description: Schedule a channel post
85
- usage_hint: "<ISO-8601 timestamp> <message>"
86
- should_escape: false
87
- - command: /remind-cancel
88
- url: https://your-domain.com/api/webhooks/slack
89
- description: Cancel a scheduled post
90
- usage_hint: "<schedule-id>"
91
- should_escape: false
92
- ```
93
-
94
- If you're starting from scratch rather than the Slack guide, make sure your app also has the `commands` and `chat:write` bot scopes in **OAuth & Permissions**, then reinstall the app after changing scopes.
95
-
96
- This guide keeps parsing simple by accepting an ISO timestamp like `2026-03-15T09:00:00Z`.
97
-
98
- ## Create the schedule table in Neon
99
-
100
- Run this SQL in the Neon SQL Editor:
101
-
102
- ```sql title="schema.sql"
103
- CREATE TABLE scheduled_posts (
104
- id text PRIMARY KEY,
105
- channel_id text NOT NULL,
106
- message text NOT NULL,
107
- post_at timestamptz NOT NULL,
108
- status text NOT NULL CHECK (status IN ('pending', 'sent', 'canceled')),
109
- workflow_run_id text,
110
- created_by text NOT NULL,
111
- sent_at timestamptz,
112
- canceled_at timestamptz,
113
- created_at timestamptz NOT NULL DEFAULT now()
114
- );
115
-
116
- CREATE INDEX scheduled_posts_status_post_at_idx
117
- ON scheduled_posts (status, post_at);
118
- ```
119
-
120
- ## Create the Postgres client
121
-
122
- Use a shared `pg` pool for your own schedule records:
123
-
124
- ```typescript title="lib/db.ts" lineNumbers
125
- import pg from "pg";
126
-
127
- if (!process.env.POSTGRES_URL) {
128
- throw new Error("POSTGRES_URL is required");
129
- }
130
-
131
- export const pool = new pg.Pool({
132
- connectionString: process.env.POSTGRES_URL,
133
- });
134
- ```
135
-
136
- ## Create the Chat instance
137
-
138
- Use Neon for Chat SDK state by switching to the PostgreSQL state adapter:
139
-
140
- ```typescript title="lib/bot.ts" lineNumbers
141
- import { createSlackAdapter } from "@chat-adapter/slack";
142
- import { createPostgresState } from "@chat-adapter/state-pg";
143
- import { Chat } from "chat";
144
-
145
- export const bot = new Chat({
146
- userName: "reminder-bot",
147
- adapters: {
148
- slack: createSlackAdapter(),
149
- },
150
- state: createPostgresState(),
151
- });
152
- ```
153
-
154
- ## Add schedule helpers
155
-
156
- Create a small data layer for inserting, loading, updating, and canceling scheduled posts:
157
-
158
- ```typescript title="lib/scheduled-posts.ts" lineNumbers
159
- import { randomUUID } from "node:crypto";
160
- import { pool } from "@/lib/db";
161
-
162
- export interface ScheduledPost {
163
- id: string;
164
- channelId: string;
165
- message: string;
166
- postAt: string;
167
- status: "pending" | "sent" | "canceled";
168
- workflowRunId: string | null;
169
- }
170
-
171
- export async function createScheduledPost(input: {
172
- channelId: string;
173
- createdBy: string;
174
- message: string;
175
- postAt: Date;
176
- }) {
177
- const id = randomUUID();
178
-
179
- await pool.query(
180
- `INSERT INTO scheduled_posts (
181
- id, channel_id, message, post_at, status, created_by
182
- ) VALUES ($1, $2, $3, $4, 'pending', $5)`,
183
- [id, input.channelId, input.message, input.postAt, input.createdBy]
184
- );
185
-
186
- return { id, ...input, postAt: input.postAt.toISOString() };
187
- }
188
-
189
- export async function getScheduledPost(id: string): Promise<ScheduledPost | null> {
190
- const result = await pool.query(
191
- `SELECT id, channel_id, message, post_at, status, workflow_run_id
192
- FROM scheduled_posts
193
- WHERE id = $1
194
- LIMIT 1`,
195
- [id]
196
- );
197
-
198
- if (result.rows.length === 0) {
199
- return null;
200
- }
201
-
202
- const row = result.rows[0];
203
- return {
204
- id: row.id as string,
205
- channelId: row.channel_id as string,
206
- message: row.message as string,
207
- postAt: (row.post_at as Date).toISOString(),
208
- status: row.status as ScheduledPost["status"],
209
- workflowRunId: (row.workflow_run_id as string | null) ?? null,
210
- };
211
- }
212
-
213
- export async function attachWorkflowRun(id: string, runId: string) {
214
- await pool.query(
215
- `UPDATE scheduled_posts
216
- SET workflow_run_id = $2
217
- WHERE id = $1`,
218
- [id, runId]
219
- );
220
- }
221
-
222
- export async function markScheduledPostSent(id: string) {
223
- await pool.query(
224
- `UPDATE scheduled_posts
225
- SET status = 'sent', sent_at = now()
226
- WHERE id = $1 AND status = 'pending'`,
227
- [id]
228
- );
229
- }
230
-
231
- export async function cancelScheduledPost(id: string, createdBy: string) {
232
- const result = await pool.query(
233
- `UPDATE scheduled_posts
234
- SET status = 'canceled', canceled_at = now()
235
- WHERE id = $1 AND created_by = $2 AND status = 'pending'`,
236
- [id, createdBy]
237
- );
238
-
239
- return (result.rowCount ?? 0) > 0;
240
- }
241
- ```
242
-
243
- ## Create the workflow
244
-
245
- The workflow only orchestrates. All Postgres access and Slack posting stay in `"use step"` functions:
246
-
247
- ```typescript title="workflows/send-scheduled-post.ts" lineNumbers
248
- import { sleep } from "workflow";
249
- import { bot } from "@/lib/bot";
250
- import {
251
- getScheduledPost,
252
- markScheduledPostSent,
253
- } from "@/lib/scheduled-posts";
254
-
255
- async function loadScheduledPost(scheduleId: string) {
256
- "use step";
257
- return getScheduledPost(scheduleId);
258
- }
259
-
260
- async function postToChannel(channelId: string, message: string) {
261
- "use step";
262
-
263
- await bot.initialize();
264
-
265
- const channel = bot.channel(channelId);
266
- await channel.post(message);
267
- }
268
-
269
- async function completeScheduledPost(scheduleId: string) {
270
- "use step";
271
- await markScheduledPostSent(scheduleId);
272
- }
273
-
274
- export async function sendScheduledPost(scheduleId: string) {
275
- "use workflow";
276
-
277
- const schedule = await loadScheduledPost(scheduleId);
278
- if (!schedule || schedule.status !== "pending") {
279
- return;
280
- }
281
-
282
- await sleep(new Date(schedule.postAt));
283
-
284
- // Re-check the row after waking up in case it was canceled.
285
- const fresh = await loadScheduledPost(scheduleId);
286
- if (!fresh || fresh.status !== "pending") {
287
- return;
288
- }
289
-
290
- await postToChannel(fresh.channelId, fresh.message);
291
- await completeScheduledPost(scheduleId);
292
- }
293
- ```
294
-
295
- This is the key Workflow pattern:
296
-
297
- - schedule creation writes a row in Neon first
298
- - the workflow sleeps until `postAt`
299
- - the workflow reloads the row after waking up
300
- - canceled schedules no-op instead of posting
301
-
302
- That makes cancellation simple and durable.
303
-
304
- ## Register the slash command handlers
305
-
306
- Create a side-effect module that parses commands, writes rows to Neon, starts workflows, and handles cancellation:
307
-
308
- ```typescript title="lib/reminder-handlers.ts" lineNumbers
309
- import { start } from "workflow/api";
310
- import { bot } from "@/lib/bot";
311
- import {
312
- attachWorkflowRun,
313
- cancelScheduledPost,
314
- createScheduledPost,
315
- } from "@/lib/scheduled-posts";
316
- import { sendScheduledPost } from "@/workflows/send-scheduled-post";
317
-
318
- function parseReminderInput(input: string) {
319
- const trimmed = input.trim();
320
- const firstSpace = trimmed.indexOf(" ");
321
-
322
- if (firstSpace === -1) {
323
- return null;
324
- }
325
-
326
- const timestamp = trimmed.slice(0, firstSpace);
327
- const message = trimmed.slice(firstSpace + 1).trim();
328
- const postAt = new Date(timestamp);
329
-
330
- if (!message || Number.isNaN(postAt.getTime()) || postAt <= new Date()) {
331
- return null;
332
- }
333
-
334
- return { message, postAt };
335
- }
336
-
337
- bot.onSlashCommand("/remind", async (event) => {
338
- const parsed = parseReminderInput(event.text);
339
-
340
- if (!parsed) {
341
- await event.channel.postEphemeral(
342
- event.user,
343
- "Usage: `/remind 2026-03-15T09:00:00Z Review launch checklist`",
344
- { fallbackToDM: false }
345
- );
346
- return;
347
- }
348
-
349
- const scheduled = await createScheduledPost({
350
- channelId: event.channel.id,
351
- createdBy: event.user.userId,
352
- message: parsed.message,
353
- postAt: parsed.postAt,
354
- });
355
-
356
- const run = await start(sendScheduledPost, [scheduled.id]);
357
- await attachWorkflowRun(scheduled.id, run.runId);
358
-
359
- await event.channel.postEphemeral(
360
- event.user,
361
- `Scheduled \`${scheduled.id}\` for ${parsed.postAt.toISOString()}.`,
362
- { fallbackToDM: false }
363
- );
364
- });
365
-
366
- bot.onSlashCommand("/remind-cancel", async (event) => {
367
- const scheduleId = event.text.trim();
368
-
369
- if (!scheduleId) {
370
- await event.channel.postEphemeral(
371
- event.user,
372
- "Usage: `/remind-cancel <schedule-id>`",
373
- { fallbackToDM: false }
374
- );
375
- return;
376
- }
377
-
378
- const canceled = await cancelScheduledPost(scheduleId, event.user.userId);
379
-
380
- await event.channel.postEphemeral(
381
- event.user,
382
- canceled
383
- ? `Canceled \`${scheduleId}\`.`
384
- : `No pending schedule found for \`${scheduleId}\`.`,
385
- { fallbackToDM: false }
386
- );
387
- });
388
- ```
389
-
390
- The cancellation path is intentionally simple. It only updates the database row, and it only allows the creator of the schedule to cancel it. If the workflow wakes up later, it re-checks the row and exits without posting.
391
-
392
- If you want workspace admins or moderators to cancel other users' schedules, extend the cancellation query with your own permission rules instead of removing the ownership check.
393
-
394
- ## Create the webhook route
395
-
396
- Import the handler module once so the slash command handlers are registered:
397
-
398
- ```typescript title="app/api/webhooks/[platform]/route.ts" lineNumbers
399
- import "@/lib/reminder-handlers";
400
- import { after } from "next/server";
401
- import { bot } from "@/lib/bot";
402
-
403
- type Platform = keyof typeof bot.webhooks;
404
-
405
- export async function POST(
406
- request: Request,
407
- context: RouteContext<"/api/webhooks/[platform]">
408
- ) {
409
- const { platform } = await context.params;
410
-
411
- const handler = bot.webhooks[platform as Platform];
412
- if (!handler) {
413
- return new Response(`Unknown platform: ${platform}`, { status: 404 });
414
- }
415
-
416
- return handler(request, {
417
- waitUntil: (task) => after(() => task),
418
- });
419
- }
420
- ```
421
-
422
- ## Test locally
423
-
424
- 1. Start your development server with `pnpm dev`
425
- 2. Expose it with a tunnel such as `ngrok http 3000`
426
- 3. Update your Slack slash command URLs to the tunnel URL
427
- 4. In Slack, run `/remind 2026-03-15T09:00:00Z Review launch checklist`
428
- 5. Confirm that you receive an ephemeral confirmation with a schedule ID
429
- 6. Wait for the scheduled time and verify the bot posts to the channel
430
- 7. Create another one and cancel it with `/remind-cancel <schedule-id>`
431
-
432
- ## Extending the pattern
433
-
434
- From here you can add:
435
-
436
- - rescheduling by canceling the old row and creating a new one
437
- - recurring posts by having the workflow create the next schedule before it exits
438
- - approvals by pausing the workflow with a hook before posting
439
- - thread reminders by storing `thread.toJSON()` instead of `channelId` and restoring it with `bot.reviver()`
440
-
441
- ## Next steps
442
-
443
- - [Slack bot with Next.js and Redis](/docs/guides/slack-nextjs) — Base Slack app and webhook setup
444
- - [Slash Commands](/docs/slash-commands) — Command handling patterns
445
- - [PostgreSQL State Adapter](/docs/state/postgres) — Using Postgres for Chat SDK state
446
- - [Channel API](/docs/api/channel) — Posting to channels and working with channel IDs
447
- - [Chat API](/docs/api/chat) — `bot.channel()` and lifecycle management
@@ -1,234 +0,0 @@
1
- ---
2
- title: Slack bot with Next.js and Redis
3
- description: This guide walks through building a Slack bot with Next.js, covering project setup, Slack app configuration, event handling, interactive features, and deployment.
4
- type: guide
5
- prerequisites: []
6
- related:
7
- - /adapters/slack
8
- - /docs/cards
9
- - /docs/modals
10
- - /docs/streaming
11
- ---
12
-
13
- ## Prerequisites
14
-
15
- - Node.js 18+
16
- - [pnpm](https://pnpm.io) (or npm/yarn)
17
- - A Slack workspace where you can install apps
18
- - A Redis instance for state management
19
-
20
- ## Create a Next.js app
21
-
22
- Scaffold a new Next.js project and install Chat SDK dependencies:
23
-
24
- ```sh title="Terminal"
25
- npx create-next-app@latest my-slack-bot --typescript --app
26
- cd my-slack-bot
27
- pnpm add chat @chat-adapter/slack @chat-adapter/state-redis
28
- ```
29
-
30
- ## Create a Slack app
31
-
32
- 1. Go to [api.slack.com/apps](https://api.slack.com/apps)
33
- 2. Click **Create New App** then **From an app manifest**
34
- 3. Select your workspace and paste the following manifest:
35
-
36
- ```yaml title="slack-manifest.yml"
37
- display_information:
38
- name: My Bot
39
- description: A bot built with Chat SDK
40
-
41
- features:
42
- bot_user:
43
- display_name: My Bot
44
- always_online: true
45
-
46
- oauth_config:
47
- scopes:
48
- bot:
49
- - app_mentions:read
50
- - channels:history
51
- - channels:read
52
- - chat:write
53
- - groups:history
54
- - groups:read
55
- - im:history
56
- - im:read
57
- - mpim:history
58
- - mpim:read
59
- - reactions:read
60
- - reactions:write
61
- - users:read
62
-
63
- settings:
64
- event_subscriptions:
65
- request_url: https://your-domain.com/api/webhooks/slack
66
- bot_events:
67
- - app_mention
68
- - message.channels
69
- - message.groups
70
- - message.im
71
- - message.mpim
72
- interactivity:
73
- is_enabled: true
74
- request_url: https://your-domain.com/api/webhooks/slack
75
- org_deploy_enabled: false
76
- socket_mode_enabled: false
77
- token_rotation_enabled: false
78
- ```
79
-
80
- 4. Replace `https://your-domain.com/api/webhooks/slack` with your deployed webhook URL
81
- 5. Click **Create**
82
-
83
- ### Get credentials
84
-
85
- After creating the app:
86
-
87
- 1. Go to **OAuth & Permissions**, click **Install to Workspace**, and copy the **Bot User OAuth Token** (`xoxb-...`) — you'll need this as `SLACK_BOT_TOKEN`
88
- 2. Go to **Basic Information** → **App Credentials** and copy the **Signing Secret** — you'll need this as `SLACK_SIGNING_SECRET`
89
-
90
- If you're distributing the app across multiple workspaces via OAuth instead of installing it to one workspace, configure `clientId` and `clientSecret` on the Slack adapter and pass the same redirect URI used during the authorize step into `handleOAuthCallback(request, { redirectUri })` in your callback route.
91
-
92
- ## Configure environment variables
93
-
94
- Create a `.env.local` file in your project root:
95
-
96
- ```bash title=".env.local"
97
- SLACK_BOT_TOKEN=xoxb-your-bot-token
98
- SLACK_SIGNING_SECRET=your-signing-secret
99
- REDIS_URL=redis://localhost:6379
100
- ```
101
-
102
- ## Create the bot
103
-
104
- Create `lib/bot.ts` with a `Chat` instance configured with the Slack adapter:
105
-
106
- ```typescript title="lib/bot.ts" lineNumbers
107
- import { Chat } from "chat";
108
- import { createSlackAdapter } from "@chat-adapter/slack";
109
- import { createRedisState } from "@chat-adapter/state-redis";
110
-
111
- export const bot = new Chat({
112
- userName: "mybot",
113
- adapters: {
114
- slack: createSlackAdapter(),
115
- },
116
- state: createRedisState(),
117
- });
118
-
119
- // Respond when someone @mentions the bot
120
- bot.onNewMention(async (thread) => {
121
- await thread.subscribe();
122
- await thread.post("Hello! I'm listening to this thread now.");
123
- });
124
-
125
- // Respond to follow-up messages in subscribed threads
126
- bot.onSubscribedMessage(async (thread, message) => {
127
- await thread.post(`You said: ${message.text}`);
128
- });
129
- ```
130
-
131
- The adapter auto-detects `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from your environment, and `createRedisState()` reads `REDIS_URL` automatically.
132
-
133
- `onNewMention` fires when a user @mentions your bot. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage`.
134
-
135
- ## Create the webhook route
136
-
137
- Create a dynamic API route that handles incoming webhooks:
138
-
139
- ```typescript title="app/api/webhooks/[platform]/route.ts" lineNumbers
140
- import { after } from "next/server";
141
- import { bot } from "@/lib/bot";
142
-
143
- type Platform = keyof typeof bot.webhooks;
144
-
145
- export async function POST(
146
- request: Request,
147
- context: RouteContext<"/api/webhooks/[platform]">
148
- ) {
149
- const { platform } = await context.params;
150
-
151
- const handler = bot.webhooks[platform as Platform];
152
- if (!handler) {
153
- return new Response(`Unknown platform: ${platform}`, { status: 404 });
154
- }
155
-
156
- return handler(request, {
157
- waitUntil: (task) => after(() => task),
158
- });
159
- }
160
- ```
161
-
162
- This creates a `POST /api/webhooks/slack` endpoint. The `waitUntil` option ensures message processing completes after the HTTP response is sent — required on serverless platforms where the function would otherwise terminate before your handlers finish.
163
-
164
- ## Test locally
165
-
166
- 1. Start your development server (`pnpm dev`)
167
- 2. Expose it with a tunnel (e.g. `ngrok http 3000`)
168
- 3. Update the Slack Event Subscriptions **Request URL** to your tunnel URL
169
- 4. Invite your bot to a Slack channel (`/invite @mybot`)
170
- 5. @mention the bot — it should respond with "Hello! I'm listening to this thread now."
171
- 6. Reply in the thread — it should echo your message back
172
-
173
- ## Add interactive features
174
-
175
- Chat SDK supports rich interactive messages using a JSX-like syntax. Update your bot to send cards with buttons:
176
-
177
- ```typescript title="lib/bot.ts" lineNumbers
178
- import { Chat, Card, CardText as Text, Actions, Button, Divider } from "chat";
179
- import { createSlackAdapter } from "@chat-adapter/slack";
180
- import { createRedisState } from "@chat-adapter/state-redis";
181
-
182
- export const bot = new Chat({
183
- userName: "mybot",
184
- adapters: {
185
- slack: createSlackAdapter(),
186
- },
187
- state: createRedisState(),
188
- });
189
-
190
- bot.onNewMention(async (thread) => {
191
- await thread.subscribe();
192
- await thread.post(
193
- <Card title="Welcome!">
194
- <Text>I'm now listening to this thread. Try clicking a button:</Text>
195
- <Divider />
196
- <Actions>
197
- <Button id="hello" style="primary">Say Hello</Button>
198
- <Button id="info">Show Info</Button>
199
- </Actions>
200
- </Card>
201
- );
202
- });
203
-
204
- bot.onAction("hello", async (event) => {
205
- await event.thread.post(`Hello, ${event.user.fullName}!`);
206
- });
207
-
208
- bot.onAction("info", async (event) => {
209
- await event.thread.post(`You're on ${event.thread.adapter.name}.`);
210
- });
211
- ```
212
-
213
- <Callout type="info">
214
- The file extension must be `.tsx` (not `.ts`) when using JSX components like `Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"` and `"jsxImportSource": "chat"`.
215
- </Callout>
216
-
217
- ## Deploy to Vercel
218
-
219
- Deploy your bot to Vercel:
220
-
221
- ```sh title="Terminal"
222
- vercel deploy
223
- ```
224
-
225
- After deployment, set your environment variables in the Vercel dashboard (`SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `REDIS_URL`). If your manifest used a placeholder URL, update the **Event Subscriptions** and **Interactivity** Request URLs in your [Slack app settings](https://api.slack.com/apps) to your production URL.
226
-
227
- ## Next steps
228
-
229
- - [Cards](/docs/cards) — Build rich interactive messages with buttons, fields, and selects
230
- - [Modals](/docs/modals) — Open forms and dialogs from button clicks
231
- - [Streaming](/docs/streaming) — Stream AI-generated responses to chat
232
- - [Actions](/docs/actions) — Handle button clicks, select menus, and other interactions
233
- - [Slack adapter](/adapters/slack) — Multi-workspace OAuth, token encryption, and full configuration reference
234
- - [State Adapters](/docs/state) — PostgreSQL, ioredis, and other state adapter options