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
- Update
app-home-opened.tsto publish a richer home view with quick actions - Add button action handler that opens the bug report modal
- Test by clicking your bot → see home view → click button → modal opens
Building on Previous Lessons
- From Shortcuts and Modals: Reuse the bug report modal and handler
- From Bolt Middleware: Correlation logging in all handlers
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:
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:
- Update
server/listeners/events/app-home-opened.tsto publish a view with a "Report Bug" button - Add button action handler (
action_id: "home_report_bug") that opens the bug report modal from lesson 3.2 - Register the action handler in
server/listeners/actions/index.ts - Add correlation logging to all handlers
- Test the full flow: open App Home → click button → modal opens → submit → see confirmation
Implementation hints:
- App Home uses
client.views.publish()withtype: "home" - Buttons in App Home provide
trigger_idfor 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
-
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)
-
Test the quick action:
- Click "🐛 Report Bug" button
- Modal opens (same modal from lesson 3.2)
- Fill and submit
-
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: truein manifest - Run
slack runto reinstall after manifest changes - Check
app_home_openedevent is registered inserver/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.publishis 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_idfrom 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.listto validate channel exists
Button doesn't open modal:
- Verify
action_idin home view matches handler registration ("home_report_bug") - Check
trigger_idis extracted frombody.trigger_id - Ensure
ack()called beforeclient.views.open()(both within 3 seconds)
Modal opens but uses wrong handler:
- Check
callback_idin modal definition matchesapp.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.tswith exportedbugReportModal - Updated lesson 3.2's
bug-report.tsto import shared modal - Updated
server/listeners/events/app-home-opened.tswith enhanced home view - Home view includes "Report Bug" button with
action_id: "home_report_bug" - Created
server/listeners/actions/home-report-bug.tsimporting 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:
// 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:
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:
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:
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.
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:
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.
Was this helpful?