SapotaCorp

The Slack App Manifest: YAML Structure and Versioning

Building the same Slack app across dev, staging, and prod by clicking through the UI is how config drifts and events stop firing. The Slack app manifest turns that whole setup into one YAML file you can version, diff, and promote.

The Slack App Manifest: YAML Structure and Versioning

Key takeaways

  • A Slack app manifest is a single YAML file that fully describes an app: display info, features, OAuth scopes, events, and settings, so you can recreate the app without clicking through 30 UI tabs.
  • Declaring a slash command in features.slash_commands does nothing unless you also add the commands scope under oauth_config.scopes.bot; the two must always match.
  • Adding a scope to a live app forces users to reinstall before the new permission takes effect, while removing a scope shrinks the existing token with no reinstall needed.
  • Slack verifies your event and interactivity request_url with a challenge at import time, so the backend must be deployed and responding before you apply the manifest.
  • Keep dev, staging, and prod manifests as separate YAML files in git; the only real difference between them is the URLs, which a tiny promotion script can swap.

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: true for local dev so you do not have to expose an endpoint, false in production where Slack POSTs to your real URL.
  • org_deploy_enabled: true only for Enterprise Grid org-level installs. A Vietnamese commercial bank running on Grid used this; single-workspace clients leave it false. Turning it on for a non-Grid workspace lets the manifest save but the install will not work.
  • token_rotation_enabled: true expires 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 it false. Do not flip this on for a running app whose code does not handle refresh tokens, or you will get random token_expired errors.

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.

Engineering certifications

Sapota engineers hold credentials on Slack. Each badge links to the individual engineer's credly profile.

Browse Slack certs

Need this on your team?

Sapota engineers ship the patterns you read here. Two-week paid trial, direct pricing from $1,800/ engineer/month, no agency markup.

Get a quote
Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project