Vercel Logo

App Manifests: Version Control for Your Slack App Configuration

Clicking through 20+ settings in the Slack app dashboard for every environment is tedious and error prone. "Works on my machine" because someone forgot to enable a scope in staging. Permission creep as apps accumulate unnecessary scopes with no audit trail. Manifests fix this: one JSON file defines your entire app config, versioned in git, deployed consistently across dev/staging/prod.

Outcome

Understand how scopes enable features and how to modify manifests without breaking your app.

Fast Track

  1. Add reactions:read scope to manifest.json
  2. Subscribe to reaction_added and reaction_removed events
  3. Create handlers and verify events fire in logs

Understanding Scopes

Scopes are permissions your bot requests. Too few = broken features. Too many = security risk and user distrust. Start minimal, add as needed. Every scope should map to a specific feature.

Hands-On Exercise 2.1

Add reaction tracking to your bot:

Requirements:

  1. Add reactions:read scope to manifest.json under oauth_config.scopes.bot
  2. Subscribe to reaction_added and reaction_removed events in settings.event_subscriptions.bot_events
  3. Create server/listeners/events/reactions.ts with handlers that log reaction details
  4. Register handlers in server/listeners/events/index.ts

Implementation hints:

  • Run slack run to apply manifest changes (approve when prompted)
  • Test by reacting to messages in channels where the bot is present
  • Check logs for reaction events with reaction, item, user fields

Try It

  1. Apply manifest changes:

    slack run
    # Approve when prompted: "Do you want to apply manifest changes?"
  2. Test reaction events:

    • Find a channel where your bot is present
    • React to any message with 👍
    • Check terminal logs for:
      [INFO] reaction_added { reaction: "thumbsup", item: {...}, user: "U..." }
      
    • Remove the reaction, verify reaction_removed logs
  3. Verify scope in dashboard:

    • Open api.slack.com/apps → your app → Event Subscriptions
    • Confirm "Verified" status and reaction_added/reaction_removed appear in subscribed events

Troubleshooting

Events not firing:

  • Ensure bot is actually in the channel (invite it with /invite @bot)
  • Reinstall app after manifest changes (slack run handles this)
  • Check that manifest validation passed (no errors during slack run)

Scope mismatch errors:

  • Every event requires matching scopes (reaction_added needs reactions:read)
  • Slack validates this and will reject inconsistent manifests
  • Fix by adding the missing scope and re-running slack run

Commit

git add -A
git commit -m "feat(manifest): add reactions:read and reaction event handlers
 
- Add reactions:read scope to manifest
- Subscribe to reaction_added and reaction_removed events
- Create reaction event handlers with structured logging
- Verify events fire when reactions are added/removed"

Done-When

  • reactions:read scope added to manifest.json
  • Subscribed to reaction_added and reaction_removed events
  • Created server/listeners/events/reactions.ts with handlers
  • Handlers registered in server/listeners/events/index.ts
  • Events fire in logs when reacting to messages

Step by Step Solution

Add an events listener file and register it:

/slack-agent/server/listeners/events/reactions.ts
import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
 
/**
 * Handle reaction events for basic monitoring and future state/metrics.
 * Requires `reactions:read` scope and subscription to `reaction_added` and `reaction_removed`.
 */
export const reactionAddedCallback = async ({
  event,
  logger,
}: AllMiddlewareArgs & SlackEventMiddlewareArgs<"reaction_added">) => {
  try {
    logger.info(
      {
        reaction: event.reaction,
        item: event.item,
        user: event.user,
        item_user: event.item_user,
        event_ts: event.event_ts,
      },
      "reaction_added",
    );
  } catch (error) {
    logger.error("reaction_added handler failed:", error);
  }
};
 
export const reactionRemovedCallback = async ({
  event,
  logger,
}: AllMiddlewareArgs & SlackEventMiddlewareArgs<"reaction_removed">) => {
  try {
    logger.info(
      {
        reaction: event.reaction,
        item: event.item,
        user: event.user,
        item_user: event.item_user,
        event_ts: event.event_ts,
      },
      "reaction_removed",
    );
  } catch (error) {
    logger.error("reaction_removed handler failed:", error);
  }
};
 
export default { reactionAddedCallback, reactionRemovedCallback };

Register in your events index:

/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 { reactionAddedCallback, reactionRemovedCallback } from "./reactions";
 
const register = (app: App) => {
  app.event("app_home_opened", appHomeOpenedCallback);
  app.event("app_mention", appMentionCallback);
  app.event("assistant_thread_started", assistantThreadStartedCallback);
  app.event("reaction_added", reactionAddedCallback);
  app.event("reaction_removed", reactionRemovedCallback);
};
 
export default { register };

Manifest changes (diff)

Add reactions:read and subscribe to reaction events:

/slack-agent/manifest.json
@@ oauth_config.scopes.bot @@
   "scopes": {
     "bot": [
       "channels:history",
       "chat:write",
       "commands",
       "app_mentions:read",
       "groups:history",
       "im:history",
       "mpim:history",
       "assistant:write",
       "reactions:write",
 +     "reactions:read"
     ]
   }
 
@@ settings.event_subscriptions.bot_events @@
   "bot_events": [
     "app_home_opened",
     "app_mention",
     "assistant_thread_context_changed",
     "assistant_thread_started",
 +   "reaction_added",
 +   "reaction_removed",
     "message.channels",
     "message.groups",
     "message.im",
     "message.mpim"
   ]