Guides
Setting up Slack
Approval requests arrive in Slack with Approve, Deny and Freeze, in the approvals channel and by direct message to the people who decide them. Each person’s App Home shows what is waiting for them, today’s decisions and their agents. Console links unfurl, owners can choose a digest, and escalations, freezes, incidents and kill-switch drill results arrive in the channel. /immiscible reports status, lists what is waiting for you, and freezes an agent.
A decision in Slack is the console’s decision, not a shortcut around it. The same rules apply:
- The person who clicks must be a member of the Immiscible workspace, mapped from their Slack account by verified email.
- They must be able to decide for that agent in the console (an owner or admin, or the member the agent acts for).
- Named approvers, when the workspace has them, are the only people who decide.
- Separation of duties: above the workspace’s line, neither the person the agent acts for nor whoever wrote its mandate may approve.
- A frozen agent’s requests cannot be approved.
- The request is checked against the mandate again at the moment of approval.
Every decision records the channel (slack) and the Slack user id on the approval and in the evidence ledger, and the Slack message is updated in place to say who decided. A decision made in the console updates the Slack message too.
#1. Create the Slack app (once per Immiscible server)
Immiscible’s hosted service already has a Slack app; skip to step 3. If you run Immiscible yourself, create your own app from the manifest in this folder.
- Open api.slack.com/apps, choose Create New App, then From an app manifest.
- Choose the Slack workspace to develop it in.
- Paste
slack-app-manifest.json, withYOUR-IMMISCIBLE-HOSTreplaced by your server’s public host (the host inPUBLIC_URL). It sets:- Bot scopes:
chat:write(post and update messages),commands(the slash command),im:write(direct messages to the people who decide, and digests),users:read.email(find a member’s Slack account by their email withusers.lookupByEmail, and check the email of someone linking their own account) andusers:read, which Slack requires alongsideusers:read.email,links:readandlinks:write(see a console link shared, and unfurl it). Nothing else: the App Home needs no scope of its own, and the app never reads messages. - App Home with the Home tab on, and the Messages tab read only (where approvals and digests arrive).
- Event subscriptions request URL
https://YOUR-IMMISCIBLE-HOST/slack/events, bot eventsapp_home_openedandlink_shared, and the unfurl domainYOUR-IMMISCIBLE-HOST. - Interactivity request URL:
https://YOUR-IMMISCIBLE-HOST/slack/interactions - Slash command
/immiscible, request URLhttps://YOUR-IMMISCIBLE-HOST/slack/commands - Redirect URL:
https://YOUR-IMMISCIBLE-HOST/slack/oauth/callback - Token rotation off: the bot token does not expire, and is sealed at rest.
- Bot scopes:
- Create the app. Under Basic Information, copy the Client ID, Client Secret and Signing Secret.
- To install into Slack workspaces other than the one you developed in, turn on Manage Distribution and activate public distribution. You do not need to list the app in the Slack Marketplace.
#2. Give the server the app’s credentials
Set three environment variables and restart:
SLACK_CLIENT_ID=...
SLACK_CLIENT_SECRET=...
SLACK_SIGNING_SECRET=...The signing secret is how Immiscible knows a button click, a form, a command or an event came from Slack. Every request to /slack/interactions, /slack/commands and /slack/events must carry Slack’s X-Slack-Signature, an HMAC-SHA256 of v0:<timestamp>:<body> with this secret, and a X-Slack-Request-Timestamp within five minutes of the server’s clock. Signatures are compared in constant time, and each signature is accepted once. Slack retries an event it thinks was missed with a new signature, so each event id is also acted on once. Anything else is refused before it is read.
#3. Install into your Slack workspace
An owner or admin of the Immiscible workspace:
- Opens Connections, then Slack, then Connect (this is
GET /slack/install?workspace=<workspace id>). - Approves the scopes on Slack’s consent page.
- Comes back to Connections, which says Slack is connected (or why it is not). The install link is bound to the person who started it, works once and expires after ten minutes.
One Slack workspace connects to one Immiscible workspace. Members are mapped straight away: for each member, Immiscible asks Slack for the account with that email. Slack accounts that are deactivated, bots, or whose email Slack has not confirmed are not mapped. Run the mapping again after people join (POST /api/w/:wid/chat/slack/members/sync), or map one person by email (POST /api/w/:wid/chat/slack/members). A person can also link themselves: their App Home offers Link my account, which applies the same test to their own Slack account (Slack’s confirmed email must be a member’s email), so nobody can link to someone else’s account.
#4. Choose the channel
In Slack, create or choose a channel, and invite the bot: /invite @Immiscible. Copy the channel id (channel details, at the bottom; it starts with C). Set it as the approvals channel, then send a test message. The console does not have these two controls yet, so use the API with a signed-in session:
PUT /api/w/:wid/chat/slack { "channelId": "C0123456789", "channelName": "agent-approvals" }
POST /api/w/:wid/chat/slack/test#5. Who gets a direct message
Every approval posts to the channel, which stays the record. With directMessages set to deciders (the default), it also goes by direct message to the people who decide it: the workspace’s named approvers (approvalRouting.approvers) when it has them, and otherwise the agent’s sponsor and the person it acts for, each only if they are already linked in Slack. off is the channel only.
PUT /api/w/:wid/chat/slack { "directMessages": "deciders" } or "off"#6. Optional: the chat line
Above a line you set, Slack refuses to approve and links to the console instead, where the person’s own signed-in session stands behind the click, not a chat account. The line is in reference pence (5000 is about fifty pounds in any currency). A data release has no amount, so with a line set it is always approved in the console. Denying is never stepped up.
PUT /api/w/:wid/chat/settings { "chatStepUpAbove": 5000 } null for no line#Using it
Approvals. Each request posts to the channel and by direct message to the people who decide it (step 5). The message leads with who wants what (for a payment, the agent, the amount and the payee), then the agent’s own summary, why it was held in a line each, Approve (with a confirmation that restates the payment), Deny, Open in console, Freeze agent, and when it must be decided by, shown in each reader’s own time zone. At or above the chat line, Approve is a link to the console instead of a button, and Deny stays. Once decided, wherever it was decided, the message is updated in place: the buttons go, the headline moves into the past (“asked to pay”), and it says who decided, where, when and, for a Deny, why. If a click is refused (not mapped, not allowed, above the chat line, separation of duties, already decided), the person who clicked sees why, privately, and nothing changes.
Deny with a reason. Deny opens a short form with an optional reason (up to 300 characters). The reason is kept on the decision, shown with it in Slack and the console, and the decision is made when the form is submitted. If Slack cannot open the form in time, the click denies without a reason, as it asked. A refusal (already decided, not allowed) is shown on the form.
Freeze. Freeze agent, on a held request or beside an agent in the App Home, opens a form that says what a freeze does and asks for a reason (from a held request it starts as that request). Who may freeze, and the hold, follow the console: the agent’s kill owner, a security lead, or a workspace owner or admin may; the hold is a security hold when they are stopping an agent they do not manage, or as its kill owner or security lead, unless it acts for them, and an owner hold otherwise. It is recorded in the evidence ledger with their name, and every request the agent has waiting is cancelled. Stopping is never stepped up; lifting a hold is done in the console.
App Home. Open Immiscible in Slack’s sidebar. Linked, you see what is waiting for you with the same Approve, Deny and Freeze buttons, confirmation and chat line as the channel; today’s decisions; your agents with their status, and Freeze where you may; and a link to the console. It is redrawn after any decision, request or freeze, wherever it happened, for everyone who has opened theirs in the last thirty days. Not linked, you see how to link, and nothing of the workspace.
Link unfurls. A console link to an approval, an agent or a receipt (/app/approvals/..., /app/agents/..., /app/receipts/...) shows a compact card, when the person who shared it is linked and may see it in the console. Every lookup is in the workspace this Slack team is connected to, so a link from another workspace stays a plain link. As with any unfurl, everyone in the conversation sees the card.
Digest. Owners and admins can choose a daily or weekly digest by direct message, in their App Home. It is off unless chosen. It comes after 08:00 UTC: decisions made by people, requests held for a person and the money in them, requests refused by the rules, the busiest agents, and what is waiting. When nothing happened, nothing is sent.
Notices. Posted to the channel: an approval half way to its deadline with no answer (when the workspace has escalation contacts or a webhook), a freeze and a bulk freeze (not a drill’s own freezes), an incident (an agent settling above its authorisation, or moving more records than it declared), and each kill-switch drill’s result.
/immiscible status says how many agents are active and frozen, and how many approvals are waiting.
/immiscible approvals lists up to five requests you can decide, with buttons.
/immiscible freeze <agent> <reason> freezes an agent, with the same people and holds as the Freeze button. Name the agent by id, or by name (in quotes if it has spaces). A reason is required and recorded. Lifting the hold is done in the console, under the console’s rules.
#Disconnecting
Settings, Slack, Disconnect. The bot token is revoked at Slack, and the connection, member mappings, message references and App Home and digest preferences are deleted. Approvals still arrive by email and in the console.
#Console API
All need a signed-in owner or admin and the x-immiscible-csrf header, like every console route.
GET /api/w/:wid/chat both connections, the endpoints to paste into Slack, the chat line
PUT /api/w/:wid/chat/settings { chatStepUpAbove }
PUT /api/w/:wid/chat/slack { channelId, channelName, directMessages }
POST /api/w/:wid/chat/slack/test send a test message now
GET /api/w/:wid/chat/slack/members who is mapped
POST /api/w/:wid/chat/slack/members { email } map one member
POST /api/w/:wid/chat/slack/members/sync map every member by email
DELETE /api/w/:wid/chat/slack/members/:slackUserId
DELETE /api/w/:wid/chat/slack disconnect