Add OpenClaw to Slack with Socket Mode, no public webhook

Connect OpenClaw to a Slack workspace over Socket Mode, limit it to the channels and people you choose, and route Slack to a separate work agent.

· ranx.cloud

Illustration of a small closed house with one cable leaving through the chimney to a socket on a distant hill

This is for a small team, or one person in a work Slack, who wants an OpenClaw agent they can @-mention in a few channels and DM directly. It uses Slack's Socket Mode, so OpenClaw opens an outbound WebSocket to Slack and Slack never has to reach your server. At the end the bot answers in the channels you list, ignores the rest of the workspace, and can run as a separate "work" agent so Slack conversations never mix with your personal chats.

Socket Mode or HTTP Request URLs

OpenClaw's Slack plugin supports both transports and says they're at feature parity for messages, slash commands, App Home and interactivity (transports). Pick by deployment shape:

Socket Mode (default) HTTP Request URLs
Public URL for Slack to call Not needed Required, at /slack/events by default
Tokens Bot token (xoxb-) + app-level token (xapp-) Bot token + signing secret
Network Outbound WSS to Slack Inbound HTTPS from Slack

Use Socket Mode for a single gateway. It needs no inbound route, no reverse-proxy rules, and nothing in front of your gateway's login has to let Slack through. HTTP mode is for several gateway replicas behind a load balancer. This guide covers Socket Mode only.

Create the Slack app

  1. Go to api.slack.com/apps → Create New App → From a manifest → pick your workspace.
  2. Paste the manifest below. It is OpenClaw's "Minimal" manifest from the setup docs, which covers DMs, channel history, mentions and the /openclaw slash command. If you also want files, reactions and pins, use the "Recommended" manifest from the same page.
{
  "display_information": { "name": "OpenClaw", "description": "Slack connector for OpenClaw" },
  "features": {
    "bot_user": { "display_name": "OpenClaw", "always_online": true },
    "app_home": {
      "home_tab_enabled": true,
      "messages_tab_enabled": true,
      "messages_tab_read_only_enabled": false
    },
    "slash_commands": [
      { "command": "/openclaw", "description": "Send a message to OpenClaw", "should_escape": false }
    ]
  },
  "oauth_config": {
    "scopes": {
      "bot": [
        "app_mentions:read", "assistant:write", "channels:history", "channels:read",
        "chat:write", "commands", "groups:history", "groups:read",
        "im:history", "im:read", "im:write", "users:read"
      ]
    }
  },
  "settings": {
    "socket_mode_enabled": true,
    "event_subscriptions": {
      "bot_events": [
        "app_home_opened", "app_mention", "message.channels", "message.groups", "message.im"
      ]
    }
  }
}

The docs' version also includes an agent_view block and the app_context_changed / agent_session_* events for Slack's Agent View. Keep them if your workspace offers Agent View. They're trimmed here for length.

  1. Basic Information → App-Level Tokens → Generate Token and Scopes. Add the scope connections:write, save, and copy the token (xapp-…).
  2. Install App → Install to Workspace. Copy the Bot User OAuth Token (xoxb-…).

If your workspace requires admin approval for apps, step 4 becomes a request. Nothing else changes.

Connect it in the Control UI

  1. Settings → Channels → Add a channel → Slack (or More channels…). Choose Guided step-by-step setup. The Slack plugin (@openclaw/slack) installs on first use if it isn't already there.
  2. The wizard asks for the Slack socket mode tokens: paste the xapp- token at Enter Slack app token and the xoxb- token at Enter Slack bot token.
  3. At Slack allowFrom (usernames or ids), enter your member ID. In Slack, click your profile → the three-dot menu → Copy member ID. It starts with U.
  4. Finish. The Slack card reads Running once the socket connects.5. DM the bot in Slack. If DM policy is pairing (the default), approve yourself in Settings → Channels → DM access requests.

Choose the channels

By default groupPolicy is allowlist, so channels stay silent until you list them. Slack channel keys must be channel IDs, not names: under an allowlist, a name OpenClaw can't resolve is denied. To get the ID, right-click the channel → Copy link. The C… part at the end is the ID.

In Settings → Advanced → Raw:

{
  channels: {
    slack: {
      mode: "socket",
      dmPolicy: "allowlist",
      allowFrom: ["U01AAAAAAA", "U01BBBBBBB"],
      groupPolicy: "allowlist",
      channels: {
        C01ENGINEER: {
          requireMention: true,
          systemPrompt: "Engineering channel. Be concise; link to sources.",
        },
        C01OPSDESK1: {
          requireMention: true,
          users: ["U01AAAAAAA"],          // only this person can trigger it here
        },
      },
      replyToMode: "first",
      thread: { historyScope: "thread", initialHistoryLimit: 20 },
      joinIntro: false,                    // skip the self-introduction on invite
    },
  },
}

Then /invite @OpenClaw in each listed channel. With joinIntro left at its default (true), the bot posts one short introduction built from the channel's name, purpose and recent messages the first time it joins. Set it to false if your team would find that odd.

Threads are their own sessions (historyScope: "thread"). When someone @-mentions the bot in a thread, it fetches up to 20 earlier thread messages as context, so "summarize this thread" works without copy-paste.

Group DMs are off by default (dm.groupEnabled: false), and the app can only see group DMs it was added to.

Give Slack its own agent

If the same gateway also runs your personal WhatsApp or Discord assistant, you probably don't want work Slack sharing a memory and workspace with it. OpenClaw routes each inbound message to exactly one agent through bindings (channel routing).

  1. In the Control UI, choose New agent in the sidebar (or Settings → Agents). Name it work and give it its own model under the agent's Overview tab if you want a different one.
  2. Add bindings in Settings → Advanced → Raw. Once more than one agent exists, every channel needs a route, because the docs say routing no longer falls back to the first agent. Bind them all explicitly:
{
  bindings: [
    { agentId: "work", match: { channel: "slack", accountId: "*" } },
    { agentId: "main", match: { channel: "whatsapp", accountId: "*" } },
    { agentId: "main", match: { channel: "discord", accountId: "*" } },
  ],
}

To route a single channel instead of the whole workspace, use a peer match: { agentId: "work", match: { channel: "slack", peer: { kind: "channel", id: "C01ENGINEER" } } }. An exact peer match beats a channel-wide match.

Each agent has its own workspace and session store. Things you tell the work agent in Slack stay out of the main agent's memory, and the reverse holds too.

Costs and limits

Slack doesn't charge for a custom app on top of your plan. The model is the cost. Each mention in a thread sends the system prompt, tool definitions and up to initialHistoryLimit thread messages.

With Claude Sonnet 5.5 at $2 input / $10 output per million tokens (Anthropic pricing, November 2026):

~22,000 input tokens × $2  / 1e6 = $0.044
~700 output tokens   × $10 / 1e6 = $0.007
≈ $0.05 per mention

team of 8, ~5 mentions each per workday = 40/day
40 × $0.05 × 21 workdays ≈ $43/month, before prompt caching

Repeated prefixes are billed at the cache-read rate ($0.10 per million on Sonnet 5.5), so real spend is usually lower. Watch the Usage page for a week before deciding. Lowering thread.initialHistoryLimit and historyLimit is the easiest way to cut cost.

Slack limits how fast an app can post (Web API rate limits are per method and workspace, see Slack's rate limit docs). A conversational bot rarely reaches them. A runaway automation posting to a channel can.

What goes wrong

The card says Running but the bot is deaf. This is the one to watch. An open issue (#128809), reported on a GKE deployment, describes Socket Mode reconnects leaking sockets until Slack's 10-connection cap returns too_many_websockets. It also describes half-open connections that die without a log line while the health check stays green. A related report, where an account that never reconnected was still shown as healthy (#186756), is closed, but its last review note says the repair was still in an open PR. Don't assume your version has it. If the bot goes quiet, restart the whole gateway: send /restart in the Control UI chat. A channel restart from the UI may not close the stale sockets. If it happens repeatedly, add the details to the issue.

One invalid_auth disabled Slack for good. A single transient invalid_auth with a valid token can mark Socket Mode as permanently failed (#180942; also closed, also noted as not yet fixed on main at the time). Restarting the gateway brings it back.

Mentions in a channel get no reply. Check the channel ID is under channels.slack.channels (a name won't do under allowlist), the bot was invited, and the sender isn't excluded by that channel's users list. The Slack card in Settings → Channels shows the effective policy.

DMs are ignored. You're not in allowFrom and haven't been approved through pairing. Or dm.enabled was set to false.

The slash command does nothing. In Socket Mode the command arrives over the WebSocket, and the manifest's url field is ignored. If /openclaw fails, the app was probably created without socket_mode_enabled: true. Fix the manifest under App Manifest in the Slack app settings and reinstall.

Hosting notes. On ranx.cloud the Control UI sits behind your ranx.cloud login, but POST requests to OpenClaw's default Slack path, /slack/events, go straight to OpenClaw, where Slack's signing secret verifies them. So HTTP Request mode can work there too, as long as you keep that default path (or put a custom one under /webhooks/) and set the signing secret. Socket Mode is still the simpler choice for a single gateway: there's no Request URL to configure and nothing inbound to debug.

Variations

  • Turn on Slack-native approval buttons for risky commands with channels.slack.execApprovals: { enabled: true, approvers: ["U01AAAAAAA"] }. The group-chat safety post covers why you'd want that.
  • Set streaming.mode: "partial" to stream answers into threads as they're written.