Slink

A lightweight Slack bot toolkit for Elixir. One event-handling contract, two interchangeable transports:

Write your bot once; pick the transport per environment. Built on Mint.WebSocket and Req, with JSON handled by Elixir's built-in JSON module — no hand-rolled WebSocket protocol, no Jason dependency of our own.

Why not slack, slack_elixir, or raw WebSockex?

There is no official Slack SDK for Elixir (Bolt covers JS/Python/Java), and the existing options each leave a gap:

Installation

def deps do
[
{:slink, "~> 0.6"}
]
end

Requires Elixir ~> 1.19 (built-in JSON module, refined compiler type warnings).

Define a bot

defmodule MyBot do
use Slink
alias Slink.Event
@impl true
def handle_event(%Slink.Event{type: :app_mention} = event, context) do
# Reply to what triggered us — threaded if it was in a thread, else inline.
reply(context, "hi <@#{Event.user(event)}> 👋")
end
def handle_event(_event, _context), do: :ok
end

Handlers are stateless. Known Slack event types arrive as atoms (:app_mention, :message, …); unknown ones stay strings. To respond, call reply(context, text, opts) (imported by use Slink) — the channel and thread come from the event carried in the context, opts[:to] picks placement (:auto, :thread, :channel), and extra keys like blocks: ride along for rich replies. reply/3 returns :ok, so a clause can end with it. You can also just return{:reply, text} (or {:reply, text, opts}) and slink sends it. Replies route through a per-channel rate limiter (Slack's ~1 msg/sec/channel). For other Web API calls use Slink.API directly.

Both transports acknowledge the event to Slack before your handler runs and dispatch it off-process, so a slow handler never blows Slack's 3-second ACK window.

Quickstart — connect to Slack in 5 minutes

Socket Mode needs no public URL, no ngrok, no webhooks — the bot dials out to Slack. This is the fastest way to see it work.

1. Create the Slack app

  1. Go to https://api.slack.com/appsCreate New AppFrom a manifest.
  2. Pick your workspace.
  3. Paste the contents of manifest.json (shipped in this repo) → NextCreate.

That's all your scopes, event subscriptions, and Socket Mode configured in one shot.

Name your bot. The name is entirely yours — slink (the library) never hardcodes it, it runs purely off tokens. Before creating, edit these fields in manifest.json (and use your name in place of @slink below):

FieldControls
display_information.namethe app's name in Slack's directory
features.bot_user.display_namethe bot's @handle (what members @mention)
features.slash_commands[].commandthe slash command, e.g. /yourbot

2. Grab two tokens

  1. Bot token — left sidebar → OAuth & PermissionsInstall to WorkspaceAllow → copy the Bot User OAuth Token (starts with xoxb-).
  2. App token — left sidebar → Basic Information → scroll to App-Level TokensGenerate Token and Scopes → add the connections:write scope → Generate → copy the token (starts with xapp-).

3. Export them

export SLACK_BOT_TOKEN=xoxb-your-bot-token
export SLACK_APP_TOKEN=xapp-your-app-token

4. Run your bot

Add it to your application's supervision tree:

children = [
{Slink.SocketMode,
module: MyBot,
app_token: System.fetch_env!("SLACK_APP_TOKEN"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")}
]
Supervisor.start_link(children, strategy: :one_for_one)

…or try it right now in IEx, no app needed:

iex -S mix
{:ok, _} =
Slink.SocketMode.start_link(
module: Slink.ExampleBot,
app_token: System.fetch_env!("SLACK_APP_TOKEN"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")
)

5. Say hi

In Slack, invite the bot to a channel with /invite @slink, then mention it: @slink hello — it replies 👋. Done.

Want the bot to auto-join channels on boot? Pass join: ["C0123456789"] to Slink.SocketMode. Tune the outbound rate limit with config :slink, :rate_interval_ms, 1_000.

Beyond messages — slash commands, buttons & modals

The same handle_event/2 handles slash commands, button clicks, and modals — match on the event type and respond the way you always do. Slink.BlockKit builds the block maps without a DSL:

def handle_event(%Event{type: :block_actions} = event, context) do
update_original(context, "Deploying #{Event.action_value(event)} 🚀")
end

Slash commands, buttons & modals — the full tour, including modals, Block Kit builders, and how replies route. For chaining helpers with with, see Composing helpers.

AI apps — assistant threads & streamed replies

Assistant events normalise like any other; set_status/2 shows "is thinking…", and stream_reply/3 renders an LLM token stream as one live, progressively-updating message — degrading to a plain reply where streaming isn't enabled:

def handle_event(%Event{type: :message} = event, context) do
set_status(context, "is thinking…")
stream_reply(context, MyLLM.stream(Event.text(event)))
end

AI apps — assistant threads, suggested prompts, streaming semantics, scopes.

Testing your bot

Slink.Testing runs a handler against realistic fixtures with every Slack call captured — offline and synchronous; mix slink.smoke live-checks a real token and workspace (including whether AI streaming is enabled):

run = run(MyBot, event(:app_mention, text: "<@U1BOT> hi"))
assert [{"chat.postMessage", %{text: "hi" <> _}}] = run.calls

Testing your bot

Going to production

Run the Events API plug (standalone Bandit or mounted in Phoenix), or stay on Socket Mode with connections: 2 for high availability. Signature verification, ACK windows, retries, rate limiting, and secrets hygiene are handled; telemetry and the operational knobs are documented alongside.

Going to production

Serving many workspaces

Both transports route a per-workspace token; Slink.OAuth + Slink.OAuth.Plug handle the "Add to Slack" consent flow and code exchange — you own only the token store.

Serving many workspaces

Transport choice, per Slack's own guidance

Socket ModeEvents API (HTTP)
Public URL requiredNoYes
Slack recommends fordevelopment, internal, behind-firewallproduction, reliability, scale
Marketplace / distributed apps✗ not allowed✓ required
Concurrencycapped at 10 connections/appscales horizontally

Roadmap

Planned, non-blocking capability work lives in ROADMAP.md.

Development

mix deps.get
mix test

License

MIT — see LICENSE.