Handle assistant threads that adapt as users navigate Slack
Slack's AI Assistant interface persists as users navigate channels—it's a split-view that stays open while they browse. The assistant_thread_started event fires when users open your assistant. The assistant_thread_context_changed event fires when they switch channels while it's open. Handle these to set and update suggested prompts dynamically.
Outcome
Handle assistant thread lifecycle events and use AI to generate context-aware suggested prompts based on the channel the user is viewing.
Fast Track
- Create handlers for
assistant_thread_startedandassistant_thread_context_changed - Use AI to generate context-aware prompts based on channel name
- Set suggested prompts with
client.assistant.threads.setSuggestedPrompts() - Test by opening assistant and switching channels—prompts adapt to context
Enterprise Sandbox/Grid: Assistant features work out of the box
Pro/Business+: May require admin enablement + user opt-in (Preferences → Navigation → App agents & assistants)
If assistants aren't available in your workspace, skip this lesson—your bot still works fine. The template already has placeholder handlers you can enhance later.
Building on Previous Lessons
- From Bolt Middleware: Use
context.correlationfor tracking thread lifecycle - Sets up Section 4: You'll learn AI orchestration patterns—system prompts, tools, status updates, error resilience
How Assistant Threads Work
Slack's AI Assistant is a persistent split-view that stays open as users navigate:
assistant_thread_started- User opens assistant → set initial suggested promptsassistant_thread_context_changed- User switches channels → update prompts based on new context
Both events include assistant_thread.channel_id (where assistant lives) and assistant_thread.context.channel_id (where user is browsing).
Hands-On Exercise 3.4
Implement handlers for both assistant thread events:
Requirements:
- Add required scopes to manifest:
channels:read,groups:read,im:read,mpim:read(needed forconversations.info) - Create
server/listeners/events/assistant-thread-started.tswith generic starter prompts - Create
server/listeners/events/assistant-thread-context-changed.tsthat generates AI prompts - Fetch channel name with
conversations.info()when context changes - Use
generateObjectwithgpt-4o-minito create 3 context-relevant prompts - Call
client.assistant.threads.setSuggestedPrompts()with AI-generated prompts - Add correlation logging to all handlers
- Register both in
server/listeners/events/index.ts
Implementation hints:
- Prompts format:
{ title: "Label (max 75 chars)", message: "Full prompt text" } - Use schema:
z.object({ prompts: z.array(z.object({ title: z.string().max(75), message: z.string() })).length(3) }) - Prompt AI: "User is viewing #${channelName}. Suggest 3 specific, helpful assistant prompts relevant to this channel"
- Graceful fallback: If AI or
conversations.infofails, use static generic prompts - Zod's
.length(3)enforces exactly 3 prompts (Slack supports 1-10) - Titles >75 chars get truncated by Slack—
.max(75)prevents this
conversations.info() requires :read scopes to fetch channel metadata (name, topic, etc.):
channels:read- Public channelsgroups:read- Private channelsim:read- Direct messagesmpim:read- Group DMs
Your manifest already has channels:history (for reading messages), but that's different from channels:read (for metadata). Add all four :read scopes to your manifest and run slack run to reinstall. Without these, conversations.info() returns missing_scope errors.
Try It
-
Enable and open assistant:
- If needed: Slack Preferences → Navigation → enable your app under "App agents & assistants"
- Click assistant icon in top-right of Slack (or use split-view)
- Verify 3 suggested prompts appear
-
Test context changes:
- Keep assistant open, switch to a different channel (e.g., #engineering)
- Check if prompts update to context-specific suggestions
- Switch to another channel (e.g., #marketing) and observe behavior
- Verify in logs:
"Assistant thread context changed"with channel IDs"Generated AI prompts for context"with channel namesetSuggestedPromptsAPI call succeeds ("ok":true)
-
Verify logs:
[INFO] bolt-app { event_id: '...', user_id: 'U09D6B53WP4', channel_id: 'D09EFQZUW6P', thread_ts: '...' } Assistant thread started
Commit
git add -A
git commit -m "feat(assistant): handle thread lifecycle events
- Create assistant_thread_started handler with suggested prompts
- Create assistant_thread_context_changed handler for navigation tracking
- Add correlation logging to both handlers
- Register handlers in events/index.ts"Done-When
- Created
server/listeners/events/assistant-thread-started.ts - Created
server/listeners/events/assistant-thread-context-changed.ts - Both handlers set suggested prompts with
setSuggestedPrompts() - All handlers include correlation logging
- Registered both events in
server/listeners/events/index.ts - Tested opening assistant and switching channels
Step-by-Step Solution
Step 1: Create assistant_thread_started handler
Create server/listeners/events/assistant-thread-started.ts:
import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
export const assistantThreadStartedCallback = async ({
event,
client,
logger,
context,
}: AllMiddlewareArgs & SlackEventMiddlewareArgs<"assistant_thread_started">) => {
const { assistant_thread } = event;
logger.info({
...context.correlation,
user_id: assistant_thread.user_id,
channel_id: assistant_thread.channel_id,
thread_ts: assistant_thread.thread_ts,
}, "Assistant thread started");
// Set initial suggested prompts
const prompts = [
{ title: "Tell me a joke", message: "Tell me a programming joke" },
{ title: "Random fact", message: "Share a random tech fact" },
{ title: "Hello there!", message: "Hello! How are you today?" },
];
try {
await client.assistant.threads.setSuggestedPrompts({
channel_id: assistant_thread.channel_id,
thread_ts: assistant_thread.thread_ts,
prompts,
});
} catch (error) {
logger.error({
...context.correlation,
error: error instanceof Error ? error.message : String(error),
}, "Failed to set suggested prompts");
}
};Step 2: Create assistant_thread_context_changed handler
Create server/listeners/events/assistant-thread-context-changed.ts:
import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
import { generateObject } from "ai";
import { z } from "zod";
export const assistantThreadContextChangedCallback = async ({
event,
client,
logger,
context,
}: AllMiddlewareArgs & SlackEventMiddlewareArgs<"assistant_thread_context_changed">) => {
const { assistant_thread } = event;
// Two channel IDs: assistant_thread.channel_id = where assistant lives (DM)
// assistant_thread.context.channel_id = where user is browsing
const currentChannelId = assistant_thread.context?.channel_id;
logger.info({
...context.correlation,
thread_ts: assistant_thread.thread_ts,
assistant_dm: assistant_thread.channel_id,
browsing_channel: currentChannelId,
}, "Assistant thread context changed");
// Generate context-aware prompts with AI
let prompts = [
{ title: "What's this about?", message: "Summarize this channel's recent activity" },
{ title: "Help me understand", message: "Explain what's happening here" },
{ title: "Anything important?", message: "Highlight key points from recent messages" },
];
try {
// Fetch channel name to give AI context
if (currentChannelId) {
const channelInfo = await client.conversations.info({ channel: currentChannelId });
const channelName = channelInfo.channel?.name || "unknown-channel";
// Use AI to generate context-specific prompts
const aiPrompts = await generateObject({
model: "openai/gpt-4o-mini",
schema: z.object({
prompts: z.array(z.object({
title: z.string().max(75),
message: z.string(),
})).length(3),
}),
prompt: `User is viewing Slack channel #${channelName}. Suggest 3 helpful, specific assistant prompts relevant to this channel context. Keep titles under 75 characters.`,
});
prompts = aiPrompts.object.prompts;
logger.info({
...context.correlation,
channel: channelName,
}, "Generated AI prompts for context");
}
} catch (error) {
logger.error({
...context.correlation,
error: error instanceof Error ? error.message : String(error),
}, "Failed to generate context-aware prompts (API or AI error), using fallback");
// Falls back to static prompts defined above
}
try {
await client.assistant.threads.setSuggestedPrompts({
channel_id: assistant_thread.channel_id,
thread_ts: assistant_thread.thread_ts,
prompts,
});
} catch (error) {
logger.error({
...context.correlation,
error: error instanceof Error ? error.message : String(error),
}, "Failed to set suggested prompts");
}
};Step 3: Register both handlers
Update server/listeners/events/index.ts:
import type { App } from "@slack/bolt";
import appHomeOpenedCallback from "./app-home-opened";
import appMentionCallback from "./app-mention";
import { assistantThreadStartedCallback } from "./assistant-thread-started";
import { assistantThreadContextChangedCallback } from "./assistant-thread-context-changed"; // Add import
const register = (app: App) => {
app.event("app_home_opened", appHomeOpenedCallback);
app.event("app_mention", appMentionCallback);
app.event("assistant_thread_started", assistantThreadStartedCallback);
app.event("assistant_thread_context_changed", assistantThreadContextChangedCallback); // Add registration
};
export default { register };Troubleshooting
Assistant doesn't appear:
- Verify manifest has
assistant:writescope and assistant events subscribed - Run
slack runto reinstall after manifest changes - Check user enabled your app in Preferences → Navigation → App agents & assistants
Events not firing:
- Handlers must be registered in
server/listeners/events/index.ts - Check logs for event payloads to verify they're reaching your server
- Assistants may not be available on all workspace types
Suggested prompts don't appear:
- Verify
setSuggestedPrompts()is called in your handler - Prompts array cannot be empty
- Prompt titles must be ≤75 characters
- Check logs to verify API calls succeed (
"ok":truein response)
Assistant responds with blank messages:
- The template's AI response handler (
server/lib/ai/respond-to-message.ts) usesstepCountIs(5)to limit tool calls. If the assistant tries to call multiple tools (fetching thread context, updating status, etc.), it hits the limit before generating text and sends a blank response. - Quick fix: Increase the limit in
respond-to-message.ts: changestopWhen: stepCountIs(5)tostopWhen: stepCountIs(10)or more as needed. - Why it exists: Step limits prevent infinite tool-calling loops. You'll learn about agent loop control in Section 5 (AI Orchestration). For now, just bump the limit so the assistant can work.
- See AI SDK Loop Control docs for details on
stopWhen,stepCountIs, and other conditions.
What's Next
You've built handlers for Slack's interaction surfaces—commands, shortcuts, modals, App Home, and assistant threads. These trigger actions but don't leverage the bot's full AI capabilities. Section 5 (AI Orchestration & Tools) teaches you to combine interaction surfaces with intelligent AI responses: system prompts that shape behavior, tools that fetch context and take actions, streaming status updates, and error resilience. Your bot stops being a form processor and becomes an actual agent.
Was this helpful?