Vercel Logo

Build a persistent dashboard your users can always access

Messages scroll away. Slash commands require memorization. App Home gives your bot a persistent UI that's always one click away—users click your bot in the sidebar and see a dashboard. No hunting through channels, no remembering syntax. Use it for quick actions, settings, status displays.

Outcome

Enhance App Home with a "Report Bug" button that opens a modal and updates the home view after submission.

Fast Track

  1. Update app-home-opened.ts to publish a richer home view with quick actions
  2. Add button action handler that opens the bug report modal
  3. Test by clicking your bot → see home view → click button → modal opens

Building on Previous Lessons

How App Home Works

App Home is accessible via the Apps sidebar—users click your bot and see a persistent UI (home tab). The app_home_opened event fires every time a user opens the Home tab (not Messages or About). You publish views with client.views.publish() in response:

views.publish Is Aggressively Rate-Limited

Slack applies strict rate limits to client.views.publish(). Treat it as a once-per-interaction operation, not something you call in loops or on high-frequency events. For dynamic updates after user actions, republishing once is fine. For real-time dashboards, consider caching views or using background jobs to pre-build them, and always check the official Slack API docs for the latest limits.

The template already has a basic app_home_opened handler that publishes a simple welcome view. You'll enhance it with quick action buttons.

Hands-On Exercise 3.3

Enhance App Home with a "Report Bug" quick action:

Requirements:

  1. Update server/listeners/events/app-home-opened.ts to publish a view with a "Report Bug" button
  2. Add button action handler (action_id: "home_report_bug") that opens the bug report modal from lesson 3.2
  3. Register the action handler in server/listeners/actions/index.ts
  4. Add correlation logging to all handlers
  5. Test the full flow: open App Home → click button → modal opens → submit → see confirmation

Implementation hints:

  • App Home uses client.views.publish() with type: "home"
  • Buttons in App Home provide trigger_id for opening modals (same as shortcuts)
  • Reuse the bug report modal from lesson 3.2—don't rebuild it
  • After modal submission, you can republish the home view to show a confirmation message
  • Check event.tab === "home" to ignore Messages/About tabs

Try It

  1. Find your bot's App Home:

    • In Slack sidebar, scroll to Apps section (below Direct Messages)
    • Click your bot's name (e.g., "Slack Agent (local)")
    • Optional: Click the 3-dot menu → "Show app in your top bar" to pin it for easier access
    • You'll see the home view (not to be confused with the main "Home" button at the top)
  2. Test the quick action:

    • Click "🐛 Report Bug" button
    • Modal opens (same modal from lesson 3.2)
    • Fill and submit
  3. Verify:

    • Bug report posts to selected channel
    • Check logs for correlation IDs tracking: app_home_opened → action → view submission

Troubleshooting

Home tab shows "This app doesn't have a Home tab":

  • Verify app_home.home_tab_enabled: true in manifest
  • Run slack run to reinstall after manifest changes
  • Check app_home_opened event is registered in server/listeners/events/index.ts

Note: app_home_opened fires every time a user clicks the Home tab (not Messages or About). That's why the code checks event.tab === "home"—to avoid republishing when users switch to other tabs.

View doesn't update after button click:

  • Check action_id matches between button and handler
  • Verify views.publish is called with correct user_id
  • Look for Block Kit validation errors in logs

Modal doesn't open from Home tab button:

  • Home tab actions don't provide trigger_id directly
  • Must use body.trigger_id from action payload
  • Ensure ack() called within 3 seconds

"channel_not_found" when posting to triage:

  • Create #bug-triage channel first
  • Or use fallback to post to user's DM
  • Consider using conversations.list to validate channel exists

Button doesn't open modal:

  • Verify action_id in home view matches handler registration ("home_report_bug")
  • Check trigger_id is extracted from body.trigger_id
  • Ensure ack() called before client.views.open() (both within 3 seconds)

Modal opens but uses wrong handler:

  • Check callback_id in modal definition matches app.view() registration from lesson 3.2
  • If reusing 3.2's modal, use "bug_report_modal" as callback_id

Commit

git add -A
git commit -m "feat(app-home): add quick actions to home view
 
- Update app_home_opened handler with Report Bug button
- Create action handler that opens bug report modal
- Register action handler in actions/index.ts
- Reuse bug report modal from lesson 3.2
- Add correlation logging throughout"

Done-When

  • Created server/lib/slack/modals.ts with exported bugReportModal
  • Updated lesson 3.2's bug-report.ts to import shared modal
  • Updated server/listeners/events/app-home-opened.ts with enhanced home view
  • Home view includes "Report Bug" button with action_id: "home_report_bug"
  • Created server/listeners/actions/home-report-bug.ts importing shared modal
  • Registered action handler in server/listeners/actions/index.ts
  • Button opens bug report modal when clicked
  • All handlers include correlation logging

Step-by-Step Solution

Step 1: Extract the bug report modal to a shared file

Create server/lib/slack/modals.ts to store reusable modal definitions:

/slack-agent/server/lib/slack/modals.ts
// The 'as const' assertions tell TypeScript to use literal types ("modal" not string)
// This improves type safety when Slack's API expects specific literal values
export const bugReportModal = {
  type: "modal" as const,
  callback_id: "bug_report_modal",
  title: {
    type: "plain_text" as const,
    text: "Report a Bug",
  },
  submit: {
    type: "plain_text" as const,
    text: "Submit Report",
  },
  blocks: [
    {
      type: "section" as const,
      text: {
        type: "mrkdwn" as const,
        text: "Help us track and fix bugs faster with detailed reports.",
      },
    },
    {
      type: "input" as const,
      block_id: "bug_title",
      label: {
        type: "plain_text" as const,
        text: "Bug Title",
      },
      element: {
        type: "plain_text_input" as const,
        action_id: "title_input",
        placeholder: {
          type: "plain_text" as const,
          text: "Brief description of the issue",
        },
      },
    },
    {
      type: "input" as const,
      block_id: "bug_description",
      label: {
        type: "plain_text" as const,
        text: "Description",
      },
      element: {
        type: "plain_text_input" as const,
        action_id: "description_input",
        multiline: true,
        placeholder: {
          type: "plain_text" as const,
          text: "Steps to reproduce, expected vs actual behavior",
        },
      },
    },
    {
      type: "input" as const,
      block_id: "channel_select",
      label: {
        type: "plain_text" as const,
        text: "Report to Channel",
      },
      element: {
        type: "channels_select" as const,
        action_id: "channel_input",
        placeholder: {
          type: "plain_text" as const,
          text: "Select channel",
        },
      },
    },
  ],
};

Then update server/listeners/shortcuts/bug-report.ts from lesson 3.2 to import it:

/slack-agent/server/listeners/shortcuts/bug-report.ts
import { bugReportModal } from "~/lib/slack/modals";
 
// In the handler, replace the inline modal definition:
await client.views.open({
  trigger_id: body.trigger_id,
  view: bugReportModal,  // Use shared modal instead of inline definition
});

Important: The extracted modal must match your lesson 3.2 implementation exactly—same block_id, action_id, and field structure. The view submission handler from 3.2 expects these specific IDs. This is a refactoring step (DRY), not creating a new modal.

Step 2: Update the app_home_opened handler

Modify server/listeners/events/app-home-opened.ts to publish an enhanced view:

/slack-agent/server/listeners/events/app-home-opened.ts
import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
 
const appHomeOpenedCallback = async ({
  client,
  event,
  logger,
  context,
}: AllMiddlewareArgs & SlackEventMiddlewareArgs<"app_home_opened">) => {
  // Only handle Home tab (event.tab can be "home", "messages", or "about")
  if (event.tab !== "home") return;
 
  logger.info({
    ...context.correlation,
    user: event.user,
    tab: event.tab,
  }, "Publishing App Home view");
 
  try {
    await client.views.publish({
      user_id: event.user,
      view: {
        type: "home",
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: `*Welcome, <@${event.user}>!* 👋`,
            },
          },
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "*Quick Actions*",
            },
          },
          {
            type: "actions",
            elements: [
              {
                type: "button",
                text: {
                  type: "plain_text",
                  text: "🐛 Report Bug",
                },
                action_id: "home_report_bug",
                style: "primary",
              },
            ],
          },
          {
            type: "divider",
          },
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "*Recent Activity*",
            },
          },
          {
            type: "context",
            elements: [
              {
                type: "mrkdwn",
                text: "No recent activity",
              },
            ],
          },
        ],
      },
    });
  } catch (error) {
    logger.error({
      ...context.correlation,
      error: error instanceof Error ? error.message : String(error),
    }, "Failed to publish home view");
  }
};
 
export default appHomeOpenedCallback;

Step 3: Create the button action handler

Create server/listeners/actions/home-report-bug.ts:

/slack-agent/server/listeners/actions/home-report-bug.ts
import type { AllMiddlewareArgs, SlackActionMiddlewareArgs } from "@slack/bolt";
import { bugReportModal } from "~/lib/slack/modals";
 
const homeReportBugCallback = async ({
  ack,
  body,
  client,
  logger,
  context,
}: AllMiddlewareArgs & SlackActionMiddlewareArgs) => {
  try {
    logger.info({
      ...context.correlation,
      user: body.user.id,
    }, "Opening bug report modal from App Home");
 
    await ack();
 
    // Button actions provide trigger_id just like shortcuts do
    await client.views.open({
      trigger_id: body.trigger_id,
      view: bugReportModal,
    });
  } catch (error) {
    logger.error({
      ...context.correlation,
      error: error instanceof Error ? error.message : String(error),
    }, "Failed to open bug report modal from App Home");
  }
};
 
export default homeReportBugCallback;

Now both the shortcut (lesson 3.2) and App Home button use the same modal definition—change it once, updates everywhere.

Why the View Handler 'Just Works'

The bug report modal from both entry points (shortcut AND App Home button) uses callback_id: "bug_report_modal". When a user submits the modal, Slack sends a view submission event with that callback_id. The view handler from lesson 3.2 (bug-report-view.ts) is registered with app.view("bug_report_modal", ...), so it handles submissions from any source that uses that callback_id—shortcut, App Home button, or future triggers you add. This is the power of the callback_id pattern: one handler, multiple entry points.

Step 4: Register the action handler

Add to server/listeners/actions/index.ts:

/slack-agent/server/listeners/actions/index.ts
import type { App } from "@slack/bolt";
import { sampleActionCallback } from "./sample-action";
import { feedbackButtonsCallback } from "./feedback-button-action";
import homeReportBugCallback from "./home-report-bug"; // Add import
 
const register = (app: App) => {
  app.action("sample_action_id", sampleActionCallback);
  app.action("feedback", feedbackButtonsCallback);
  app.action("home_report_bug", homeReportBugCallback); // Add registration
};
 
export default { register };

Note: Since you're reusing the bug_report_modal callback_id, the existing view handler from lesson 3.2 (bug-report-view.ts) will handle the submission—no need to create a new one.