“You don’t need frameworks anymore.”
“Just let the agent write it. It’s one prompt away. Why would you depend on somebody’s gem?”
How many of you heard this this year? Yeah.
And it’s spreading. My friend Andrzej Krzywda goes to every conference and talks to everybody, and a few weeks ago he wrote that 40% of devs try to reinvent RubyLLM.
Forty percent! That’s one person’s impression. But the idea is in the air. You can feel it.
So here’s what I think about that, dear Ruby developers.
Bullshit.
Frameworks matter. And I won’t just say it. I tested it.
Same Agent. Same App. #
I took a coding agent, Codex on GPT-6 Astra, a really good one. And I gave it the same app to build four times.
With RubyLLM on Rails. With LangChain. With the Vercel AI SDK. And with no framework at all, plain Ruby, “just let the agent write it”.
A real chat app. Streaming, conversations, cancel, cost. A second provider you can switch to in the middle of a conversation. Fallbacks. Surviving a restart. Approvals. Files and a sandbox. MCP. Then Gemini, Azure, Bedrock.
Eleven steps. And after every step, a test suite reruns everything, on every provider. 198 checks at the end.
Here’s what I learned.
The agent writes it. Streaming on Bedrock? Done. Tools on Gemini? Done. Honestly, they’re amazing at it.
Then you own it. Every feature, on every provider.
Every provider streams differently. Calls tools differently. Caches differently. Counts tokens differently. Forty features. Twenty-three providers.
Want to see one feature for one provider? With no framework, the agent signed every Bedrock request by hand, AWS signature version 4. Then it decoded Amazon’s binary event stream by hand, frame by frame, checking the CRCs. Ninety-three lines. For one provider. It works.
You own this now. Forever.
And with RubyLLM:
config.bedrock_api_key = ENV["AWS_ACCESS_KEY_ID"]
config.bedrock_secret_key = ENV["AWS_SECRET_ACCESS_KEY"]
config.bedrock_region = ENV["AWS_REGION"]
config.bedrock_api_base = ENV["BEDROCK_BASE_URL"]
Four lines of configuration. Same agent wrote them. And then Bedrock does approvals, fallbacks, caching, cancel, everything the app already did.
So frameworks matter. Case closed?
Not quite.
Frameworks Matter. Only If Agents Can Use Them Well. #
An agent takes the easiest path it can see. If it can’t see your framework’s way, it writes its own. Then your framework is dead weight: a dependency, and the agent’s own code on top.
And that’s exactly what happened in my first runs. RubyLLM installed, my own skill loaded, and the agent still:
- Wrote its own price table. With the wrong price! RubyLLM keeps a usage ledger, and
chat.costreads it. - Patched the gem to count usage. RubyLLM already sends an event for every call.
- Rebuilt HTTP cancellation by hand. RubyLLM has
chat.cancel. - Read 280 thousand tokens of my source code. To find things the guides already said.
The agent isn’t dumb. It couldn’t see the RubyLLM way from where it stood. So I showed it.
The Loop #
Agents build the same app, on RubyLLM main.
I watch where they stumble. Or where they write code they shouldn’t need.
I fix it in RubyLLM. The gem, a generator, the docs, or the skill.
Then I run it again.
And again. Twenty-one fixes.
And it found more than stumbles. It found real bugs:
Sandbox couldn’t see your filesAgent refused another provider’s modelStale cancel killed the next replyOne huge tool result broke the chatNo caching on Anthropic, BedrockRefresh lost messagesDead worker held its chat
If you had written your own, you would have every one of these bugs right now, and you wouldn’t even know.
Built for Coding Agents #
The new user of a framework is a coding agent. So I spent a ton of time on that this cycle. As much as on features.
RubyLLM ships a skill, inside the gem, so it matches your version. And to install any skill just using Ruby, I made a new gem, skill:
gem exec skill add "$(bundle show ruby_llm)" --skill rubyllm --agents coding
One command, and Claude Code, Codex, Cursor know the RubyLLM way. It works like npx skills. But it also adds skills to RubyLLM itself, in your Rails app, with a skills.lock. Gemfile.lock, for skills.
The docs are written so an agent finds the answer. llms.txt, Markdown for every page.
And the API stays small on purpose. One verb per meaning. Every public method is in api.txt, and CI fails when it changes without me. Every extra name is a wrong guess an agent can make.
And every fix goes out to everyone. One bundle update. That’s what a framework is.
Let’s Count #
Code you own at the end. Lines the agent wrote itself, averaged across its attempts. Tests don’t count. Generated code doesn’t count.
| Lines | |
|---|---|
| RubyLLM on Rails | 246 |
| Vercel AI SDK | 748 |
| LangChain | 962 |
| No framework | 1,070 |
And every provider you add? Lines that speak one provider’s format: RubyLLM, zero. Without a framework, up to 97. Every. Single. Provider.
And yes, I tuned RubyLLM against this benchmark. On purpose. So here’s the real test.
A second app. A support desk, with documents, a sandbox, MCP tools and approvals. No agent saw it while I tuned anything.
About 420 lines of app code with RubyLLM. 867 without a framework. Half the code.
Same Bill. Half the Code. #
Now let me be honest with you, because you’d find out anyway.
It wasn’t cheaper. On the chat app it cost less, about $12 against $17 or more in earlier runs. On the support desk it cost more, between $14 and $19 against $13. Same bill, give or take. The agent still reads a lot, and you pay for every read. The new skill in 2.2 already cut the chat app’s bill by about a quarter, and I’m not done. With Claude Opus it was 1,088 lines against 2,969, and the RubyLLM run cost more, $19 against $11. I’m not going to stand here and lie to you about it.
One model at a time, one task each, a handful of attempts, fake providers in the tests. Take it for what it is.
Half the code.
And think about what happens after the agent is done.
The Agent Leaves. The Code Stays. #
After you’ve used it, the agent is out! The accountability of that code stays with you. A thousand lines of it, in your app.
And providers change every week. A new model, a new parameter, an error format that changes overnight. Who fixes your thousand lines? You do. Forever.
With a framework, somebody else fixes it, and everybody gets the fix.
And here’s what’s in this one now.
RubyLLM 2.2 #
Tools ask a person. Window or aisle? The call s, no process sits there waiting, and when they answer, execute runs again from the top. Ask first, then act.
class BookFlight < RubyLLM::Tool
description "Books a flight"
parameter :destination, description: "The city to fly to"
def execute(destination:)
seat = ask("Window or aisle?", choices: %w[window aisle]) or return "No seat chosen."
Flights.book!(destination, seat:)
end
end
And from any process, three verbs. Answer, approve, decline.
question = chat.pending_questions.first
question.answer "window"
question.approve remember: true
question.decline "I'll choose at the gate"
chat.complete
Let me show you the rest, one at a time.
A chat survives a deploy. One process runs it at a time, so a double click never runs a tool twice. Deploy in the middle of a reply? Chat.interrupted finds it, and the generated ResumeInterruptedChatsJob picks it up as soon as the server boots. A tool call that had started never runs again: the model hears it may have partly run, and decides.
Code in a sandbox, on your own servers. That’s thanks to mxc, my new gem. It’s Microsoft’s MXC, from Ruby, and improved. The operating system’s own isolation. The files it makes come back as attachments.
chat = RubyLLM.chat.with_code_execution(python: %w[pandas matplotlib])
chat.ask "Plot monthly revenue from this export", with: "revenue.csv"
File stores. That 40-page PDF doesn’t travel with every question anymore. The model reads it with tools, like a coding agent does.
chat = RubyLLM.chat.with_file_store
Context budgets. One giant tool result can’t break the chat.
chat.with_context_budget(per_result: 0.25, per_request: 0.7)
Skills. Your workflows, in folders, loaded only when a task needs them. In the open format every coding agent speaks.
Tests, no provider. Script what the model says, and run your real agents and pages.
stub_replies tool_call(:weather, city: "Berlin"), "It is sunny.", cost: 0.01
A generator for every building block. Agents, tools, schemas, MCP servers, skills, sandboxes, evaluations, judges. And the chat UI, with a model switcher on every message.
Twenty-three providers. 518 built in, every one checked against each provider’s own docs. One API.
There’s a lot more in What’s New in 2.2.
And where do all these features come from? I didn’t sit in a room and invent them.
Rails Came from Basecamp. RubyLLM Comes from Chat with Work. #
DHH didn’t design a web framework. He built Basecamp and pulled out what kept working.
Same for me. Everything new in 2.2 earned its place in a real product. And I think what we’ve built is amazing.
Chat with Work. AI bots that work for you. They work. You decide.
A bot starts on an event or a time, and works across your tools. What needs you comes to one Decisions dashboard. Changes ask first, unless you let them run. And every one of those chats is rows in my database. My tables, my backups, my queries. The application owns the conversation.
This Is Our Time. #
Python had the training. JavaScript had the browser. But building AI products? Products people pay for, with approvals and billing and a database you own? That’s what Rails was made for.
And now we have the framework for the AI part too.
RubyLLM 2.2. Out today.
bundle update ruby_llm
bin/rails generate ruby_llm:upgrade
bin/rails db:migrate
Still on 1.16? One release at a time. The upgrade guide has every step.
Try it, and send me what you find.
Frameworks matter.