Background dispatch with automatic rate-limit retries -- Lazy at call time, diligent at delivery time.
SlackSender provides a simple, reliable way to send Slack messages from Ruby applications. It handles rate limiting, retries, error notifications, and development environment redirects automatically.
SlackSender is a Ruby gem that simplifies sending messages to Slack by:
- Background dispatch with automatic rate-limit retries via Sidekiq or ActiveJob
- Multi-channel async delivery for broadcasting to multiple channels efficiently
- Development mode redirects to prevent accidental production notifications
- Automatic error handling for common Slack API errors (NotInChannel, ChannelNotFound, IsArchived)
- Multiple profile support for managing multiple Slack workspaces
- File upload support with synchronous and async delivery
- User group mention formatting with development mode substitution
Sending Slack messages from Ruby applications often requires:
- Managing rate limits and retries manually
- Handling various Slack API errors
- Preventing accidental production notifications in development
- Coordinating multiple Slack workspaces or bots
SlackSender abstracts these concerns, allowing you to focus on your application logic while it handles the complexities of reliable Slack message delivery.
Add this line to your application's Gemfile:
gem 'slack_sender'And then execute:
bundle installOr install it yourself as:
gem install slack_sender- Ruby >= 3.2.1
- A Slack API token (Bot User OAuth Token)
- For async delivery: Sidekiq or ActiveJob (auto-detected if available)
Your Slack app needs specific OAuth scopes depending on which features you use. Add these under OAuth & Permissions → Bot Token Scopes in your Slack app settings.
Minimum scopes for basic messaging:
chat:write
Recommended scopes for full functionality:
| Scope | Required For | Notes |
|---|---|---|
chat:write |
All messaging | Required for chat.postMessage — sending text, blocks, and attachments |
chat:write.public |
Public channels | Post to public channels your bot hasn't been added to |
files:write |
File uploads | Required for files.getUploadURLExternal and files.completeUploadExternal |
files:read |
File metadata | Required if you need thread timestamps from file uploads (used internally by SlackSender) |
After adding scopes, reinstall the app to your workspace to apply the changes.
Register a profile with your Slack token and channel configuration:
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
channels: {
ops_alerts: 'C1111111111',
deployments: 'C2222222222',
reports: 'C3333333333',
},
user_groups: {
engineers: 'S1234567890',
},
sandbox: { # Optional: redirect messages/mentions when in sandbox mode (non-production)
channel: {
replace_with: 'C1234567890',
message_prefix: ':construction: _This message would have been sent to %s in production_'
},
user_group: {
replace_with: 'S_DEV_GROUP'
}
}
)# Async delivery (recommended) - uses Sidekiq or ActiveJob
SlackSender.call(
channel: :ops_alerts,
text: ":rotating_light: High error rate on checkout"
)
# Synchronous delivery (returns thread timestamp)
thread_ts = SlackSender.call!(
channel: :deployments,
text: ":rocket: Deploy finished for #{ENV.fetch('APP_NAME', 'my-app')} (#{Rails.env})"
)Note: If text: is explicitly provided but blank (and you did not provide blocks, attachments, or files), SlackSender treats it as a no-op and returns false (it will not enqueue a job and will not send anything to Slack).
# Simple text message
SlackSender.call(
channel: :ops_alerts,
text: ":warning: Redis latency is elevated"
)
# With markdown formatting
SlackSender.call(
channel: :deployments,
text: "Deploy started by *#{user.name}* for `#{ENV.fetch('APP_NAME', 'my-app')}`"
)Note: Text is parsed as Slack mrkdwn by default. For formatting user mentions, channels, links, and other special content, use the Slack::Messages::Formatting helpers from slack-ruby-client:
SlackSender.call(
channel: :ops_alerts,
text: [
":rotating_light: Incident acknowledged by #{Slack::Messages::Formatting.user(user.slack_id)}",
Slack::Messages::Formatting.url('https://status.example.com/incidents/123', 'Incident timeline'),
].join("\n")
)Channels can be specified as symbols (resolved from profile config) or channel IDs:
# Using symbol (resolved from channels hash)
SlackSender.call(channel: :ops_alerts, text: ":rotating_light: Alert")
# Using channel ID directly
SlackSender.call(channel: "C1234567890", text: ":rotating_light: Alert")Configure a default channel for a profile to avoid passing channel: on every call:
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
default_channel: :ops_alerts, # Used when no channel is specified
channels: {
ops_alerts: 'C1111111111',
deployments: 'C2222222222',
}
)
# These are equivalent:
SlackSender.call(text: "Alert!") # Uses default_channel
SlackSender.call(channel: :ops_alerts, text: "Alert!") # Explicit channel
# Override when needed
SlackSender.call(channel: :deployments, text: "Hello") # Uses :deployments insteadThe default_channel can be a symbol (resolved from channels hash) or a channel ID string.
Send the same message to multiple channels with a single call using channels: (plural):
# Async delivery to multiple channels
SlackSender.call(
channels: [:ops_alerts, :deployments],
text: ":rocket: Deploy finished for my-app"
)Key behaviors:
- Multi-channel delivery is only supported via async (
call). Usingcall!withchannels:raises an error - Files are uploaded once and shared to all channels efficiently
- Each channel receives a separate background job with independent retry handling
- Single-element arrays (e.g.,
channels: [:ops_alerts]) are normalized tochannel:
# ❌ Sync multi-channel not supported
SlackSender.call!(channels: [:a, :b], text: "...") # Raises ArgumentError
# ✅ Use async instead
SlackSender.call(channels: [:a, :b], text: "...")
# ✅ Or send individually if you need sync
[:a, :b].each { |ch| SlackSender.call!(channel: ch, text: "...") }# With blocks
SlackSender.call(
channel: :deployments,
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: ":rocket: *Deploy finished* for `my-app`" }
}
]
)
# With attachments
SlackSender.call(
channel: :ops_alerts,
attachments: [
{
color: "good",
text: "Autoscaling event completed successfully"
}
]
)
# With custom emoji
SlackSender.call(
channel: :ops_alerts,
text: "Background job queue is healthy",
icon_emoji: "robot"
)File uploads are supported with both synchronous (call!) and async (call) delivery. Use file: for a single file or files: for multiple files.
# Single file - use file: (singular)
SlackSender.call!(
channel: :reports,
text: "Daily ops report attached",
file: File.open("report.pdf")
)
# Multiple files - use files: (plural)
SlackSender.call!(
channel: :reports,
text: "Daily ops report (details + raw export)",
files: [
File.open("report.pdf"),
File.open("data.csv")
]
)
# Async delivery (background job handles sharing)
SlackSender.call(
channel: :alerts,
text: "Multiple files attached",
files: [
File.open("report.pdf"),
File.open("data.csv")
]
)Important: Channel ID required for file uploads
Slack's files_upload_v2 API requires channel IDs (e.g., C024BE91L, D032AC32T), not usernames (@user) or channel names (#channel). This is a limitation of the newer Slack file upload APIs.
# ✅ Works - using channel ID from profile
SlackSender.call!(channel: :alerts, file: file)
# ✅ Works - using channel ID directly
SlackSender.call!(channel: "C024BE91L", file: file)
# ❌ Fails - @username not supported for file uploads
SlackSender.call!(channel: "@username", file: file)
# ❌ Fails - #channel-name not supported for file uploads
SlackSender.call!(channel: "#general", file: file)To send files as a DM, use the DM channel ID (starts with D) which you can find in Slack's URL when viewing the conversation.
Async file upload behavior:
- Small files (<
max_inline_file_size, default 512 KB): Serialized directly to the job payload - Larger files: Uploaded to Slack's servers synchronously, then the background job shares them to the channel
This means call with larger files may block briefly during the upload phase. The background job then handles sharing to the channel with automatic retry support for rate limits.
Size limits:
- Individual files cannot exceed 1 GB (Slack's hard limit)
- Total file size for async uploads is limited by
max_async_file_upload_size(default 25 MB) to prevent blocking web processes on large uploads - Use
call!for synchronous upload when you need to upload files larger thanmax_async_file_upload_size
Note: Filenames are automatically detected from file objects. For custom filenames, use objects that respond to original_filename (e.g., ActionDispatch::Http::UploadedFile) or ensure the file path contains the desired filename.
Supported file types:
FileobjectsTempfileobjectsStringIOobjectsActiveStorage::Attachmentobjects (if ActiveStorage is available)- String file paths (will be opened automatically)
- Any object that responds to
readand hasoriginal_filenameorpath
# Reply to a thread
SlackSender.call(
channel: :ops_alerts,
text: "Mitigation: rolled back to previous release",
thread_ts: "1234567890.123456"
)
# Get thread timestamp from initial message
thread_ts = SlackSender.call!(
channel: :ops_alerts,
text: ":rotating_light: Elevated 500s detected on /checkout"
)
# thread_ts => "1234567890.123456"Format user group mentions (automatically redirects to sandbox user_group when in sandbox mode):
SlackSender.format_group_mention(:on_call)
# => "<!subteam^S1234567890|@on_call>"If sandbox.user_group.replace_with is configured and the app is in sandbox mode (per config.sandbox_mode?), format_group_mention will replace the requested group with the sandbox user_group instead, similar to how sandbox channel redirects messages:
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
user_groups: {
engineers: 'S1234567890', # Would be replaced with sandbox user_group in sandbox mode
},
sandbox: {
user_group: { replace_with: 'S_DEV_GROUP' } # All group mentions use this in sandbox mode
}
)
# In sandbox mode, this returns the sandbox user_group mention
SlackSender.format_group_mention(:engineers)
# => "<!subteam^S_DEV_GROUP>"Use a callable for the token to fetch it dynamically:
SlackSender.register(
token: -> { SecretsManager.get_slack_token },
channels: { ops_alerts: 'C123' }
)The token is memoized after first access, so the callable is only evaluated once per profile instance.
Register multiple profiles for different Slack workspaces:
# Internal engineering workspace
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
channels: { ops_alerts: 'C123', deployments: 'C234' }
)
# Customer support workspace
SlackSender.register(:support,
token: ENV['SUPPORT_SLACK_TOKEN'],
channels: { support_tickets: 'C456' }
)
# Use specific profile
SlackSender.profile(:support).call(
channel: :support_tickets,
text: "New high-priority ticket received"
)
# Or use bracket notation
SlackSender[:support].call(
channel: :support_tickets,
text: "New high-priority ticket received"
)
# Or override default profile with profile parameter
SlackSender.call(
profile: :support,
channel: :support_tickets,
text: "New high-priority ticket received"
)SlackSender provides deep integration with Axn for building Slack-enabled actions and dedicated notifier classes.
Add Slack messaging capabilities to any Axn action using the :slack strategy:
class Deployments::Finish
include Axn
use :slack, channel: :deployments # Default channel for all slack() calls
expects :deployment, type: Deployment
on_success { slack ":rocket: Deploy finished for `#{deployment.service}`" }
on_failure { slack ":x: Deploy failed for `#{deployment.service}`", channel: :ops_alerts }
def call
# slack() is async (background job) - recommended for fire-and-forget
slack "Finalizing deploy for `#{deployment.service}`..."
# slack!() is sync - use when you need the thread_ts
thread_ts = slack! "Starting rollout..."
# ... rollout / status checks / persistence ...
slack "Rollout complete!", thread_ts: thread_ts
end
enduse :slack, channel: :general # Default channel for all slack() calls
use :slack, channel: :general, profile: :support # Use a specific SlackSender profile
use :slack, channels: [:alerts, :ops] # Default to multiple channels (async only)
use :slack # No default channel (must pass channel: each time)The strategy adds two instance methods for sending Slack messages:
| Method | Delivery | Return Value | Use When |
|---|---|---|---|
slack(...) |
Async (background job) | true or false |
Default; enables auto-retry for rate limits |
slack!(...) |
Sync (immediate) | Thread timestamp or false |
You need the thread_ts return value |
# Async delivery (recommended) - uses Sidekiq or ActiveJob
slack "Hello world"
slack "Hello", channel: :other_channel
# Sync delivery - immediate execution, returns thread_ts
thread_ts = slack! "Starting deployment..."
slack! "Deployment finished", thread_ts: thread_ts
# Full kwargs work with both methods
slack text: "Hello", channel: :ops_alerts, icon_emoji: "robot"
slack! channel: :ops_alerts, blocks: [{ type: "section", text: { type: "mrkdwn", text: "*Bold*" } }]Note: slack(...) requires an async backend to be configured (Sidekiq or ActiveJob). If no async backend is available, it raises SlackSender::Error with instructions to either use slack!(...) or configure an async backend.
For actions whose sole purpose is sending Slack notifications, inherit from SlackSender::Notifier:
# app/slack_notifiers/deployments/finished.rb
module SlackNotifiers
module Deployments
class Finished < SlackSender::Notifier
expects :deployment_id, type: Integer
# Post to the deployments channel for production releases
notify do
channel :deployments
only_if { production_release? }
text { ":rocket: *Deploy finished* for `#{deployment.service}` (#{deployment.environment})" }
end
# Optionally also post in the incident channel if this deploy is related to an incident
notify do
channel :incident_channel_id
only_if { incident_channel_id.present? }
text { ":rocket: *Deploy finished* for `#{deployment.service}` (#{deployment.environment})" }
end
private
def production_release? = deployment.environment.to_s == "production"
# Dynamic channel ID string (e.g., "C123...") sourced from your domain model
def incident_channel_id = deployment.incident_slack_channel_id
def deployment = @deployment ||= Deployment.find(deployment_id)
end
end
end
# Call it like any Axn
SlackNotifiers::Deployments::Finished.call(deployment_id: 123)The notify block groups all Slack message configuration together, keeping it visually separated from Axn declarations like expects:
notify do
channel :notifications # Single channel
text { "Hello!" } # Dynamic text (block)
end
notify do
channels :ops_alerts, :ic # Multiple channels (files uploaded once, shared to all)
only_if { priority == :high } # Conditional send
text :message_text # Text from method
attachments :build_attachments # Attachments from method
endDSL Options:
| Option | Description |
|---|---|
channel :sym |
Single channel (symbol resolved via profile, or method if defined) |
channels :a, :b |
Multiple channels |
text { ... } |
Text content (block evaluated in instance context) |
text :method |
Text from method |
text "static" |
Static text |
blocks { ... } |
Slack blocks |
attachments { ... } |
Slack attachments |
icon_emoji :emoji |
Custom emoji |
thread_ts :method |
Thread timestamp |
files { ... } |
File attachments |
only_if { ... } |
Condition (block) — only send if truthy |
only_if :method |
Condition (method) — only send if truthy |
profile :name |
Use a specific SlackSender profile |
Value Resolution:
For each field, values are resolved in this order:
- Block:
text { "dynamic #{value}" }— evaluated in instance context - Symbol:
text :my_method— calls method if it exists, otherwise treated as literal - Literal:
text "static"— used as-is
Required Fields:
- At least one
channelorchannels - At least one payload field (
text,blocks,attachments, orfiles)
Since SlackSender::Notifier inherits from Axn, you get:
expects/exposesfor input/output contracts- Hooks (
before,after,on_success,on_failure) - Automatic logging and error handling
- Async execution with
call_async
class SlackNotifiers::DailyReport < SlackSender::Notifier
expects :date, type: Date, default: -> { Date.current }
notify do
channel :reports
text { "Daily Report for #{date.strftime('%B %d, %Y')}" }
attachments { [{ color: "good", text: "All systems operational" }] }
end
end
# Sync
SlackNotifiers::DailyReport.call(date: Date.yesterday)
# Async (via Sidekiq or ActiveJob)
SlackNotifiers::DailyReport.call_async(date: Date.yesterday)Configure async backend and other global settings:
SlackSender.configure do |config|
# Set async backend (auto-detects Sidekiq or ActiveJob if available)
config.async_backend = :sidekiq # or :active_job
# Set sandbox mode (affects sandbox channel/user_group redirects)
# Defaults to true in non-production, false in production
config.sandbox_mode = !Rails.env.production?
# Set default sandbox behavior when sandbox_mode is true but profile
# doesn't specify a sandbox.mode or sandbox.channel.replace_with
# Options: :noop (default), :redirect, :passthrough
config.sandbox_default_behavior = :noop
# Enable/disable SlackSender globally
config.enabled = true
# Silence archived channel exceptions (default: false)
config.silence_archived_channel_exceptions = false
end| Option | Type | Default | Description |
|---|---|---|---|
async_backend |
Symbol or nil |
Auto-detected (:sidekiq or :active_job if available) |
Backend for async delivery. Supported: :sidekiq, :active_job |
sandbox_mode |
Boolean or nil |
!Rails.env.production? if Rails available, else true |
Whether app is in sandbox mode (affects sandbox behavior) |
sandbox_default_behavior |
Symbol |
:noop |
Default behavior when in sandbox mode if profile doesn't specify. Options: :noop, :redirect, :passthrough |
enabled |
Boolean |
true |
Global enable/disable flag. When false, call and call! return false without sending |
silence_archived_channel_exceptions |
Boolean |
false |
If true, silently ignores IsArchived errors instead of reporting them |
max_inline_file_size |
Integer |
524_288 (512 KB) |
Max total file size to serialize directly to job payload. Files larger than this are uploaded to Slack first. |
max_async_file_upload_size |
Integer or nil |
26_214_400 (25 MB) |
Max total file size for async uploads. Exceeding raises error immediately. Set to nil to disable (only Slack's 1 GB limit applies). |
| Option | Type | Default | Description |
|---|---|---|---|
token |
String or callable |
Required | Slack Bot User OAuth Token. Can be a proc/lambda for dynamic fetching |
default_channel |
Symbol, String, or nil |
nil |
Default channel to use when no channel is specified in call/call!. Can be a symbol (resolved from channels hash) or a channel ID string |
channels |
Hash |
{} |
Hash mapping symbol keys to channel IDs (e.g., { alerts: 'C123' }) |
user_groups |
Hash |
{} |
Hash mapping symbol keys to user group IDs (e.g., { engineers: 'S123' }) |
slack_client_config |
Hash |
{} |
Additional options passed to Slack::Web::Client constructor |
sandbox |
Hash |
{} |
Sandbox mode configuration (see below) |
| Option | Type | Default | Description |
|---|---|---|---|
behavior |
Symbol or nil |
Inferred (see below) | Explicit sandbox behavior: :redirect, :noop, or :passthrough |
channel.replace_with |
String or nil |
nil |
Channel ID to redirect all messages when behavior is :redirect |
channel.message_prefix |
String or nil |
":construction: _This message would have been sent to %s in production_" |
Custom prefix for sandbox channel redirects. Use %s placeholder for channel name |
user_group.replace_with |
String or nil |
nil |
User group ID to replace all group mentions when in sandbox mode |
When config.sandbox_mode? is true, the effective sandbox behavior is determined by:
- Explicit
sandbox.behavior— if set, use it - Inferred from
sandbox.channel.replace_with— if present, behavior is:redirect - Global default —
config.sandbox_default_behavior(defaults to:noop)
| Behavior | Description |
|---|---|
:redirect |
Redirect messages to sandbox.channel.replace_with (required). Adds message prefix. |
:noop |
Don't send anything. Logs what would have been sent. Returns false. |
:passthrough |
Send to real channel (explicit opt-out of sandbox safety). |
Note: If behavior: :redirect is set but channel.replace_with is not provided, an ArgumentError is raised at profile registration.
Exception notifications to error tracking services (e.g., Honeybadger) are handled via Axn's on_exception handler. Configure it separately:
Axn.configure do |c|
c.on_exception = proc do |e, action:, context:|
Honeybadger.notify(e, context: { axn_context: context })
end
endSee Axn configuration documentation for details.
When config.sandbox_mode? is true (default in non-production), SlackSender applies sandbox behavior based on the profile's sandbox configuration.
Redirect all messages to a sandbox channel:
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
channels: {
production_alerts: 'C9999999999'
},
sandbox: {
behavior: :redirect, # Optional - inferred when channel.replace_with is set
channel: {
replace_with: 'C1234567890',
message_prefix: '🚧 Sandbox redirect from %s' # Optional custom prefix
}
}
)
# In sandbox mode, this goes to C1234567890 with a prefix
SlackSender.call(channel: :production_alerts, text: "Critical alert")Don't send anything, just log what would have been sent:
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
channels: { alerts: 'C999' },
sandbox: { behavior: :noop }
)
# In sandbox mode, this logs the message but doesn't send to Slack
SlackSender.call(channel: :alerts, text: "Test message")
# => Logs: "[SANDBOX NOOP] Profile: default | Channel: <#C999> | Text: Test message"
# => Returns falseIf no sandbox config is provided, the global config.sandbox_default_behavior is used (defaults to :noop).
Explicitly opt out of sandbox safety and send to real channels:
SlackSender.register(
token: ENV['SLACK_BOT_TOKEN'],
channels: { alerts: 'C999' },
sandbox: { behavior: :passthrough }
)
# In sandbox mode, this still sends to the real channel
SlackSender.call(channel: :alerts, text: "This goes to production!")Set the default sandbox behavior for profiles that don't specify a behavior:
SlackSender.configure do |config|
config.sandbox_default_behavior = :noop # :noop, :redirect, or :passthrough
endNote: Setting :redirect as the global default will raise an error at send time if the profile doesn't have sandbox.channel.replace_with configured.
SlackSender automatically handles common Slack API errors by logging warnings and letting Axn's exception flow handle reporting:
- Not In Channel: Logs warning and re-raises (non-retryable)
- Channel Not Found: Logs warning and re-raises (non-retryable)
- Channel Is Archived: Logs warning and re-raises (non-retryable). Can be silently ignored via
config.silence_archived_channel_exceptions = true - Rate Limits: Automatically retries with delay from
Retry-Afterheader (up to 5 retries) - Other Slack API Errors: Logs warning and re-raises
For exception notifications to error tracking services (e.g., Honeybadger), configure Axn's on_exception handler. See Axn configuration documentation for details.
If Sidekiq is available, it's automatically used:
# No configuration needed - auto-detected
SlackSender.call(channel: :ops_alerts, text: "Message")If ActiveJob is available, it can be used:
SlackSender.configure do |config|
config.async_backend = :active_job
endFor synchronous delivery (no background job):
# Returns thread timestamp immediately
thread_ts = SlackSender.call!(
channel: :ops_alerts,
text: "Message"
)Note: Synchronous delivery doesn't include automatic retries for rate limits.
When using async delivery, SlackSender automatically:
- Detects rate limit errors from Slack API responses
- Extracts
Retry-Afterheader value - Schedules retry with appropriate delay
- Retries up to 5 times before giving up
Rate limit handling works with both Sidekiq and ActiveJob backends.
The following errors are not retried (discarded immediately):
NotInChannel- Bot not in channelChannelNotFound- Channel doesn't existIsArchived- Channel is archived (unlesssilence_archived_channel_exceptionsis true)
SlackSender.call(
channel: :deployments,
text: ":rocket: Deploy finished for `my-app` (#{Rails.env})",
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: "*Environment:*\n#{Rails.env}" },
{ type: "mrkdwn", text: "*Version:*\n#{ENV['APP_VERSION']}" }
]
}
]
)SlackSender.call(
channel: :ops_alerts,
text: ":rotating_light: Payment processing error",
attachments: [
{
color: "danger",
fields: [
{ title: "Error", value: error.message, short: false },
{ title: "User", value: user.email, short: true }
]
}
]
)# Generate and send report (synchronous for file upload)
report = generate_daily_report
thread_ts = SlackSender.call!(
channel: :reports,
text: "Daily Report - #{Date.today}",
file: report.to_file
)
# Follow up in thread
SlackSender.call(
channel: :reports,
text: "Summary: no SEV incidents; deploys are healthy",
thread_ts: thread_ts
)A: Check the following:
- Ensure
SlackSender.config.enabledistrue(default) - Verify your profile is registered:
SlackSender.profile(:default) - Check that an async backend is available if using
call(notcall!) - Verify your Slack token is valid and has the required scopes
A: If sandbox channel is configured, all messages are redirected there when in sandbox mode. Check:
SlackSender.config.sandbox_mode?- should betruein development- Your
sandbox.channel.replace_withchannel ID is correct - The bot is invited to the sandbox channel
A: The bot must be invited to the channel. Options:
- Invite the bot to the channel manually
- See: https://stackoverflow.com/a/68475477
A: Your Slack app is missing required OAuth scopes. The error message will tell you which scope is needed:
Slack API missing_scope error: required scope 'files:write' is not granted.
Add this scope to your Slack app at https://api.slack.com/apps and reinstall the app.
To fix:
- Go to https://api.slack.com/apps and select your app
- Navigate to OAuth & Permissions → Bot Token Scopes
- Add the missing scope (e.g.,
files:write) - Reinstall the app to your workspace
See Required Slack Scopes for a complete list of scopes needed for each feature.
A: Slack's file upload APIs require channel IDs, not usernames or channel names:
# ❌ These don't work for file uploads
SlackSender.call!(channel: "@username", file: file)
SlackSender.call!(channel: "#general", file: file)
# ✅ Use channel IDs instead
SlackSender.call!(channel: "C024BE91L", file: file) # Public channel
SlackSender.call!(channel: "D032AC32T", file: file) # DM channelFor DMs, find the DM channel ID (starts with D) from Slack's URL when viewing the conversation.
A: File uploads with async delivery (call) are supported, but have size limits:
- Files smaller than
max_inline_file_size(default 512 KB) are serialized directly to the job - Larger files are uploaded to Slack synchronously, then shared via background job
- Total file size cannot exceed
max_async_file_upload_size(default 25 MB)
If you're hitting the async size limit, either:
- Use
call!for synchronous upload (no size limit except Slack's 1 GB per file) - Increase
config.max_async_file_upload_size(may block web processes longer)
# For large files, use synchronous delivery
SlackSender.call!(channel: :alerts, file: large_file)
# Or increase the async limit
SlackSender.config.max_async_file_upload_size = 100_000_000 # 100 MBA: Set SlackSender.config.enabled = false. All call and call! methods will return false without sending messages.
A: Yes, use channels: (plural) with async delivery:
SlackSender.call(channels: [:alerts, :ops], text: "Broadcast message")Multi-channel is only supported for async (call). Sync (call!) requires sending to each channel individually. Files are uploaded once and shared to all channels efficiently.
A: Yes, register multiple profiles:
SlackSender.register(:workspace1, token: TOKEN1, channels: {...})
SlackSender.register(:workspace2, token: TOKEN2, channels: {...})
SlackSender.profile(:workspace1).call(...)
SlackSender.profile(:workspace2).call(...)A: SlackSender automatically detects rate limit errors and retries with the delay specified in Slack's Retry-After header. Retries happen up to 5 times before giving up.
- Ruby: >= 3.2.1 (uses endless methods from Ruby 3.0+ and literal value omission from 3.1+)
- Dependencies:
axn(0.1.0-alpha.3)slack-ruby-client(latest)
- Optional dependencies:
sidekiq(for async delivery)active_job(for async delivery)active_storage(for ActiveStorage::Attachment file support)
After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
bundle exec rspecBug reports and pull requests are welcome on GitHub at https://github.com/teamshares/slack_sender.
The gem is available as open source under the terms of the MIT License.