approval_required decision posts a message into a channel you choose, and a reviewer resolves it with Approve or Deny without leaving Slack.
Setup takes about two minutes and produces three values you paste into VisIQ. The dashboard walks you through it at Connectors → Human-in-the-Loop → Slack; this page is the same flow written down, plus the one setting people most often miss.
Before you start
- A Slack workspace where you can create and install an app. Installing grants workspace-admin OAuth consent, so if you are not an admin you will need one to approve the install.
- A VisIQ account with the
connectors:managepermission — the save and delete endpoints are guarded by it. - A channel for approvals. A public channel is easiest; the app can post to a public channel without being invited.
Step 1 — Create the Slack app from VisIQ’s manifest
Go to api.slack.com/apps, choose Create New App, and pick From an app manifest. Select your workspace, then paste this:Slack app manifest
Self-hosted or non-production VisIQ? Replace the
request_url host with your own VisIQ origin, keeping the path exactly /api/integrations/slack/interactions. The dashboard’s setup panel renders the correct URL for the environment you are signed in to — copy it from there rather than typing it.What each permission is for
Every scope in the manifest is there for a specific delivery behaviour. None of them can read your channel history, your DMs, or your files.
Two of these are worth calling out:
users:readis not optional if you wantusers:read.email. Slack documents that the two must be requested together, so a hand-built app that asks only forusers:read.emailis incomplete.- A missing
im:writedegrades silently. Owner direct messages need bothusers:read.email(to find the person) andim:write(to open the DM). Drop either one and approvals for a claimed agent quietly fall back to the shared channel and the owner’s email instead of failing loudly. VisIQ’s connection test reads the granted scopes back from Slack and warns you when this is the case — it is a warning, not an error, because channel-only installs are fully supported.
Step 2 — Collect the three values VisIQ needs
Open Connectors → Human-in-the-Loop → Slack in the VisIQ dashboard and fill in these three fields. They are the only inputs the connector takes.
For a private channel, invite the bot first with
/invite @VisIQ. Public channels are already covered by chat:write.public.
Both secrets are envelope-encrypted at rest and are never returned by any VisIQ API — the connector list endpoint projects rows to a secret-free shape.
Step 3 — Run Test connection, then save
Test connection is a required step on first setup, not a nicety: the Save button stays disabled until a test passes. It does four things, and the third is the one that makes the buttons work.- Calls Slack’s
auth.testwith your bot token to confirm the token is valid, and reports the workspace name back to you. - Posts a real test message to your channel, so
chat:writeand channel reachability are proven by delivery rather than assumed. - Reads your workspace’s team id from Slack and stores it with the connector. This is how inbound button clicks find your tenant — see below.
- Checks that your app was granted
users:read.emailandim:write, and warns (without blocking) if owner direct messages will be unavailable.
- The signing secret is only format-checked. Slack signs requests to your Request URL, so there is no API call that validates a signing secret — it is proven live the first time someone clicks Approve or Deny.
- Whether the Interactivity Request URL is set cannot be read back from Slack at all. Confirm it yourself in the app’s Interactivity & Shortcuts page. This is exactly why the manifest path is recommended.
Why the team id matters
An approval message posted by VisIQ carries buttons. When a reviewer clicks one, Slack posts the interaction to your Interactivity Request URL — a public endpoint that carries no VisIQ session. VisIQ therefore identifies the tenant by the workspace team id in Slack’s payload, looks up the matching connector, decrypts that tenant’s signing secret, and verifies the request signature before it will resolve anything. That chain only works if a team id is stored against your connector, and Test connection is what puts it there. A connector saved without a passing test has no team id, so every button click is answered with an opaque401 and the approval is never recorded.
How VisIQ authenticates a button click
The interactions endpoint is public by design — it is a webhook, and Slack does not carry a user session. Authenticity comes from cryptography instead, in this order:- The raw request bytes are read before any parsing, because Slack signs the exact bytes it sent.
- The workspace team id in the payload resolves the tenant’s connector.
- That connector’s signing secret verifies the
X-Slack-SignatureHMAC, and a request whose timestamp is more than 5 minutes off is rejected as a replay. - Only after the signature verifies does VisIQ check that the signed button value names this connector’s own tenant.
401, so the endpoint cannot be used to discover which workspaces have a VisIQ connector.
The buttons a reviewer sees
Clicking a button on an item somebody already resolved is safe — Slack shows “This request was already resolved” rather than double-applying a decision.
Routing approvals to an agent’s owner
By default approvals go to the shared channel you configured. Setowner_email and a hitl_pathway of slack on an agent and its approvals are direct-messaged to that person instead, with the shared channel kept as the fail-safe if the DM cannot be delivered. Owner DMs are the feature that needs users:read.email and im:write; without them the approval still arrives, just in the channel.
The API behind the card
The dashboard is a client of the same endpoints you can drive yourself. All three require a session and theconnectors:manage permission.
The
PUT body is validated strictly — an unrecognised key is rejected, not ignored:
PUT /api/connectors/hitl-channels/slack
secret may be omitted on a later update to keep the stored credentials, but the first save of a Slack connector is rejected with a 400 unless a secret is present. display_label is capped at 120 characters.
Troubleshooting
Approve and Deny do nothing — the message never updates
Approve and Deny do nothing — the message never updates
Slack is not reaching VisIQ, or VisIQ cannot identify your workspace. Check, in order: the app’s Interactivity & Shortcuts page has interactivity on with the Request URL pointing at your VisIQ origin’s
/api/integrations/slack/interactions; and that you ran Test connection successfully, which is what stores the workspace team id the callback needs.Test connection says the bot isn't in the channel
Test connection says the bot isn't in the channel
That is a warning for a private channel — run
/invite @VisIQ in it. Public channels do not need the invite because chat:write.public covers them.Test connection says the channel wasn't found
Test connection says the channel wasn't found
The Channel ID field wants the id (something like
C0A1B2C3D4E), not the #channel-name. Copy it from the bottom of the channel’s details panel.Test connection warns that owner DM routing is off
Test connection warns that owner DM routing is off
Your installed app is missing
users:read.email, im:write, or both. Re-install or re-consent the app from the current manifest to pick them up. Approvals keep working in the meantime — they deliver to the channel and the owner’s email instead of a DM.Approvals expire before anyone answers
Approvals expire before anyone answers
The approval window is capped at 120 seconds, and an unanswered request fails closed. That ceiling is a property of the harness, not of Slack — see Human-in-the-Loop for the pause-and-resume model and the mask fallback.
Related
Human-in-the-Loop
How approvals pause a tool call, what the queue contains, and the timeout model.
Email approvals
The catch-all channel for agents that have no registered owner.