💎 ChatSDK Ruby

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
end

Routes

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"
end

Set 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
end

Using 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
end

Background 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
  )
end
class 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
end

Background 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.

On this page