SlackBot WS

CIHex.pmDocumentationLicense: MIT

SlackBot WS (WebSocket) is a production-ready Slack bot framework for Elixir built for Slack's Socket Mode. It gives you a supervised WebSocket connection, Slack's API tier rate limiting, an elegant slash-command parsing DSL, Plug-like middleware, and comprehensive Telemetry coverage. All the typical side-mission complexity that pulls you away from just building features is eliminated.

Slack's Socket Mode shines when you need real-time event delivery without a public HTTP endpoint: laptops, firewalled environments, or stacks where inbound webhooks are undesirable. Persistent connections keep latency low, interactive payloads flowing, and local development simple. Socket Mode is fantastic for internal, private bots within an organization; it's not for Slack's public marketplace, where you'd advertise your application to other Slack organizations.

Highlights

New to Slack bots? The Getting Started guide walks through creating a Slack App, enabling Socket Mode, obtaining tokens, and running your first handler.

See it in action

Declarative slash commands

defmodule MyApp.SlackBot do
  use SlackBot, otp_app: :my_app

  # /deploy api        → %{service: "api"}
  # /deploy api canary → %{service: "api", canary?: true}
  slash "/deploy" do
    value :service
    optional literal("canary", as: :canary?)
    repeat do
      literal "env"
      value :envs
    end

    handle payload, ctx do
      %{service: svc, envs: envs} = payload["parsed"]
      Deployments.kick(svc, envs, ctx)
    end
  end
end
Input Parsed
/deploy api%{service: "api"}
/deploy api canary env staging env prod%{service: "api", canary?: true, envs: ["staging", "prod"]}

See the Slash Grammar Guide for the full macro reference.

Plug-like middleware pipeline

SlackBot routes events through a Plug-like pipeline. Middleware runs before handlers and can short-circuit with {:halt, response}. Multiple handle_event clauses for the same type run in declaration order.

defmodule MyApp.Router do
  use SlackBot

  defmodule LogMiddleware do
    def call("message", payload, ctx) do
      Logger.debug("incoming: #{payload["text"]}")
      {:cont, payload, ctx}
    end

    def call(_type, payload, ctx), do: {:cont, payload, ctx}
  end

  middleware LogMiddleware

  handle_event "message", payload, ctx do
    Cache.record(payload)
  end

  handle_event "message", payload, ctx do
    Replies.respond(payload, ctx)
  end
end

Event handlers + Web API helpers

defmodule MyApp.SlackBot do
  use SlackBot, otp_app: :my_app

  handle_event "app_mention", event, _ctx do
    MyApp.SlackBot.push({"chat.postMessage", %{
      "channel" => event["channel"],
      "text" => "Hi <@#{event["user"]}>!"
    }})
  end
end

Quick Start

1. Install

Add SlackBot to your mix.exs:

def deps do
  [
    {:slack_bot_ws, "~> 0.1.0"}
  ]
end

Then fetch dependencies:

mix deps.get

If you have Igniter installed, run mix slack_bot_ws.install to scaffold a bot module, config, and supervision wiring automatically.

2. Define a bot module

defmodule MyApp.SlackBot do
  use SlackBot, otp_app: :my_app

  handle_event "message", event, _ctx do
    MyApp.SlackBot.push({"chat.postMessage", %{
      "channel" => event["channel"],
      "text" => "Hello from MyApp.SlackBot!"
    }})
  end
end

How this works:

3. Configure tokens

In config/config.exs:

config :my_app, MyApp.SlackBot,
  app_token: System.fetch_env!("SLACK_APP_TOKEN"),
  bot_token: System.fetch_env!("SLACK_BOT_TOKEN")

4. Supervise

children = [
  MyApp.SlackBot
]

Supervisor.start_link(children, strategy: :one_for_one)

That's it. SlackBot boots a Socket Mode connection with ETS-backed cache and event buffer, per-workspace/per-channel rate limiting, and default backoff/heartbeat settings. When you're ready to tune behavior, read on.

Multiple bots and instances

The ergonomic path is one module per bot using the otp_app pattern. Each module gets its own push/1, push_async/1, emit/1, and config/0 helpers so you always call the right instance:

defmodule MyApp.CustomerSuccessBot do
  use SlackBot, otp_app: :my_app
end

defmodule MyApp.IncidentBot do
  use SlackBot, otp_app: :my_app
end

children = [
  MyApp.CustomerSuccessBot,
  MyApp.IncidentBot
]
Supervisor.start_link(children, strategy: :one_for_one)

Need distinct runtime instances of the same router module (for example, dynamically named bots per workspace)? Start SlackBot directly with an explicit :name and call the explicit APIs:

children = [
  {SlackBot, name: :team_alpha_bot, module: MyApp.DynamicRouter, app_token: ..., bot_token: ...},
  {SlackBot, name: :team_beta_bot, module: MyApp.DynamicRouter, app_token: ..., bot_token: ...}
]

SlackBot.push(:team_alpha_bot, {"chat.postMessage", %{"channel" => "C123", "text" => "hi"}})

Avoid mixing the module helpers in this scenario—the helpers assume the supervised process is registered under the module name. Pick one style per instance so the codebase stays predictable.

Background jobs and tooling can also pass a %SlackBot.Config{} directly when they already have one on hand:

config = SlackBot.config(MyApp.SlackBot)
SlackBot.emit(config, {"daily_digest", %{"channels" => ["C123"]}})

Use this sparingly (for example telemetry probes or test helpers) and prefer the module helpers inside your application code.

Advanced Configuration

Every option below is optional—omit them and SlackBot uses production-ready defaults.

Connection & backoff

config :my_app, MyApp.SlackBot,
  backoff: %{min_ms: 1_000, max_ms: 30_000, max_attempts: :infinity, jitter_ratio: 0.2},
  log_level: :info,
  health_check: [enabled: true, interval_ms: 30_000]

Telemetry

config :my_app, MyApp.SlackBot,
  telemetry_prefix: [:slackbot],
  telemetry_stats: [enabled: true, flush_interval_ms: 15_000, ttl_ms: 300_000]

When telemetry_stats is enabled, SlackBot.TelemetryStats.snapshot/1 returns rolled-up counters for API calls, handlers, rate/tier limiters, and connection states.

Cache & event buffer

# ETS (default)
cache: {:ets, []}
event_buffer: {:ets, []}

# Redis for multi-node
event_buffer:
  {:adapter, SlackBot.EventBuffer.Adapters.Redis,
    redis: [host: "127.0.0.1", port: 6379], namespace: "slackbot"}

Rate limiting

Per-channel and per-workspace shaping is enabled by default. Disable it only if you're shaping traffic elsewhere:

rate_limiter: :none

Slack's per-method tier quotas are also enforced automatically. Override entries via the tier registry:

config :slack_bot_ws, SlackBot.TierRegistry,
  tiers: %{
    "users.list" => %{max_calls: 10, window_ms: 45_000},
    "users.conversations" => %{group: :metadata_catalog}
  }

SlackBot ships with default specs for every Slack Web API method listed in the published tier tables (including special cases like chat.postMessage). Overrides are only necessary when Slack revises quotas or when custom grouping is desired.

See Rate Limiting Guide for a full explanation of how tier-aware limiting works and how to tune it.

Slash-command acknowledgements

ack_mode: :silent          # default: no placeholder
ack_mode: :ephemeral       # sends "Processing…" via response_url
ack_mode: {:custom, &MyApp.custom_ack/2}

Diagnostics

diagnostics: [enabled: true, buffer_size: 300]

When enabled, SlackBot captures inbound/outbound frames. See Diagnostics Guide for IEx workflows and replay.

Metadata cache & background sync

cache_sync: [
  enabled: true,
  kinds: [:channels],       # :users is opt-in
  interval_ms: :timer.hours(1)
]

user_cache: [
  ttl_ms: :timer.hours(1),
  cleanup_interval_ms: :timer.minutes(5)
]

Event Pipeline & Middleware

SlackBot routes events through a Plug-like pipeline. Middleware runs before handlers and can short-circuit with {:halt, response}. Multiple handle_event clauses for the same type run in declaration order.

defmodule MyApp.Router do
  use SlackBot

  defmodule LogMiddleware do
    def call("message", payload, ctx) do
      Logger.debug("incoming: #{payload["text"]}")
      {:cont, payload, ctx}
    end

    def call(_type, payload, ctx), do: {:cont, payload, ctx}
  end

  middleware LogMiddleware

  handle_event "message", payload, ctx do
    Cache.record(payload)
  end

  handle_event "message", payload, ctx do
    Replies.respond(payload, ctx)
  end
end

Slash Command Grammar

The slash/2 DSL compiles grammar declarations into deterministic parsers:

slash "/deploy" do
  value :service
  optional literal("canary", as: :canary?)
  repeat do
    literal "env"
    value :envs
  end

  handle payload, ctx do
    %{service: svc, envs: envs} = payload["parsed"]
    Deployments.kick(svc, envs, ctx)
  end
end
Input Parsed
/deploy api%{service: "api"}
/deploy api canary env staging env prod%{service: "api", canary?: true, envs: ["staging", "prod"]}

See Slash Grammar Guide for the full macro reference.

Web API Helpers

Both variants route through the rate limiter and Telemetry pipeline automatically. Reach for the explicit SlackBot.* forms when you start bots under dynamic names (multi-tenant supervisors, {:via, ...} tuples) or when you're operating on a cached %SlackBot.Config{} outside the router (for example a background job or probe). The module-scoped helpers stay the recommended default for static otp_app bots.

Diagnostics & Replay

iex> SlackBot.Diagnostics.list(MyApp.SlackBot, limit: 5)
[%{direction: :inbound, type: "slash_commands", ...}, ...]

iex> SlackBot.Diagnostics.replay(MyApp.SlackBot, types: ["slash_commands"])
{:ok, 3}

Replay feeds events back through your handlers—useful for reproducing production issues locally. See Diagnostics Guide.

Telemetry & LiveDashboard

SlackBot emits events for connection state, handler execution, rate limiting, and health checks. Integrate with LiveDashboard or attach plain handlers:

:telemetry.attach(
  :slackbot_logger,
  [:slackbot, :connection, :state],
  fn _event, _measurements, %{state: state}, _ ->
    Logger.info("Slack connection: #{state}")
  end,
  nil
)

See Telemetry Guide for metric definitions and LiveDashboard wiring.

Example Bot

The examples/basic_bot/ directory contains a runnable project demonstrating:

Follow the README inside that folder to run it against a Slack dev workspace.

Guides

Development

mix deps.get
mix test
mix format

Test helpers

SlackBot.TestTransport and SlackBot.TestHTTP in lib/slack_bot/testing/ let you simulate Socket Mode traffic and stub Web API calls without hitting Slack.

Live Redis tests

mix test now exercises the Redis event buffer adapter against a live Redis instance:

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for your changes
  4. Run mix test and mix format
  5. Open a pull request

For larger changes, open an issue first to discuss the approach.

License

MIT.