The Slack app manifest is the single JSON document that declares the Slack app a Chickpea deployment installs: its identity, its permission scopes, the events Slack sends it, and the URLs Slack calls. Where a channel grant decides which Agent may answer inside Slack, the manifest decides what the Slack app may ask Slack for at all. This page lists every field of that manifest, the 20 bot scopes and 3 user scopes with the code path behind each, the 11 bot events, and the settings that are deliberately off.
Source of truth: slack-app-manifest.json at the repository root. It is generated by canonicalSlackAppManifestJson() in src/slack/app-manifest.ts, and tests/slack-app-manifest.test.ts asserts the committed file is byte-identical to that builder’s output. Editing the JSON without editing the builder fails the test.
Quick reference
| Field group | Value |
|---|---|
| App name and bot display name | Chickpea, both |
| Description | A self-hosted, model-agnostic AI agent for Slack. |
| Background color | #bd5732 |
| Bot token scopes | 20 |
| User token scopes | 3 (openid, profile, email) |
| Redirect URLs | 2, under /auth/slack/ |
| Events request URL | /channels/slack/events |
| Bot events | 11 |
| Interactivity request URL | /channels/slack/interactions |
| Socket Mode, org deploy, token rotation, Slack MCP | all false |
Where the manifest applies
The manifest describes the customer-owned app created in the Use your own Slack app lane. Admin builds a Slack deep link from it: the committed placeholder origin https://chickpea.example is replaced with the deployment’s own origin before the link is handed to Slack, so no URL is ever copied by hand. Over a plain HTTP origin, which Slack rejects anyway, only the Events request URL is substituted.
The scope list is wider than one lane. REQUIRED_SLACK_BOT_SCOPES in src/slack/scopes.ts is read directly from the committed manifest and is the authority for every bot installation, so the same 20 scopes are requested and verified whichever lane a deployment uses. The Add to Slack lane installs Chickpea’s shared app instead of this manifest, and the deployment reaches Slack through a fixed allowlist of 33 gateway operations rather than with a token of its own.
Display information
| Field | Value |
|---|---|
name |
Chickpea, at most 35 characters |
description |
A self-hosted, model-agnostic AI agent for Slack. |
background_color |
#bd5732 |
The builder accepts a different app name and bot display name for a deployment that needs one. The description and background color are fixed.
App features
| Feature | Value | What it enables |
|---|---|---|
app_home.home_tab_enabled |
true |
The App Home directory that lists an operator’s available Agents |
app_home.messages_tab_enabled |
true |
Direct messages to an Agent from the Messages tab |
app_home.messages_tab_read_only_enabled |
false |
The Messages tab accepts input |
bot_user.display_name |
Chickpea, at most 80 characters |
The installed bot’s name |
bot_user.always_online |
true |
The bot never shows as away |
agent_view.agent_description |
Same string as the app description | Slack’s agent view header |
agent_view.suggested_prompts |
3 entries | Starter prompts: Summarize supplied text, Investigate a question, Plan a task |
Bot token scopes
Twenty scopes, in manifest order. Each line names the Slack API method or event that requires it.
| Scope | Why Chickpea needs it |
|---|---|
app_mentions:read |
Receives the app_mention event, the mention that addresses an Agent handle in a channel. |
assistant:write |
Drives the Slack agent view through assistant.threads.setStatus, setTitle, and setSuggestedPrompts. |
channels:history |
Reads public channel context with conversations.history and conversations.replies. |
channels:join |
Calls conversations.join when an Agent is published to a public channel the app has not joined. |
channels:read |
Resolves public channel facts with conversations.info, conversations.list, and conversations.members. |
chat:write |
Posts and edits replies: chat.postMessage, chat.postEphemeral, chat.update, chat.delete, and the three chat.*Stream calls. |
chat:write.customize |
Posts each reply under that Agent’s own username and icon_url instead of one shared bot identity. |
files:read |
Reads an attachment’s bytes by file id before normalization and analysis. |
files:write |
Uploads files an Agent produces, with files.uploadV2. |
groups:history |
The same context reads inside private channels, where message.groups arrives. |
groups:read |
The same channel facts for private channels. |
im:history |
Reads direct message context for a DM turn. |
im:write |
Opens a DM with conversations.open to deliver a schedule result or a management receipt. |
mpim:read |
Identifies group DMs, so a turn is classified mpim rather than as a channel. |
reactions:read |
Receives reaction_added and reads the reacted message with reactions.get. |
reactions:write |
Adds and clears reactions with reactions.add and reactions.remove. |
users:read |
Reads users.info and users.list for display names and account class: deleted, bot, guest, or foreign. |
usergroups:read |
Lists user groups with usergroups.list to find the group behind an Agent handle. |
usergroups:write |
Creates, renames, disables, and re-enables the user group that is an Agent handle, with usergroups.create, update, disable, and enable. |
users:read.email |
Adds profile.email to users.info. The address is stored as a member contact detail, never as an identity key. |
User token scopes
Three scopes, requested only during Slack OIDC sign-in and never used to call the Slack Web API. Slack OIDC is the only human sign-in. No passwords.
| Scope | Why Chickpea needs it |
|---|---|
openid |
Issues the ID token that proves a Slack workspace and user identity to Admin. |
profile |
Supplies the display name on the openid.connect.userInfo response. |
email |
Supplies the email claim on the same response. |
Redirect URLs
Two, both on the deployment’s own origin.
| Path | Purpose |
|---|---|
/auth/slack/install/callback |
Completes the bot installation OAuth exchange. |
/auth/slack/oidc/callback |
Completes a Slack OIDC sign-in. |
Event subscriptions
The request URL is /channels/slack/events on the deployment origin. Eleven bot events are subscribed.
| Event | What it does |
|---|---|
app_context_changed |
Accepted and ignored. No handler runs. |
app_home_opened |
Publishes the App Home Agent directory to the user who opened it. |
app_mention |
The primary trigger. Routes the turn to the Agent whose handle was mentioned. |
member_joined_channel |
Greets the channel when the app itself joins a channel that has an active grant. On by default, disabled with SLACK_TAG_WELCOME_ON_JOIN=false. |
message.channels |
Public channel messages. In every channel, a top-level message that mentions no one is dropped before any model is involved. |
message.groups |
The same for private channels. |
message.im |
Direct messages to the app. |
reaction_added |
May start a turn from a reaction, using the reacted message as the thread root. |
app_uninstalled |
Marks the stored workspace installation revoked. |
tokens_revoked |
Marks the stored workspace installation revoked. |
user_change |
Suspends a member’s stored authority when Slack reports that account deleted or converted to a bot. |
Interactivity
interactivity.is_enabled is true and its request URL is /channels/slack/interactions. The only interaction the app handles is the App Home button that starts a private thread with a chosen Agent, action id chickpea.agent.start.
Settings that are off
| Setting | Value | Consequence |
|---|---|---|
socket_mode_enabled |
false |
Slack delivers events over HTTPS to the request URL. No app-level token exists. |
org_deploy_enabled |
false |
The app installs into one workspace, not an Enterprise Grid org. |
token_rotation_enabled |
false |
The bot token does not expire and is not refreshed. |
is_mcp_enabled |
false |
Slack’s own MCP surface is not exposed by this app. |
One deployment currently serves one Slack workspace.
What is compared when a manifest is validated
validateSlackAppManifest compares a reduced contract rather than the whole document, so cosmetic drift in an exported manifest is tolerated and a capability change is not. The compared fields are the app name, the bot display name, the redirect URLs, the sorted user scopes, the sorted bot scopes, the events request URL, the sorted bot events, whether the App Home tab is enabled, whether interactivity is enabled, the interactivity request URL, and pkce_enabled, which Slack materializes as false when it is omitted. A mismatch raises Slack app manifest does not match the expected callbacks, scopes, or events.
Recovery has a narrower check. validateSlackAppManifestUrlRepair overwrites the two redirect URLs, the events request URL, and the interactivity request URL with the expected values before comparing, so a deployment that moved to a new origin may repair its own URLs and nothing else.
Scope enforcement
The granted scopes on a live token are checked against the manifest at three points.
- On install. An OAuth exchange missing a manifest scope fails with
missing_scopes. An exchange that returns a scope absent from the manifest fails withunexpected_scopes. - On verification. A missing scope raises
Reinstall this Slack app to grant the required permissions:followed by the missing scope names. When the response carries no scope header at all, verification raisesSlack did not return the installed permission scopes. - On health. An installation with a missing scope reports
needs_reauthorizationwith the action labelReinstall Chickpea in Slack. When Slack omits the scope header, the status isunverifiablerather than healthy.
