Teaching AI Agents and Human Contributors Through Custom Rails Generators

Open on YouTube ↗
Overview

Rachael Wright-Munn is one of four maintainers of RubyEvents.org, an open-source application that indexes Ruby community events. Her Rails World 2026 talk asks how to teach both AI agents and a growing pool of more than 180 contributors to work with a codebase they don't know. Her answer is custom Rails generators, paired with an AI "skill" and later exposed as MCP tools. The talk has three parts: a walkthrough of building a generator from scratch, practical fixes for generator tests, and a benchmark comparing AI approaches across models.

16 min read
0:23

Why RubyEvents Stores Its Data in YAML

RubyEvents describes each event in YAML files. Wright-Munn showed the full set of YAML needed to describe Rails World 2026 and gave four reasons for the format. It is an inherited architectural decision. It lets the maintainers review event changes for spam. It lets any developer update events in the index. Most importantly, if the app goes offline or "is acquired by Meetup.com," the data still persists as part of the Ruby community's historical record.

The format creates a technical problem: every data change requires a commit. Any automation needed a tool that could turn unstructured information into commits, which she described as a hard proposition before AI. Her current workflow uses GitHub Copilot, which turns an issue into a pull request. She can see an event update while scrolling Bluesky, start an agent session, and get a PR she can merge. She said this is how the Rails World 2026 CFP got into RubyEvents.

2:04

What Went Wrong When AI Didn't Know the System

That workflow did not work at first. In her words, the AI was "super smart, but it doesn't know our system." It invented keys. It used old or deprecated formats, such as an event_name key that RubyEvents only uses for meetups. It left out to-do comments.

She explained the to-do problem with the videos file, which goes through three phases: when speakers are announced, when the schedule is released, and when the videos are published. Agents struggle with intermediate states like these because they mostly see finished examples. The AI also tended to skip useful keys such as cancelled, or original_title, which is used for talks given in other languages. It also spent many tokens trying to understand examples.

She said contributors have the same problems. They know when events update and have data the project needs, but they mistype keys and spend a long time decoding examples. That led to her central question: how to teach both groups about RubyEvents.

3:18

Choosing Skill Plus CLI Over MCP

For AI, she called the problem "kind of a solved problem," with two options. An MCP is an AI-native SDK that fetches context or executes commands for a model. A skill is Markdown documentation that AI can discover, paired with an ordinary CLI.

She acknowledged that AI models prefer MCP tooling and tend to reach for it. A skill plus CLI, though, works for everyone: anyone can read Markdown and anyone can run a CLI. She also mentioned a common argument in the debate, that describing MCP tools consumes context, and context means tokens and money. She chose skill plus CLI, partly for those built-in benefits and partly because it supports the 180+ contributors, and she knows not all of them have tokens to spend.

4:23

Why Rails Generators

Rails generators were the natural CLI format. Most Rails developers have run one to create a migration or scaffold an endpoint. Their biggest benefit, she said, was that she didn't have to make the usual design decisions herself. Arguments and options, templating and file generation, documentation, testing, and file conflicts were all already handled. She noted that file conflicts weren't even on her radar when she started. That freed her to focus on the actual tooling. Rails didn't have to make those decisions from scratch either, because Rails generators are built on Thor, a toolkit for building CLI applications.

5:22

Generating a Generator: NamedBase vs. Base

Her running example was a generator that produces the YAML file describing an event's CFP. She started by running the generator that generates generators. It produces a CfpGenerator class, a USAGE file, and a CfpGeneratorTest.

The generated class inherits from NamedBase, which explains how the name "CFP" is threaded through every file. NamedBase takes one argument, defined as argument :name, type: :string, and provides many helper methods that output different forms of the name. She looked at how Rails defines them and found they are mostly string interpolation.

She didn't need any of that. She already knew where her file should go, and she didn't need the name helpers. So she switched to Base. Her summary: NamedBase accepts a single name argument, provides many helper methods derived from it, and inherits from Base. Base has none of those features, and she picked it.

7:00

Arguments vs. Options

A CLI can take input through arguments or options. An argument is declared like argument :event, type: :string, desc: "event slug" and invoked as bin/rails g cfp rails-world-2026. Arguments can be required or have defaults, and they depend on order, so she compared them to positional method arguments.

Options are declared almost the same way with class_option, but the value is passed with a flag such as --event. They can be required, defaulted, or optional, and they don't depend on order. She compared them to keyword arguments.

She chose options. She felt they make it easier for contributors and agents to avoid ordering mistakes, and they give her more flexibility over which values to pass or leave out. She added all the class options needed to describe a CFP. Some extras, such as aliases and banners, she had no time to cover.

8:11

Auto-Generated Help and the USAGE File as Context

Class options do something else she found very useful. Running help on a generator shows documentation, and she admitted she didn't know this existed before the project. The options section of that help output is generated from the class options and updates whenever they change.

The contents of the USAGE file are appended to the end of the help output. She uses this to inject context for AI, because, as she put it, AI is reluctant to read documentation and files she references but is pretty good at running commands.

She showed the current USAGE file for the RubyEvents CFP generator. It has a minimal example, an example for an ongoing meetup CFP (meetups often have no end date because the CFP stays open), an example for lightning talks, and an example for updating or extending an existing CFP, since changing states are hard for AI to capture. All of this context loads automatically when the agent calls help. She joked that humans struggle to read documentation too, but since she herself didn't know this help existed, she isn't sure how much it helps them.

10:11

Templates and File Creation

To build the template, she pasted the real Rails World 2026 CFP YAML into a .tt file. TT stands for Thor template, and these templates use regular ERB, so she replaced each value with ERB tags reading from the options. She noted that you can also customize the templates of the default Rails generators by placing replacements in lib/templates, as described in section six of the Rails generators guide. Most of those generators use NamedBase, so the name helpers are available there.

She added two methods to the generator: cfp_file_path and create_cfp_file. The second calls template with the template file and a destination. The template is found through source_root File.expand_path("templates", __dir__), which the generator-generator added at the top of the class. She called this the kind of boilerplate generators are good at capturing. She wrote the destination herself. RubyEvents stores CFPs under data/<event series>/<event>, combined with destination_root, which Rails passes in and which defaults to the directory where the generator was called. Running the generator produced a working CFP file with all the values filled in.

12:31

File Conflicts, Conditionals, and Whitespace

Next she ran the generator without start and end dates to simulate an ongoing meetup CFP. A cfp.yml already existed, so she hit a file conflict. template handles conflicts natively and prompted her, and she chose to overwrite.

The output then failed validation. In RubyEvents, if open and close dates are present, they must have values and be formatted as dates. She wrapped those fields in ERB conditionals such as if options[:open_date]. That passed validation but left stray blank lines at the bottom. With HTML ERB, the browser hides that whitespace. In YAML it is visible. She switched to ERB's minus tags to trim it, which left only one extra line at the end.

14:03

Testing Generators and the Flakiness Trap

With conditional logic in the template, she wanted tests. Uncommenting the generated test and running it produced a green dot, but the run also reported that no value was provided for the required event option. run_generator takes an array of the same values you would pass on the command line, and she likes to pair each flag with its value on one line for readability. Then she added assert_file, a Rails test helper, with the file path and a content check. She said comparing against a regex is a common pattern.

"Green dots don't inspire much confidence," she said. She found the generated file still sitting in tmp/generators after the run. That raised a question: if the file persists, why didn't the test hit a file conflict? The default test sets destination to tmp/generators, and her cfp_file_path uses destination_root, which explains the location. setup :prepare_destination does the cleanup by deleting the entire destination root and recreating it.

That made her realize that with parallel test cases, a shared directory being deleted and recreated would be flaky. If you just uncomment the default test and move on, she said, you might eventually end up with flaky generator tests. Her recommendation is to set the class's destination to a Ruby temp directory and clean it up in teardown instead of using prepare_destination. RubyEvents has used this approach for a while with no issues.

17:01

Debugging Gotcha: Captured Output

With a temp directory she could no longer inspect the file directly, so she dropped in a binding to look at it. The debugger seemed to swallow her session: no green dot, no visible prompt. Control-C didn't work, and she eventually escaped with quit plus Enter, after which the IRB output appeared. The test framework was capturing the debugger's output. According to section 10 of the Rails guide on testing generators, you need to set RAILS_LOG_TO_STDOUT=true for debugging tools to work.

She explained why this isn't the default: if you run the full RubyEvents suite without the output captured, the results get very messy. Her testing roundup has three points:

  • Use RAILS_LOG_TO_STDOUT=true when you need to see output.
  • Replace the destination root with a temp directory.
  • Don't use prepare_destination.

For further learning she recommended Garrett Dimon's blog, the Rails guides, the RubyEvents generators themselves, and the generators in Rails, both open source, to see their testing patterns.

19:23

Writing the Skill

The skill half is simple. It is front matter describing when to use the skill, followed by regular Markdown. She includes important notes about things AI commonly gets wrong, stressing that RubyEvents is an index and archive, so accurate descriptions and data matter a lot. She adds to-do list items, which "tend to get loaded into the workflow, you know, if you're lucky." The generator section tells the agent to run bin/rails g cfp --help to load the full context, gives a sample command, and asks it to take a screenshot.

20:17

Adding MCP: Generators as Tools

Having built skill plus CLI, she joked about whether "or MCP" could become "and MCP." At RubyConf, Andy gave a talk about abusing schemas with metaprogramming, showing how to use an OpenAI schema to generate a RubyLLM MCP tool. She paired with him to build MCP tooling for the generators, because the detailed class options already describe everything an MCP tool needs. She said the full story is on the Ruby on Rails Podcast.

The result is a script that turns the CFP generator, or any generator, into a RubyLLM MCP tool for their server. Each generator is now both a CLI and an MCP tool. What she called the best part is that merged generator changes show up in the MCP tools when the server restarts. It also let her compare skill plus CLI against MCP directly.

21:55

The Benchmark

She started with GPT-5.4. She removed all the improvements she had made to the codebase and added them back one at a time. Each run used the instruction to add Rails World 2026, given a Markdown file of the event details so that different web fetches wouldn't skew results. She validated output with bin/rails validate_all, since validation could also affect results heavily.

No skill, no generator, no MCP. Working from older context, the model used 2023 sponsor logos instead of 2026 ones. It wrote "1Password" in lowercase and capitalized "TableCheck" wrong. It also added a URL for the Palmer Events Center, which told her it had done a web fetch she hadn't authorized. She counted four errors, at about 136,000 tokens. The transcript renders the cost as "$183," most likely $1.83.

Generator only, which she described as where most Rails developers stand with generators today. The prompt asked it to use custom generators. It made two errors: it left out PlanetScale and replaced em dashes with hyphens in descriptions. She quipped that "you can tell the big AI companies are really listening to us." It used 131,000 tokens.

Skill plus CLI. One error: the em dashes again.

MCP. The run had no videos, no schedule, and no sponsors. It quit partway through. It was still the cheapest option with the lowest token usage of all of them.

She also tested the smaller sibling model. With no tools it did worst. It renamed the keynotes, left every talk without a description, and used the lunch-and-break schedule format for all talks. It copied involvements from earlier years, including old MCs and volunteers. It deleted all the to-dos and added the deprecated event_name key. The MCP run was fairly good. It added one involvement, which was a problem, but only the Rails Foundation, and it made the em-dash mistake. The result she found most interesting was a run with this smaller model that cost 39 cents and had zero mistakes.

From this she saw a pattern: on scoped, repeatable tasks like this one, smaller models with better tools outperformed larger models. She said they didn't go off script, didn't try to rewrite her descriptions, and didn't look at other files to second-guess the file the generator produced.

25:26

Testing Luna

The Rails Foundation recently published an AI model report that found Luna had surprisingly high accuracy for its very low cost, so she tested it too. Luna struggled with schedules. Earlier configurations produced wrong times, and the MCP run had no schedule. With skill plus CLI, its only schedule error was leaving out day zero. Its other errors involved replacing descriptions with one-line summaries, plus, for MCP, the em-dash substitution. All of the Luna runs together cost less than a quarter.

Conclusions and Takeaways

She drew two conclusions from the experiments. Skills produced fewer mistakes but cost more. And concerns about the cost of MCP tooling may be overblown, since MCP was among the cheapest options in every set of runs.

She closed with several recommendations. Attendees now know enough to build a custom generator. They should remind their agents to use Rails generators for better reliability and lower token costs. They should try a small model with better tools instead of "leaving things to Opus all the time." They should try customizing existing generator templates to match company standards and get closer to the desired result on the first pass. Finally, they should build for humans too: humans and AI both need help understanding codebases, so the tools should serve both.