Skip to docs content
Browse documentation
On this page

Connect Slack

Connect Slack to Minion with a bot token or a user-token OAuth app, call the Web API, and receive Slack events through a package webhook.

Connecting Slack lets your agents and packages post messages, read channels and look up users through the Slack Web API at slack.com, without the token ever reaching their code. A package can also receive Slack events through a Minion webhook.

Minion has no built-in Slack preset. You have two options:

  • A bot token saved as a secret. This is the simplest path and the one to pick for most automations: posting alerts, reading a channel the bot is in, looking up users. Messages appear as your Slack app, not as you. Bot tokens don't expire unless you turn on token rotation.
  • A custom OAuth app with a user token. Pick this when code has to act as you: search your messages, read channels the bot isn't in, or post under your name.

Both options call the same API host, slack.com. File downloads come from files.slack.com.

Before you start

  • You need to be an owner or admin of your Minion organization to save a secret or add an OAuth app. Members can use them once they exist.
  • You need permission to install apps in your Slack workspace. Some workspaces require an admin to approve new apps.
  • Create a Slack app first. Open api.slack.com/apps , click Create New App, choose From scratch, name it and pick your workspace. Both options below start from this app.
  • Leave Token Rotation off on the app's OAuth & Permissions page. With rotation off, Slack tokens don't expire.

Option A: bot token

Create the token on Slack

  1. On your app's OAuth & Permissions page, under Bot Token Scopes, add only the scopes you need. For posting and reading: chat:write, channels:read, channels:history, users:read. Add chat:write.public to post in public channels without inviting the bot.
  2. Click Install to Workspace and approve.
  3. Copy the Bot User OAuth Token. It starts with xoxb-.
  4. In Slack, invite the bot to each channel it should read or post in: type /invite @your-app-name in the channel.

Add it in Minion

  1. Open app.minion.codes/app/secrets .
  2. Set Name to slackBotToken. Secret names use letters, digits and underscores only.
  3. Paste the token into Value.
  4. Set Approved hosts to slack.com.
  5. Click Save secret.

Never paste the token into chat. See /docs/secrets .

Check it works

Run this with execute. It only asks Slack who the token belongs to.

export default async function main() {
  const res = await fetch('https://slack.com/api/auth.test', {
    method: 'POST',
    headers: { Authorization: 'Bearer {{secret:slackBotToken}}' },
  })
  const data = await res.json()
  if (!data.ok) throw new Error(`Slack returned ${data.error}`)
  return { team: data.team, user: data.user, botId: data.bot_id }
}

Slack answers most errors with HTTP 200 and "ok": false, so always check data.ok, not just the status.

Use it in a package

export default async function main({ channel, text }) {
  const res = await fetch('https://slack.com/api/chat.postMessage', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer {{secret:slackBotToken}}',
      'Content-Type': 'application/json; charset=utf-8',
    },
    body: JSON.stringify({ channel, text }),
  })
  const data = await res.json()
  if (!data.ok) throw new Error(`Slack returned ${data.error}`)
  return { ts: data.ts }
}

channel is a channel ID such as C0123456789. Find it in Slack under the channel's details. Say in the package's README that it needs the slackBotToken secret approved for slack.com.

Option B: custom OAuth app with a user token

Slack has a user-token flow that behaves like standard OAuth: oauth/v2_user/authorize plus oauth.v2.user.access. Minion needs that flow. Slack's usual oauth/v2/authorize puts the user token inside authed_user, where Minion doesn't look for it.

Set up the app on Slack

  1. On your app's OAuth & Permissions page, under Redirect URLs, add this URL exactly and click Save URLs:
https://app.minion.codes/oauth/callback
  1. Under User Token Scopes, add the scopes code needs. For example: channels:read, channels:history, search:read, users:read, chat:write.
  2. On Basic Information, copy the Client ID and Client Secret under App Credentials.

Add it in Minion

  1. Open app.minion.codes/app/integrations .
  2. In the OAuth apps card, set Provider to Custom (any OAuth 2 provider).
  3. Set App name (optional) to slack. Don't leave it empty: a custom app's name defaults to custom.
  4. Paste the Client ID and Client secret.
  5. Set Default scopes (optional) to the User Token Scopes you added on Slack, separated by spaces.
  6. Set API hosts (custom: required) to slack.com files.slack.com. A wildcard like *.slack.com doesn't cover slack.com itself.
  7. Open Custom provider endpoints and fill in:
    • Authorization URL: https://slack.com/oauth/v2_user/authorize
    • Token URL: https://slack.com/api/oauth.v2.user.access
    • Revocation URL (optional): leave empty.
    • Client authentication: Secret in the request body
    • Keep Use PKCE (S256) checked.
  8. Click Save app.

Connect

  1. In the Integrations card, choose slack under OAuth app.
  2. Leave Name (optional) empty to call the integration slack.
  3. Leave Extra scopes (optional) empty to use the app's default scopes. On a first connect, anything you type here replaces the defaults, so list every scope you need.
  4. Click Connect and approve on Slack.

Slack sends you back to the Integrations page with Connected "slack". and status connected. The account column stays empty for custom apps. That is expected.

An agent can start the same flow with minion.integrations.connect({ provider: 'slack' }). An owner or admin opens the returned connectUrl while signed in to Minion.

Check it works

Run this with execute. A user token returns a user_id and no bot_id.

export default async function main() {
  const res = await minion.integrations.fetch({
    name: 'slack',
    url: 'https://slack.com/api/auth.test',
    init: { method: 'POST' },
  })
  const data = await res.json()
  if (!data.ok) throw new Error(`Slack returned ${data.error}`)
  if (data.bot_id) throw new Error('This is a bot token. Check the Authorization and Token URLs.')
  return { team: data.team, user: data.user, userId: data.user_id }
}

Use it in a package

package.json:

{
  "name": "@your-org/slack-tools",
  "exports": { "./search": "./src/search.ts" }
}

src/search.ts:

import { createAuthenticatedFetch } from 'minion:runtime'

const slack = createAuthenticatedFetch('slack')

export default async function main({ query }) {
  const res = await slack(`https://slack.com/api/search.messages?query=${encodeURIComponent(query)}&count=10`)
  const data = await res.json()
  if (!data.ok) throw new Error(`Slack returned ${data.error}`)
  return data.messages.matches.map((m) => ({ channel: m.channel?.name, text: m.text, permalink: m.permalink }))
}

Save and publish it with minion.packages.save and publish: true. Publishing needs an owner or admin and a README.md that says the package needs the slack integration. Other code can only import a published version. Then call it from execute:

import search from 'minion:@your-org/slack-tools/search'

export default async function main() {
  return search({ query: 'billing outage' })
}

Replace your-org with your organization slug. See /docs/packages for saving and publishing.

Receive Slack events

A package can run when something happens in Slack, such as a message in a channel or a mention of your app. Slack's Events API sends each event as a POST to a URL you give it. In Minion, that URL is a package webhook.

  1. On your app's Basic Information page, copy the Signing Secret.
  2. Save it on the Secrets page as slackSigningSecret. It needs no approved hosts. Then open Manage on the secret and click Allow webhook use.
  3. Declare the webhook in your package's package.json:
{
  "name": "@your-org/slack-events",
  "exports": { "./on-event": "./src/on-event.ts" },
  "minion": {
    "webhooks": [{
      "name": "slack",
      "export": "./on-event",
      "challenge": { "type": "slack-url-verification", "secretName": "slackSigningSecret" }
    }]
  }
}
  1. Write the export. It receives the request, with Slack's event in request.json.event:
export default async function main({ request }) {
  const body = request.json
  if (body?.type !== 'event_callback') return { ignored: true }
  const { type, channel, user, text } = body.event
  return { type, channel, user, text }
}
  1. Save and publish the package with minion.packages.save and publish: true, with a README.md that says it needs the slackSigningSecret secret. The webhook runs the published version.
  2. Mint the URL with minion.webhooks.mint({ package: 'slack-events', name: 'slack' }), or click Mint URL on app.minion.codes/app/webhooks . Click Reveal and copy the Payload URL.
  3. On your Slack app, open Event Subscriptions, turn it on, and paste the URL into Request URL. Slack sends a verification request at once. Minion checks Slack's signature on it and answers the challenge, and Slack shows Verified.
  4. Under Subscribe to bot events, add the events you want, such as message.channels or app_mention. Save, and reinstall the app if Slack asks.

Things to know:

  • Minion checks Slack's signature on the verification request only. For ordinary events, the secret webhook URL is the protection. Treat the URL like a password, and rotate it with minion.webhooks.rotate if it leaks.
  • Don't add a verification block for Slack. Minion's signature options don't match Slack's v0: signing format, so every event would be rejected.
  • Minion answers Slack right away and runs your export afterwards, so Slack's 3-second limit isn't a problem.
  • Slack retries an event it thinks failed, with an x-slack-retry-num header. Each retry runs your export again. If that matters, keep the event_id values you've handled in package storage and skip repeats.

Test the export without Slack using minion.webhooks.dispatchSynthetic, and read what arrived with minion.webhooks.deliveries. See /docs/webhooks .

Troubleshooting

  • invalid_auth or not_authed. The token is wrong or missing. For the bot token, check the secret's value. For OAuth, reconnect.
  • not_in_channel or channel_not_found. The bot isn't in that channel. Invite it with /invite @your-app-name, or add chat:write.public for posting to public channels.
  • missing_scope. The token lacks a scope the method needs. Slack's error lists the needed scope. For the bot token, add the scope, reinstall the app and rotate the secret with the new token. For OAuth, add the scope to the Slack app, type it in Extra scopes, and click Connect again.
  • Slack says the redirect URL doesn't match. The app's redirect URL must be exactly https://app.minion.codes/oauth/callback. No trailing slash.
  • Connect fails at the token step, or auth.test returns a bot_id on the OAuth integration. The app uses Slack's standard oauth/v2/authorize or oauth.v2.access. Set the Authorization URL and Token URL to the v2_user pair above, then disconnect and connect again.
  • Secret "slackBotToken" isn't approved for slack.com. Open the secret on the Secrets page and approve slack.com.
  • An error saying integration "slack" may not send its token to a host. You called a host outside the app's API hosts. Slack's API is on slack.com, file downloads on files.slack.com.
  • The Request URL won't verify. Check that the slackSigningSecret secret holds the Signing Secret, not the client secret, and that Allow webhook use is on. Then read minion.webhooks.deliveries for the reason.
  • The OAuth integration turns error and says it needs a person to reconnect. The token was revoked, or the app was uninstalled from the workspace. Minion fires an integration.auth.failed event with a reconnectUrl. Click Connect next to the integration on the Integrations page.

Disconnecting the OAuth integration in Minion deletes the token in Minion but doesn't revoke it at Slack. Remove the app from your workspace too if you're done with it.

For how OAuth integrations work in general, see /docs/oauth . For webhooks, see /docs/webhooks .