Slack app setup
The slack-app-manifest.json next to this file is a Slack app manifest you can import to provision the auto-swe Slack app in one step.
- Visit https://api.slack.com/apps → Create New App → From an app manifest.
- Pick your workspace.
- Paste the contents of
docs/slack-app-manifest.json. - Replace the
https://YOUR-GATEWAY-HOST/...placeholders with your gateway’s externally reachable host:slash_commands[0].url→/api/v1/auth/slack/commandssettings.interactivity.request_url→/api/v1/auth/slack/interactivesettings.event_subscriptions.request_url→/api/v1/auth/slack/eventsoauth_config.redirect_urls[0]→/api/v1/auth/slack/callbackoauth_config.redirect_urls[1]→/api/v1/auth/slack/install/callback(multi-workspace install)
- Create, then Install to Workspace to grant the bot scopes (or use the one-click “Add to Slack” install flow below for multi-workspace installs).
Event URL verification. When you set the Event Subscriptions request URL, Slack sends a one-time
url_verificationchallenge to it. The/eventsendpoint echoes the challenge automatically, so the URL verifies as soon as the gateway is reachable — no manual step. Note: the handshake is signature-verified like every other event, so the signing secret must be saved in the admin UI (/admin/integrations → Slack) before you complete Slack’s Events URL verification — otherwise the challenge is rejected with a 401/503.
Endpoints the manifest assumes
Section titled “Endpoints the manifest assumes”All under the gateway’s /api/v1/auth/slack prefix:
| Endpoint | Used by |
|---|---|
POST /commands |
/auto-swe slash command (HMAC-verified via SLACK_SIGNING_SECRET) |
POST /interactive |
Modal submissions (/auto-swe run picker, HITL approve/reject comment form), HITL buttons, and shortcuts (global “Run a workflow” + message “Ask auto-swe about this”) |
POST /events |
Slack Events API — the @mention teammate + thread-reply task steering (HMAC-verified; acks within 3s, then starts the assistant workflow or signals an in-flight task run) |
GET /callback |
OAuth redirect target for the account-link flow (/connect) |
GET /connect |
Account-link flow surfaced in the unknown-Slack-user ephemeral hint (user scope identity.basic) |
GET /install |
One-click app install — redirects to Slack’s bot-scope authorize URL (admin-only) |
GET /install/callback |
Install OAuth redirect target — captures the per-workspace bot token |
Linking a Slack account
Section titled “Linking a Slack account”A signed-in user links their Slack account from Settings → Connect, a full-page visit to
GET /connect. The OAuth state it sends to Slack names that user, and the flow is also bound to
the browser that started it: /connect sets a random nonce in the state and in a short-lived
cookie (auto_swe_slack_link: HttpOnly, SameSite=Lax, Secure when the public URL is https,
scoped to the callback path, ten minutes). GET /callback refuses with 403 STATE_MISMATCH unless
the cookie is present and matches the state’s nonce, clears it either way, and refuses with
403 USER_INACTIVE when the user who started the flow has since been deactivated. So an authorize
link started by one person cannot be finished by another to attach the second person’s Slack
account to the first one’s user. The callback must be served from the same host as /connect —
the gateway’s public URL — or the cookie is not sent and every link is refused. The cookie is
host-only but not __Host--prefixed, so a site on a sibling subdomain that can set cookies for the
parent domain could plant its own nonce; serve the gateway from a host whose siblings you trust.
Multi-workspace install (“Add to Slack”)
Section titled “Multi-workspace install (“Add to Slack”)”One Slack app can be installed into many workspaces, each with its own bot
token, via the install flow (/admin/integrations → Slack → Add to Slack):
- An admin clicks Add to Slack, which hits
GET /api/v1/auth/slack/installand redirects to Slack’soauth/v2/authorizewith the app’s bot scopes. The signed state carries a random nonce, and the same nonce is set in a short-lived (10 minute)httpOnly,SameSite=Laxcookie scoped to the install callback path (Securewhen the public URL is https). - Slack redirects back to
GET /api/v1/auth/slack/install/callback, which exchanges the code viaoauth.v2.accessand stores the returnedxoxb-…bot token encrypted (AES-256-GCM,CONFIG_ENCRYPTION_KEY) on theSlackWorkspacerow, along withappId/botUserId/installedAt. The callback requires the cookie nonce to match the state’s (timing-safe compare) and clears the cookie on every outcome, so an authorize URL handed to another browser cannot complete the install (403 STATE_MISMATCH). It also re-checks that the installer is still an active admin. The install must start on the gateway’s public URL (PUBLIC_URL), the host the callback is served from, or the cookie is not sent back and every install is refused. - The admin lands back on the integrations page; the Slack tab lists each workspace’s install status (own token vs. singleton fallback).
The bot token is the only per-workspace secret — the signing secret + OAuth
client id/secret stay singleton in SlackConfig (one app). Every Slack post/read
resolves the per-workspace token by the channel (C…) or workspace (T…) id it’s
acting on (resolveSlackBotTokenForSlackChannel / resolveSlackBotTokenForWorkspace),
falling back to the singleton SlackConfig bot token when a workspace hasn’t
completed install — so single-workspace deployments and the legacy single bot
token keep working unchanged.
@mention teammate (channel assistant)
Section titled “@mention teammate (channel assistant)”Once Event Subscriptions are enabled with the app_mention, message.channels, message.im, and app_home_opened bot events, the bot becomes a conversational teammate:
- @mention it in a channel (
@auto-swe how do I …) → the gateway strips the mention, auto-provisions aSlackChannelrow for that channel (mapped to the default team + its org), and starts aChannelAssistantWorkflow. The worker generates the answer and posts it back in-thread. - DM the bot → same flow; DMs (
message.im) are treated like a private thread. - Reply in a thread that has an in-flight task run → the reply steers that run instead of starting a fresh turn. The gateway reconstructs the task run’s deterministic Temporal id from the channel + thread root and delivers the new guidance via a
steersignal (a brief “:writing_hand: noted — steering the task.” ack lands in-thread). This works for plain (non-mention) replies too — which is why the bot now subscribes tomessage.channels. Steering takes precedence: a thread reply that is also an@mentionstill steers an active run rather than launching a new turn.
The gateway acks Slack within the 3-second window and starts the workflow (or sends the steer signal) out-of-band, so the HTTP response never waits on the LLM. Redelivered events (x-slack-retry-num header) are acked but skipped to avoid duplicate turns. The bot ignores its own messages and Slack system messages (anything with a bot_id or subtype).
Why
message.channelsdoes not make the bot a firehose. The bot receives every public-channel message viamessage.channels, but it acts on a plain (non-mention) channel message only when it is a thread reply and an in-flight task run is bound to that thread (a successfulsteersignal). A non-thread message, or a thread reply with no matching active task, is ignored — it never starts a turn or otherwise responds. So ambient channel chatter stays silent; the subscription exists solely to enable steering an active task by replying in its thread.
The first @mention in a channel auto-creates the channel mapping using the default team from /admin/workflow-defaults → Default team slug (and that team’s owning organization). If the default team is missing, the turn is dropped and a warning is logged — run yarn db:seed or create the team first.
App Home tab (Gap I — packaged UX)
Section titled “App Home tab (Gap I — packaged UX)”Enable the App Home feature in the Slack app config (Home Tab on) and subscribe to the app_home_opened bot event. When a user opens the app’s Home tab, the gateway publishes a Block Kit Home view (via views.publish) describing what the assistant does and how to drive it (@mention, thread steering, follow-up sessions, slash commands). The view is static, published per-user on open; the messages tab is ignored. No extra bot scope is required beyond chat:write.
Shortcuts
Section titled “Shortcuts”The manifest declares two Slack shortcuts (both delivered to the interactivity endpoint /api/v1/auth/slack/interactive, so no extra setup beyond interactivity being enabled):
- Global shortcut — “Run a workflow” (
callback_id: auto_swe_run_shortcut). From Slack’s ⚡ composer menu anywhere, opens the same run-picker modal as/auto-swe run(views.open). Requires a linked account (the modal lists the templates + repos you can access); an unlinked clicker gets a small “link your account first” modal instead of a bare failure (a global shortcut has no channel/response_url, so an ephemeral hint can’t reach them). - Message shortcut — “Ask auto-swe about this” (
callback_id: auto_swe_ask_shortcut). From a message’s ⋯ (More actions) menu, starts a channel-assistant turn seeded with that message’s text and replies in its thread — the same path an@mentionuses (so it does NOT require the clicker to have linked their account). Text-less messages (file/image only) are skipped rather than spawning an empty turn. The per-message workflow id is deterministic + REJECT_DUPLICATE and namespaced (chan-<id>-ask-<ts>) so it never collides with the@mentionpath’schan-<id>-<ts>for the same message, and clicking twice is idempotent.
No extra bot scopes are needed beyond those already listed. Adding shortcuts to an installed app requires a reinstall (see the scope-change note below).
Bot scopes
Section titled “Bot scopes”app_mentions:read, channels:history, channels:read, chat:write, chat:write.public, commands, groups:history, groups:read, im:history, users:read, users:read.email — slash-command handling, channel posts, resolving Slack users to platform users, plus receiving @mentions (app_mentions:read) and DMs (im:history) for the conversational teammate.
channels:read and groups:read let the gateway ask Slack whether a channel is private when it is first registered, and let the Slack channel sync re-check that and read each private channel’s member list (channel-assistant.md §6). Without them both fall back: registration guesses privacy from the event payload, and the sync records nothing, so a private channel’s content stays admin-only.
The bot now reads the current thread’s recent messages via conversations.replies to provide full in-thread context on each turn. This requires two additional history scopes beyond the original im:history:
channels:history— read message history in public channelsgroups:history— read message history in private channels
Scope change requires reinstall. Adding new OAuth scopes to a Slack app requires reinstalling the app to the workspace. After updating the manifest, go to Settings → Install App and click Reinstall to Workspace to grant the new scopes.
After install
Section titled “After install”Configure the credentials via the admin UI at /admin/integrations → Slack tab:
| Field | Source | Effect |
|---|---|---|
| Client ID | Basic Information → App Credentials → Client ID | Required for the OAuth /connect flow |
| Client Secret | Basic Information → App Credentials → Client Secret | Required for the OAuth /connect flow |
| Signing Secret | Basic Information → App Credentials → Signing Secret | Required for HMAC verification of slash commands and interactive payloads |
| Bot Token | OAuth & Permissions → Bot User OAuth Token (xoxb-…) |
Required for run notifications and opening modals |
Click Save. Every value here takes effect immediately, with no restart: the gateway reads them on each request, not at startup.
Env var fallback.
SLACK_BOT_TOKEN,SLACK_SIGNING_SECRET,SLACK_CLIENT_ID, andSLACK_CLIENT_SECRETenvironment variables are still accepted as fallbacks when no DB row exists — useful during initial bootstrapping before the admin UI is available.
© 2026 Jorge Barnaby
Source-available under FSL-1.1-MIT