dogfood
A scenario walkthrough engine for UX iteration.
dogfood helps you find UX blind spots before your users do. You document the
real scenarios your product will face — a new user signing up, a customer
cancelling a subscription, a support ticket escalating — and dogfood turns
them into reproducible walkthrough scripts. You follow the script through your
app, spot where it breaks, fix it, and iterate.
It works for anything with a user pursuing an end goal: a CLI, a TUI, a web app, a mobile app, an API. The gem knows nothing about your domain; you supply the content via a pack. See SPEC.md for the full contract.
How it works
- Document a scenario — write a YAML file (or a Ruby class) describing the steps a user takes to reach a goal, including the branches and edge cases they might hit.
-
Generate a walkthrough — run the CLI.
dogfoodpicks scenarios, walks them through their stages, and writes a Markdown log of every action. - Walk through your app — open the log and follow it, clicking through your real product exactly as the script describes.
- Find gaps and iterate — where the script doesn't match reality, or the flow feels broken, that's a UX bug. Fix it, re-run with the same seed, and compare.
Because runs are seeded, you can reproduce the exact same walkthrough for a teammate or after a fix.
Install
gem install dogfoodOr add to a Rails Gemfile:
gem "dogfood"Quick start (plain Ruby)
Write a pack file that registers a step module and loads scenarios:
require "dogfood"
Dogfood::Pack.current = Dogfood::Pack.new(:app) do |p|
p.include_steps MySteps
p.load_dir "scenarios/*.yml"
endThen run the CLI:
ruby -r dogfood -e 'Dogfood::CLI.new(["--pack","my_pack.rb","--list"]).run'Quick start (Rails)
- Add
gem "dogfood"to theGemfile. - Run
bin/rails g dogfood:installto scaffoldconfig/dogfood.rbandconfig/dogfood/. - Edit
config/dogfood.rbto register your step modules. - Drop scenarios into
config/dogfood/*.yml. - Run
bin/rails dogfood --listandbin/rails dogfood --days 3. - Run
bin/rails dogfood:smoketo run the YAML smoke test.
Running the bare dogfood executable from a Rails app root also works. The
CLI detects the app via config/application.rb and exposes Rails.root (a
minimal shim, no full boot) so config/dogfood.rb can use Rails.root.join(...)
as usual.
A scenario, end to end
Here's a small, domain-agnostic example: a new user signing up for a SaaS product and reaching their first value moment. It's not about auto repair or work orders — it's about any user with a goal.
name: signup_to_first_report
title: Signup to First Report
stages: [landing, signup, onboarding, first_action, value_moment]
branches:
landing:
organic: 0.60
referral: 0.25
paid_ad: 0.15
stages_def:
landing:
- call: visit_landing
when_branch: organic
with:
source: "search"
out: visitor
- call: visit_landing
when_branch: referral
with:
source: "referral"
out: visitor
- call: visit_landing
when_branch: paid_ad
with:
source: "ad"
out: visitor
- call: view_pricing
with:
plan: ${visitor.plan}
signup:
- call: fill_signup_form
with:
email: "alex@example.com"
out: account
- call: verify_email
with:
email: ${account.email}
- call: create_workspace
with:
name: "Acme Inc"
out: workspace
onboarding:
- call: choose_plan
with:
plan: ${visitor.plan}
out: plan_choice
- when: ${plan_choice == :self_serve}
then:
- call: start_self_serve_onboarding
else:
- call: book_sales_call
with:
rep_name: "Jordan"
- call: connect_data_source
with:
source: "csv"
out: data_source
first_action:
- call: run_first_report
with:
data_source: ${data_source}
out: report
- maybe: 0.2
then:
- call: hit_data_error
with:
report: ${report}
error: "column mismatch"
- call: share_report
with:
report: ${report}
channel: "email"
value_moment:
- call: see_insight
with:
report: ${report}
out: insight
- call: invite_teammate
with:
email: "sam@example.com"
- call: update_state
with:
status: activated
substatus: "First report shared"The call: names (visit_landing, fill_signup_form, ...) are methods on
your step modules — the gem doesn't define them. You implement each one to
record a UI action and return data. See EXTENDING.md for how.
Run it:
dogfood --pack my_pack.rb --scenario signup_to_first_report --seed 42dogfood writes a Markdown walkthrough to tmp/dogfood/. Each story is a
numbered, actor-labeled log of actions:
# Walkthrough Log
## Day 1 — 2026-08-06
### Summary
- New stories today: 1
- Carry-overs: 0
### Morning
#### S-001
**Status:** activated
**Substatus:** First report shared
1. **Visitor** — Visit landing page
- **Action:** Open /pricing from search
2. **Visitor** — View pricing
- **Action:** Select plan
...The explain command
Inspect a scenario without randomness — a static sanity check of the scenario definition:
dogfood --pack my_pack.rb --explain signup_to_first_report
dogfood --pack my_pack.rb --explain scenarios/example.yml
dogfood --pack my_pack.rb signup_to_first_report --explainEach compiles the scenario and prints its stage/decision tree without invoking any RNG.
Days are optional
Most scenarios complete in a single pass — --days 1 is the default and is
perfectly fine. Multi-day only matters when a scenario pauses and resumes, e.g.
"waiting for an external event" (a part to arrive, an approval, a payment). If
your scenario doesn't pause, you don't need to think about days at all.
The depth-1 nesting limit
when and maybe blocks may only contain plain call steps — no nested
conditionals (this is enforced and documented in SPEC.md §4.7). Treat it as a
feature: keep scenarios shallow and declarative. If a story needs deeper logic,
write it as a Ruby Dogfood::StoryBase subclass instead.
Extending the gem
See EXTENDING.md for the three extension tiers: adding domain behavior (host app, no gem changes), new YAML step constructs, and new core engine classes.
Configuration
| Flag | Description |
|---|---|
--pack PATH |
Ruby file that registers a Dogfood::Pack (default: config/dogfood.rb in the current directory) |
--scenario NAME |
Run a specific named scenario (otherwise: random mix) |
--days N |
Number of simulated days (default: 1) |
--stories-per-day N or M-N |
Stories per day (default: 3-6) |
--seed N |
Set random seed for reproducibility |
--random |
Use a random seed |
--output PATH, -o PATH |
Output file path |
--list, -l |
List available scenarios |
--explain [NAME|FILE] |
Print the stage/decision tree for a scenario name or YAML file, no RNG |
--dry-run --scenario N |
Same as --explain but takes a registered name |
--help, -h |
Show help |
Development
bundle exec rake testRequires Ruby >= 3.1.
License
MIT. See LICENSE.txt.