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.
- package/dist/{chunk-OPV5U4WG.js → chunk-V25FKIIL.js} +44 -1
- package/dist/index.d.ts +485 -33
- package/dist/index.js +862 -135
- package/dist/{jsx-runtime-DxATbnrP.d.ts → jsx-runtime-DxGwoLu2.d.ts} +49 -5
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/docs/actions.mdx +52 -1
- package/docs/adapters.mdx +43 -37
- package/docs/api/cards.mdx +4 -0
- package/docs/api/chat.mdx +172 -6
- package/docs/api/index.mdx +2 -0
- package/docs/api/markdown.mdx +28 -5
- package/docs/api/message.mdx +58 -1
- package/docs/api/meta.json +2 -0
- package/docs/api/modals.mdx +50 -0
- package/docs/api/postable-message.mdx +55 -1
- package/docs/api/thread.mdx +33 -3
- package/docs/api/transcripts.mdx +220 -0
- package/docs/cards.mdx +6 -0
- package/docs/concurrency.mdx +4 -0
- package/docs/contributing/building.mdx +73 -1
- package/docs/contributing/publishing.mdx +33 -0
- package/docs/conversation-history.mdx +137 -0
- package/docs/direct-messages.mdx +13 -4
- package/docs/ephemeral-messages.mdx +1 -1
- package/docs/error-handling.mdx +15 -3
- package/docs/files.mdx +2 -1
- package/docs/getting-started.mdx +1 -11
- package/docs/index.mdx +7 -5
- package/docs/meta.json +14 -5
- package/docs/modals.mdx +97 -1
- package/docs/posting-messages.mdx +7 -3
- package/docs/streaming.mdx +74 -18
- package/docs/subject.mdx +53 -0
- package/docs/threads-messages-channels.mdx +43 -0
- package/docs/usage.mdx +11 -2
- package/package.json +3 -2
- package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +180 -0
- package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +134 -0
- package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +220 -0
- package/resources/guides/run-and-track-deploys-from-slack.md +270 -0
- package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +147 -0
- package/resources/guides/triage-form-submissions-with-chat-sdk.md +178 -0
- package/resources/templates.json +19 -0
- package/docs/guides/code-review-hono.mdx +0 -241
- package/docs/guides/discord-nuxt.mdx +0 -227
- package/docs/guides/durable-chat-sessions-nextjs.mdx +0 -337
- package/docs/guides/meta.json +0 -10
- package/docs/guides/scheduled-posts-neon.mdx +0 -447
- package/docs/guides/slack-nextjs.mdx +0 -234
package/docs/streaming.mdx
CHANGED
|
@@ -6,7 +6,7 @@ prerequisites:
|
|
|
6
6
|
- /docs/usage
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Chat SDK accepts any `AsyncIterable<string>` as a message, enabling real-time streaming of AI responses and other incremental content to chat platforms. For platforms with native streaming support
|
|
9
|
+
Chat SDK accepts any `AsyncIterable<string>` as a message, enabling real-time streaming of AI responses and other incremental content to chat platforms. For platforms with native or structured streaming support, you can also stream `StreamChunk` objects for rich content like task progress cards and plan updates.
|
|
10
10
|
|
|
11
11
|
## AI SDK integration
|
|
12
12
|
|
|
@@ -59,9 +59,14 @@ await thread.post(stream);
|
|
|
59
59
|
| Platform | Method | Description |
|
|
60
60
|
|----------|--------|-------------|
|
|
61
61
|
| Slack | Native streaming API | Uses Slack's `chatStream` for smooth, real-time updates |
|
|
62
|
-
| Teams |
|
|
62
|
+
| Teams | Native (DMs) / Buffered (group chats) | Uses the Teams SDK's native `stream.emit()` for direct messages; accumulates chunks and posts one final message when no native streamer is active |
|
|
63
63
|
| Google Chat | Post + Edit | Posts a message then edits it as chunks arrive |
|
|
64
64
|
| Discord | Post + Edit | Posts a message then edits it as chunks arrive |
|
|
65
|
+
| Telegram | Post + Edit | Posts a message then edits it as chunks arrive |
|
|
66
|
+
| GitHub | Buffered | Accumulates chunks and posts one final comment |
|
|
67
|
+
| Linear | Agent sessions / Post + Edit | Uses agent session activities in agent-session threads; falls back to post+edit comments in issue threads |
|
|
68
|
+
| WhatsApp | Buffered | Accumulates chunks and sends one final message |
|
|
69
|
+
| Messenger | Buffered | Accumulates chunks and sends one final message |
|
|
65
70
|
|
|
66
71
|
The post+edit fallback throttles edits to avoid rate limits. Configure the update interval when creating your `Chat` instance:
|
|
67
72
|
|
|
@@ -104,9 +109,9 @@ When streaming content that contains GFM tables (e.g. from an LLM), the SDK auto
|
|
|
104
109
|
|
|
105
110
|
This happens transparently — no configuration needed.
|
|
106
111
|
|
|
107
|
-
## Structured streaming chunks
|
|
112
|
+
## Structured streaming chunks
|
|
108
113
|
|
|
109
|
-
For Slack
|
|
114
|
+
For Slack native streams and Linear agent-session streams, you can yield `StreamChunk` objects alongside plain text for rich progress updates:
|
|
110
115
|
|
|
111
116
|
```typescript title="lib/bot.ts" lineNumbers
|
|
112
117
|
import type { StreamChunk } from "chat";
|
|
@@ -118,6 +123,7 @@ const stream = (async function* () {
|
|
|
118
123
|
type: "task_update",
|
|
119
124
|
id: "search-1",
|
|
120
125
|
title: "Searching documents",
|
|
126
|
+
details: "Querying internal docs and ranking the best matches",
|
|
121
127
|
status: "in_progress",
|
|
122
128
|
} satisfies StreamChunk;
|
|
123
129
|
|
|
@@ -127,6 +133,7 @@ const stream = (async function* () {
|
|
|
127
133
|
type: "task_update",
|
|
128
134
|
id: "search-1",
|
|
129
135
|
title: "Searching documents",
|
|
136
|
+
details: "Ranked 3 relevant results",
|
|
130
137
|
status: "complete",
|
|
131
138
|
output: "Found 3 results",
|
|
132
139
|
} satisfies StreamChunk;
|
|
@@ -142,33 +149,42 @@ await thread.post(stream);
|
|
|
142
149
|
| Type | Fields | Description |
|
|
143
150
|
|------|--------|-------------|
|
|
144
151
|
| `markdown_text` | `text` | Streamed text content |
|
|
145
|
-
| `task_update` | `id`, `title`, `status`, `output?` | Tool/step progress
|
|
146
|
-
| `plan_update` | `title` | Plan title updates |
|
|
152
|
+
| `task_update` | `id`, `title`, `status`, `details?`, `output?` | Tool/step progress updates (`pending`, `in_progress`, `complete`, `error`) with optional extra task context |
|
|
153
|
+
| `plan_update` | `title` | Plan title updates on supported platforms |
|
|
147
154
|
|
|
148
|
-
###
|
|
155
|
+
### Streaming with options
|
|
149
156
|
|
|
150
|
-
|
|
157
|
+
Wrap a stream in a `StreamingPlan` to pass platform-specific options through `thread.post()` without dropping down to `adapter.stream()` directly:
|
|
151
158
|
|
|
152
159
|
```typescript
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
import { StreamingPlan } from "chat";
|
|
161
|
+
|
|
162
|
+
const planned = new StreamingPlan(stream, {
|
|
163
|
+
groupTasks: "plan", // Slack: render task cards as a single grouped block
|
|
164
|
+
endWith: [feedbackBlock], // Slack: Block Kit elements appended after stream stops
|
|
165
|
+
updateIntervalMs: 750, // Post+edit cadence on supported adapters
|
|
155
166
|
});
|
|
167
|
+
|
|
168
|
+
await thread.post(planned);
|
|
156
169
|
```
|
|
157
170
|
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
| `"timeline"`
|
|
161
|
-
| `
|
|
171
|
+
| Option | Platform | Description |
|
|
172
|
+
|--------|----------|-------------|
|
|
173
|
+
| `groupTasks` | Slack | `"timeline"` (default) renders task cards inline; `"plan"` groups them into one plan block |
|
|
174
|
+
| `endWith` | Slack | Block Kit elements attached when the stream stops (e.g. retry / feedback buttons) |
|
|
175
|
+
| `updateIntervalMs` | Post+edit adapters | Minimum interval between post+edit cycles in ms (default `500`) |
|
|
162
176
|
|
|
163
|
-
Adapters without structured chunk support extract text from `markdown_text` chunks and ignore other types.
|
|
177
|
+
Adapters without structured chunk support extract text from `markdown_text` chunks and ignore other types. Slack-only options are silently ignored on other platforms.
|
|
164
178
|
|
|
165
179
|
## Stop blocks (Slack only)
|
|
166
180
|
|
|
167
|
-
|
|
181
|
+
Use `endWith` on `StreamingPlan` to attach Block Kit elements to the final message. This is useful for adding action buttons after a streamed response completes:
|
|
168
182
|
|
|
169
183
|
```typescript title="lib/bot.ts" lineNumbers
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
import { StreamingPlan } from "chat";
|
|
185
|
+
|
|
186
|
+
const planned = new StreamingPlan(textStream, {
|
|
187
|
+
endWith: [
|
|
172
188
|
{
|
|
173
189
|
type: "actions",
|
|
174
190
|
elements: [{
|
|
@@ -179,8 +195,48 @@ await thread.stream(textStream, {
|
|
|
179
195
|
},
|
|
180
196
|
],
|
|
181
197
|
});
|
|
198
|
+
|
|
199
|
+
await thread.post(planned);
|
|
182
200
|
```
|
|
183
201
|
|
|
202
|
+
## Plan API
|
|
203
|
+
|
|
204
|
+
For step-by-step task progress that lives outside an LLM stream, post a `Plan` directly. `Plan` is a `PostableObject` you can mutate after posting — every mutation re-renders the block in place.
|
|
205
|
+
|
|
206
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
207
|
+
import { Plan } from "chat";
|
|
208
|
+
|
|
209
|
+
const plan = new Plan({ initialMessage: "Researching options..." });
|
|
210
|
+
await thread.post(plan);
|
|
211
|
+
|
|
212
|
+
const lookup = await plan.addTask({ title: "Look up customer record" });
|
|
213
|
+
// ...do work...
|
|
214
|
+
await plan.updateTask("Found 3 matches");
|
|
215
|
+
|
|
216
|
+
await plan.addTask({ title: "Summarize findings" });
|
|
217
|
+
await plan.complete({ completeMessage: "Done!" });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
By default `updateTask()` mutates the most recent `in_progress` task. Pass `{ id }` to target a specific task — useful when work runs in parallel or out of order:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const fetchTask = await plan.addTask({ title: "Fetch data" });
|
|
224
|
+
const transformTask = await plan.addTask({ title: "Transform" });
|
|
225
|
+
|
|
226
|
+
// Update a specific task by id, even if it isn't the most recent in_progress one.
|
|
227
|
+
await plan.updateTask({ id: fetchTask.id, output: "Got 42 rows" });
|
|
228
|
+
await plan.updateTask({ id: transformTask.id, status: "complete" });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Adapters that don't support PostableObject editing (e.g. WhatsApp) render the plan as a fallback emoji-list message; the plan still posts, but mutations are no-ops.
|
|
232
|
+
|
|
233
|
+
| Method | Description |
|
|
234
|
+
|--------|-------------|
|
|
235
|
+
| `addTask({ title, children? })` | Append a new task. The previous in-progress task is auto-completed |
|
|
236
|
+
| `updateTask(input)` | Mutate the current (or `{ id }`-targeted) task's `output`, `status`, or `title` |
|
|
237
|
+
| `complete({ completeMessage })` | Mark all in-progress tasks complete and update the plan title |
|
|
238
|
+
| `reset({ initialMessage })` | Discard all tasks and start fresh with a new initial message — useful when re-using a plan handle for a new run |
|
|
239
|
+
|
|
184
240
|
## Streaming with conversation history
|
|
185
241
|
|
|
186
242
|
Combine message history with streaming for multi-turn AI conversations.
|
package/docs/subject.mdx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Message Subject
|
|
3
|
+
description: Fetch the parent resource that a message is about.
|
|
4
|
+
type: guide
|
|
5
|
+
prerequisites:
|
|
6
|
+
- /docs/handling-events
|
|
7
|
+
related:
|
|
8
|
+
- /docs/conversation-history
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
When your bot receives a comment on a Linear issue or GitHub PR, `message.subject` resolves the parent resource so your handler knows what the conversation is about.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
16
|
+
bot.onNewMention(async (thread, message) => {
|
|
17
|
+
const subject = await message.subject;
|
|
18
|
+
|
|
19
|
+
if (subject) {
|
|
20
|
+
await thread.post(
|
|
21
|
+
`This is about: ${subject.title} (${subject.status})\n${subject.url}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On Linear and GitHub, comment webhooks deliver the comment text but not the parent issue or pull request — `message.subject` fetches it from the platform API on first access. The result is cached on the message instance. On chat platforms (which have no parent-resource concept), or if the API call fails, it returns `null`.
|
|
28
|
+
|
|
29
|
+
See [`MessageSubject`](/docs/api/message#messagesubject) for the full type shape.
|
|
30
|
+
|
|
31
|
+
### Platform support
|
|
32
|
+
|
|
33
|
+
| Platform | `message.subject` returns |
|
|
34
|
+
|----------|--------------------------|
|
|
35
|
+
| Linear | Parent issue (from comment webhooks) |
|
|
36
|
+
| GitHub | Parent issue or PR (from comment webhooks) |
|
|
37
|
+
|
|
38
|
+
All other platforms return `null`.
|
|
39
|
+
|
|
40
|
+
## User info
|
|
41
|
+
|
|
42
|
+
For user profile details, use [`bot.getUser`](/docs/api/chat#getuser):
|
|
43
|
+
|
|
44
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
45
|
+
bot.onNewMention(async (thread, message) => {
|
|
46
|
+
const user = await bot.getUser(message.author);
|
|
47
|
+
if (user) {
|
|
48
|
+
await thread.post(`Hi ${user.fullName} (${user.email})`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For anything beyond `message.subject`, access the platform's typed API client via [`bot.getAdapter(...).client`](/docs/api/chat#getadapter).
|
|
@@ -13,6 +13,15 @@ related:
|
|
|
13
13
|
|
|
14
14
|
A `Thread` represents a conversation thread on any platform. It provides methods for posting messages, managing subscriptions, and accessing message history.
|
|
15
15
|
|
|
16
|
+
Thread instances are most often supplied by the SDK to your event handlers. You can also construct one explicitly from a thread ID — useful for cron jobs, workflow steps, or any other context outside an inbound webhook:
|
|
17
|
+
|
|
18
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
19
|
+
const thread = bot.thread("slack:C123ABC:1234567890.123456");
|
|
20
|
+
await thread.post("Reminder from a cron job");
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For DM-style conversations, use [`bot.openDM(userIdOrAuthor)`](/docs/direct-messages) instead. It resolves the right channel and thread for user ID formats the SDK can infer.
|
|
24
|
+
|
|
16
25
|
### Post a message
|
|
17
26
|
|
|
18
27
|
```typescript title="lib/bot.ts" lineNumbers
|
|
@@ -40,6 +49,33 @@ await thread.unsubscribe();
|
|
|
40
49
|
const subscribed = await thread.isSubscribed();
|
|
41
50
|
```
|
|
42
51
|
|
|
52
|
+
### Participants
|
|
53
|
+
|
|
54
|
+
Get the unique human participants in a thread. Returns deduplicated authors, excluding all bots. Useful for deciding whether to subscribe based on how many humans are in the conversation.
|
|
55
|
+
|
|
56
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
57
|
+
bot.onNewMention(async (thread) => {
|
|
58
|
+
const participants = await thread.getParticipants();
|
|
59
|
+
if (participants.length === 1) {
|
|
60
|
+
await thread.subscribe();
|
|
61
|
+
await thread.post("I'm here to help!");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
bot.onSubscribedMessage(async (thread) => {
|
|
66
|
+
const participants = await thread.getParticipants();
|
|
67
|
+
if (participants.length > 1) {
|
|
68
|
+
await thread.unsubscribe();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// respond...
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
<Callout type="warn">
|
|
76
|
+
Each call fetches the full message history to find all participants. On threads with long history this makes multiple API calls to the platform. Consider checking `message.author` against a known set before calling `getParticipants()` on every incoming message.
|
|
77
|
+
</Callout>
|
|
78
|
+
|
|
43
79
|
### Typing indicator
|
|
44
80
|
|
|
45
81
|
```typescript title="lib/bot.ts"
|
|
@@ -144,6 +180,13 @@ interface Author {
|
|
|
144
180
|
}
|
|
145
181
|
```
|
|
146
182
|
|
|
183
|
+
For richer user info (email, avatar), use [`chat.getUser()`](/docs/api/chat#getuser):
|
|
184
|
+
|
|
185
|
+
```typescript title="lib/bot.ts"
|
|
186
|
+
const user = await bot.getUser(message.author);
|
|
187
|
+
console.log(user?.email); // "alice@company.com"
|
|
188
|
+
```
|
|
189
|
+
|
|
147
190
|
### Sent messages
|
|
148
191
|
|
|
149
192
|
When you post a message, you get back a `SentMessage` with methods to edit, delete, and react:
|
package/docs/usage.mdx
CHANGED
|
@@ -34,7 +34,7 @@ bot.onNewMention(async (thread) => {
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
<Callout type="info">
|
|
37
|
-
This example uses Redis. Chat SDK also supports [PostgreSQL](/
|
|
37
|
+
This example uses Redis. Chat SDK also supports [PostgreSQL](/adapters/postgres) and [ioredis](/adapters/ioredis) as production state adapters. See [State Adapters](/docs/state) for all options.
|
|
38
38
|
</Callout>
|
|
39
39
|
|
|
40
40
|
Each adapter factory auto-detects credentials from environment variables (`SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `REDIS_URL`, etc.), so you can get started with zero config. Pass explicit values to override.
|
|
@@ -89,6 +89,15 @@ await slack.setSuggestedPrompts(channelId, threadTs, [
|
|
|
89
89
|
]);
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
For typed access to the platform's native API client (Linear, GitHub), use `.client`:
|
|
93
|
+
|
|
94
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
95
|
+
const linear = bot.getAdapter("linear").client; // LinearClient
|
|
96
|
+
const github = bot.getAdapter("github").client; // Octokit
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [`getAdapter`](/docs/api/chat#getadapter) for multi-tenant constraints.
|
|
100
|
+
|
|
92
101
|
## Webhook routing
|
|
93
102
|
|
|
94
103
|
The `webhooks` property provides type-safe handlers for each registered adapter. Wire these up to your HTTP framework's routes:
|
|
@@ -139,7 +148,7 @@ const bot = Chat.getSingleton();
|
|
|
139
148
|
Open a DM thread with a user by passing their platform user ID or an `Author` object:
|
|
140
149
|
|
|
141
150
|
```typescript title="lib/bot.ts" lineNumbers
|
|
142
|
-
const dm = await bot.openDM("
|
|
151
|
+
const dm = await bot.openDM("U123ABC");
|
|
143
152
|
await dm.post("Hey! Just wanted to follow up on your request.");
|
|
144
153
|
```
|
|
145
154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chat",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.28.1",
|
|
4
4
|
"description": "Unified chat abstraction for Slack, Teams, Google Chat, and Discord",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
|
-
"docs"
|
|
25
|
+
"docs",
|
|
26
|
+
"resources"
|
|
26
27
|
],
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@workflow/serde": "4.1.0-beta.2",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Create a Discord support bot with Nuxt and Redis
|
|
2
|
+
|
|
3
|
+
**Author:** Hayden Bleasel, Ben Sabic
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You can build a Discord support bot that answers questions with AI, sends interactive cards with buttons, and escalates to human agents on demand by combining Chat SDK, AI SDK, and Nuxt. Chat SDK handles the platform integration (Gateway connection, event parsing, and the Discord API), while AI SDK generates responses using Claude. A Redis state adapter tracks subscribed threads across serverless invocations so conversations stay in context.
|
|
8
|
+
|
|
9
|
+
This guide will walk you through scaffolding a Nuxt app, configuring a Discord application, wiring up Chat SDK with the Discord adapter, adding AI-powered responses and interactive cards, setting up the Gateway forwarder, and deploying to Vercel.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before you begin, make sure you have:
|
|
14
|
+
|
|
15
|
+
* Node.js 18+
|
|
16
|
+
|
|
17
|
+
* [pnpm](https://pnpm.io/) (or npm/yarn)
|
|
18
|
+
|
|
19
|
+
* A Discord server where you have admin access
|
|
20
|
+
|
|
21
|
+
* A Redis instance (local or hosted, such as [Upstash](https://vercel.com/marketplace/upstash))
|
|
22
|
+
|
|
23
|
+
* An [Anthropic API key](https://console.anthropic.com/)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
Chat SDK is a unified TypeScript SDK for building chatbots across Discord, Slack, Teams, and other platforms. You register event handlers (like `onNewMention` and `onSubscribedMessage`), and the SDK routes incoming events to them. The Discord adapter handles Gateway connection, webhook verification, and the Discord API. The Redis state adapter tracks which threads your bot has subscribed to and manages distributed locking for concurrent message handling.
|
|
29
|
+
|
|
30
|
+
Discord doesn't push messages to HTTP webhooks like Slack does. Instead, messages arrive through Discord's Gateway WebSocket. The Discord adapter includes a built-in Gateway listener that connects to the WebSocket and forwards events to your webhook endpoint, so the rest of your bot logic looks the same as any other Chat SDK adapter.
|
|
31
|
+
|
|
32
|
+
When someone @mentions the bot, `onNewMention` fires and posts a support card. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage` where AI SDK generates a response using Claude.
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
### 1\. Scaffold the project and install dependencies
|
|
37
|
+
|
|
38
|
+
Create a new Nuxt app and add the Chat SDK, AI SDK, and adapter packages:
|
|
39
|
+
|
|
40
|
+
`npx nuxi@latest init my-discord-bot cd my-discord-bot pnpm add chat @chat-adapter/discord @chat-adapter/state-redis ai @ai-sdk/anthropic`
|
|
41
|
+
|
|
42
|
+
The `chat` package is the Chat SDK core. The `@chat-adapter/discord` and `@chat-adapter/state-redis` packages are the [Discord platform adapter](https://chat-sdk.dev/adapters/discord) and [Redis state adapter](https://chat-sdk.dev/adapters/redis). The `ai` and `@ai-sdk/anthropic` packages are used to generate responses with Claude.
|
|
43
|
+
|
|
44
|
+
### 2\. Create a Discord app
|
|
45
|
+
|
|
46
|
+
1. Go to [discord.com/developers/applications](https://discord.com/developers/applications)
|
|
47
|
+
|
|
48
|
+
2. Click **New Application**, give it a name, and click **Create**
|
|
49
|
+
|
|
50
|
+
3. Go to **Bot** in the sidebar and click **Reset Token**. Copy the token, you'll need this as `DISCORD_BOT_TOKEN`
|
|
51
|
+
|
|
52
|
+
4. Under **Privileged Gateway Intents**, enable **Message Content Intent**
|
|
53
|
+
|
|
54
|
+
5. Go to **General Information** and copy the **Application ID** and **Public Key**. You'll need these as `DISCORD_APPLICATION_ID` and `DISCORD_PUBLIC_KEY`
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
Then set up the Interactions endpoint:
|
|
58
|
+
|
|
59
|
+
1. In **General Information**, set the **Interactions Endpoint URL** to [`https://your-domain.com/api/webhooks/discord`](https://your-domain.com/api/webhooks/discord)
|
|
60
|
+
|
|
61
|
+
2. Discord will send a PING to verify the endpoint. You'll need to deploy first or use a tunnel
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Then invite the bot to your server:
|
|
65
|
+
|
|
66
|
+
1. Go to **OAuth2** in the sidebar
|
|
67
|
+
|
|
68
|
+
2. Under **OAuth2 URL Generator**, select the `bot` scope
|
|
69
|
+
|
|
70
|
+
3. Under **Bot Permissions**, select:
|
|
71
|
+
|
|
72
|
+
* Send Messages
|
|
73
|
+
|
|
74
|
+
* Create Public Threads
|
|
75
|
+
|
|
76
|
+
* Send Messages in Threads
|
|
77
|
+
|
|
78
|
+
* Read Message History
|
|
79
|
+
|
|
80
|
+
* Add Reactions
|
|
81
|
+
|
|
82
|
+
* Use Slash Commands
|
|
83
|
+
|
|
84
|
+
4. Copy the generated URL and open it in your browser to invite the bot
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
### 3\. Configure environment variables
|
|
88
|
+
|
|
89
|
+
Create a `.env` file in your project root:
|
|
90
|
+
|
|
91
|
+
`DISCORD_BOT_TOKEN=your_bot_token DISCORD_PUBLIC_KEY=your_public_key DISCORD_APPLICATION_ID=your_application_id REDIS_URL=redis://localhost:6379 ANTHROPIC_API_KEY=your_anthropic_api_key`
|
|
92
|
+
|
|
93
|
+
The Discord adapter reads `DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY`, and `DISCORD_APPLICATION_ID` automatically. The Redis state adapter reads `REDIS_URL`, and AI SDK's Anthropic provider reads `ANTHROPIC_API_KEY`.
|
|
94
|
+
|
|
95
|
+
### 4\. Create the bot
|
|
96
|
+
|
|
97
|
+
Create `server/lib/bot.tsx` with a `Chat` instance configured with the Discord adapter. This bot uses AI SDK to answer support questions:
|
|
98
|
+
|
|
99
|
+
``import { Chat, Card, CardText as Text, Actions, Button, Divider } from "chat"; import { createDiscordAdapter } from "@chat-adapter/discord"; import { createRedisState } from "@chat-adapter/state-redis"; import { generateText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; export const bot = new Chat({ userName: "support-bot", adapters: { discord: createDiscordAdapter(), }, state: createRedisState(), }); bot.onNewMention(async (thread) => { await thread.subscribe(); await thread.post( <Card title="Support"> <Text>Hey! I'm here to help. Ask your question in this thread and I'll do my best to answer it.</Text> <Divider /> <Actions> <Button id="escalate" style="danger">Escalate to Human</Button> </Actions> </Card> ); }); bot.onSubscribedMessage(async (thread, message) => { await thread.startTyping(); const { text } = await generateText({ model: anthropic("claude-sonnet-4-5-20250514"), system: "You are a friendly support bot. Answer questions concisely. If you don't know the answer, say so and suggest the user click 'Escalate to Human'.", prompt: message.text, }); await thread.post(text); }); bot.onAction("escalate", async (event) => { await event.thread.post( `${event.user.fullName} requested human support. A team member will follow up shortly.` ); });``
|
|
100
|
+
|
|
101
|
+
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"`.
|
|
102
|
+
|
|
103
|
+
`onNewMention` fires when a user @mentions the bot. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage` where AI SDK generates a response.
|
|
104
|
+
|
|
105
|
+
### 5\. Create the webhook route
|
|
106
|
+
|
|
107
|
+
Create a server route that handles incoming Discord webhooks:
|
|
108
|
+
|
|
109
|
+
``import { bot } from "../lib/bot"; type Platform = keyof typeof bot.webhooks; export default defineEventHandler(async (event) => { const platform = getRouterParam(event, "platform") as Platform; const handler = bot.webhooks[platform]; if (!handler) { throw createError({ statusCode: 404, message: `Unknown platform: ${platform}` }); } const request = toWebRequest(event); return handler(request, { waitUntil: (task) => event.waitUntil(task), }); });``
|
|
110
|
+
|
|
111
|
+
This creates a `POST /api/webhooks/discord` endpoint. The `waitUntil` option ensures message processing completes after the HTTP response is sent.
|
|
112
|
+
|
|
113
|
+
### 6\. Set up the Gateway forwarder
|
|
114
|
+
|
|
115
|
+
Discord doesn't push messages to webhooks like Slack does. Instead, messages arrive through the Gateway WebSocket. The Discord adapter includes a built-in Gateway listener that connects to the WebSocket and forwards events to your webhook endpoint.
|
|
116
|
+
|
|
117
|
+
Create a route that starts the Gateway listener:
|
|
118
|
+
|
|
119
|
+
``import { bot } from "../../lib/bot"; export default defineEventHandler(async (event) => { await bot.initialize(); const discord = bot.getAdapter("discord"); if (!discord) { throw createError({ statusCode: 404, message: "Discord adapter not configured" }); } const baseUrl = process.env.NUXT_PUBLIC_SITE_URL || "http://localhost:3000"; const webhookUrl = `${baseUrl}/api/webhooks/discord`; const durationMs = 10 * 60 * 1000; // 10 minutes return discord.startGatewayListener( { waitUntil: (task: Promise<unknown>) => event.waitUntil(task) }, durationMs, undefined, webhookUrl, ); });``
|
|
120
|
+
|
|
121
|
+
The Gateway listener connects to Discord's WebSocket, receives messages, and forwards them to your webhook endpoint for processing. In production, you'll want a cron job to restart it periodically.
|
|
122
|
+
|
|
123
|
+
### 7\. Test locally
|
|
124
|
+
|
|
125
|
+
1. Start your development server (`pnpm dev`)
|
|
126
|
+
|
|
127
|
+
2. Trigger the Gateway listener by visiting [`http://localhost:3000/api/discord/gateway`](http://localhost:3000/api/discord/gateway) in your browser
|
|
128
|
+
|
|
129
|
+
3. Expose your server with a tunnel (e.g. `ngrok http 3000`)
|
|
130
|
+
|
|
131
|
+
4. Update the **Interactions Endpoint URL** in your Discord app settings to your tunnel URL (e.g. [`https://abc123.ngrok.io/api/webhooks/discord`](https://abc123.ngrok.io/api/webhooks/discord))
|
|
132
|
+
|
|
133
|
+
5. @mention the bot in your Discord server. It should respond with a support card
|
|
134
|
+
|
|
135
|
+
6. Reply in the thread. AI SDK should generate a response
|
|
136
|
+
|
|
137
|
+
7. Click **Escalate to Human**. The bot should post an escalation message
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### 8\. Add a cron job for production
|
|
141
|
+
|
|
142
|
+
The Gateway listener runs for a fixed duration. In production, set up a cron job to restart it automatically. If you're deploying to Vercel, add a `vercel.json`:
|
|
143
|
+
|
|
144
|
+
`{ "crons": [ { "path": "/api/discord/gateway", "schedule": "*/9 * * * *" } ] }`
|
|
145
|
+
|
|
146
|
+
This restarts the Gateway listener every 9 minutes, ensuring continuous connectivity. Protect the endpoint with a `CRON_SECRET` environment variable in production.
|
|
147
|
+
|
|
148
|
+
### 9\. Deploy to Vercel
|
|
149
|
+
|
|
150
|
+
Deploy your bot to Vercel:
|
|
151
|
+
|
|
152
|
+
`vercel deploy`
|
|
153
|
+
|
|
154
|
+
After deployment, set your environment variables in the Vercel dashboard (`DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY`, `DISCORD_APPLICATION_ID`, `REDIS_URL`, `ANTHROPIC_API_KEY`). Update the **Interactions Endpoint URL** in your Discord app settings to your production URL.
|
|
155
|
+
|
|
156
|
+
## Troubleshooting
|
|
157
|
+
|
|
158
|
+
### Bot doesn't respond to mentions
|
|
159
|
+
|
|
160
|
+
Check that **Message Content Intent** is enabled under **Privileged Gateway Intents** in your Discord app settings. Without it, the bot can't read message content and won't see @mentions. Also confirm the Gateway listener is running by visiting `/api/discord/gateway` or checking that the cron job is configured in production.
|
|
161
|
+
|
|
162
|
+
### Interactions endpoint verification fails
|
|
163
|
+
|
|
164
|
+
Discord sends a signed PING request to verify your endpoint. Confirm that `DISCORD_PUBLIC_KEY` matches the value in your Discord app's **General Information** page. A mismatched or missing public key will cause the adapter to reject the verification request.
|
|
165
|
+
|
|
166
|
+
### Gateway listener disconnects frequently
|
|
167
|
+
|
|
168
|
+
The listener runs for a fixed duration (10 minutes in this guide) and must be restarted. In production, use the cron job shown in step 8 to restart it every 9 minutes. If disconnections happen sooner, check your server logs for WebSocket errors and verify that `DISCORD_BOT_TOKEN` is valid.
|
|
169
|
+
|
|
170
|
+
### AI responses are slow or time out
|
|
171
|
+
|
|
172
|
+
`generateText` blocks until the full response is returned. For long answers, consider switching to streaming with `streamText` and passing the stream directly to [`thread.post`](http://thread.post)`()`. See the [Streaming docs](https://chat-sdk.dev/docs/streaming) for details.
|
|
173
|
+
|
|
174
|
+
### Redis connection errors
|
|
175
|
+
|
|
176
|
+
Verify that `REDIS_URL` is reachable from your deployment environment. The state adapter uses Redis for distributed locking, so the bot won't process messages without a working connection.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
[View full KB sitemap](/kb/sitemap.md)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# How to build a Slack bot with Next.js and Redis
|
|
2
|
+
|
|
3
|
+
**Author:** Hayden Bleasel, Ben Sabic
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You can build a Slack bot that responds to @mentions, tracks thread context, and sends rich interactive messages using Chat SDK with Next.js. Chat SDK handles the platform integration (webhook verification, message parsing, and the Slack API) while a Redis state adapter tracks which threads your bot has subscribed to across serverless invocations. Together with Vercel for deployment, you get a production-ready Slack bot without managing infrastructure or writing platform-specific glue code.
|
|
8
|
+
|
|
9
|
+
This guide will walk you through scaffolding a Next.js app, configuring a Slack app, wiring up event handlers with Chat SDK, adding interactive cards and buttons, and deploying to Vercel.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before you begin, make sure you have:
|
|
14
|
+
|
|
15
|
+
* Node.js 18+
|
|
16
|
+
|
|
17
|
+
* [pnpm](https://pnpm.io/) (or npm/yarn)
|
|
18
|
+
|
|
19
|
+
* A Slack workspace where you can install apps
|
|
20
|
+
|
|
21
|
+
* A Redis instance (local or hosted, such as [Upstash](https://vercel.com/marketplace/upstash))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
Chat SDK is a unified TypeScript SDK for building chatbots across Slack, Teams, Discord, and other platforms. You register event handlers (like `onNewMention` and `onSubscribedMessage`), and the SDK routes incoming webhooks to them. The Slack adapter handles webhook verification, message parsing, and the Slack API. The Redis state adapter tracks which threads your bot has subscribed to and manages distributed locking for concurrent message handling.
|
|
27
|
+
|
|
28
|
+
When a user @mentions your bot, `onNewMention` fires. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage`. This lets your bot maintain conversation context across multiple turns without you managing thread state yourself.
|
|
29
|
+
|
|
30
|
+
## Steps
|
|
31
|
+
|
|
32
|
+
### 1\. Scaffold the project and install dependencies
|
|
33
|
+
|
|
34
|
+
Create a new Next.js app and add the Chat SDK and adapter packages:
|
|
35
|
+
|
|
36
|
+
`npx create-next-app@latest my-slack-bot --typescript --app cd my-slack-bot pnpm add chat @chat-adapter/slack @chat-adapter/state-redis`
|
|
37
|
+
|
|
38
|
+
The `chat` package is the Chat SDK core. The `@chat-adapter/slack` and `@chat-adapter/state-redis` packages are the [Slack platform adapter](https://chat-sdk.dev/adapters/slack) and [Redis state adapter](https://chat-sdk.dev/adapters/redis).
|
|
39
|
+
|
|
40
|
+
### 2\. Create a Slack app
|
|
41
|
+
|
|
42
|
+
Go to [api.slack.com/apps](https://api.slack.com/apps), click **Create New App**, then **From an app manifest**.
|
|
43
|
+
|
|
44
|
+
Select your workspace and paste the following manifest:
|
|
45
|
+
|
|
46
|
+
`display_information: name: My Bot description: A bot built with Chat SDK features: bot_user: display_name: My Bot always_online: true oauth_config: scopes: bot: - app_mentions:read - channels:history - channels:read - chat:write - groups:history - groups:read - im:history - im:read - mpim:history - mpim:read - reactions:read - reactions:write - users:read settings: event_subscriptions: request_url: https://your-domain.com/api/webhooks/slack bot_events: - app_mention - message.channels - message.groups - message.im - message.mpim interactivity: is_enabled: true request_url: https://your-domain.com/api/webhooks/slack org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false`
|
|
47
|
+
|
|
48
|
+
Replace [`https://your-domain.com/api/webhooks/slack`](https://your-domain.com/api/webhooks/slack) with your deployed webhook URL, then click **Create**.
|
|
49
|
+
|
|
50
|
+
After creating the app:
|
|
51
|
+
|
|
52
|
+
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`
|
|
53
|
+
|
|
54
|
+
2. Go to **Basic Information** → **App Credentials** and copy the **Signing Secret**. You'll need this as `SLACK_SIGNING_SECRET`
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
### 3\. Configure environment variables
|
|
60
|
+
|
|
61
|
+
Create a `.env.local` file in your project root:
|
|
62
|
+
|
|
63
|
+
`SLACK_BOT_TOKEN=xoxb-your-bot-token SLACK_SIGNING_SECRET=your-signing-secret REDIS_URL=redis://localhost:6379`
|
|
64
|
+
|
|
65
|
+
The Slack adapter auto-detects `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from your environment, and `createRedisState()` reads `REDIS_URL` automatically.
|
|
66
|
+
|
|
67
|
+
### 4\. Create the bot
|
|
68
|
+
|
|
69
|
+
Create `lib/bot.ts` with a `Chat` instance configured with the Slack adapter:
|
|
70
|
+
|
|
71
|
+
``import { Chat } from "chat"; import { createSlackAdapter } from "@chat-adapter/slack"; import { createRedisState } from "@chat-adapter/state-redis"; export const bot = new Chat({ userName: "mybot", adapters: { slack: createSlackAdapter(), }, state: createRedisState(), }); // Respond when someone @mentions the bot bot.onNewMention(async (thread) => { await thread.subscribe(); await thread.post("Hello! I'm listening to this thread now."); }); // Respond to follow-up messages in subscribed threads bot.onSubscribedMessage(async (thread, message) => { await thread.post(`You said: ${message.text}`); });``
|
|
72
|
+
|
|
73
|
+
`onNewMention` fires when a user @mentions your bot. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage`.
|
|
74
|
+
|
|
75
|
+
### 5\. Create the webhook route
|
|
76
|
+
|
|
77
|
+
Create a dynamic API route that handles incoming webhooks:
|
|
78
|
+
|
|
79
|
+
``import { after } from "next/server"; import { bot } from "@/lib/bot"; type Platform = keyof typeof bot.webhooks; export async function POST( request: Request, context: RouteContext<"/api/webhooks/[platform]"> ) { const { platform } = await context.params; const handler = bot.webhooks[platform as Platform]; if (!handler) { return new Response(`Unknown platform: ${platform}`, { status: 404 }); } return handler(request, { waitUntil: (task) => after(() => task), }); }``
|
|
80
|
+
|
|
81
|
+
This creates a `POST /api/webhooks/slack` endpoint. The `waitUntil` option ensures message processing completes after the HTTP response is sent. This is required on serverless platforms where the function would otherwise terminate before your handlers finish.
|
|
82
|
+
|
|
83
|
+
### 6\. Test locally
|
|
84
|
+
|
|
85
|
+
1. Start your development server (`pnpm dev`)
|
|
86
|
+
|
|
87
|
+
2. Expose it with a tunnel (e.g. `ngrok http 3000`)
|
|
88
|
+
|
|
89
|
+
3. Update the Slack Event Subscriptions **Request URL** to your tunnel URL
|
|
90
|
+
|
|
91
|
+
4. Invite your bot to a Slack channel (`/invite @mybot`)
|
|
92
|
+
|
|
93
|
+
5. @mention the bot. It should respond with "Hello! I'm listening to this thread now."
|
|
94
|
+
|
|
95
|
+
6. Reply in the thread. It should echo your message back
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
### 7\. Add interactive features
|
|
99
|
+
|
|
100
|
+
Chat SDK supports rich interactive messages using a JSX-like syntax. Update your bot to send cards with buttons:
|
|
101
|
+
|
|
102
|
+
``import { Chat, Card, CardText as Text, Actions, Button, Divider } from "chat"; import { createSlackAdapter } from "@chat-adapter/slack"; import { createRedisState } from "@chat-adapter/state-redis"; export const bot = new Chat({ userName: "mybot", adapters: { slack: createSlackAdapter(), }, state: createRedisState(), }); bot.onNewMention(async (thread) => { await thread.subscribe(); await thread.post( <Card title="Welcome!"> <Text>I'm now listening to this thread. Try clicking a button:</Text> <Divider /> <Actions> <Button id="hello" style="primary">Say Hello</Button> <Button id="info">Show Info</Button> </Actions> </Card> ); }); bot.onAction("hello", async (event) => { await event.thread.post(`Hello, ${event.user.fullName}!`); }); bot.onAction("info", async (event) => { await event.thread.post(`You're on ${event.thread.adapter.name}.`); });``
|
|
103
|
+
|
|
104
|
+
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"`.
|
|
105
|
+
|
|
106
|
+
### 8\. Deploy to Vercel
|
|
107
|
+
|
|
108
|
+
Deploy your bot to Vercel:
|
|
109
|
+
|
|
110
|
+
`vercel deploy`
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
## Troubleshooting
|
|
115
|
+
|
|
116
|
+
### Bot doesn't respond to mentions
|
|
117
|
+
|
|
118
|
+
Check that your Slack app has the `app_mentions:read` scope and that the **Event Subscriptions** Request URL is correct. Slack sends a challenge request when you first set the URL, so your server must be running and reachable.
|
|
119
|
+
|
|
120
|
+
### Webhook signature verification fails
|
|
121
|
+
|
|
122
|
+
Confirm that `SLACK_SIGNING_SECRET` matches the value in your Slack app's **Basic Information** → **App Credentials**. A mismatched or missing signing secret will cause the adapter to reject incoming webhooks.
|
|
123
|
+
|
|
124
|
+
### Redis connection errors
|
|
125
|
+
|
|
126
|
+
Verify that `REDIS_URL` is reachable from your deployment environment. If running locally, make sure your Redis instance is started. The state adapter uses Redis for distributed locking, so the bot won't process messages without a working connection.
|
|
127
|
+
|
|
128
|
+
### Handlers don't run to completion on Vercel
|
|
129
|
+
|
|
130
|
+
Make sure your webhook route passes `waitUntil` to the handler, as shown in step 5. Without it, serverless functions can terminate before your event handlers finish.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
[View full KB sitemap](/kb/sitemap.md)
|