---
title: "Views and App Home"
description: "Learn how App Home provides a persistent UI for your bot. Create a dashboard with quick action buttons, dynamically update views based on user interaction, and handle modal submissions that post to multiple destinations."
canonical_url: "https://vercel.com/academy/slack-agents/views-and-app-home"
md_url: "https://vercel.com/academy/slack-agents/views-and-app-home.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-09T11:09:46.446Z"
content_type: "lesson"
course: "slack-agents"
course_title: "Slack Agents on Vercel with the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Views and App Home

# 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

- **From [Shortcuts and Modals](./shortcuts-and-modals)**: Reuse the bug report modal and handler
- **From [Bolt Middleware](./bolt-nitro-middleware-and-logging)**: 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:

\*\*Warning: 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

```bash
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

- [x] Created `server/lib/slack/modals.ts` with exported `bugReportModal`
- [x] Updated lesson 3.2's `bug-report.ts` to import shared modal
- [x] Updated `server/listeners/events/app-home-opened.ts` with enhanced home view
- [x] Home view includes "Report Bug" button with `action_id: "home_report_bug"`
- [x] Created `server/listeners/actions/home-report-bug.ts` importing shared modal
- [x] Registered action handler in `server/listeners/actions/index.ts`
- [x] Button opens bug report modal when clicked
- [x] 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:

```typescript title="/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:

```typescript title="/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:

```typescript title="/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`:

```typescript title="/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.

\*\*Note: 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`:

```typescript title="/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.

\*\*Side Quest: What You Missed: AI-Powered Channel Digest\*\*


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
