Slack setup and operations
The Slack integration exposes Ask SABLE through direct messages and explicit app mentions. It accepts signed events in Vercel, processes them in durable Trigger.dev tasks, and sends answers only by direct message. The integration is read-only: it does not provide slash commands, accept file attachments, or mutate business systems.
Slack app configuration
Create separate development and production apps from slack/manifest.yaml. Do not reuse credentials between environments.
The bot scopes are:
app_mentions:readchannels:readchat:writegroups:readim:historyim:readim:writeusers:read
Subscribe the bot to app_mention and message.im. Set the Events API request URL to the deployed /api/integrations/slack/events route. Interactivity, slash commands, Socket Mode, and organization-wide deployment are not used.
Enable the App Home Messages Tab and its option that lets users send messages from the tab. Configure https://sable.indigotrigger.ai/api/integrations/slack/oauth-callback as the production OAuth redirect URL. A later change to OAuth scopes requires reinstalling the app; changing only SABLE's Slack Connect channel policy does not.
Store SLACK_SIGNING_SECRET, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, and a separate random SLACK_OAUTH_STATE_SECRET in the web service secret controls. Set SABLE_WEB_URL to the trusted SABLE origin used for OAuth callbacks and self-link URLs. Portal leads can install the same SABLE app in another Slack workspace from Communication preferences; the returned bot token is encrypted in Supabase Vault and the installation record stores only its Vault reference. SLACK_BOT_TOKEN and the environment credential-reference mapping remain supported for a manually provisioned installation.
The receiver runs in Vercel, while processing and delivery run in Trigger.dev. Configure and enable the four controls in this order:
- In Vercel, set
SLACK_SIGNING_SECRETandSABLE_SLACK_INTAKE_ENABLED=true, deploy the signed intake route, and then let Slack verify the Events API request URL. - In Trigger.dev, set
SLACK_BOT_TOKEN,SABLE_WEB_URL, and the existing Supabase and Anthropic runtime credentials. Then setSABLE_SLACK_PROCESSING_ENABLED=true. - Set
SABLE_SLACK_DELIVERY_ENABLED=truefor the approved pilot, verify a private direct-message response, and only then widen access. - Set
SABLE_SLACK_RETENTION_ENABLED=trueafter the transport retention schedule is configured.
Keep the scheduled receipt, delivery, and retention tasks enabled. Intake can acknowledge an event before processing, so the reconcilers are part of normal delivery rather than an optional repair path. Scheduled dispatch normally begins within about one minute; generation and retries add time.
Authorize people and channels
Set people.slack_access_enabled=true only for approved, active SABLE people. An eligible unlinked user receives a one-time, 10-minute browser challenge. Completion binds the authenticated SABLE person to the Slack user; it never trusts a person ID from the browser request or matches identities by email.
Internal channel mentions require an active sable_private.slack_channel_contexts row with response_policy='dm_only'. The channel selects operating context but never grants project access.
Identity links default to scope_mode='me'. A controlled service-role update may set one active admin or lead link to scope_mode='all' after that person opts in. Ordinary users cannot obtain broader scope, and no public browser or Slack endpoint changes the preference. Bind the update to the installation and person, then verify prepare_slack_conversation returns the expected effective scope. Role, identity, scope, and channel-policy changes rotate the authorization fingerprint so old history and queued answers cannot be reused.
Approve one Slack Connect channel
Shared and external channels are denied by default. Approval is exact: review one channel, allow the external workspace team IDs connected to it, and keep DM-only response policy. Use controlled service-role SQL with environment-specific UUIDs and Slack IDs:
insert into sable_private.slack_channel_contexts
(installation_id, org_id, channel_id, channel_type, response_policy,
status, reviewed_by, reviewed_at, slack_connect_enabled,
allowed_external_team_ids)
values
('INSTALLATION_UUID', 'ORG_UUID', 'C_REVIEWED_CHANNEL',
'private_channel', 'dm_only', 'active', 'REVIEWER_PERSON_UUID',
now(), true, array['T_EXTERNAL_WORKSPACE'])
on conflict (installation_id, channel_id) do update set
status = 'active',
reviewed_by = excluded.reviewed_by,
reviewed_at = excluded.reviewed_at,
slack_connect_enabled = true,
allowed_external_team_ids = excluded.allowed_external_team_ids;
The worker verifies the signed event authorization against the installed home workspace. For every external request and again before delivery, it verifies the user's home team, the exact connected teams, user and bot membership, the reviewed originating channel, and the active SABLE identity. External users must begin self-linking from an app mention in that channel. Their later direct messages remain authorized through the originating channel, so removal or disconnection blocks access.
Known guest flags, deleted or frozen users, frozen channels, membership removal, unsharing, an unexpected connected workspace, disabled access, and revoked links fail closed. Slack omits guest fields from some external profiles; in that case eligibility still requires the exact approved home team, live membership, and a verified SABLE account. Do not describe an omitted guest field as verified guest status.
The existing manifest scopes support the live conversations.info and paginated conversations.members checks. Slack Connect adds no event subscription, so an app already installed with the checked-in manifest does not need reinstalling solely for this opt-in.
Troubleshooting and recovery
| Symptom | Check |
|---|---|
Events return 401 | Preserve the raw request body and confirm the signing secret, Slack timestamp window, and signature. Middleware must not parse or alter the body first. |
| Event is acknowledged but no answer arrives | Check all four controls and Trigger.dev receipt runs. Scheduled dispatch normally begins within about one minute; queued work and retries can take longer. |
| User receives no setup link | Confirm the installation is active, people.slack_access_enabled is true, and the event originated in an eligible DM or approved Connect channel. |
| Link completes but the original question has no answer | This is expected for an initially unknown identity. Ask the user to resend after linking. |
| Channel mention is rejected | Confirm the channel context is active, reviewed, DM-only, and still matches live Slack membership and connected teams. |
| Direct message opens but delivery stops | Recheck the person's active link, home team, authorization channel, membership, and SABLE access. These checks run again immediately before posting. |
Delivery is delivery_ambiguous | Investigate the Slack acceptance and stored response hash/message timestamp. Never resend blindly. |
| Slack desktop does not reflect a completed link | Ask the user to reload Slack, reopen the SABLE DM, and resend. A cached desktop view may lag behind the link completed in the browser. |
The intake budget is 20 accepted events per rolling hour and 100 per rolling day for each installation and Slack user. Slack retries reuse the original receipt. Receipt envelopes expire after 30 days; deliveries, conversations, and their chat history expire after 90 days. Run the transport purge task on schedule and avoid logging message text, tokens, or link challenges.
For the end-user flow, see Ask SABLE in Slack. The source runbook in slack/README.md contains the current migration verification sequence and controlled operator SQL details.