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:
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.
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?