Setting up a Slack app by hand means clicking through tabs at api.slack.com/apps: name and icon on one page, scopes on another, event subscriptions somewhere else, interactivity toggle in yet another corner. Do that once and it is tedious. Do it three times for dev, staging, and prod and the configs quietly drift apart, an event subscription goes missing in staging, and you spend an afternoon wondering why a handler never fires. The slack app manifest fixes this by collapsing the entire app config into one YAML file you can copy, version, diff, and promote between environments.
A manifest describes everything: the app name, description, bot user, slash commands, shortcuts, OAuth scopes, event subscriptions, interactivity URLs, Socket Mode, and OAuth redirect URLs. You either paste it into "Create New App" -> "From an app manifest" to bootstrap a brand-new app, or paste it into the "App Manifest" tab of an existing app to update it. Slack shows you a diff and applies the change. One file, no clicking.
The overall shape of a manifest
A manifest has four top-level sections under an optional _metadata block: display_information, features, oauth_config, and settings. Here is the skeleton, drawn from an internal ops bot we built for an e-commerce operations team that creates a private channel for every order issue:
_metadata:
major_version: 1
minor_version: 1
display_information:
name: Ops Bot
description: Automates the order-issue triage workflow
background_color: "#1A94FF"
long_description: >
Creates a private channel for each order issue (thousands per day),
invites the agent and warehouse lead, tracks SLA, and archives the
channel on resolution. Integrates with the OMS via webhook plus the
Slack Web API.
features:
bot_user:
display_name: OpsBot
always_online: true
app_home:
home_tab_enabled: false
messages_tab_enabled: false
messages_tab_read_only_enabled: true
slash_commands:
- command: /ops-issue
description: Look up or create an order issue
usage_hint: ORDER-ID [reason]
should_escape: false
shortcuts:
- name: Escalate to Manager
type: message
callback_id: escalate_issue
description: Escalate this issue message to a manager
oauth_config:
redirect_urls:
- https://api.example.com/slack/oauth/callback
scopes:
bot:
- chat:write
- channels:manage
- groups:write
- users:read
- commands
- app_mentions:read
settings:
event_subscriptions:
request_url: https://api.example.com/slack/events
bot_events:
- app_mention
- member_joined_channel
interactivity:
is_enabled: true
request_url: https://api.example.com/slack/interactivity
socket_mode_enabled: false
The current manifest format is major_version: 1. Slack will bump it if the schema ever changes, but version 1 is all you need today.
display_information and features
display_information controls how the app looks in Slack: the name (max 35 characters, used in mentions and the app directory), a one-line description (max 140), an optional background_color for the icon, and an optional long_description (max 4000) shown on the app detail page. One thing the manifest does not cover: the app icon PNG. That has to be uploaded separately under Basic Information, there is no icon field in YAML.
features is where the app's actual capabilities live. The bot_user block sets the bot's display_name and always_online flag. Be honest with always_online: if you set it to true but the bot only runs during business hours, users see a green dot, message it after hours, get nothing back, and lose trust. Set it to true only for something genuinely running 24/7.
The slash_commands array declares each command with its command name, description, usage_hint, and (when you are on HTTP rather than Socket Mode) a url endpoint. The shortcuts array supports two type values: message for a right-click action on a message, and global for a command-bar shortcut. Each shortcut needs a callback_id your code routes on, and every callback_id must be unique inside the app or handler routing breaks. A large engineering org we worked with used a message shortcut named "Rollback this deploy" with callback_id: rollback_deploy so a lead could right-click a deploy message and roll it back in one move.
There is also unfurl_domains, an array of domains your app handles link previews for, and a legacy workflow_steps field tied to the old Workflow Builder. Skip workflow_steps for any app built today.
oauth_config: scopes are the part that bites
oauth_config holds two things: redirect_urls and scopes. The redirect URLs are the allowlist Slack checks during the OAuth flow; if the redirect_uri in your authorize request is not in this list, Slack returns bad_redirect_uri. For a distributed app spanning staging and prod, declare both URLs here.
Scopes are the part that actually causes incidents. You declare bot scopes and, only when the app acts on behalf of a real user, user scopes:
oauth_config:
scopes:
bot:
- chat:write
- channels:history
- groups:history
- users:read
- users:read.email
- commands
- app_mentions:read
user:
- search:read
The rule is minimum scope. Request only what the app actually calls. Extra scopes are a security liability, they make marketplace review harder, and they spook users at the install screen. The classic trap, and one I have watched cost real debugging time, is declaring slash_commands in features but forgetting commands under oauth_config.scopes.bot. The app installs fine, the command shows up, and then it silently does nothing because the bot lacks the scope to receive it. Whenever you add a command, check both places.
settings: events, interactivity, and the enterprise flags
settings wires up the runtime behavior. The event_subscriptions block sets the request_url where Slack POSTs events and lists bot_events (and optionally user_events). The interactivity block enables buttons, modals, and select menus and points at the URL that receives block_actions, view_submission, and shortcut payloads. You can route events and interactivity to one endpoint and let the Bolt SDK sort them out, or split them; the e-commerce team split theirs because interactivity volume was far lower than event volume and they wanted to monitor each independently.
The remaining flags are toggles you set per environment and per client:
socket_mode_enabled:truefor local dev so you do not have to expose an endpoint,falsein production where Slack POSTs to your real URL.org_deploy_enabled:trueonly for Enterprise Grid org-level installs. A Vietnamese commercial bank running on Grid used this; single-workspace clients leave itfalse. Turning it on for a non-Grid workspace lets the manifest save but the install will not work.token_rotation_enabled:trueexpires access tokens after 12 hours and forces a refresh-token flow. That same bank enabled it for compliance reasons; most internal single-workspace apps leave itfalse. Do not flip this on for a running app whose code does not handle refresh tokens, or you will get randomtoken_expirederrors.
A real pitfall here: Slack verifies your request_url with a challenge the moment you import the manifest. If the URL 404s or does not respond within three seconds, the import fails. Deploy the backend first, confirm the endpoint answers, then apply the manifest.
Managing versions across dev, staging, and prod
Slack does not keep a version history for manifests, so you have to version them yourself in git. The pattern that holds up: a manifests/ folder in the app repo with one file per environment.
manifests/dev.yaml
manifests/staging.yaml
manifests/prod.yaml
Every config change is an edit to one of these files, committed with a message saying who changed what and why. To apply, either use the Slack CLI:
slack manifest update --app A0XXXXX --file manifests/prod.yaml
or paste the YAML into the App Manifest tab, where Slack renders a diff against the current config. Read that diff before clicking Update, especially the scopes section.
Scope changes have asymmetric consequences, and this is the one rule that catches teams repeatedly. Expanding scope (adding a permission) requires users to reinstall the app before the new scope takes effect; the old token keeps its old, narrower permissions until then. Add groups:write to a live app, save, and your call to archive a private channel still fails with missing_scope until someone clicks "Reinstall to Workspace." Put "requires reinstall" in the release note every time. Shrinking scope (removing a permission) needs no reinstall; the existing token simply loses that access.
For dev versus prod, the common setup is two separate apps, say "Ops Bot Dev" and "Ops Bot," each with its own manifest file. The display_information, features, and scopes are identical between them. The only real difference is the URLs: dev points at an ngrok tunnel, prod at the cloud endpoint. A small script that swaps the URLs when you promote dev to prod keeps the two files honest and removes the temptation to hand-edit. One last warning learned the hard way: if you copy a manifest off the internet, replace every URL with your own domain, or Slack will dutifully send your events to a stranger's server.
Takeaway
The manifest turns Slack app config from a pile of UI clicks into a file you can review, diff, and promote like any other code. Keep features and oauth_config.scopes in sync, remember that commands scope is mandatory for slash commands, and treat scope expansion as a reinstall event. Deploy the backend before importing so URL verification passes. Store one manifest per environment in git, differing only in URLs, and let a script handle the promotion. Do that and "set up the app in staging" stops being an afternoon and becomes a paste.
If your team is wiring Slack into real workflows and wants the manifest, scopes, and multi-environment setup done right the first time, we can help. Reach out via /contact or see what we build at /service.








