ChatSDK Ruby: One Gem, Every Channel

sylvainkalache1 pts0 comments

ChatSDK Ruby: One Gem, Every Channel | Quentin Rousseau

DraculaAbyssNordDefaultClaude

Light<br>Dark<br>System

DraculaAbyssNordDefaultClaude

Light<br>Dark<br>System

At Rootly<br>, we&rsquo;ve been building incident management bots for years. Slack first, then Teams, then Google Chat. Every time a customer asked for a new platform, we&rsquo;d look at the handler code we&rsquo;d already written and think: this logic is identical. The only thing that changes is how the platform wants it formatted.<br>That got old fast.<br>So I built ChatSDK Ruby<br>— a unified SDK that lets you write one bot and deploy it across nine platforms. It&rsquo;s open-source as of today.<br>The thing that finally pushed me<br>Last month, a customer asked for Google Chat support. I opened our Slack bot codebase, looked at the incident acknowledgment handler, the escalation flow, the status card — and realized I was about to copy-paste 400 lines of Ruby into a new file, swap out the API calls, and maintain two parallel implementations forever.<br>I&rsquo;d done this before. Slack to Teams was painful enough. Each platform has its own webhook format, its own card schema, its own way of handling buttons. The bot logic — &ldquo;when someone mentions me, acknowledge the incident and subscribe to the thread&rdquo; — was the same every time. The plumbing was different.<br>I knew about Vercel&rsquo;s Chat SDK<br>for TypeScript. I liked the API design. But we&rsquo;re a Ruby shop, and there was nothing equivalent in the Ruby ecosystem.<br>What it actually looks like<br>Here&rsquo;s the incident bot I described above, working on both Slack and Teams simultaneously:

RUBY

Collapse

Copy

bot = ChatSDK::Chat.new(<br>user_name: "incident-bot",<br>adapters: {<br>slack: ChatSDK::Slack::Adapter.new,<br>teams: ChatSDK::Teams::Adapter.new(<br>app_id: ENV["TEAMS_APP_ID"],<br>app_password: ENV["TEAMS_APP_PASSWORD"]<br>},<br>state: ChatSDK::State::Redis.new(url: ENV["REDIS_URL"])

bot.on_new_mention do |thread, message|<br>thread.post("Incident acknowledged. I'll track updates in this thread.")<br>thread.subscribe<br>end

bot.on_subscribed_message do |thread, message|<br>thread.post("Noted. Added to the timeline.")<br>end<br>Click to expand and view more

Same handler, both platforms. The subscribe call means every follow-up message in that thread gets routed to on_subscribed_message — which is exactly how incident timelines work. Context accumulates in the thread, and the bot follows along.<br>How this was built<br>I&rsquo;ll be honest — this project exists because of Claude Code<br>13 gems. 708 specs. A documentation site<br>. CI pipeline. OIDC publishing for all gems. A Rails demo app. Nine adapters with proper webhook verification, card rendering, and comprehensive tests. I built this in days, not months.<br>The architecture was designed collaboratively. Adapters were generated in parallel by subagents — one agent building the Discord adapter while another was writing the Telegram one. Specs were written alongside the implementation. /simplify passes cleaned up the code after each feature landed.<br>It&rsquo;s still experimental — there are edge cases I haven&rsquo;t hit yet and platform quirks waiting to be discovered. That&rsquo;s why it&rsquo;s marked beta. But the sheer volume — nine platforms, each with their own webhook format, card schema, event structure, and auth mechanism — would have been months of solo work without AI. I know because I&rsquo;ve done this work manually before at Rootly, one platform at a time.<br>The cards DSL is the thing I&rsquo;m most proud of<br>This is where the abstraction really earns its keep. Slack uses Block Kit. Teams uses Adaptive Cards. Google Chat uses Card V2. Discord uses embeds. Telegram uses inline keyboards. Five completely different JSON formats expressing the same concepts: title, fields, buttons.<br>ChatSDK has a block-based DSL that compiles to all of them:

RUBY

Collapse

Copy

card = ChatSDK.card(title: "Incident INC-2847") do<br>text "Payment processing is degraded. Error rate at 12%."<br>fields do<br>field "Severity", "SEV-1"<br>field "Status", "Investigating"<br>field "Commander", "Sarah Chen"<br>field "Started", "14:23 UTC"<br>end<br>actions do<br>button "Acknowledge", id: "ack"<br>button "Escalate", id: "escalate"<br>link_button "Status Page", url: "https://status.example.com"<br>end<br>end

thread.post(card)<br>Click to expand and view more

One Ruby object. Nine renderers. Each produces the platform&rsquo;s native card format. No one sees generic markup — Slack users get Block Kit, Teams users get Adaptive Cards, Discord users get embeds. And when someone clicks &ldquo;Acknowledge&rdquo;:

RUBY

Collapse

Copy

bot.on_action("ack") do |event|<br>event.thread.post("Acknowledged by #{event.user.name}")<br>end<br>Click to expand and view more

Block Kit interactive payloads, Adaptive Card action submits, Discord button clicks, Telegram callback queries — they all arrive as the same Action event with the same action_id. I don&rsquo;t have to care where the click came from.<br>Streaming AI summaries<br>We&rsquo;re increasingly using LLMs in incident workflows...

rsquo thread ruby card chatsdk incident

Related Articles