How to Write a Spec an AI App Builder Can Actually Build From
TL;DR AI app builders fill every gap in your prompt with a guess. A spec is just a way of making fewer gaps. The seven things that matter most: users, core jobs, screen list, data model, navigation, states, constraints. Write the data model before the screens. Most "the AI built the wrong thing" mo
If you've tried any AI app generator, you've probably had this experience. You type "build me a habit tracker app," hit enter, and get something that looks impressive for about ninety seconds. Then you notice it has a social feed you didn't want, no way to edit a habit, and a settings screen that doesn't do anything.
The model didn't fail. It did exactly what you asked, which was almost nothing. Every detail you left out, it had to invent. And its inventions are the average of every habit tracker it has ever seen, not the one in your head.
This post is about closing that gap with the same thinking you'd put into a brief for a human developer, written down more explicitly.
A short prompt isn't a simple request. It's a request with a huge number of unstated decisions. "Habit tracker" leaves open:
A human developer would come back with twenty questions. Most AI tools just pick defaults. Your job is to answer those questions up front.
Here's the structure I use. You don't need every section to be long. You need every section to exist.
One or two sentences per user type. Who they are and what context they're in when they open the app.
Primary user: someone trying to build 1 to 3 new habits, opening the app once or twice a day, usually for under 30 seconds.
That last bit ("under 30 seconds") is doing a lot of work. It tells the generator that check-ins need to be one tap, not a form.
What the user is trying to get done. Keep it to three to five. If you have twelve, you have a different problem.
Write them as verbs ("log today's check-in," "add a new habit"), not features ("calendar view"). Jobs tell the model why a screen exists.
Name every screen and give it one line of purpose. This is the single highest-leverage section. If a screen isn't on the list, it probably shouldn't exist.
The entities, their fields, and how they relate. Plain English is fine, but be specific about types and required fields. Screens, forms, and the backend all depend on this, so mistakes here cause the most rework.
How screens connect. Tabs or a stack? What's the first screen after login? Where does "back" go from a detail screen? Mobile navigation has a lot of conventions, and stating which one you want saves a round of corrections.
For every screen that shows data: what does it look like when it's empty, loading, errored, or full? Empty states especially. Every new user sees them first, and generators often skip them unless asked.
The stuff that's easy to forget: platforms, auth method, offline needs, accessibility, things you explicitly don't want. "No social features" is a perfectly good constraint.
Here's a complete spec for a deliberately small app. It's about 400 words, which is roughly the size I'd aim for on a first generation.
# App: Streakly (working name) ## Users - Primary: an adult building 1 to 3 habits. Opens the app 1 to 2 times a day, usually for under 30 seconds. Wants a quick check-in, not a dashboard. ## Core jobs 1.
Check in on today's habits in one tap each 2. Add, edit, or archive a habit 3. See progress for the current week and the current streak ## Screens 1.
Sign in: email + magic link only 2. Today (home tab): list of active habits with a check-in button each 3. Habit detail: name, schedule, current streak, last 4 weeks as a grid 4.
Add / edit habit: form (name, schedule, reminder time) 5. Progress (tab): this week across all habits, one row per habit 6. Settings (tab): reminder toggle, sign out, delete account ## Data model - User: id, email, created_at - Habit: id, user_id, name (required, max 40 chars), schedule ("daily" or list of weekdays), reminder_time (optional), archived (bool, default false), created_at - CheckIn: id, habit_id, date (one per habit per day), created_at - Streak is computed from CheckIns, not stored. ## Navigation - Bottom tabs: Today, Progress, Settings - Today -> Habit detail (push) - Habit detail -> Edit habit (modal) - "+" button on Today -> Add habit (modal) - After sign in, land on Today ## States - Today, no habits: friendly message + "Add your first habit" button - Today, all done: show a short "All done for today" banner - Habit detail, no check-ins yet: show empty grid, streak = 0 - Any network error: inline retry message, keep last loaded data visible ## Constraints - iOS and Android, portrait only - Must work with large text sizes (Dynamic Type / font scaling) - No social features, no sharing, no gamification badges - Check-in must be a single tap with immediate visual feedback - Archived habits are hidden from Today but kept in the database Enter fullscreen mode Exit fullscreen mode A few things worth pointing out:
Here's the same idea expressed two ways.
Build a habit tracking app with streaks, reminders, and nice charts. Make it modern and clean. Enter fullscreen mode Exit fullscreen mode After (the condensed version for a chat-style prompt):
Build a mobile habit tracker for adults building 1 to 3 habits who open the app for under 30 seconds at a time. Screens: Sign in (email magic link), Today (home tab, list of habits with one-tap check-in), Habit detail (streak + last 4 weeks grid), Add/Edit habit (modal form: name, schedule, reminder time), Progress tab (this week, one row per habit), Settings tab (reminders toggle, sign out). Data: Habit (name max 40 chars, schedule daily or weekdays, optional reminder time, archived flag).
CheckIn (one per habit per day). Compute streaks from check-ins, don't store them. Empty state on Today: message plus "Add your first habit" button.
No social features. Portrait only. Support large text sizes.
Enter fullscreen mode Exit fullscreen mode "Nice charts" and "modern and clean" mean nothing specific. The "after" prompt is longer but leaves little room to wander, and it swaps "charts" for a 4-week grid, which is more concrete.
Even a great spec won't produce a perfect app in one pass. The trick is how you handle the second pass.
The tempting move is to write a giant follow-up: "fix the Today screen, also the colors are off, also add dark mode, also the detail screen should show a chart." That usually breaks something that was working.
Small asks are easier to apply and to verify. If something breaks, you know which change did it.
This is the workflow I use most days, because I work on RapidNative, which turns a plain-English description into a React Native + Expo project with screens, navigation, styling, and a Supabase backend. You can select a single screen and describe a change to just that screen, which pairs well with the loop above. The output is a standard Expo project (Expo SDK 54, React Native 0.81, Expo Router 6, NativeWind 4, TypeScript) that you can export whenever you want. But nothing in this post depends on a specific tool. The same spec and the same loop apply whatever builder you're using, or if you're handing the spec to a human developer.
A few patterns I see in prompts that reliably lead to rework:
What an AI app builder gives you is capped by what you give it. Users, jobs, screens, data, navigation, states, constraints. Write them down (twenty minutes, usually), generate a skeleton, then iterate one screen at a time.
What's the one section of a spec you always forget until the generated app makes it painfully obvious? For me it's empty states. I'd love to hear yours in the comments.