Vercel Logo

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

  1. Create handlers for assistant_thread_started and assistant_thread_context_changed
  2. Use AI to generate context-aware prompts based on channel name
  3. Set suggested prompts with client.assistant.threads.setSuggestedPrompts()
  4. Test by opening assistant and switching channels—prompts adapt to context
Assistant Availability

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.correlation for 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:

  1. assistant_thread_started - User opens assistant → set initial suggested prompts
  2. assistant_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:

  1. Add required scopes to manifest: channels:read, groups:read, im:read, mpim:read (needed for conversations.info)
  2. Create server/listeners/events/assistant-thread-started.ts with generic starter prompts
  3. Create server/listeners/events/assistant-thread-context-changed.ts that generates AI prompts
  4. Fetch channel name with conversations.info() when context changes
  5. Use generateObject with gpt-4o-mini to create 3 context-relevant prompts
  6. Call client.assistant.threads.setSuggestedPrompts() with AI-generated prompts
  7. Add correlation logging to all handlers
  8. 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.info fails, 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
Required Scopes for Channel Info

conversations.info() requires :read scopes to fetch channel metadata (name, topic, etc.):

  • channels:read - Public channels
  • groups:read - Private channels
  • im:read - Direct messages
  • mpim: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

  1. 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
  2. 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 name
      • setSuggestedPrompts API call succeeds ("ok":true)
  3. 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:

/slack-agent/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:

/slack-agent/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:

/slack-agent/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:write scope and assistant events subscribed
  • Run slack run to 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":true in response)

Assistant responds with blank messages:

  • The template's AI response handler (server/lib/ai/respond-to-message.ts) uses stepCountIs(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: change stopWhen: stepCountIs(5) to stopWhen: 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.