How to write acceptance criteria AI agents can verify
Acceptance criteria (AC) are the conditions that must be true before a change counts as done. Given/When/Then is the step shape in Gherkin, the language Cucumber uses for behaviour-driven development (BDD): Given a starting state, When one action happens, Then an observable result follows. A product requirements document (PRD) is the prose brief that names the problem and the intended product. Spec-driven development (SDD) treats a structured specification, not that prose alone, as the source the implementation is checked against. EARS (Easy Approach to Requirements Syntax) is a way to constrain a requirement sentence so its clauses stay in a fixed order. Continuous integration (CI) is an automatic check on every proposed change; this site describes the two SpecScore checks on the validators page. This page shows how to write testable acceptance criteria an artificial intelligence (AI) coding agent can verify, with acceptance criteria examples, and what the SpecScore command-line interface (CLI) actually checks.
Updated 3 November 2026.
What acceptance criteria are
An acceptance criterion answers "what must be true?" It names a condition a reviewer, a test, or an agent can accept or reject. It does not name the function to call or the file to edit. Those choices belong to the implementation.
A requirement is the rule. In the SpecScore requirement specification, a requirement is a #### REQ: heading inside a feature: one obligation, with its own identifier. An acceptance criterion bundles related requirements into one condition. A scenario is one concrete case that shows the condition holding. The acceptance criteria specification puts it this way: criteria answer "what must be true?", and scenarios answer "how do we prove it?"
A PRD can contain the same intent in paragraphs. Paragraphs help a person. An agent still has to guess which sentence is the pass/fail test. Writing the criterion as its own statement removes that guess.
What makes acceptance criteria testable
An AI coding agent can follow a criterion only when a later reader can point at evidence and say pass or fail. Five habits make that possible. None of them is a sentence template. They are checks you can apply to any template, including Given/When/Then and a checklist.
Observable
The result is something an outsider can see: a message on the page, a record that was or was not stored, a redirect, a rejected form. "The system understands the shopper" is not observable. "The page shows Enter a valid email address beside the email field" is.
Specific
Name the actor, the input, and the result. "Bad input is handled" does not say which input or which handling. "The shopper submits the email not-an-email" does. Specific does not mean you paste every production value. It means a reader can invent a fixture and know what should happen.
One behaviour
One criterion, one behaviour. "The shopper can sign in, reset a password, and see a lockout message" is three criteria sharing a sentence. An agent that implements the first and skips the third can still claim the sentence is done. Split them.
Pass or fail
A testable acceptance criterion has two outcomes. "The form should feel fast" has a slope, not a result. If you need a bound, write the bound: "the checkout page shows the error without a further request to the server." If you cannot say what failure looks like, the sentence is not a criterion yet.
No vague words
Drop words that hide the result: graceful, intuitive, robust, user-friendly, seamless, fast, should, and appropriate. Replace each one with the evidence you would accept. "Handle errors gracefully" becomes "show Enter a valid email address beside the field, and do not place the order."
Given/When/Then and a checklist
Two shapes cover most acceptance criteria examples teams actually keep. Given/When/Then is a scenario. A checklist is a list of outcomes. Use the scenario when the order of state, action, and result matters. Use the checklist when you are listing independent outcomes of one action.
Cucumber's Gherkin reference gives steps the keywords Given, When, Then, And, and But. The reference uses English; Gherkin also has translations of those keywords. The words after the keyword are matched to code, called a step definition, when Cucumber runs the file. SpecScore does not use that step-definition layer. The example below is Gherkin, shown so the shape is familiar. It is not a SpecScore file.
Given/When/Then
Scenario: Reject an email that is not a valid address
Given the shopper is on the checkout form
When the shopper submits the email "not-an-email"
Then the page shows "Enter a valid email address" beside the email field
And the order is not placed
Checklist
- Submitting the email "not-an-email" shows "Enter a valid email address" beside the email field.
- The order is not placed.
Keep one When. A second action is a second scenario. And continues the same keyword: another Given, or another Then. Do not use And to sneak in a second behaviour.
A checklist is the same contract without the keywords. Each line is still one observable result. The checklist above is the Then of the scenario, written as checks.
Alistair Mavin's guide defines EARS (Easy Approach to Requirements Syntax) as a mechanism to gently constrain textual requirements. The clauses stay in one order. The guide gives the generic shape: While <optional pre-condition>, when <optional trigger>, the <system name> shall <system response>. A requirement has zero or more preconditions, zero or one trigger, one system name, and one or more system responses.
That sentence is the obligation: the rule the system shall meet. An acceptance criterion is the condition that must be true for the obligation to count as met. A Given/When/Then scenario is one concrete case of that condition. You can write the requirement in an EARS sentence and still keep the criterion free of sample inputs.
In a SpecScore feature, the requirement can be an EARS-style sentence under a #### REQ: heading. The criterion stays the abstract condition. The scenario carries the example. EARS does not replace either of those. It only constrains the requirement sentence.
Acceptance criteria examples: vague and testable
Name the evidence
Vague
The checkout form should handle bad input gracefully and feel fast.
Testable
When the shopper submits the email "not-an-email", the checkout page shows "Enter a valid email address" beside that field and does not place the order.
The vague line has two hidden demands (errors, speed) and no evidence. The testable line has one action and two results a person or an agent can check.
One behaviour
Vague
Users can sign in, reset a password, and see an error if the account is locked.
Testable
When a visitor submits a known email and the matching password, the app sets a session cookie and redirects to /dashboard.
Sign-in, reset, and lockout are three behaviours. The testable line keeps sign-in. Reset and lockout each get their own criterion, with their own inputs and results.
Outcome, not the function name
Vague
The handler returns 400 from validateEmail().
Testable
When the shopper submits an email that is not a valid address, the checkout page shows "Enter a valid email address" and does not place the order.
A function name and a status code tie the criterion to today's code. The outcome still holds if the check moves to another function. Keep status codes when the status code is the behaviour you promised, and say so as the observable result.
How SpecScore stores them, and what CI checks
SpecScore publishes the format in the specscore/specscore repository and the CLI in specscore/specscore-cli. The two documents do not describe acceptance criteria in exactly the same shape. Both are current. This section says which is which, and then what specscore spec lint actually rejects.
The acceptance criteria specification — the feature README served at that URL — puts criteria inline in the feature README. Every feature has an ## Acceptance Criteria section. Each criterion is a ### AC: {slug} heading. The slug is lowercase and hyphenated. The next line is **Requirements:** and lists the requirements it bundles, as {feature-id}#req:{slug}. After that comes prose: the composite condition. The specification says that prose stays abstract. It must not contain Given/When/Then steps, sample inputs, or implementation detail. When no criteria exist yet, the section reads "Not defined yet." and the Open Questions section says "Acceptance criteria not yet defined for this feature." Criteria are optional grouping: the specification says to add one when it bundles two or more requirements, and to skip it when it would only restate a single requirement.
Concrete proof is a scenario file under the feature's _tests/ directory. The scenario specification requires a # Scenario: title, a ## Steps section, uppercase GIVEN, WHEN, THEN, and AND, and exactly one WHEN. An optional **Validates:** line links the scenario to {feature-path}#ac:{slug} or to a requirement. A ## Rehearse section may hold an executable script in a rehearse fence that starts with a shebang.
The CLI also reads thin criterion files, one per criterion, at _acs/<slug>.ac.md. The login example and the Rehearse guide document this shape: # AC: <slug>, optional **Status:**, **Verifies:**, and **Applies-to:**, and the condition under ## Statement. specscore rehearse acs <feature-dir> prints a summary table from those files. specscore rehearse acs --write replaces the feature README's Acceptance Criteria section with that table and a comment that the section is generated and should not be edited by hand. A file with no # AC: heading is an error from that command.
CI in this repository is the dogfood workflow. It installs CLI v0.39.1 and runs specscore spec lint on pull requests and on pushes to main. Checked on 3 November 2026, that release and the CLI default branch both include the rules below. The linter checks structure. It does not grade English.
Rule ac-heading-format is an error. In a feature README (a file whose title is # Feature:), a line that looks like an acceptance-criterion heading must be exactly ### AC: <id> with one space after the colon and a kebab-case id. A heading of the form ### AC: <id> (verifies ...) is also accepted. --fix repairs spacing only. An id with uppercase letters or underscores is reported and left for a person. The rule does not require the **Requirements:** line, does not require the "Not defined yet." placeholder, and does not warn when the prose contains Given, When, or Then. The specification's own scenario for that warning is not a lint rule in this CLI.
On a plan that names a source feature, the linter reads ### AC: headings inside that feature's Acceptance Criteria section. Rule P-002 reports a task **Verifies:** value that is not <feature-slug>#ac:<ac-slug> for one of those headings. Rule P-001 reports a heading that no task covers and that is not listed under ## Deferred AC Coverage. Those two rules do not open _acs/*.ac.md. A summary table from rehearse acs --write has no ### AC: headings, so the plan rules do not see the thin files.
The same lint command does not check scenario step keywords. specscore rehearse runs scenario files: the Rehearse guide uses a heading such as ## Given or ## When above a fenced command, and **Use:** to call a reusable check. A file with one Given and several When branches runs as separate cases. specscore rehearse new <feature-slug>#ac:<ac-slug> reads the feature README and extracts Given/When/Then lines from that criterion into spec/features/<feature-slug>/_tests/<ac-slug>.md. None of that runs in the dogfood workflow. The workflow runs specscore spec lint.
Inline criterion, from the acceptance criteria specification
## Acceptance Criteria
### AC: invalid-email-rejected
**Requirements:** checkout#req:reject-invalid-email
An email that is not a valid address is rejected, and the order is not placed.
Thin criterion file, the shape rehearse acs reads
# AC: invalid-email-rejected
**Status:** accepted
**Verifies:** checkout#req:reject-invalid-email
## Statement
An email that is not a valid address is rejected, and the order is not placed.
Scenario file, from the scenario specification
# Scenario: Reject an invalid email
**Validates:** [checkout#ac:invalid-email-rejected](../README.md#ac-invalid-email-rejected)
## Steps
GIVEN the shopper is on the checkout form
WHEN the shopper submits the email "not-an-email"
THEN the page shows "Enter a valid email address" beside the email field
AND the order is not placed
Rehearse scenario, the shape in the Rehearse guide
# Rehearse: Reject an invalid email
## Given a submitted email that is not a valid address
```bash
printf 'email=not-an-email\n' > form.txt
```
## When the response is recorded
```bash
printf 'Enter a valid email address\n' > out.txt
```
## Then the message is the invalid-email text
```bash
grep -q 'Enter a valid email address' out.txt
```
The four samples are illustrations of those shapes. They are not a complete feature tree, and this page does not claim they pass specscore spec lint on their own. A feature README also needs its other required sections before that command is clean.
If you want a question list for drawing the criterion out of a vague request, use Asking good questions. The login walkthrough shows one thin criterion, session-hardened, in place among the other artifacts.
FAQ
What are acceptance criteria?
Acceptance criteria (AC) are the conditions that must be true before a change counts as done. In SpecScore's acceptance criteria specification they are abstract: they say what must be true, and they leave the concrete steps to a scenario.
What is Given/When/Then?
Given/When/Then is the step shape in Gherkin: Given a starting state, When one action happens, Then an observable result follows. Cucumber documents the keywords in the Gherkin reference. SpecScore's scenario specification uses the uppercase keywords GIVEN, WHEN, THEN, and AND, with exactly one WHEN per scenario file.
How do I write acceptance criteria for an AI coding agent?
Write one observable behaviour, with the actor, the input, and the result, and with no vague words. Put sample data in a scenario, not in the abstract criterion. The agent can then implement against the criterion and check the scenario.
Are acceptance criteria the same as test cases?
No. A criterion states the condition. A test case, or a SpecScore scenario, is one run with concrete inputs that shows the condition holding or failing. Several scenarios can support one criterion.
Does specscore spec lint judge vague wording?
No. In CLI v0.39.1, the release this repository's CI installs, ac-heading-format checks the ### AC: heading. It does not score words like "graceful" or "fast", and it does not warn when a criterion contains Given/When/Then. Wording is a writing task. The validators page separates that deterministic lint from the manual AI review.
Where do Given/When/Then steps go in SpecScore?
In a scenario, not in the acceptance criterion. The acceptance criteria specification forbids Given/When/Then steps inside the criterion. The scenario specification puts them in _tests/, under ## Steps. Rehearse runs a related shape: headings and fenced commands under _tests/, documented in the Rehearse guide. The linter does not enforce the step keywords.
Sources
Read 3 November 2026. Third-party pages are quoted only for the definitions they publish.