Ask a model for acceptance criteria and you will get three of them. They will be well formatted, correctly structured, and they will all describe the feature working.
That omission is the whole subject of this post. Not how to write Given/When/Then, which the Gherkin guide for PMs covers with twelve worked examples, but the specific and predictable ways generated criteria come up short, and what to do about it.
Why the gap exists
When an engineer picks up "as a user, I want to reset my password," they bring years of accumulated scar tissue. They know about token expiry because they have been burned by a reset link that worked three weeks later. They know about email enumeration because someone made them read the pentest report.
A model has read about all of those things. It just will not volunteer them, because you did not ask, and the prompt defined the scope.
This is not a limitation you can prompt away with "be thorough." I have tried. Thoroughness is not a direction the model can act on, because it has no way to know which of the infinite possible conditions matter to you. What works is enumerating the categories, which converts a judgment call into a coverage checklist.
Put bluntly: the gap between a good engineer's output and a model's output on the same story is roughly equal to the acceptance criteria nobody wrote down.
The 12 categories models skip by default
These are the ones that go missing almost every time. Paste them into the prompt as required coverage rather than hoping.
- Empty state. The list, table, or result set has nothing in it. What renders?
- Single item. Exactly one result. Do the labels pluralise correctly?
- Maximum limits. 10,000 rows, a 4MB upload, a 32-character field. What rejects, what degrades, what silently truncates?
- Minimum limits. Zero characters, a 1-byte file, a $0 price. Allowed or not, and what happens if so?
- Concurrent action. Two people edit the same record at once. Who wins, and does the loser find out?
- Stale session. The token expires halfway through the flow. What does the user see, and is their input preserved?
- Network failure mid-transaction. Request sent, no response received. Can they safely retry, or does retrying double-charge them?
- Partial success. Three of five items saved. What does the user see, and what is the recovery path?
- Double submission. Someone double-clicks submit. Does the backend deduplicate idempotently?
- Cascading delete. A parent entity is deleted. What happens to its children, and was that deliberate?
- Permission change mid-session. An admin is demoted while sitting on the admin page. What happens on their next action?
- Very long input. A name field receives 10,000 characters. Does validation catch it before the database does?
Cover all twelve on a meaningful story and you land somewhere around 30 to 40 criteria. That sounds excessive right up until one of them is the incident you did not have.
I would not run all twelve on every story. Numbers 5, 7, 9, and 11 matter enormously for anything touching money or permissions and are mostly noise on a settings toggle. Pick by blast radius.
The prompt
Copy this as is. One story at a time. The rules section is load-bearing, and removing any single rule measurably degrades the output.
Generate acceptance criteria for this user story:
<PASTE USER STORY>
Additional context:
- Product: <1-line product description>
- User roles that exist in the system: <list>
- Relevant constraints (rate limits, compliance, SLAs): <list>
Rules (do not skip any):
1. Output at least 3 happy path, 4 edge case, 4 failure state criteria.
2. Use Gherkin: "Given <preconditions> When <action> Then <result>".
3. Tag each criterion: happyPath | edgeCase | failureState.
4. Edge cases MUST cover: empty state, max limits, concurrent
actions, stale session.
5. Failure states MUST cover: unauthenticated access, invalid input,
server error, and rate limit if applicable.
6. Use specific values, never placeholders. "10,000 items", not
"a large number".
7. Every criterion must be independently verifiable. "It should be
fast" is banned.
8. Time limits get a real number. "Within 60 seconds", not "quickly".
9. Field references use the exact field name, not "the field".
10. If a rule cannot be satisfied without inventing product scope,
output [OPEN: <question>] instead of guessing.
Output as JSON:
[{ "category": "happyPath|edgeCase|failureState",
"criterion": "Given... When... Then..." }]
Rule 10 is the one I would add to any prompt of this shape. Without it, the model fills gaps by inventing product decisions, and those inventions read exactly like the criteria you asked for. With it, you get a visible list of the things it needed to know and did not.
On model choice: a reasoning model produces usable output in one pass. Faster models need two or three rounds and tend to skip edge cases even with the rules present, which makes them a false economy here. This is the stage worth spending on.
What good output looks like
Running the prompt above on "as a user, I want to reset my password" produces something like the following. Note how much of it is about the ways it goes wrong.
# Happy path
Given the user is on the login page
When they click "Forgot password" and enter a registered email
Then a reset email is sent within 60 seconds
And a confirmation screen says "Check your email"
Given the user clicks the reset link within 15 minutes
When they enter a new password meeting complexity requirements
Then the password updates and they are redirected to login
And a confirmation email is sent noting the change
# Edge cases
Given the user enters an email that is not registered
When they submit the forgot-password form
Then the same confirmation message shows as for valid emails
And no email is sent, preventing account enumeration
Given the user requested a reset within the last 2 minutes
When they submit another request
Then they are rate-limited with "Please wait 2 minutes before
requesting another reset"
Given the reset link is older than 15 minutes
When the user clicks it
Then "This link has expired" displays with a prompt to request
a fresh one
Given the account exists but has been disabled by an admin
When a reset is requested
Then the same generic confirmation displays, no email is sent,
and a security alert is logged
# Failure states
Given the email service is unavailable
When the system attempts to send the reset email
Then the send is queued with exponential backoff up to 24 hours
And the user is notified if it fails after the final retry
Given the new password fails complexity validation
When they submit
Then inline errors appear beside each failing requirement
And the existing password remains active
Given the reset token has been tampered with
When they attempt to set a new password
Then the request is rejected and a security event is logged
The enumeration criterion is the tell that the prompt worked. A model left to its own devices will write "shows an error if the email is not found," which is a reasonable-sounding requirement that also happens to be a security hole.
Reviewing generated criteria
The output arrives fluent and well formatted, which makes reviewing it harder than reviewing a human draft. Fluency reads as correctness. Here is what I check, roughly in order of how often it catches something.
Criteria referencing things that do not exist. The story is about password reset and a criterion mentions the user's profile photo. The model expanded scope. Delete it, and check whether the rest of the batch inherited the invention.
Vagueness that survived rule 7. "Handles the error gracefully" occasionally slips through. It is not a criterion, it is a promise to decide later.
Placeholder numbers. "Within X seconds" or "after N attempts." If you accept these, someone downstream picks the number, and it will not be you.
Duplicate criteria in different words. Two entries describing the same check with different phrasing. Inflates the count, adds nothing, and makes the set look more thorough than it is.
Tests disguised as criteria. "The login test passes" describes your test suite, not the product's behaviour.
Happy-path-only output. If you got three criteria and they all describe success, the rules did not apply. Do not patch it by hand. Regenerate, because a model that ignored the rules once probably ignored others.
Suspicious symmetry. Every story coming back with exactly one edge case and one failure state means the model is satisfying the format rather than thinking about the feature. Real features are lopsided.
How this connects to everything downstream
Acceptance criteria are the point where a spec stops being descriptive and starts being checkable, which makes them the most useful thing you can hand to a coding agent.
The practical version: number your criteria. When the agent's output is wrong, "criterion 7 fails, unknown emails return a different message than known ones" is a fix instruction. "It doesn't work" is not. That single change turns reviewing AI output from an argument about vibes into a pass/fail list that someone non-technical can actually run.
Criteria also propagate. A failure-state criterion implies an error state on a page. A rate-limit criterion implies a table or a counter in your schema. If you are working through a full spec, those implications are the thing worth tracking, and losing track of them is how you end up with criteria that no page can satisfy.
Related reading
- Gherkin acceptance criteria for non-technical PMs, the format itself, with twelve examples and a rewrite gallery.
- Generating user stories with AI, the critique loop that applies one layer up.
- How to generate an app spec from a prompt, where this sits in the wider pipeline.
To get a first set to review, the Acceptance Criteria Generator turns a plain-English feature description into Given/When/Then scenarios. Free, no signup. The review checklist above is still your job.
Sources and further reading
- Dan North, Introducing BDD: the origin of Given/When/Then.
- Cucumber, Gherkin Reference: canonical syntax.
- Atlassian, Acceptance Criteria for User Stories: industry examples across formats.
- OWASP, Forgot Password Cheat Sheet: why the account-enumeration criterion above is not optional.



