> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Slack approvals

> Connect Slack so Human-in-the-Loop approvals arrive as an interactive message with working Approve and Deny buttons.

Slack is the richest approval channel VisIQ ships: an `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.

<Warning>
  **Do not skip the Interactivity Request URL.** It is the only thing that tells Slack where to send a button click. Without it, VisIQ's message still posts and the buttons still render — but pressing one never reaches VisIQ, the approval sits unresolved, and the agent's call times out and fails closed. The manifest below sets it for you; if you build the app by hand instead, set it yourself.
</Warning>

***

## 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:manage`** permission — 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](https://api.slack.com/apps?new_app=1), choose **Create New App**, and pick **From an app manifest**. Select your workspace, then paste this:

```json Slack app manifest theme={null}
{
  "display_information": {
    "name": "VisIQ Approvals",
    "description": "Human-in-the-loop approvals from VisIQ",
    "background_color": "#4a154b"
  },
  "features": {
    "bot_user": {
      "display_name": "VisIQ",
      "always_online": true
    }
  },
  "oauth_config": {
    "scopes": {
      "bot": [
        "chat:write",
        "chat:write.public",
        "users:read.email",
        "users:read",
        "im:write"
      ]
    }
  },
  "settings": {
    "interactivity": {
      "is_enabled": true,
      "request_url": "https://app.visiqlabs.com/api/integrations/slack/interactions"
    },
    "org_deploy_enabled": false,
    "socket_mode_enabled": false,
    "token_rotation_enabled": false
  }
}
```

The manifest pre-fills the permissions **and** the Interactivity Request URL in one step, which is why it is the recommended path.

<Note>
  **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.
</Note>

Create the app, then **install it to your workspace** and approve the consent screen.

### 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.

| Scope               | What it does                                                             | Why VisIQ asks for it                                                                                |
| ------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `chat:write`        | Send messages as the VisIQ bot.                                          | Posts each approval request into your chosen channel.                                                |
| `chat:write.public` | Post to a public channel without joining it first.                       | Saves you from inviting the bot to every channel by hand.                                            |
| `users:read.email`  | Resolve a teammate's Slack account from their email.                     | Lets VisIQ DM an agent's owner directly instead of the shared channel.                               |
| `users:read`        | Read a teammate's basic Slack profile (the id and name behind an email). | Slack requires it alongside `users:read.email` — the email lookup reads the base user profile first. |
| `im:write`          | Open a direct-message channel with a teammate.                           | Lets VisIQ open the owner's DM (`conversations.open`) after resolving them by email.                 |

Two of these are worth calling out:

* **`users:read` is not optional if you want `users:read.email`.** Slack documents that the two must be requested together, so a hand-built app that asks only for `users:read.email` is incomplete.
* **A missing `im:write` degrades silently.** Owner direct messages need both `users:read.email` (to find the person) and `im: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.

| Field                    | Where to find it in Slack                                                           | What VisIQ accepts                                                |
| ------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| **Bot User OAuth Token** | **OAuth & Permissions → Bot User OAuth Token**                                      | Must begin with `xoxb-`. Stored encrypted; never shown again.     |
| **Signing Secret**       | **Basic Information → App Credentials → Signing Secret** (click *Show*)             | At least 16 characters. Stored encrypted; never shown again.      |
| **Channel ID**           | Open the channel → **View channel details** → copy the **Channel ID** at the bottom | The channel id itself, e.g. `C0A1B2C3D4E` — not the channel name. |

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.

1. Calls Slack's `auth.test` with your bot token to confirm the token is valid, and reports the workspace name back to you.
2. Posts a real test message to your channel, so `chat:write` and channel reachability are proven by delivery rather than assumed.
3. **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.
4. Checks that your app was granted `users:read.email` and `im:write`, and warns (without blocking) if owner direct messages will be unavailable.

Two things it deliberately cannot check, and says so rather than claiming otherwise:

* **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.

Once the test passes, click **Save**. The card shows **Connected**.

### 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 opaque `401` 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:

1. The raw request bytes are read **before** any parsing, because Slack signs the exact bytes it sent.
2. The workspace team id in the payload resolves the tenant's connector.
3. That connector's signing secret verifies the `X-Slack-Signature` HMAC, and a request whose timestamp is more than **5 minutes** off is rejected as a replay.
4. Only after the signature verifies does VisIQ check that the signed button value names this connector's own tenant.

An unknown workspace and an invalid signature return a byte-identical `401`, so the endpoint cannot be used to discover which workspaces have a VisIQ connector.

***

## The buttons a reviewer sees

| Button               | What happens                                                                                                                                                                                                                                                                                                |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Approve**          | The tool call runs with its full arguments. The Slack message is replaced with a confirmation.                                                                                                                                                                                                              |
| **Deny**             | The tool never runs; the agent receives the standard block message as the tool's output.                                                                                                                                                                                                                    |
| **Let VisIQ decide** | Hands the pending item to VisIQ's automated reviewer, which either resolves it or leaves it pending for a human. It can never manufacture an independent approver. This third button is added to the message **only** when automated review is enabled for the agent — otherwise it is not rendered at all. |

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. Set `owner_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 the `connectors:manage` permission.

| Method   | Path                                         | Body                                             | Purpose                                                         |
| -------- | -------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------- |
| `POST`   | `/api/connectors/hitl-channels/slack/verify` | `{ config, secret }`                             | Test candidate credentials. Persists nothing.                   |
| `PUT`    | `/api/connectors/hitl-channels/slack`        | `{ enabled?, display_label?, config?, secret? }` | Create or update the connector.                                 |
| `DELETE` | `/api/connectors/hitl-channels/slack`        | —                                                | Remove the connector.                                           |
| `POST`   | `/api/integrations/slack/interactions`       | Slack's signed payload                           | Slack's callback for button clicks. Public; HMAC-authenticated. |

The `PUT` body is validated strictly — an unrecognised key is rejected, not ignored:

```json PUT /api/connectors/hitl-channels/slack theme={null}
{
  "enabled": true,
  "display_label": "Security approvals",
  "config": {
    "default_channel_id": "C0A1B2C3D4E"
  },
  "secret": {
    "slack_bot_token": "xoxb-…",
    "slack_signing_secret": "…"
  }
}
```

`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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](/rules/action/hitl) for the pause-and-resume model and the mask fallback.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Human-in-the-Loop" icon="user-check" href="/rules/action/hitl">
    How approvals pause a tool call, what the queue contains, and the timeout model.
  </Card>

  <Card title="Email approvals" icon="envelope" href="/connectors/email">
    The catch-all channel for agents that have no registered owner.
  </Card>
</CardGroup>
