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
- Add
reactions:readscope tomanifest.json - Subscribe to
reaction_addedandreaction_removedevents - 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:
- Add
reactions:readscope tomanifest.jsonunderoauth_config.scopes.bot - Subscribe to
reaction_addedandreaction_removedevents insettings.event_subscriptions.bot_events - Create
server/listeners/events/reactions.tswith handlers that log reaction details - Register handlers in
server/listeners/events/index.ts
Implementation hints:
- Run
slack runto 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,userfields
Try It
-
Apply manifest changes:
slack run # Approve when prompted: "Do you want to apply manifest changes?" -
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_removedlogs
-
Verify scope in dashboard:
- Open api.slack.com/apps → your app → Event Subscriptions
- Confirm "Verified" status and
reaction_added/reaction_removedappear 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 runhandles this) - Check that manifest validation passed (no errors during
slack run)
Scope mismatch errors:
- Every event requires matching scopes (
reaction_addedneedsreactions: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:readscope added tomanifest.json- Subscribed to
reaction_addedandreaction_removedevents - Created
server/listeners/events/reactions.tswith 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:
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:
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:
@@ 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"
]Was this helpful?