SlackBot (slack_bot_ws v0.1.0-rc.2)
View SourceProduction-ready Slack bot framework for Socket Mode.
SlackBot provides a supervised WebSocket connection, tier-aware rate limiting, declarative slash-command parsing, and full observability for building Slack bots in Elixir.
Quick Start
The recommended approach is the otp_app pattern:
1. Define your 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!"
}})
end
slash "/ping" do
handle _payload, _ctx do
{:ok, %{"text" => "Pong!"}}
end
end
end2. Configure tokens:
# config/config.exs
config :my_app, MyApp.SlackBot,
app_token: System.fetch_env!("SLACK_APP_TOKEN"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")3. Supervise:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.SlackBot
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endAlternative: Inline Supervision
You can also supervise SlackBot directly without the otp_app pattern:
children = [
{SlackBot,
name: MyBot,
app_token: System.fetch_env!("SLACK_APP_TOKEN"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN"),
module: MyBot}
]
Supervisor.start_link(children, strategy: :one_for_one)This approach is useful for dynamic bot instances or when you prefer explicit configuration over application environment.
Public API
Web API Calls
MyApp.SlackBot.push/1- Synchronous API call (waits for response) for bot modules started via theotp_apppattern.SlackBot.push/2- Explicit variant when supervising bots under custom names or passing%SlackBot.Config{}directly.MyApp.SlackBot.push_async/1/SlackBot.push_async/2- Fire-and-forget async calls routed through the runtime task supervisor.
All variants route through rate limiters and Telemetry automatically.
Cache Queries
find_user/2- Lookup cached users by ID, email, or namefind_channel/2- Lookup channels by ID or name
Event Injection
MyApp.SlackBot.emit/1/SlackBot.emit/2- Inject synthetic events into the handler pipeline
Configuration
MyApp.SlackBot.config/0/SlackBot.config/1- Read the immutable%SlackBot.Config{}struct
Multiple Bot Instances
The ergonomic approach is one module per bot using the otp_app pattern:
defmodule MyApp.BotOne do
use SlackBot, otp_app: :my_app
end
defmodule MyApp.BotTwo do
use SlackBot, otp_app: :my_app
end
children = [
MyApp.BotOne,
MyApp.BotTwo
]Each module gets its own push/1, push_async/1, emit/1, and config/0
helpers so your handlers can call MyApp.BotOne.push/1 without guessing which
instance is running.
Need multiple runtime copies of the same router module (for multi-tenant
bots, for example)? Start SlackBot directly with a unique :name per instance:
children = [
{SlackBot, name: :team_alpha_bot, module: MyApp.DynamicRouter, ...},
{SlackBot, name: :team_beta_bot, module: MyApp.DynamicRouter, ...}
]In that setup, call the explicit APIs (SlackBot.push(:team_alpha_bot, request),
SlackBot.emit(:team_beta_bot, event), etc.) and avoid the module helpers so
there is no ambiguity about which instance is targeted.
See Also
- Getting Started Guide
- Rate Limiting Guide
- Slash Grammar Guide
- Telemetry Dashboard
- Example app:
examples/basic_bot/
Summary
Functions
Returns a child specification so SlackBot can be supervised directly.
Reads the immutable %SlackBot.Config{} from the config server registered under server.
Injects a synthetic event into your handler pipeline.
Finds a cached Slack channel, fetching from Slack API if not cached.
Finds a cached Slack user, fetching from Slack API if not cached.
Sends a Web API request to Slack using the bot token.
Sends a Web API request asynchronously without blocking the caller.
Starts a SlackBot supervision tree.
Types
Functions
@spec child_spec(keyword()) :: Supervisor.child_spec()
Returns a child specification so SlackBot can be supervised directly.
@spec config(GenServer.server()) :: SlackBot.Config.t()
Reads the immutable %SlackBot.Config{} from the config server registered under server.
@spec emit( SlackBot.Config.t() | GenServer.server(), {String.t(), map()} ) :: :ok
Injects a synthetic event into your handler pipeline.
Prefer the injected module helper (MyApp.SlackBot.emit/1) unless you need to
target a dynamically named instance. This lets you programmatically trigger
handlers as if Slack sent the event, useful for testing, scheduled tasks, or
cross-bot communication.
Use Cases
- Testing: Replay production events in development
- Scheduled jobs: Trigger handlers from a cron-like scheduler
- Internal events: Coordinate between different parts of your bot
- Simulations: Test handler behavior without hitting Slack
Arguments
server_or_config- Bot instance name (atom/pid/via) or%SlackBot.Config{}.type- Event type matching yourhandle_eventdeclarationspayload- Event payload map (structure depends on event type)
Examples
Trigger a message handler
MyApp.SlackBot.emit({"message", %{
"type" => "message",
"channel" => "C123456",
"user" => "U123456",
"text" => "simulated message",
"ts" => "1234567890.123456"
}})Scheduled reminder
def send_daily_reminder do
MyApp.SlackBot.emit({"daily_reminder", %{
"time" => DateTime.utc_now(),
"channels" => ["C123", "C456"]
}})
end
# In your router:
handle_event "daily_reminder", payload, _ctx do
Enum.each(payload["channels"], fn channel ->
MyApp.SlackBot.push({"chat.postMessage", %{
"channel" => channel,
"text" => "Daily standup in 10 minutes!"
}})
end)
endTest a handler
# In your test
test "processes mentions correctly" do
MyBot.emit({"app_mention", %{
"channel" => "C123",
"user" => "U123",
"text" => "<@BOTID> help"
}})
# Assert side effects
assert_receive {:message_sent, "C123", text}
endDiagnostics
Emitted events are recorded in the diagnostics buffer (when enabled) with
direction: :outbound and meta: %{origin: :emit}.
See Also
SlackBot.Diagnostics.replay/2- Replay captured events- Your
handle_eventdeclarations in your bot module
@spec find_channel(GenServer.server(), channel_matcher()) :: map() | nil
Finds a cached Slack channel, fetching from Slack API if not cached.
Returns the full channel object including name, topic, member count, and metadata. Checks cache first, then hits Slack's API if needed.
Matchers
{:id, "C123"}- Exact channel ID (fastest){:name, "#general"}- Channel name with or without#(case-insensitive){:name, "general"}- Same as above
Returns
- Channel map with full Slack channel data
nilif no matching channel found or bot not a member
Examples
Find by ID
iex> SlackBot.find_channel(MyApp.SlackBot, {:id, "C123456"})
%{
"id" => "C123456",
"name" => "general",
"is_channel" => true,
"is_private" => false,
"is_archived" => false,
"topic" => %{
"value" => "Company announcements",
"creator" => "U123",
"last_set" => 1234567890
},
"purpose" => %{
"value" => "This channel is for team-wide communication"
},
"num_members" => 150,
"created" => 1234567890
}Find by name
# With hash
iex> SlackBot.find_channel(MyApp.SlackBot, {:name, "#engineering"})
%{"id" => "C789", "name" => "engineering", ...}
# Without hash (same result)
iex> SlackBot.find_channel(MyApp.SlackBot, {:name, "engineering"})
%{"id" => "C789", "name" => "engineering", ...}
# Not found
iex> SlackBot.find_channel(MyApp.SlackBot, {:name, "#nonexistent"})
nilPractical Usage
def post_to_channel(channel_name, message) do
case SlackBot.find_channel(MyBot, {:name, channel_name}) do
%{"id" => channel_id} ->
MyBot.push({"chat.postMessage", %{
"channel" => channel_id,
"text" => message
}})
nil ->
{:error, :channel_not_found}
end
endImportant: Bot Membership
The bot must be a member of a channel to find it. Private channels won't appear unless the bot has been invited.
Caching Behavior
- Channel cache syncs automatically (see
cache_syncconfig) - Default sync runs hourly via
conversations.list - Sync only includes channels the bot has joined
Performance Tips
- Prefer
{:id, ...}when you have the ID - Name lookups scan the cache (no extra API call once synced)
See Also
find_user/2- Similar function for users- Config option
cache_sync- Controls channel cache refresh
@spec find_user(GenServer.server(), user_matcher()) :: map() | nil
Finds a cached Slack user, fetching from Slack API if not cached.
This is your go-to function for resolving users by ID, email, or name. SlackBot checks the cache first, then hits Slack's API if needed, then caches the result.
Matchers
{:id, "U123"}- Exact Slack user ID (fastest){:email, "alice@example.com"}- Profile email (case-insensitive){:name, "alice"}- Username or display name (case-insensitive)
Returns
- User map with full Slack profile data
nilif no matching user found
Examples
Find by ID (fastest)
iex> SlackBot.find_user(MyApp.SlackBot, {:id, "U123456"})
%{
"id" => "U123456",
"name" => "alice",
"real_name" => "Alice Smith",
"profile" => %{
"email" => "alice@example.com",
"display_name" => "Alice",
"title" => "Software Engineer",
"image_72" => "https://...",
"status_text" => "In a meeting",
"status_emoji" => ":calendar:"
},
"is_bot" => false,
"is_admin" => false,
"tz" => "America/Los_Angeles"
}Find by email
iex> SlackBot.find_user(MyApp.SlackBot, {:email, "bob@example.com"})
%{"id" => "U789", "name" => "bob", ...}
iex> SlackBot.find_user(MyApp.SlackBot, {:email, "nobody@example.com"})
nilFind by name
# Matches username
iex> SlackBot.find_user(MyApp.SlackBot, {:name, "alice"})
%{"id" => "U123", "name" => "alice", ...}
# Also matches display name
iex> SlackBot.find_user(MyApp.SlackBot, {:name, "Alice Smith"})
%{"id" => "U123", "profile" => %{"display_name" => "Alice Smith"}, ...}Practical Usage
def send_dm(email, message) do
case SlackBot.find_user(MyBot, {:email, email}) do
%{"id" => user_id} ->
MyBot.push({"chat.postMessage", %{
"channel" => user_id,
"text" => message
}})
nil ->
{:error, :user_not_found}
end
endCaching Behavior
- First lookup fetches from Slack and caches (respects
user_cache.ttl_ms) - Subsequent lookups are instant from cache
- Cache auto-refreshes on expiry
Performance Tips
- Prefer
{:id, ...}when you have the ID—it's a direct cache lookup - Email and name matchers scan the cache first before hitting the API
See Also
find_channel/2- Similar function for channels
@spec push( SlackBot.Config.t() | GenServer.server(), {String.t(), map()} ) :: {:ok, map()} | {:error, term()}
Sends a Web API request to Slack using the bot token.
This is your primary way to call Slack's Web API. SlackBot automatically:
- Routes through per-channel and tier-level rate limiters
- Emits Telemetry events for observability
- Uses the configured HTTP pool (Finch by default)
Arguments
server_or_config- Bot instance name (atom, pid, or{:via, ...}) or a%SlackBot.Config{}reference. Most applications should call the injected module helper (MyApp.SlackBot.push/1) instead of invoking this function directly.{method, body}- Tuple of API method name and request parameters
Returns
{:ok, response}- Successful API call with Slack's JSON response{:error, reason}- Failed call (see Error Handling below)
Examples
Post a message
iex> MyApp.SlackBot.push({"chat.postMessage", %{
...> "channel" => "C123456",
...> "text" => "Hello from SlackBot!",
...> "blocks" => [...]
...> }})
{:ok, %{
"ok" => true,
"channel" => "C123456",
"ts" => "1234567890.123456",
"message" => %{"text" => "Hello from SlackBot!", ...}
}}Upload a file
iex> MyApp.SlackBot.push({"files.upload", %{
...> "channels" => "C123456",
...> "content" => "log data here",
...> "filename" => "debug.log",
...> "title" => "Debug Logs"
...> }})
{:ok, %{"ok" => true, "file" => %{"id" => "F123", ...}}}Update a message
iex> MyApp.SlackBot.push({"chat.update", %{
...> "channel" => "C123456",
...> "ts" => "1234567890.123456",
...> "text" => "Updated text"
...> }})
{:ok, %{"ok" => true, ...}}Error Handling
case MyApp.SlackBot.push({"chat.postMessage", body}) do
{:ok, %{"ok" => true} = response} ->
Logger.info("Message posted: #{response["ts"]}")
{:ok, %{"ok" => false, "error" => error}} ->
Logger.error("Slack API error: #{error}")
{:error, reason} ->
Logger.error("HTTP error: #{inspect(reason)}")
endCommon Errors
"channel_not_found"- Invalid channel ID or bot not invited"not_in_channel"- Bot needs to join the channel first"invalid_auth"- Check your bot token"rate_limited"- You've exceeded Slack's quotas (SlackBot mitigates this)"msg_too_long"- Message exceeds 40,000 characters
Rate Limiting
SlackBot automatically queues requests when approaching rate limits. Your call
may block briefly if the limiter needs to wait. Use the module helper
MyApp.SlackBot.push_async/1 (or SlackBot.push_async/2 when targeting a
dynamic instance) for fire-and-forget calls that shouldn't block your handler.
See Also
MyApp.SlackBot.push_async/1- Non-blocking variant for the otp_app helperpush_async/2- Non-blocking variant when you need to target a specific instance name- Slack Web API Reference
- Rate Limiting Guide
@spec push_async( GenServer.server(), {String.t(), map()} ) :: Task.t()
Sends a Web API request asynchronously without blocking the caller.
Prefer the injected module helper (MyApp.SlackBot.push_async/1) unless you
need to target a dynamically named instance. Use this when you want
fire-and-forget behavior—your handler continues
immediately while the API call happens in a supervised task.
When to Use push_async
- Posting multiple messages in a loop
- Sending notifications that don't need confirmation
- Updating UI elements in the background
- Any call where you don't need the response immediately
Returns
A Task.t() you can await later if needed, or ignore for true fire-and-forget.
Examples
Send multiple notifications
def notify_team(user_ids, message) do
Enum.each(user_ids, fn user_id ->
MyApp.SlackBot.push_async({"chat.postMessage", %{
"channel" => user_id,
"text" => message
}})
end)
:ok # Returns immediately, messages send in background
endFire and forget
# Don't care about the result
MyApp.SlackBot.push_async({"reactions.add", %{
"channel" => channel,
"timestamp" => ts,
"name" => "white_check_mark"
}})Await if needed
task = MyApp.SlackBot.push_async({"chat.postMessage", body})
# ... do other work ...
result = Task.await(task, 5_000)Task Supervision
All async tasks run under SlackBot's runtime task supervisor, so crashes won't take down your bot. Failed tasks are logged automatically.
See Also
MyApp.SlackBot.push/1- Synchronous variant that waits for responsepush/2- Explicit synchronous variant when you need to target a dynamic instanceTask.await/2- If you need to wait for the result later
@spec start_link(keyword()) :: Supervisor.on_start()
Starts a SlackBot supervision tree.
Accepts the same options as child_spec/1, returning Supervisor.on_start().