chat 4.29.0 → 4.30.0
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/ai/index.d.ts +3 -3
- package/dist/{chat-D9UYaaNO.d.ts → chat-BPjXsoIl.d.ts} +9 -7
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -9
- package/dist/{jsx-runtime-CFq1K_Ve.d.ts → jsx-runtime-CnDs8rPr.d.ts} +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/docs/adapters.mdx +2 -2
- package/docs/ai/index.mdx +6 -0
- package/docs/getting-started.mdx +5 -1
- package/docs/index.mdx +3 -1
- package/docs/meta.json +1 -0
- package/docs/slack-primitives.mdx +320 -0
- package/docs/streaming.mdx +1 -1
- package/package.json +1 -1
- package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +5 -1
- package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +5 -1
- package/resources/guides/human-in-the-loop-with-chat-sdk-and-workflow-sdk.md +176 -0
- package/resources/guides/liveblocks-chat-sdk-ai-sdk.md +165 -0
- package/resources/guides/run-and-track-deploys-from-slack.md +7 -5
- package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +5 -1
- package/resources/guides/slack-bot-vercel-blob.md +254 -0
- package/resources/guides/triage-form-submissions-with-chat-sdk.md +3 -1
- package/resources/templates.json +5 -0
|
@@ -35,7 +35,7 @@ Create a new Next.js app and add the Chat SDK and adapter packages:
|
|
|
35
35
|
|
|
36
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
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).
|
|
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/official/slack) and [Redis state adapter](https://chat-sdk.dev/adapters/official/redis).
|
|
39
39
|
|
|
40
40
|
### 2\. Create a Slack app
|
|
41
41
|
|
|
@@ -129,6 +129,10 @@ Verify that `REDIS_URL` is reachable from your deployment environment. If runnin
|
|
|
129
129
|
|
|
130
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
131
|
|
|
132
|
+
* * *
|
|
133
|
+
|
|
134
|
+
## Build with a template or read more.
|
|
135
|
+
|
|
132
136
|
---
|
|
133
137
|
|
|
134
138
|
[View full KB sitemap](/kb/sitemap.md)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Human-in-the-Loop with Chat SDK and Workflow SDK
|
|
2
|
+
|
|
3
|
+
**Author:** Ben Sabic
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You can pause a durable workflow until a human approves it in Slack by combining Chat SDK and Workflow SDK. Chat SDK posts an interactive card with Approve and Deny buttons; Workflow SDK's `createWebhook` generates a URL that those buttons POST to when clicked, suspending the workflow until the click arrives. The workflow resumes with the click payload, decides what to do, and continues, with no `onAction` handler, no custom approval database, and no polling.
|
|
8
|
+
|
|
9
|
+
This guide walks you through suspending a workflow with `createWebhook`, posting a Chat SDK approval card with `callbackUrl` buttons, and resuming the workflow based on the user's choice. You'll add a timeout so abandoned approvals can't suspend forever, and see how to extend the pattern to multiple decision points within a single workflow.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before you begin, make sure you have:
|
|
14
|
+
|
|
15
|
+
* Node.js 18 or later
|
|
16
|
+
|
|
17
|
+
* An existing [Chat SDK](https://chat-sdk.dev) bot (see the [Slack agent guide](https://vercel.com/kb/guide/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk) or [Slack file bot guide](https://vercel.com/kb/guide/slack-bot-vercel-blob))
|
|
18
|
+
|
|
19
|
+
* A project configured for [Workflow SDK](https://workflow-sdk.dev/docs/getting-started)
|
|
20
|
+
|
|
21
|
+
* A [Vercel account](https://vercel.com/signup) if you're deploying to Vercel
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
> Workflow SDK runs against any [world](https://workflow-sdk.dev/worlds); a pluggable backend for storage, queuing, and authentication. When you deploy to Vercel, [Vercel Workflows](https://vercel.com/workflows) is selected automatically with zero configuration. For self-hosted setups, see [Postgres World](https://workflow-sdk.dev/worlds/postgres) and the other providers.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
Three pieces fit together:
|
|
29
|
+
|
|
30
|
+
* **Workflow SDK** runs the long-lived process. A function becomes durable when you mark it with `"use workflow"`: it can suspend, resume, and survive crashes without losing state. `createWebhook()` returns an object with a `url` that's a public endpoint. When you `await` it, the workflow is suspended until that endpoint receives an HTTP request.
|
|
31
|
+
|
|
32
|
+
* **Chat SDK** presents the decision to a human. A `<Button callbackUrl="...">` posts the click's action data to the URL you supply, in addition to firing any `onAction` handler. When the URL is the workflow's `webhook.url`, the click resumes the workflow directly.
|
|
33
|
+
|
|
34
|
+
* **The pairing** removes the usual glue. There's no separate approvals table, no `onAction` callback that has to look up which workflow is waiting, and no polling. The workflow suspends, the user clicks, and the same workflow function picks up where it left off with the click payload in hand.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
`createWebhook()` is the right primitive when the workflow itself owns the resume URL. For cases where you'd rather keep the URL private and resume from your own server code, use `createHook()` with a deterministic business token instead.
|
|
38
|
+
|
|
39
|
+
## Steps
|
|
40
|
+
|
|
41
|
+
### 1\. Install dependencies
|
|
42
|
+
|
|
43
|
+
Add Workflow SDK to a project that already has a Chat SDK bot set up:
|
|
44
|
+
|
|
45
|
+
`pnpm add workflow`
|
|
46
|
+
|
|
47
|
+
If you're starting from scratch, also install a few Chat SDK packages:
|
|
48
|
+
|
|
49
|
+
`pnpm add chat @chat-adapter/slack @chat-adapter/state-redis`
|
|
50
|
+
|
|
51
|
+
The `workflow` package is Workflow SDK's core. The `chat` package is the Chat SDK core, and `@chat-adapter/slack` and `@chat-adapter/state-redis` are the [Slack platform adapter](https://chat-sdk.dev/adapters/official/slack) and [Redis state adapter](https://chat-sdk.dev/adapters/official/redis). See the [Chat SDK getting started guide](https://vercel.com/kb/guide/the-complete-guide-to-chat-sdk) and [Slack agent guide](https://vercel.com/kb/guide/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk) if you don't have a bot yet.
|
|
52
|
+
|
|
53
|
+
### 2\. Define the approval workflow
|
|
54
|
+
|
|
55
|
+
Create `workflows/approval.ts`:
|
|
56
|
+
|
|
57
|
+
``import { createWebhook } from "workflow"; import { finalizeApprovalCard, postApprovalCard, postReply, } from "@/lib/slack"; export async function requestDeployApproval(opts: { threadId: string; version: string; requestedBy: string; }) { "use workflow"; using webhook = createWebhook(); const messageId = await postApprovalCard({ threadId: opts.threadId, version: opts.version, requestedBy: opts.requestedBy, webhookUrl: webhook.url, }); const request = await webhook; const payload = await request.json(); if (payload.actionId === "approve") { await deploy(opts.version); await finalizeApprovalCard({ threadId: opts.threadId, messageId, version: opts.version, requestedBy: opts.requestedBy, outcome: `Approved by <@${payload.user.id}> — ${opts.version} deployed.`, }); await postReply( opts.threadId, `Deployed **${opts.version}** by <@${payload.user.id}>.`, ); } else { await finalizeApprovalCard({ threadId: opts.threadId, messageId, version: opts.version, requestedBy: opts.requestedBy, outcome: `Denied by <@${payload.user.id}>.`, }); await postReply( opts.threadId, `Deploy of **${opts.version}** denied by <@${payload.user.id}>.`, ); } } async function deploy(version: string) { "use step"; // Trigger your deploy here. This runs as a durable step, // so retries and observability come for free. }``
|
|
58
|
+
|
|
59
|
+
The workflow is intentionally thin. It owns the webhook and the resume logic; everything else (posting the card, sending follow-up messages, triggering the deploy) is a step. That separation keeps the workflow body deterministic and pushes side effects into retryable units.
|
|
60
|
+
|
|
61
|
+
Next, define the helper steps in `lib/slack.ts`:
|
|
62
|
+
|
|
63
|
+
``import { Card, CardText, Actions, Button } from "chat"; import { bot } from "@/lib/bot"; export async function postApprovalCard(opts: { threadId: string; version: string; requestedBy: string; webhookUrl: string; }): Promise<string> { "use step"; const thread = bot.thread(opts.threadId); const sent = await thread.post( <Card title={`Deploy ${opts.version}?`} subtitle={`Requested by ${opts.requestedBy}`}> <CardText>Approve to roll out **{opts.version}**, or deny to abort.</CardText> <Actions> <Button id="approve" style="primary" callbackUrl={opts.webhookUrl}> Approve </Button> <Button id="deny" style="danger" callbackUrl={opts.webhookUrl}> Deny </Button> </Actions> </Card>, ); return sent.id; } // Re-render the approval card without the Actions block, swapping the // buttons for a static outcome line. The title, subtitle, and body text // stay the same so the thread keeps its context. export async function finalizeApprovalCard(opts: { threadId: string; messageId: string; version: string; requestedBy: string; outcome: string; }) { "use step"; const thread = bot.thread(opts.threadId); await thread.adapter.editMessage( thread.id, opts.messageId, <Card title={`Deploy ${opts.version}?`} subtitle={`Requested by ${opts.requestedBy}`}> <CardText>Approve to roll out **{opts.version}**, or deny to abort.</CardText> <CardText>{opts.outcome}</CardText> </Card>, ); } // Post a follow-up message as markdown so platforms with native markdown // support render bold/italic/links instead of literal asterisks. Plain // strings are passed through as-is — `{ markdown }` is the explicit form. export async function postReply(threadId: string, markdown: string) { "use step"; await bot.thread(threadId).post({ markdown }); }``
|
|
64
|
+
|
|
65
|
+
`bot.thread(threadId)` constructs a `Thread` reference from a serialized ID, which is exactly the entry point Chat SDK provides for posting outside an event handler. Thread IDs follow the format `adapter:channel:thread` (for example, `slack:C123ABC:1234567890.123456`) and round-trip cleanly through JSON, so they're safe to pass as workflow inputs.
|
|
66
|
+
|
|
67
|
+
**Key details in this workflow**
|
|
68
|
+
|
|
69
|
+
* The `using` declaration ensures the webhook is cleaned up automatically when the workflow exits, even if it throws.
|
|
70
|
+
|
|
71
|
+
* Both buttons point at the same `webhook.url`. The `actionId` travels in the callback payload, so a single webhook handles all the buttons on the card.
|
|
72
|
+
|
|
73
|
+
* `await webhook` suspends the workflow. The function pauses here until the user clicks, which could take seconds, hours, or days. Workflow SDK persists the state and resumes on the click.
|
|
74
|
+
|
|
75
|
+
* The `deploy` function is marked `"use step"`, which makes it a durable step with automatic retries and observability. Workflow SDK only re-runs steps when they fail, not on resumption.
|
|
76
|
+
|
|
77
|
+
* `postApprovalCard` returns the posted message's `id`, and `finalizeApprovalCard` calls `thread.adapter.editMessage` to re-render the card without buttons once the decision is in. That prevents a stale click on the original card from hitting the consumed webhook, and it leaves a clear audit trail in the thread.
|
|
78
|
+
|
|
79
|
+
* `postReply` posts with `{ markdown }` rather than a bare string. The bare-string form is passed through as-is, so `**bold**` would render as literal asterisks on Slack and Teams; the `{ markdown }` form is converted to the platform's native markup.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
If you need to preserve more of the original thread context across the workflow boundary (for example, the triggering message or thread metadata), use `thread.toJSON()` on the way in and [`bot.reviver()`](https://chat-sdk.dev/docs/api/thread) when restoring on the other side. For the approval flow above, the thread ID is enough.
|
|
83
|
+
|
|
84
|
+
### 3\. Start the workflow
|
|
85
|
+
|
|
86
|
+
Trigger the workflow from wherever the approval request originates.
|
|
87
|
+
|
|
88
|
+
From a Chat SDK handler:
|
|
89
|
+
|
|
90
|
+
`import { start } from "workflow/api"; import { requestDeployApproval } from "@/workflows/approval"; bot.onNewMention(async (thread, message) => { const version = parseVersion(message.text); if (!version) { await thread.post("Usage: @bot deploy v1.2.3"); return; } await start(requestDeployApproval, [ { threadId: thread.id, version, requestedBy: message.author.fullName, }, ]); });`
|
|
91
|
+
|
|
92
|
+
`start` queues the workflow run and returns immediately with a run handle, allowing the mention handler to respond without blocking. The workflow takes over from there, posting the card, suspending on the webhook, and resuming when the user clicks.
|
|
93
|
+
|
|
94
|
+
### 4\. Read the callback payload
|
|
95
|
+
|
|
96
|
+
Chat SDK POSTs a JSON body to the callback URL with this shape:
|
|
97
|
+
|
|
98
|
+
`{ "type": "action", "actionId": "approve", "value": "approve", "user": { "id": "U123", "name": "alice" }, "threadId": "slack:C123:1234567890.123", "messageId": "1234567890.456" }`
|
|
99
|
+
|
|
100
|
+
\- `actionId` is the button's `id` prop: `"approve"` or `"deny"` in this workflow.
|
|
101
|
+
|
|
102
|
+
\- `value` is the optional `value` prop you set on the button, a string for passing extra context to the handler. It's most useful when multiple buttons share an `id` (so `actionId` alone can't distinguish them) or when the button needs to carry a record identifier like `"item-123"`. In the approval workflow above, the buttons have distinct `id`s, so `value` isn't set and arrives as `undefined`.
|
|
103
|
+
|
|
104
|
+
\- `user` is the user who clicked, regardless of who triggered the workflow.
|
|
105
|
+
|
|
106
|
+
Use `actionId` to branch on which button was pressed, and [`user.id`](http://user.id) to record or validate who approved.
|
|
107
|
+
|
|
108
|
+
## Handling multiple decision points
|
|
109
|
+
|
|
110
|
+
For workflows that need more than one approval, call `createWebhook()` multiple times. Each call generates a fresh URL, so suspensions are independent:
|
|
111
|
+
|
|
112
|
+
``export async function multiStageApproval(opts: { threadId: string }) { "use workflow"; using draftReview = createWebhook(); const draftTitle = "Approve draft?"; const draftMessageId = await postPrompt(opts.threadId, draftTitle, draftReview.url); const draftPayload = await (await draftReview).json(); if (draftPayload.actionId !== "approve") { await finalizePromptCard(opts.threadId, draftMessageId, draftTitle, `Rejected by <@${draftPayload.user.id}>.`); return; } await finalizePromptCard(opts.threadId, draftMessageId, draftTitle, `Approved by <@${draftPayload.user.id}>.`); using finalReview = createWebhook(); const finalTitle = "Approve final?"; const finalMessageId = await postPrompt(opts.threadId, finalTitle, finalReview.url); const finalPayload = await (await finalReview).json(); if (finalPayload.actionId !== "approve") { await finalizePromptCard(opts.threadId, finalMessageId, finalTitle, `Rejected by <@${finalPayload.user.id}>.`); return; } await finalizePromptCard(opts.threadId, finalMessageId, finalTitle, `Approved by <@${finalPayload.user.id}>.`); await publish(); }``
|
|
113
|
+
|
|
114
|
+
This workflow relies on two helpers used at each stage. Add `postPrompt` and `finalizePromptCard` to `lib/slack.ts`:
|
|
115
|
+
|
|
116
|
+
`export async function postPrompt( threadId: string, title: string, webhookUrl: string, ): Promise<string> { "use step"; const thread = bot.thread(threadId); const sent = await thread.post( <Card title={title}> <Actions> <Button id="approve" style="primary" callbackUrl={webhookUrl}> Approve </Button> <Button id="deny" style="danger" callbackUrl={webhookUrl}> Deny </Button> </Actions> </Card>, ); return sent.id; } export async function finalizePromptCard( threadId: string, messageId: string, title: string, outcome: string, ) { "use step"; const thread = bot.thread(threadId); await thread.adapter.editMessage( thread.id, messageId, <Card title={title}> <CardText>{outcome}</CardText> </Card>, ); }`
|
|
117
|
+
|
|
118
|
+
Each `using` declaration scopes the webhook to its block, so cleanup happens as soon as the workflow moves past that approval. `finalizePromptCard` strips the buttons once a decision lands, so every stage shows its final state without leaving stale buttons in the thread. The workflow itself can suspend at any point, without you tracking which webhook is which, since the runtime handles that.
|
|
119
|
+
|
|
120
|
+
## Adding a timeout
|
|
121
|
+
|
|
122
|
+
A workflow that suspends on a webhook indefinitely is fine until someone forgets to click. Race the webhook against a durable `sleep` to bound the wait:
|
|
123
|
+
|
|
124
|
+
``import { createWebhook, sleep } from "workflow"; export async function approvalWithTimeout(opts: { threadId: string }) { "use workflow"; using webhook = createWebhook(); const title = "Proceed with the change?"; const messageId = await postPrompt(opts.threadId, title, webhook.url); const result = await Promise.race([ webhook.then(async (req) => ({ kind: "clicked" as const, body: await req.json() })), sleep("24h").then(() => ({ kind: "timeout" as const })), ]); if (result.kind === "timeout") { await finalizePromptCard(opts.threadId, messageId, title, "Timed out after 24h — no decision recorded."); return; } await finalizePromptCard( opts.threadId, messageId, title, `${result.body.actionId === "approve" ? "Approved" : "Denied"} by <@${result.body.user.id}>.`, ); if (result.body.actionId === "approve") { await proceed(); } }``
|
|
125
|
+
|
|
126
|
+
`sleep` is itself a durable suspension, so the 24-hour wait costs nothing while it's pending and survives deploys. When the timeout wins the race, the workflow continues without resuming the webhook, and the `using` cleanup releases the URL. The card is finalized in both branches, so the timeout is visible in the thread rather than silently leaving a stale set of buttons.
|
|
127
|
+
|
|
128
|
+
## Validating the approver
|
|
129
|
+
|
|
130
|
+
The callback URL is authenticated only by its token, which means anyone who can intercept the URL can resume the workflow. For sensitive operations, validate the user in the payload before continuing:
|
|
131
|
+
|
|
132
|
+
``const APPROVERS = new Set(["U_ALICE", "U_BOB"]); const request = await webhook; const payload = await request.json(); if (!APPROVERS.has(payload.user.id)) { await thread.post( `<@${payload.user.id}> isn't authorized to approve this deploy.`, ); return; }``
|
|
133
|
+
|
|
134
|
+
For stronger guarantees, switch to [`createHook()`](https://workflow-sdk.dev/docs/api-reference/workflow/create-hook) and resume the workflow from your own authenticated route with `resumeHook()`. That pattern keeps the resume URL private and gives you full control over the authorization check, at the cost of writing a route handler.
|
|
135
|
+
|
|
136
|
+
## Troubleshooting
|
|
137
|
+
|
|
138
|
+
### The workflow never resumes after a click
|
|
139
|
+
|
|
140
|
+
Confirm the button's `callbackUrl` matches `webhook.url` exactly. The URL contains a token that's bound to the suspension, so a stale or hand-edited URL won't resolve. Check the Workflow SDK CLI to see whether the workflow is still suspended on the webhook:
|
|
141
|
+
|
|
142
|
+
`npx workflow inspect runs --web`
|
|
143
|
+
|
|
144
|
+
If the click reached the URL but the workflow didn't move, look for an error in the workflow logs. The `using` declaration disposes the webhook on the next throw, so an uncaught error in the workflow body can release the URL before the click arrives.
|
|
145
|
+
|
|
146
|
+
### `ValidationError` when posting the card on Discord or Telegram
|
|
147
|
+
|
|
148
|
+
Discord's `custom_id` has a 100-character limit; Telegram's `callback_data` has a 64-byte limit. The encoded button data is the action ID plus the callback token, which can exceed those caps. Shorten the action ID (`"a"` instead of `"approve-deploy-v1-2-3"`) and move long context into `value` or out of the card entirely. Slack and Teams don't have this limit.
|
|
149
|
+
|
|
150
|
+
### The same approval card gets clicked twice
|
|
151
|
+
|
|
152
|
+
The main flow above guards against this by calling `finalizeApprovalCard` immediately after the decision lands. The card is re-rendered without its `Actions` block, so there's nothing left to click. If you skip that step, Chat SDK won't dedupe clicks for you: once `await webhook` has resolved, a second click hits a webhook that no longer exists, and the user sees the platform's default error. Either edit the card to remove the buttons (the pattern shown above, via `thread.adapter.editMessage`), or accept that only the first click matters and ignore the rest.
|
|
153
|
+
|
|
154
|
+
### A workflow run shows as suspended forever
|
|
155
|
+
|
|
156
|
+
Workflows suspend until something resumes them. If your callback URL was lost (for example, a card was deleted before anyone could click), the run will stay suspended until its associated webhook is cleaned up. Use a timeout (see above) to bound every suspension, and use the Workflow SDK CLI to cancel runs that should no longer wait:
|
|
157
|
+
|
|
158
|
+
`npx workflow inspect runs # find the run ID, then: npx workflow runs cancel <run-id>`
|
|
159
|
+
|
|
160
|
+
## Related resources
|
|
161
|
+
|
|
162
|
+
* [Chat SDK Threads, Messages, and Channels](https://chat-sdk.dev/docs/threads-messages-channels)
|
|
163
|
+
|
|
164
|
+
* [Chat SDK Actions](https://chat-sdk.dev/docs/actions)
|
|
165
|
+
|
|
166
|
+
* [Workflow SDK](https://workflow-sdk.dev/docs/api-reference/workflow/create-webhook) [`createWebhook`](https://workflow-sdk.dev/docs/api-reference/workflow/create-webhook)
|
|
167
|
+
|
|
168
|
+
* [Workflow SDK](https://workflow-sdk.dev/docs/api-reference/workflow/create-hook) [`createHook`](https://workflow-sdk.dev/docs/api-reference/workflow/create-hook)
|
|
169
|
+
|
|
170
|
+
* [Workflow SDK Human-in-the-Loop example](https://workflow-sdk.dev/cookbook/agent-patterns/human-in-the-loop)
|
|
171
|
+
|
|
172
|
+
* [How to build an AI agent for Slack with Chat SDK and AI SDK](https://vercel.com/kb/guide/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk)
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
[View full KB sitemap](/kb/sitemap.md)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# How to build an agent for Liveblocks with Chat SDK and AI SDK
|
|
2
|
+
|
|
3
|
+
**Author:** Chris Nicholas, Ben Sabic
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You can build an AI-powered bot that reads and responds to Liveblocks comment threads using [Chat SDK](https://chat-sdk.dev), [AI SDK](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent)'s `ToolLoopAgent`, and the [Liveblocks Chat SDK adapter](https://chat-sdk.dev/adapters/vendor-official/liveblocks). Chat SDK handles webhook verification, message routing, and the Liveblocks API. `ToolLoopAgent` wraps your language model with tools and runs an autonomous reasoning loop, calling tools and feeding results back until it has a complete answer. Redis tracks thread subscriptions and manages distributed locking for concurrent message handling, giving you a production-ready AI bot without managing infrastructure.
|
|
8
|
+
|
|
9
|
+
This guide walks you through wiring up a Next.js app directory project that responds to @-mentions in Liveblocks threads with streamed AI replies and tool calling.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before you begin, make sure you have:
|
|
14
|
+
|
|
15
|
+
* Node.js 18+
|
|
16
|
+
|
|
17
|
+
* A Next.js app that uses [Liveblocks Comments](https://liveblocks.io/docs/get-started/comments)
|
|
18
|
+
|
|
19
|
+
* A Liveblocks account and the [dashboard](https://liveblocks.io/dashboard) open
|
|
20
|
+
|
|
21
|
+
* A Redis instance (local or hosted, such as [Upstash](https://vercel.com/marketplace/upstash))
|
|
22
|
+
|
|
23
|
+
* A [Vercel account](https://vercel.com/signup) with AI Gateway access
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
[Liveblocks Comments](https://liveblocks.io/comments) is a fully featured React commenting system you can embed in any application, giving users threaded discussions, @-mentions, emoji reactions, and notifications out of the box. Threads are attached to specific rooms in your app, making them ideal for contextual conversations around documents, designs, or any shared content.
|
|
29
|
+
|
|
30
|
+
The `@liveblocks/chat-sdk-adapter` connects Chat SDK to Liveblocks' webhook system. You register event handlers (like `onNewMention` and `onReaction`) and the adapter routes incoming webhook payloads to them. It handles signature verification, parses comment and reaction events, and exposes a consistent thread API for posting replies or adding emoji reactions.
|
|
31
|
+
|
|
32
|
+
AI SDK's `ToolLoopAgent` wraps a language model with tools and runs an autonomous loop: the model generates text or calls a tool, the SDK executes the tool, feeds the result back, and repeats until the model finishes. When you pass a model string like `"anthropic/claude-sonnet-4-6"` and host your application on Vercel, the AI SDK routes the request through the [Vercel AI Gateway](https://vercel.com/ai-gateway) automatically. Chat SDK accepts any `AsyncIterable<string>` as a message, so you can pass the agent's `fullStream` directly to `thread.post()` for real-time streaming in Liveblocks threads.
|
|
33
|
+
|
|
34
|
+
The [Redis state adapter](https://chat-sdk.dev/adapters/official/redis) tracks which threads the bot has subscribed to, so follow-up messages in the same thread are handled automatically after the first mention.
|
|
35
|
+
|
|
36
|
+
## Steps
|
|
37
|
+
|
|
38
|
+
### 1\. Have your Comments app ready
|
|
39
|
+
|
|
40
|
+
Before continuing, make sure you have a React app with Liveblocks Comments up and running. If you haven't set that up yet, follow the [quickstart guide](https://liveblocks.io/docs/get-started/comments) first.
|
|
41
|
+
|
|
42
|
+
### 2\. Install Liveblocks, Chat SDK, and AI SDK
|
|
43
|
+
|
|
44
|
+
Install the Liveblocks adapter, Chat SDK, AI SDK, and other related packages:
|
|
45
|
+
|
|
46
|
+
`npm install @liveblocks/chat-sdk-adapter @liveblocks/node chat @chat-adapter/state-redis ai zod`
|
|
47
|
+
|
|
48
|
+
The `chat` package is the Chat SDK core. `@liveblocks/chat-sdk-adapter` is the Liveblocks platform adapter. `ai` is the AI SDK, which includes `ToolLoopAgent`. `zod` is used to define tool input schemas. `@chat-adapter/state-redis` is the [Redis state adapter,](https://chat-sdk.dev/adapters/official/redis) which handles thread subscriptions and distributed locking.
|
|
49
|
+
|
|
50
|
+
### 3\. Create a Liveblocks project
|
|
51
|
+
|
|
52
|
+
Head to the [Liveblocks dashboard](https://liveblocks.io/dashboard), open the project you'd like to use, and copy the **Secret Key** from the **API Keys** page.
|
|
53
|
+
|
|
54
|
+
### 4\. Add your environment variables
|
|
55
|
+
|
|
56
|
+
Create a `.env.local` file in your project root:
|
|
57
|
+
|
|
58
|
+
`LIVEBLOCKS_SECRET_KEY="sk_..." LIVEBLOCKS_WEBHOOK_SECRET="whsec_..." REDIS_URL="redis://localhost:6379"`
|
|
59
|
+
|
|
60
|
+
You'll create the webhook secret further on in the guide.
|
|
61
|
+
|
|
62
|
+
Instead of an AI Gateway API key, this guide uses [Vercel OIDC tokens](https://vercel.com/docs/ai-gateway/authentication-and-byok/authentication#oidc-token) to authenticate to the AI Gateway. Link your app to a Vercel project and pull the token into your local environment:
|
|
63
|
+
|
|
64
|
+
`vercel link vercel env pull`
|
|
65
|
+
|
|
66
|
+
`vercel env pull` writes a `VERCEL_OIDC_TOKEN` into `.env.local` alongside any other project environment variables. The AI SDK's [gateway provider](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway) reads this token automatically when no `AI_GATEWAY_API_KEY` is set, so no code changes are needed.
|
|
67
|
+
|
|
68
|
+
OIDC tokens expire after 12 hours, so re-run `vercel env pull` during longer development sessions to refresh it. In production on Vercel, the token is generated and rotated for you automatically.
|
|
69
|
+
|
|
70
|
+
### 5\. Set up user resolution
|
|
71
|
+
|
|
72
|
+
Create `app/database.ts` to export your bot's user ID and a `getUser` function. Chat SDK calls `resolveUsers` to convert user IDs from @-mentions into display names when constructing messages.
|
|
73
|
+
|
|
74
|
+
The function must return an object matching this shape:
|
|
75
|
+
|
|
76
|
+
`type UserInfo = { name: string; avatar?: string; color?: string; };`
|
|
77
|
+
|
|
78
|
+
In production, query your own user database or auth provider:
|
|
79
|
+
|
|
80
|
+
`export const BOT_USER_ID = "__bot__"; export const BOT_USER_NAME = "My Bot"; export async function getUser(id: string): Promise<UserInfo | undefined> { const user = await db.users.findUnique({ where: { id } }); return user ? { name: user.displayName, avatar: user.avatarUrl } : undefined; }`
|
|
81
|
+
|
|
82
|
+
### 6\. Define your agent's tools
|
|
83
|
+
|
|
84
|
+
Create `app/tools.ts` with the tools your agent can call. Each tool has a `description` that tells the model when to use it, an `inputSchema` that the model fills in, and an `execute` function that runs when the tool is called:
|
|
85
|
+
|
|
86
|
+
``import { tool } from "ai"; import { z } from "zod"; export const tools = { getWeather: tool({ description: "Get the current weather for a location", inputSchema: z.object({ location: z.string().describe("City name, e.g. San Francisco"), }), execute: async ({ location }) => { // Replace with a real weather API call const response = await fetch( `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(location)}` ); const data = await response.json(); return { location, temperature: data.current.temp_f, condition: data.current.condition.text, }; }, }), searchDocs: tool({ description: "Search the company documentation for a topic", inputSchema: z.object({ query: z.string().describe("The search query"), }), execute: async ({ query }) => { // Replace with your actual search implementation return { results: [`Result for: ${query}`] }; }, }), };``
|
|
87
|
+
|
|
88
|
+
### 7\. Create the agent and bot instance
|
|
89
|
+
|
|
90
|
+
Create `app/bot.ts` with a `ToolLoopAgent` and a `Chat` instance. The agent is configured with a model, a system prompt, and your tools. The bot wires the Liveblocks adapter and Redis state to your event handlers:
|
|
91
|
+
|
|
92
|
+
`import { Chat } from "chat"; import { createLiveblocksAdapter, LiveblocksAdapter, } from "@liveblocks/chat-sdk-adapter"; import { createRedisState } from "@chat-adapter/state-redis"; import { ToolLoopAgent } from "ai"; import { BOT_USER_ID, BOT_USER_NAME, getUser } from "./database"; import { tools } from "./tools"; const agent = new ToolLoopAgent({ model: "anthropic/claude-sonnet-4-6", instructions: "You are a helpful assistant in a Liveblocks comment thread. " + "Answer questions clearly and use your tools when you need " + "up-to-date information. Keep responses concise.", tools, }); export const bot = new Chat<{ liveblocks: LiveblocksAdapter }>({ userName: BOT_USER_NAME, adapters: { liveblocks: createLiveblocksAdapter({ apiKey: process.env.LIVEBLOCKS_SECRET_KEY!, webhookSecret: process.env.LIVEBLOCKS_WEBHOOK_SECRET!, botUserId: BOT_USER_ID, botUserName: BOT_USER_NAME, resolveUsers: ({ userIds }) => { return userIds.map((id) => getUser(id)); }, }), }, state: createRedisState(), });`
|
|
93
|
+
|
|
94
|
+
### 8\. Handle mentions and reactions
|
|
95
|
+
|
|
96
|
+
Add event handlers to `app/bot.ts` after the bot instance. When someone @-mentions the bot, `onNewMention` fires. The handler acknowledges the message with a reaction, then streams the agent's response directly into the thread. `fullStream` is preferred over `textStream` because it preserves paragraph breaks between tool-calling steps:
|
|
97
|
+
|
|
98
|
+
``// Handle @-mentions of the bot bot.onNewMention(async (thread, message) => { await thread.adapter.addReaction(thread.id, message.id, "👀"); const result = await agent.stream({ prompt: message.text }); await thread.post(result.fullStream); }); // Handle reactions to messages bot.onReaction(async (event) => { if (!event.added) return; await event.adapter.postMessage( event.threadId, `${event.user.userName} reacted with "${event.emoji.name}"` ); });``
|
|
99
|
+
|
|
100
|
+
### 9\. Create the webhook route
|
|
101
|
+
|
|
102
|
+
Create the API route at `app/api/webhooks/liveblocks/route.ts`. This is the endpoint Liveblocks will POST webhook events to:
|
|
103
|
+
|
|
104
|
+
`import { bot } from "@/app/bot"; export async function POST(request: Request) { return bot.webhooks.liveblocks(request, { waitUntil: (p) => void p, }); }`
|
|
105
|
+
|
|
106
|
+
For production deployments on Vercel, use `waitUntil` from `@vercel/functions` so your handler finishes processing after the HTTP response has been sent. This is required on serverless platforms where the function would otherwise terminate early:
|
|
107
|
+
|
|
108
|
+
`import { bot } from "@/app/bot"; import { waitUntil } from "@vercel/functions"; export async function POST(request: Request) { return bot.webhooks.liveblocks(request, { waitUntil }); }`
|
|
109
|
+
|
|
110
|
+
### 10\. Set up Liveblocks webhooks
|
|
111
|
+
|
|
112
|
+
1. Expose your dev server with a tunnel, for example using [ngrok](https://ngrok.com):
|
|
113
|
+
|
|
114
|
+
`npx ngrok http 3000`
|
|
115
|
+
|
|
116
|
+
2. Go to the [Liveblocks dashboard](https://liveblocks.io/dashboard) and create a new webhook endpoint. Set the URL to your generated ngrok URL, add your webhook route’s pathname to the end, and enable these events in the project dashboard:
|
|
117
|
+
|
|
118
|
+
* `commentCreated`
|
|
119
|
+
|
|
120
|
+
* `commentReactionAdded`
|
|
121
|
+
|
|
122
|
+
* `commentReactionRemoved`
|
|
123
|
+
|
|
124
|
+
3. Copy the **webhook secret** and add it to `.env.local` as `LIVEBLOCKS_WEBHOOK_SECRET`:
|
|
125
|
+
|
|
126
|
+
`LIVEBLOCKS_WEBHOOK_SECRET="whsec_..."`
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Now when users @-mention your bot in a Liveblocks thread, it will stream a response and call tools autonomously.
|
|
130
|
+
|
|
131
|
+
## How to add Slack, Teams, or other platforms
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
To add Slack or Teams, install the relevant adapter packages and register them alongside the Liveblocks adapter:
|
|
136
|
+
|
|
137
|
+
`npm install @chat-adapter/slack @chat-adapter/teams`
|
|
138
|
+
|
|
139
|
+
Then add them to your `Chat` instance in `app/bot.ts`:
|
|
140
|
+
|
|
141
|
+
`import { createSlackAdapter } from "@chat-adapter/slack"; import { createTeamsAdapter } from "@chat-adapter/teams"; export const bot = new Chat({ userName: BOT_USER_NAME, adapters: { liveblocks: createLiveblocksAdapter({ ... }), slack: createSlackAdapter(), teams: createTeamsAdapter(), }, state: createRedisState(), });`
|
|
142
|
+
|
|
143
|
+
The existing webhook route already uses a `[platform]` parameter, so each platform gets its own endpoint automatically: `/api/webhooks/slack` and `/api/webhooks/teams`. No additional routing code is needed.
|
|
144
|
+
|
|
145
|
+
Streaming behavior varies by platform. Slack uses its native streaming API for smooth real-time updates, while Liveblocks and Teams 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.
|
|
146
|
+
|
|
147
|
+
See the [Chat SDK adapter directory](https://chat-sdk.dev/adapters) for the full list of supported platforms and their configuration options.
|
|
148
|
+
|
|
149
|
+
## Related resources
|
|
150
|
+
|
|
151
|
+
* [Chat SDK documentation](https://chat-sdk.dev)
|
|
152
|
+
|
|
153
|
+
* [Chat SDK adapter directory](https://chat-sdk.dev/adapters)
|
|
154
|
+
|
|
155
|
+
* [AI SDK agent documentation](https://ai-sdk.dev/docs/agents/building-agents)
|
|
156
|
+
|
|
157
|
+
* [`@liveblocks/chat-sdk-adapter`](https://liveblocks.io/docs/api-reference/liveblocks-chat-sdk-adapter) [API reference](https://liveblocks.io/docs/api-reference/liveblocks-chat-sdk-adapter)
|
|
158
|
+
|
|
159
|
+
* [Next.js Chat SDK bot quickstart](https://liveblocks.io/docs/get-started/nextjs-chat-sdk-bot)
|
|
160
|
+
|
|
161
|
+
* [How to test webhooks on localhost](https://liveblocks.io/docs/guides/how-to-test-webhooks-on-localhost)
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
[View full KB sitemap](/kb/sitemap.md)
|
|
@@ -21,7 +21,7 @@ This guide walks you through a Slack bot that orchestrates the entire deploy lif
|
|
|
21
21
|
|
|
22
22
|
For production deploys, the bot gates the workflow with an approval step, so the deploy proceeds only after an authorized team member approves it.
|
|
23
23
|
|
|
24
|
-
The bot is built with [Chat SDK](https://chat-sdk.dev) and [Vercel
|
|
24
|
+
The bot is built with [Chat SDK](https://chat-sdk.dev) and [Vercel Workflows](https://vercel.com/workflow). Chat SDK handles the Slack interaction layer (cards, buttons, modals, and slash commands), while Vercel Workflow handles stateful orchestration (pausing for approval, polling GitHub, and resuming when events arrive). You write the deploy pipeline as a single function that pauses and resumes over minutes or hours without a database or state machine.
|
|
25
25
|
|
|
26
26
|
Deploy the template now, or read on for a deeper look at how it all works.
|
|
27
27
|
|
|
@@ -29,11 +29,11 @@ Deploy the template now, or read on for a deeper look at how it all works.
|
|
|
29
29
|
|
|
30
30
|
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:
|
|
31
31
|
|
|
32
|
-
`I want to build a deploy bot for Slack using Chat SDK and Vercel
|
|
32
|
+
`I want to build a deploy bot for Slack using Chat SDK and Vercel Workflows. Clone the template repo at https://github.com/vercel-labs/chat-sdk-deploy-bot, install dependencies with pnpm, and walk me through setting up the environment variables in .env.local. I need a Slack app, a GitHub fine-grained personal access token with Actions (read/write), Contents (read), Issues (write), and Pull requests (read) permissions, and Redis (Upstash) configured. After setup, help me deploy it to Vercel and test the /deploy slash command. When searching for information, check for applicable skill(s) first and review local documentation.`
|
|
33
33
|
|
|
34
34
|
### Vercel Plugin
|
|
35
35
|
|
|
36
|
-
Turn your agent into a Vercel expert with this [plugin](https://vercel.com/docs/agent-resources/vercel-plugin). The [Chat SDK](https://skills.sh/vercel/chat/chat-sdk) and [Workflow](https://skills.sh/vercel/workflow/workflow) skills are both included.
|
|
36
|
+
Turn your agent into a Vercel expert with this [plugin](https://vercel.com/docs/agent-resources/vercel-plugin). The [Chat SDK](https://skills.sh/vercel/chat/chat-sdk) and [Workflow](https://skills.sh/vercel/workflow/workflow) skills are both included. This plugin is optional; it’s not required to use Chat SDK or for this guide.
|
|
37
37
|
|
|
38
38
|
`npx plugins add vercel/vercel-plugin`
|
|
39
39
|
|
|
@@ -173,7 +173,7 @@ The bot has three interfaces: Slack for user interaction, GitHub for dispatching
|
|
|
173
173
|
6. The bot posts a final summary card to Slack with the environment, branch, commit, duration, linked issues, and a link to the workflow run
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
[Vercel
|
|
176
|
+
[Vercel Workflows](https://vercel.com/workflow) makes this possible. A Vercel Workflow function can suspend itself mid-execution and resume later with full state preserved. The approval gate and the polling loop are both regular code. The function pauses while waiting for a button click, resumes when it arrives, then loops while polling GitHub. No cron jobs, no queues, no external state store.
|
|
177
177
|
|
|
178
178
|
## Code walkthrough
|
|
179
179
|
|
|
@@ -261,7 +261,9 @@ See the [Chat SDK adapter directory](https://chat-sdk.dev/adapters) for the full
|
|
|
261
261
|
|
|
262
262
|
* [Chat SDK GitHub](https://github.com/vercel/chat)
|
|
263
263
|
|
|
264
|
-
* [
|
|
264
|
+
* [The Complete Guide to Chat SDK](https://vercel.com/kb/guide/the-complete-guide-to-chat-sdk)
|
|
265
|
+
|
|
266
|
+
* [Vercel Workflows documentation](https://vercel.com/docs/workflow)
|
|
265
267
|
|
|
266
268
|
* [Workflow SDK](https://useworkflow.dev/)
|
|
267
269
|
|
|
@@ -39,7 +39,7 @@ Create a new Hono app and add the Chat SDK, AI SDK, and adapter packages:
|
|
|
39
39
|
|
|
40
40
|
Select the `vercel` template when prompted by `create-hono`. This sets up the project for Vercel deployment with the correct entry point.
|
|
41
41
|
|
|
42
|
-
The `chat` package is the Chat SDK core. The `@chat-adapter/github` and `@chat-adapter/state-redis` packages are the [GitHub platform adapter](https://chat-sdk.dev/adapters/github) and [Redis state adapter](https://chat-sdk.dev/adapters/redis). `@vercel/sandbox` provides the ephemeral execution environment, and `bash-tool` wires it up as an AI SDK tool.
|
|
42
|
+
The `chat` package is the Chat SDK core. The `@chat-adapter/github` and `@chat-adapter/state-redis` packages are the [GitHub platform adapter](https://chat-sdk.dev/adapters/official/github) and [Redis state adapter](https://chat-sdk.dev/adapters/official/redis). `@vercel/sandbox` provides the ephemeral execution environment, and `bash-tool` wires it up as an AI SDK tool.
|
|
43
43
|
|
|
44
44
|
### 2\. Configure a GitHub webhook
|
|
45
45
|
|
|
@@ -142,6 +142,10 @@ The sandbox has a 5-minute timeout and the agent stops after 20 steps. For large
|
|
|
142
142
|
|
|
143
143
|
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.
|
|
144
144
|
|
|
145
|
+
* * *
|
|
146
|
+
|
|
147
|
+
## Build with a template or read more.
|
|
148
|
+
|
|
145
149
|
---
|
|
146
150
|
|
|
147
151
|
[View full KB sitemap](/kb/sitemap.md)
|