Rails Integration
Integrate ChatSDK with Rails using initializers, routes, credentials, and background jobs.
ChatSDK integrates cleanly with Rails. This guide covers the recommended setup.
Initializer
Create an initializer that configures your bot as a singleton:
# config/initializers/chat_bot.rb
require "chat_sdk"
require "chat_sdk/slack"
module ChatBot
def self.instance
@instance ||= build
end
private_class_method def self.build
slack = ChatSDK::Slack::Adapter.new(
bot_token: Rails.application.credentials.dig(:slack, :bot_token),
signing_secret: Rails.application.credentials.dig(:slack, :signing_secret)
)
state = if Rails.env.production?
require "chat_sdk-state-redis"
ChatSDK::State::Redis.new(url: ENV["REDIS_URL"])
else
ChatSDK::State::Memory.new
end
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state,
logger: Rails.logger,
log_level: Rails.env.production? ? :info : :debug
)
# Register handlers
bot.on_new_mention do |thread, message|
thread.post("Hello from Rails, #{message.author.name}!")
end
bot.on_action("approve_btn") do |event|
event.thread.post("Approved!")
end
bot
end
endRoutes
Mount the webhook endpoint in your routes file:
# config/routes.rb
Rails.application.routes.draw do
mount ChatBot.instance.webhooks[:slack] => "/webhooks/slack"
# Or, if using multiple adapters:
# mount ChatBot.instance.webhooks.router => "/webhooks"
endSet your Slack app's Event Subscriptions Request URL to https://your-app.com/webhooks/slack.
Multi-Adapter Setup
If your bot talks to multiple platforms:
# config/initializers/chat_bot.rb
slack = ChatSDK::Slack::Adapter.new
teams = ChatSDK::Teams::Adapter.new
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack, teams: teams },
state: ChatSDK::State::Redis.new
)# config/routes.rb
Rails.application.routes.draw do
mount ChatBot.instance.webhooks.router => "/webhooks"
# Handles /webhooks/slack and /webhooks/teams
endUsing Rails Credentials
Store secrets in Rails encrypted credentials instead of environment variables:
# config/credentials.yml.enc (decrypted)
slack:
bot_token: xoxb-...
signing_secret: abc123...slack = ChatSDK::Slack::Adapter.new(
bot_token: Rails.application.credentials.dig(:slack, :bot_token),
signing_secret: Rails.application.credentials.dig(:slack, :signing_secret)
)Proactive Messages
To send messages from controllers, jobs, or other non-handler code:
class DeployController < ApplicationController
def create
bot = ChatBot.instance
channel = bot.channel(params[:channel_id], adapter_name: :slack)
channel.post("Deploy started by #{current_user.name}")
head :ok
end
endBackground Processing
By default, event handlers run synchronously in the webhook request. For long-running work, enqueue a job:
bot.on_new_mention do |thread, message|
thread.post("Working on it...")
GenerateReportJob.perform_later(
channel_id: message.channel_id,
thread_id: message.thread_id,
text: message.text
)
endclass GenerateReportJob < ApplicationJob
def perform(channel_id:, thread_id:, text:)
bot = ChatBot.instance
channel = bot.channel(channel_id, adapter_name: :slack)
thread = channel.thread(thread_id)
result = ExpensiveService.call(text)
thread.post(result)
end
endBackground handler execution
Use handler_executor to move the full handler pipeline out of the webhook request. This helps when handlers call an LLM or do other work that can exceed a platform's acknowledgement window. Slack expects an HTTP 2xx response within 3 seconds. Teams can retry activities that take more than 15 seconds, and Google Chat allows up to 30 seconds for a synchronous response (Slack Events API, Teams activity handlers, Google Chat interactions).
With a callable executor, the webhook passes a JSON-safe payload to your queue. The worker must call dispatch_serialized to load the event and run the normal dispatch pipeline:
class ChatSdkDispatchJob < ApplicationJob
queue_as :default
def perform(payload)
ChatBot.instance.dispatch_serialized(payload)
end
end
# In the ChatSDK::Chat initializer:
handler_executor: ->(payload) { ChatSdkDispatchJob.perform_later(payload) }If the callable raises, ChatSDK raises ChatSDK::HandlerExecutorError and the webhook returns 500 so the platform redelivers; worker deduplication makes that safe.
Deduplication, locks, and concurrency strategies run in the worker. Use a persistent state backend shared by the web and worker processes, such as Redis, PostgreSQL, or MySQL. State::Memory cannot share deduplication or lock state across processes. Webhook retries can enqueue duplicate jobs; the worker deduplicates them before handlers run.
The :thread executor runs in the current process and can lose work if that process exits. In Rails, raw threads do not run inside the Rails executor. If handlers access Active Record, wrap dispatch_serialized in Rails.application.executor.wrap in your own worker, or use the Active Job approach above.
Interactions that use a short-lived trigger_id, such as opening a modal from an action or slash command, must reach the relevant API quickly in the worker. Keep handler_executor: :inline in tests that need handlers to run synchronously.