Skip to content

Approvals in Slack and Mattermost

When a run stops at an approval, Hyperspool posts it to a Slack or Mattermost channel with Approve and Reject buttons. A click answers as the person who clicked, with their roles. The message then updates to show who answered and when, whether the answer came from chat, the console, spool approval approve, a timeout or a cancel.

release is waiting for approval at ship
Ship 1.4 to prod?
Expires at 2026-10-11 09:00 UTC
[ Approve ] [ Reject ] [ Open in Hyperspool ]

and once someone clicks:

release is waiting for approval at ship
Ship 1.4 to prod?
Approved by Ann Lee at 2026-10-02 14:13 UTC
[ Open in Hyperspool ]

Posting a project’s approvals to a channel

Section titled “Posting a project’s approvals to a channel”

A chat target says which channel a project’s approvals go to. The bot’s token is a credential in the project, and Slack’s signing secret is read from stdin or a prompt:

Terminal window
printf %s "$SLACK_BOT_TOKEN" | spool credential set slack-bot --kind bearer
printf %s "$SLACK_SIGNING_SECRET" | spool chat set releases --slack \
--channel C0123ABCD --credential slack-bot --signing-secret

For Mattermost, name the server:

Terminal window
printf %s "$MM_BOT_TOKEN" | spool credential set mm-bot --kind bearer
spool chat set releases --mattermost --server-url https://chat.example.com \
--channel 4xk9s1dbp7n8z --credential mm-bot

Every approval in the project is posted to every target. To post only some, give a --when predicate. It reads the run.awaiting event the same way as a handler’s --when does (see Telling someone a run is waiting):

Terminal window
spool chat set releases --slack --channel C0123ABCD --credential slack-bot \
--when 'event.payload.workflow_name == "release"'

Setting a Slack target again keeps its signing secret unless you pass --signing-secret. spool chat list, get and delete manage targets; in the console they are under Settings > Connections > Chat. Changing targets needs admin on the project.

A gate is posted once per target, however long it waits. A run that was already waiting when you added a target is posted the next time Hyperspool picks the run up. Approvals inside a child workflow are not posted, just as they don’t announce run.awaiting.

  1. Create an app at api.slack.com/apps, from scratch, in your workspace.
  2. Under OAuth & Permissions, add the bot scopes chat:write and channels:history (groups:history for a private channel), and users:read plus users:read.email if you’ll use --match-email. The history scope lets a post that timed out be found in the channel on retry instead of being posted twice. Install the app and copy the Bot User OAuth Token (xoxb-…) into the credential.
  3. Under Basic Information, copy the Signing Secret into spool chat set … --signing-secret.
  4. Under Interactivity & Shortcuts, turn interactivity on and set the Request URL to https://api.hyperspool.dev/api/v1/chat/slack/actions. spool chat set prints it, and the console shows it on the target.
  5. Invite the bot to the channel (/invite @your-app), and use the channel’s id, from the channel’s details.

A Slack target needs no --server-url unless your workspace is off slack.com, and then it has to be a Slack host.

One app can serve every project: each target names its own credential and secret, and the request URL is the same for all of them.

  1. Create a bot account (Integrations > Bot Accounts) and a token for it; put the token in the credential. Add the bot to the channel’s team and to the channel.
  2. Allow Mattermost to reach Hyperspool: the buttons call https://api.hyperspool.dev/api/v1/chat/mattermost/actions. If Mattermost restricts outgoing connections, add that host under Developer > Allow untrusted internal connections.
  3. Use the channel’s id, from View Info on the channel.

Hyperspool calls your Mattermost server at the --server-url you give, so the server has to be reachable on a public address.

Mattermost doesn’t sign the requests its buttons send. What vouches for a click is a token Mattermost keeps on its server and never shows its users, so Hyperspool trusts the Mattermost server itself: its admins can answer as anyone linked on it.

The first time you click, Hyperspool doesn’t know which of its users you are. The click answers nothing, and a reply only you can see gives you a code. Signed in to Hyperspool, enter it under Settings > Chat accounts, check the chat account it names, and link it. Or run:

Terminal window
spool chat link hscl_4f2a9c…

Then click again. The code works once, for 15 minutes, and never comes as a link to click. Never enter a code someone else gave you: it would link their chat account to you, and whoever holds a linked chat account answers approvals as you.

The link lasts until you remove it under Settings > Chat accounts or with spool chat unlink. A Slack link belongs to the Slack app you clicked through: if the app’s signing secret is rotated, or approvals come through another app, link again the same way.

With --match-email on a target, a chat user whose email (as Slack or Mattermost reports it, and only once they have confirmed it there) matches a Hyperspool user’s is known without linking. That trusts the chat workspace’s admins with who your members are, so only an org admin can turn it on. For Slack the app needs the users:read.email scope.

A click answers only if you could answer from the console: you need the member role or above on the project. Otherwise the gate stays open and the reply says why.

  • An approval with no fields, or whose fields all have defaults, gets one Approve. It sends what the console’s one-click Approve sends, and the message says what that is.
  • An approval whose one choice is a select of up to five options gets an Approve button per option.
  • Anything someone has to type goes to the console: the message offers Reject and a link to the run, where the form is.

Reject cancels the run, as it does in the console. Slack asks you to confirm it first.

spool chat messages releases lists a target’s recent messages: posted, answered, or retrying and why (channel_not_found when the bot isn’t in the channel, for instance). The console shows the same under the target. A post that keeps failing is retried with growing delays for a few hours, then given up.

A run started by a fork’s pull request is untrusted, and its message says so: its workflow name and prompt came from outside the project. Approving it from chat is the same as approving it in the console.