LaunchKit

Convention over configuration is a context argument now

September 24, 2026

Convention over configuration has been sold as a typing argument since 2004. You do not write the table name, you do not write the foreign key, you do not write the mapping, and the framework guesses right often enough that guessing is the default. That pitch is now the second-best reason to keep the conventions, and the better one showed up when something other than a person started writing the implementation.

What convention over configuration was arguing before agents

Rails ships opinions about names, and app/models/oauth_identity.rb in this boilerplate is twenty lines that demonstrate all of them at once. The class is OauthIdentity, so the table is oauth_identities, because the inflector pluralises the demodulised class name. The model declares belongs_to :user, so the column it reads is user_id, because that is how Active Record spells a foreign key. The migration adds an index on that column, so the index is called index_oauth_identities_on_user_id, because that is how Rails builds an index name when nobody supplies one.

None of those three facts appears in the model file. The file holds two validations and a belongs_to, and the mapping is absent because the mapping is derived.

The argument for working that way was always about what you do not type, and the argument against it was always that the rules are invisible. A developer joining a Rails codebase has to learn the inflector before the code reads as code, and until they do, half the file is missing as far as they are concerned. Both halves of that trade assumed a person at the keyboard: the saving was a person's keystrokes, and the cost was a person's onboarding.

Replace the person at the keyboard and only one half survives, because a model that has read a million Rails apps did its onboarding already.

The same rule read as context

An agent asked to add an expiry column to the OAuth identity has four things to settle before it writes a line: which file holds the model, where the migration goes, what the table is called, and where the test belongs. In a conventional Rails 8 app all four fall out of the words "OAuth identity" and nothing else. The model is at app/models/oauth_identity.rb. The migration goes in db/migrate with a timestamp prefix. The table is oauth_identities. The spec is at spec/models/oauth_identity_spec.rb, and if the project uses Minitest instead it is at test/models/oauth_identity_test.rb, which is itself a convention with two known values rather than an open question.

What makes that useful is not that the agent is clever about it. It is that the four answers are not in the repository at all, so there is nothing to search, nothing to read into a context window, and nothing to get subtly stale. A convention is context that costs no tokens because it was absorbed before the session started.

That is the same claim the @rails account now makes commercially, from the other end: "Ruby on Rails scales from PROMPT to IPO. Token-efficient code that's easy for agents to write and beautiful for humans to review." The tokens a Rails app does not spend is where that side of it gets counted rather than asserted.

What one generator command actually hands over

Generators are the part of the convention that is executable, which makes them the easiest part to check. Run the scaffold generator against this boilerplate in pretend mode and it reports exactly what it would write:

$ bin/rails generate scaffold Widget name:string price:decimal --pretend
      create    db/migrate/20260924142014_create_widgets.rb
      create    app/models/widget.rb
      create      spec/models/widget_spec.rb
      create        spec/factories/widgets.rb
       route    resources :widgets
      create    app/controllers/widgets_controller.rb
      create      app/views/widgets/index.html.erb
      ...

22 files and one line appended to config/routes.rb. Some of that count belongs to this repository rather than to Rails: the four view specs, the routing spec and the factory come from rspec-rails and factory_bot being installed, and a stock rails new produces a shorter list with Minitest files instead. The shape does not change with the test framework, though. Model, migration, controller, six views, helper, route, tests, all named after one word.

An agent that has to add a second resource to this application does not need to be shown the first one. Reading app/controllers/widgets_controller.rb would tell it what seven actions look like here, and it already knew, which is why so much agent-written Rails lands correct on the first attempt and why so little of it is interesting. Testing what an agent wrote takes up the part of that sentence which should worry you: generated tests inherit the same conventions, so a test that was written from the shape rather than from the requirement passes for the wrong reason.

Seven routes nobody wrote down

RESTful routing is the convention that pays most often, because a path helper is the thing an agent has to name in code it cannot run yet. One declaration in config/routes.rb produces this:

$ bin/rails routes -g ai_templates
             admin_ai_templates GET    /admin/ai_templates              admin/ai_templates#index
                                POST   /admin/ai_templates              admin/ai_templates#create
          new_admin_ai_template GET    /admin/ai_templates/new          admin/ai_templates#new
         edit_admin_ai_template GET    /admin/ai_templates/:id/edit     admin/ai_templates#edit
              admin_ai_template GET    /admin/ai_templates/:id          admin/ai_templates#show
                                PATCH  /admin/ai_templates/:id          admin/ai_templates#update
                                DELETE /admin/ai_templates/:id          admin/ai_templates#destroy

resources :ai_templates inside a namespace :admin block, and the seven rows above are the whole of what was declared. The real output carries (.:format) on every pattern and a PUT alias beside the PATCH, both cut here for width. The helper names follow from the resource name and the namespace by a rule with no exceptions in it, so edit_admin_ai_template_path(template) can be written by something that has never opened the routes file.

The extra three routes this application declares on the same resource, test_admin_ai_templates, rollback_admin_ai_template and toggle_listed_admin_ai_template, are the counterexample sitting in the same output. Those came from three post lines carrying on: :member or on: :collection, they are not derivable from anything, and they are the three an agent will miss. A resource with two custom member routes is still mostly conventional; a resource with eleven is a controller pretending to be RESTful, and every one of those eleven is a sentence somebody now has to write down.

The codebase that invented its own structure

This site is the counterexample, which is convenient, because it means the cost can be listed rather than imagined. The page you are reading is not a database row. It is app/content/rails_agents/conventions-are-the-context.md, read by RailsAgents::Repository, and an agent told to "add an article to the rails-agents cocoon" will reach for a migration and a model, because that is what adding content to a Rails app usually means.

Here is what it has to be told instead, and every line is a real rule in this repository:

  • Content is Markdown on disk under app/content/<tree>/, with YAML front matter, and there is no table behind it.
  • RailsAgents::Repository.articles globs the directory and rejects any basename starting with an underscore, which is the only thing keeping _pillar.md out of the article list.
  • An article with a published_on in the future parses fine and is then dropped by select(&:published?), so a committed file can render nowhere and return a 404 with no error anywhere.
  • A slug reaching the filesystem is matched against /\A[a-z0-9]+(-[a-z0-9]+)*\z/ first, so a file named with an underscore or a capital is permanently unreachable.
  • The route is get "/rails-agents/:slug", not resources, so no REST helper name applies and the path helper is rails_agents_article_path.

Five sentences, and they are not unreasonable sentences. The point is that they exist at all, that somebody wrote them, and that they have to stay true through every future change to the repository or they become actively misleading. In a conventional Rails app that list is empty and stays empty for free. AGENTS.md for a Rails app is where a list like this one is supposed to live, and the length of that file is the most honest measurement of how unconventional a codebase has decided to be.

Where the convention stops being derivable

Autoloading is the place this argument gets its nose bloodied, and it is worth knowing before you lean on it. The Rails guide states the rule plainly: the autoload paths "consist of all the subdirectories of app that exist when the application boots, except for assets, javascript, and views". Each of those is a root, and a root maps to Object rather than to a namespace.

Two file paths resolved to constants, drawn as two lanes that split at the autoload root. In the top lane app/services is a root, so it maps to Object and contributes nothing, and markdown_renderer.rb sitting directly in it must define MarkdownRenderer with no Services prefix. In the bottom lane lib is the root added by autoload_lib, launchkit sits underneath it and therefore does namespace, so doctor.rb must define Launchkit::Doctor. Reasoning from the directory name instead of from the root gets Zeitwerk 2.8.3 raising expected file to define constant, but didn't, which is a boot failure.

The consequences are asymmetric enough to trip anything reasoning from the directory names:

File Constant it must define
app/services/markdown_renderer.rb MarkdownRenderer
app/forms/onboarding/base_form.rb Onboarding::BaseForm
lib/launchkit/doctor.rb Launchkit::Doctor

app/services contributes nothing to the constant, because it is a root. lib is also a root, added by config.autoload_lib(ignore: %w[assets tasks generators]) in config/application.rb, but launchkit sits underneath it and therefore does namespace. Same-looking directory, opposite behaviour, and the difference is invisible unless you already know which of the two is the root.

Getting it wrong is loud rather than subtle, at least. Zeitwerk 2.8.3 raises from lib/zeitwerk/loader/callbacks.rb with expected file #{file} to define constant #{cref}, but didn't, and in production that is a boot failure rather than a deploy that half works. Loud beats silent. It is still a convention that has to be stated, in a section of a file, in exactly the way this page claims conventions let you avoid, and pretending otherwise would be the kind of spin-shaped page that convinces nobody who has actually shipped Rails.

The position this site is taking

If an agent writes the implementation, the scarce thing is a codebase the agent can act in without guessing: conventions it already knows, tests that fail when it is wrong, and a starting point where the boring decisions are made. That is a description of a boilerplate, and it is why this site does not read the news as bad for it.

DHH has made the first half of that argument himself, on X: "Convention over configuration set the path for 20+ years of great training data for AI to use today. Not only does this mean agents do great with Rails, but also that squishy humans can quickly and confidently review the output without a jungle of distracting boilerplate."

Now the part a page selling something would leave out. At Rails World 2026 the creator of Rails told a room that "writing code by hand is no longer an economically viable skill for most programmers at most companies", and the flagship product he is shipping next, HEY 2.0, is native apps on every platform with a Rust backend. Not a bigger Rails app. A Rust one, from the person who posts "I love Rust! Rust is amazing ...if you never, ever, EVER have to look at it yourself." What DHH actually said has those quotes with their sources. Rails as good context for agents and Rails as the thing you build in are two separate claims, and he advanced the first while visibly acting on something else for the second.

None of that endorses buying anything, and reading it as one would be dishonest. Our own position is narrower and has a cost in it: agents writing the implementation makes the implementation half of a boilerplate worth less, not more. A capable model can generate authentication. What it generates less reliably is the decisions around it, which schema, which gem, which failure modes were considered, and which of those a test would go red for. Anyone selling you the typing is selling you the part that just got cheap.

What would change this position

Three things would, and two of them are measurable inside a year.

The first is the length of AGENTS.md files over time. This argument assumes conventions save context that would otherwise be written down. If projects find their agent instruction files getting shorter regardless of how conventional the codebase is, models have gotten good enough at reading arbitrary structure that conventions stopped paying, and the argument is dead. Watch that file, not the benchmarks.

The second is the review step. Half of DHH's own case for conventions is that humans can review the output confidently, which only matters while a human reviews. If review is genuinely delegated, the "beautiful for humans to review" clause drops out and the case narrows to token efficiency alone, which is a real but much smaller claim. Reviewing Rails an agent wrote is written on the assumption that the review step survives, and that assumption is an assumption.

The third is not measurable and matters most: whether Rails is the right answer for the application you are actually building. Everything above is an argument about what makes a Rails codebase work well with agents, and it has nothing to say about whether you should have a Rails codebase. When Rails is still the answer takes that question seriously, including the cases where the answer is no. José Valim published on how programming languages should evolve in this era the same week as the keynote, which is a sign the question is live in more than one community, and we have not read it closely enough to tell you what he argues.

Keep reading

← All of Rails and agents