Using OpenSpec in Rails applications

See how OpenSpec is reshaping our Rails workflow. What paid off, what failed, and what we’d do differently.

Jul 22nd, 2026
By Patricio Mac Adden

Since last year, we started using AI heavily for all kinds of tasks. What started as chatting to an agent to complete tasks later evolved into a more professional workflow doing Context Engineering and Spec-Driven Development. There are many SDD frameworks out there, but ultimately, after trying out a bunch of them, we opted for OpenSpec. OpenSpec felt like the simplest framework out there, one that aligned with our pre-AI-era workflow.

Quick intro

Spec-Driven Development is a methodology where a highly detailed, structured specification (a “spec”) is written before any code is generated. It acts as a single source of truth to guide AI coding assistants, ensuring the AI builds exactly what is intended rather than relying on vague, unverified prompts. If you’re not familiar with OpenSpec in particular, or with Spec-Driven Development in general, I recommend you pause here and read about OpenSpec.

Practical lessons

We used OpenSpec extensively for the last 4 months on production Rails applications and side projects. In the following sections we outline insights from the process. We don’t like absolute truths, so we’re sharing our experience, as always. Your mileage may vary.

Read the plan

When you ask your agent to explore a change, you’ll be guiding the LLM through your idea. The LLM will ask you questions until it’s happy with the information you’ve given, or until you told it all you have to say about the feature. With all that information, it’ll propose the change, ie., it will create the proposal.md, specs/, design.md, and task.md files.

You definitely want to read them before writing the code and make sure it’s exactly how you want it to be. If not, well, the code isn’t going to be how you want it to be.

Two paragraphs earlier, I said that the LLM will ask you questions until it’s happy with the information you’ve given, or until you told it all you have to say about the feature. The problem with this is that you never know if the information you’ve given is complete enough, and the LLM will fill the gaps. And normally, not the way you intend. This becomes obvious if you propose the change instead of exploring it first.

To put an example, you can propose “add authentication”, and the LLM will create all the artifacts, but you haven’t specified anything. Which login methods? Email/password, magic links, OAuth, etc? Do users self-register? Who are the users? Customers or internal staff? What are the protected areas? Entire app or specific routes?

The LLM will fill any gaps with its criteria. Even if you think you’ve given all the information. It’s extremely important to read the plan to try and catch any kind of drift or misunderstanding. Catching them now will be much better than generating and reading a lot of code and finding conceptual or architectural errors.

So, read the plan.

Read the code

Kind of obvious. We said a few months ago that we enforce a NO AI CODE IN PRODUCTION DIRECTIVE. We still stand by it.

Even if the plan is well outlined, models tend to over-engineer the simplest solutions, or implement them differently to your taste.

For example, a few weeks ago we were working on the CMS side of one of our client’s apps. The thing was very basic: instead of a full-blown CMS, every now and then the marketing team just needed a way to updated Some SEO optimised content in very specific places.

The first version was more left to the agent freewill. Looks like CMS is a big word because, despite having proof in the current code that this was very minimal, it wanted to create a headless cms with Page Composer, a component system, field types, and schema management, etc. After dialing down the complexity gauge and explicitly outlining the minimal solution design we were after, the plan made more sense, so we moved forward to the implementation phase. But when we took a look at the code, we found a lot of defensive code that existed only because of edge cases existing in tests that were not plausible in production. And such code was partially duplicated and distributed across the services, models, and controller layers.

The feature worked ok; it passed QA, the UX was, as specified, way better than the previous one, but long-term sustainability of the code was at a loss.

So, read the code.

Context Engineering

Context engineering is how we decide what an AI model needs to see to do useful work. It goes beyond prompt writing: instructions matter, but so do the tools, retrieved knowledge, and relevant history available at each step.

SDD is a form of Context Engineering. You work with the model to produce artifacts (markdown files) that will be fed in later phases. These artifacts should be short for several reasons:

  • context windows degrade with length
  • time and tokens are expensive
  • attention is a scarce resource

This last bullet point is valid not only for LLMs, but also for humans. Reading a 2000-line proposal/design document is not the same as reading a 200-line proposal/design document. Not to mention iterating over them several times.

OpenSpec got this right. An average proposal.md is less than 50 lines, an average design.md is less than 200 lines, and the average tasks.md is just a checkbox list. It doesn’t contain any kind of implementation detail.

There’s so much you can do to your context to improve the quality of the generated code. You can add skills for the language, framework, or libraries you’re using, and you can add bespoke skills based on your codebase. These are exceptionally good on legacy projects where things are already done in a certain way, and you want the model to be consistent on new changes.

The right amount of work

In the pre-AI era, we used to divide the work into the smallest amount of work possible for many reasons:

  • it’s easier to understand
  • it’s easier to estimate and prioritize
  • it’s easier to review a small change than a big one
  • people can work in parallel

It was always up to the different teams to decide what the smallest piece possible was.

With LLMs it’s no different, but the limits moved a little farther, and dividing a feature into smaller features might even be counterproductive: you don’t want to divide a large feature into smaller pieces because you don’t want to explain the same things many times just to say “we’re only implementing this small little piece”. Implement the whole thing in one go.

You don’t want to say “implement this huge app in one go, make no mistakes” either. That’s often too much. The LLM will fill a lot of gaps, and the outcome will be far from right. What worked for us was exploring a change and letting the LLM decide whether something is too much for a single change.

The LLM is good at deciding what’s the right amount of work.

Design is not a spec

In OpenSpec, you progress in your application by creating changes. Naturally, you think adding a design for your UI is a change, as you’re changing the way your application looks. But specs are requirements and behavior. They’re written in Gherkin notation, and design is not behavior.

Sure, you can create a change for the design, but the specs will be a bump in the road for every other change. You’ll end up with specs in this form:

## ADDED Requirements

### Requirement: [REDACTED] visual system
The system SHALL treat root `DESIGN.md` as the source of truth for all design decisions and apply the [REDACTED] design system to every user-facing Rails-rendered page using the colors, typography, spacing, radius, and flat hairline style defined there.

#### Scenario: DESIGN.md resolves design conflicts
- **WHEN** a prototype, task, implementation detail, or change artifact conflicts with root `DESIGN.md`
- **THEN** root `DESIGN.md` is treated as authoritative
- **AND** the conflicting artifact or implementation is updated to match it

#### Scenario: Screens use [REDACTED] tokens
- **WHEN** a user views [REDACTED], [REDACTED], [REDACTED], [REDACTED], or shared [REDACTED] form screens
- **THEN** the screens use the [REDACTED] primary, secondary, tertiary, neutral, surface, and on-primary colors
- **AND** the screens use the typography, spacing, radius, and hairline-border treatment defined for the design system

...

And you’ll be tied to creating an OpenSpec change for any small UI change. While it sounds great, trust me, it’s not that great when you need to quickly prototype something or need to create an OpenSpec change to correct a button or something. In these cases, OpenSpec (or any SDD framework, really) is a burden more than a benefit.

What worked for us was using a DESIGN.md file for the design tokens and rationale, and creating a library of components we can later reference.

Whether a UI change should be an OpenSpec change or not is ultimately answered by this question: is this a functionality change or visual change? If it’s functionality, then it should be an OpenSpec change. Otherwise, it’s safe to take things lightly.

Guidelines are a must

We realized that architectural and design guidelines are almost mandatory.

As it has always been before agents, there’s no single truth, and each project/team might have their own needs and preferences; that’s ok and expected. It doesn’t matter if you like clean architecture, DDD, or 37signals’ best practices; pick one and improve it as your app grows.

The quality of the output and the time you end up investing in steering the model if you don’t have specific guidelines about how you want things organized can make the whole SDD adoption worthless.

Conclusion

OpenSpec didn’t force us to replace the way we build software. We still explore a problem, agree on an approach, implement it, and review the result. The difference is that we now make those decisions explicit enough for an AI model to be useful without handing it the engineering work.

In essence, for us, engineering work didn’t change much. What changed is that we used to instruct people to work on a real project. People learn, remember, and improve autonomously. Experience and feedback from other people were enough. Now we’re adding coding agents to our workflows. They’re stateless, non-deterministic search machines. Coding agents don’t learn, don’t remember, and don’t improve autonomously. The challenge then is how we structure the information and the instructions we provide them so that their output gets better every time, and assuming that the quality of their output is not always going to be great, how we detect and improve when that information and those instructions are not enough.

This is what we continue investigating and trying to figure out. Stay tuned.