back to the notebook

How I use Ralph to build a full SaaS with Claude Code

Ralph is a technique for running AI coding agents in a loop. You run the same prompt repeatedly. The AI picks its own tasks from a PRD. It commits after each feature. You come back later to working code.

From AI Hero

I built MediaJanitor, a cleanup tool for the Jellyfin server I share with friends, mostly while I slept. Claude Code wrote the code overnight in a loop, and I spent my mornings checking what it had done. Over the whole project, that came to 350+ commits, 150+ user stories and 10 to 15 hours of my own time. This is the setup, adapted from Ralph.

There's also a visual flowchart of how Ralph works.


Quick start

# 1. Write your PRD with user stories
vim PRD.md

# 2. Convert it to a machine-readable format
/ralph-init

# 3. Run one iteration (human-in-the-loop)
./ralph-once.sh

# 4. Or let it run on its own overnight
./afk-ralph.sh 10

The loop reads and writes four files:

  • PRD.md → the user stories, written for humans, with [ ] checkboxes
  • prd.json → the same stories, in a format Ralph works through
  • CLAUDE.md → project context, plus what Claude learned along the way
  • progress.txt → a log of finished work

The problem is the context window

A long conversation with an LLM drifts. Errors pile up, the model loses track of earlier decisions, and at some point the session breaks.

Ralph's answer is to start every iteration from zero. Each iteration picks one small story from prd.json, builds it test-first inside a single context window, commits, and exits. Progress lives in files, prd.json and CLAUDE.md, rather than in the model's memory. So instead of one long session that falls apart, you get many short ones that don't depend on each other.

Feedback loops

Writing code doesn't tell the model whether the code works. It has to check, at every step. Ralph's checks:

  • Unit tests with pytest and vitest, which must pass before any commit
  • Type checking with mypy and TypeScript strict mode
  • Docker, to run the actual app and not only the tests
  • Puppeteer screenshots for UI changes
  • The logs, for runtime errors

A story is done when the tests pass, Docker runs and the browser shows what the story asked for. Without these checks you get code that looks plausible. With them, most of it works.

A PRD Ralph can work from

Ralph is only as good as the PRD it reads. I follow two rules.

Stories small enough for one context window. When a story is too big, I split it by layer:

  • Data first: models, migrations
  • Backend logic: services, APIs
  • Frontend: pages, components
  • Integration: wiring the pieces together

Acceptance criteria you can check. "Works correctly" can't be tested. These can:

VagueTestable
"Handles errors properly""Returns 401 when token is missing"
"Good UX""Shows loading spinner while fetching"
"Is secure""Passwords hashed with bcrypt"

Every story also ends with two criteria, "Typecheck passes" and "Unit tests pass". With a vague PRD, Ralph goes around in circles. With a good one, I wake up to finished stories.


The daily loop

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   EVENING              NIGHT                MORNING       DAY   │
│   ────────             ─────                ───────       ───   │
│                                                                 │
│   Start Ralph    →    AI executes    →    Human reviews  →  ↵   │
│   ./afk-ralph.sh      autonomously        /morning-routine      │
│                       (10+ stories)                             │
│                                                                 │
│   ←─────────────────── repeat daily ────────────────────────────│
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
TaskMeAI (Ralph)
Write the PRD with acceptance criteria✓
Build the stories test-first✓
Run the automated checks✓
Commit the code✓
Review the finished work✓
Triage suggestions and prioritize bugs✓
Test by hand in the browser✓
Write what it learned to CLAUDE.md✓

In short, I write the stories and review the work. Ralph does the rest.


The morning routine

After a night of Ralph, I go through six steps. /morning-routine walks me through them, or I do them by hand:

/prd-sync → Review → /prd-archive → /review-suggestions → Manual QA → /ralph-init

1. /prd-sync

Copies the stories finished in prd.json back to the checkboxes in PRD.md. Skip it if nothing got done overnight.

2. Review what was done

tail -50 progress.txt      # What Ralph completed
git log --oneline -20      # Scan commits for surprises

I look for unusually large commits, odd messages and changes to files the story had no reason to touch.

3. /prd-archive

Moves fully finished epics to ARCHIVED_PRD.md. Skip it if no epic is complete.

4. /review-suggestions

Goes through each item in SUGGESTIONS.md, with three choices:

  • Remove: not needed, or already done
  • Promote to PRD: a real issue that needs a user story
  • Skip: valid, but for later

5. Manual QA

I use the app. I click around and try the edge cases. No automated test replaces a person looking at the product. For the items I promoted in step 4, /prd writes proper user stories.

6. /ralph-init

Generates a fresh prd.json with only the unfinished stories. Skip it if Ralph isn't running tonight.

Then commit

git add PRD.md prd.json SUGGESTIONS.md progress.txt
git commit -m "chore: morning routine sync and cleanup"

What Ralph does at night

Once I run ./afk-ralph.sh N, Claude repeats this loop on its own:

  1. Picks the next story with passes: false in prd.json.
  2. Writes a failing test, then the code.
  3. Runs the checks: pytest, mypy, npm test, npm run check.
  4. Runs the app in Docker, because unit tests alone aren't enough.
  5. Checks UI changes in a browser.
  6. Sets passes: true in prd.json.
  7. Commits with a clear message.
  8. Writes what it learned to progress.txt and CLAUDE.md.
  9. Starts over, until N iterations are done or no story is left.

Every 3 to 5 iterations, it also runs /exploratory-qa. That pass looks across the whole app for inconsistent navigation, gaps in error handling and security issues, and writes what it finds to SUGGESTIONS.md.


Why it works

Every change has to pass the existing tests, so what already works keeps working, as far as the tests cover it. Precise acceptance criteria keep Ralph from wandering off. CLAUDE.md collects what each session learned, and the next session reads it. The parts that need judgment, like writing the PRD, triaging bugs and setting priorities, stay with me.


MediaJanitor in numbers

Here's what came out of letting Ralph build MediaJanitor:

MetricValue
Commits350+
User stories completed150+
My time10 to 15 hours

That's 10 to 15 hours for the whole project, not per week.

My part was:

  • Writing and refining the PRD: defining features, splitting stories, writing acceptance criteria
  • The morning reviews: checking what Ralph built and triaging its suggestions
  • Fixing the occasional bug Ralph couldn't solve
  • External setup: Slack notifications, SMTP2GO for emails, deployment configs

Ralph wrote everything else: the backend APIs, the database models, the frontend pages, the test suites and the Docker configs.


What I got wrong

When Ralph fails, it's usually my fault.

Vague PRDs waste iterations. Early on, I wrote stories like "Add user settings page." Ralph would build something, but not what I had in mind. Now I write "Settings page with email notification toggles. Saves to user_preferences table. Shows success toast on save."

It takes shortcuts. When Ralph can't verify something, say a flaky browser test or Docker not running, it sometimes marks the story done anyway. That's why the morning review matters.

Skipping the review doesn't work. I once let Ralph run 20+ iterations without checking and came back to a mess. Now I cap it at 10 iterations and review every morning.


Files

FileWhat it's for
PRD.mdUser stories for humans, with [ ] checkboxes
prd.jsonThe version Ralph works through
progress.txtLog of finished tasks
CLAUDE.mdProject context and what Claude learned
SUGGESTIONS.mdQA observations with [P1]/[P2]/[P3] priorities
prompt.mdThe prompt Ralph follows on every iteration

Skills

Planning, before Ralph runs

/prd helps write a PRD with right-sized stories and acceptance criteria you can check.

/ralph-init converts PRD.md to prd.json. It checks story sizes and orders the stories so dependencies run first.

Review, after Ralph runs

/morning-routine walks through the six steps of the morning routine: sync, review, archive, triage, QA and re-init.

/review-suggestions goes through SUGGESTIONS.md one item at a time.

/exploratory-qa runs every 3 to 5 iterations and checks navigation consistency, error handling, auth guards, UI coherence and security.

Maintenance, when needed

/prd-sync copies passes: true from prd.json back to the PRD.md checkboxes.

/prd-archive moves finished epics to ARCHIVED_PRD.md, so the active PRD stays short.


Last thoughts

This is my version of Ralph by AI Hero, adapted to Claude Code.

The main thing I learned is that coding agents do much better with structure. Give them a clear PRD, checks at every step and a fresh context for each story, and they ship real features overnight.

It's not hands-off. I still debug, review and think about the product. What changes is the split. I spend my time deciding what to build, and very little of it typing.

MediaJanitor exists because of this workflow.