Acceptance criteria an agent can verify, not just read

Stefan-Iulian Tesoi · · 7 min read

A worker measuring a metal part clamped in a vise, checking the finished thing against the figure it was supposed to hit rather than against an impression

An acceptance criterion is verifiable when it names something you can run and an outcome you can observe. "Search works correctly" is neither. "GET /api/search?q=lime returns 200 with at least one result whose name contains lime" is both, and the difference is what decides whether a coding agent can check its own work before handing it back.

Most acceptance criteria fail that test. They are written to be read by a person who already knows what the feature is for, and they work perfectly well in that setting, because the reader supplies everything the sentence left out. An agent supplies nothing. It either finds an instruction it can execute or it guesses.

What makes an acceptance criterion verifiable?

Two parts, and both have to be present: an action that can be performed, and an outcome that can be observed without judgement.

The action is a command, a request, a navigation — something with a subject and a verb that a machine can carry out. The observation is a value, a status code, a rendered string, a passing suite. If checking the criterion requires someone to form an opinion, it is not a criterion; it is a preference, and it belongs in the description where it will not be mistaken for a gate.

This is the same standard a test already meets, which is why the fastest route to testable acceptance criteria is usually to write the criterion as if you were about to write the test. Gherkin has formalised that shape for two decades — given a state, when an action, then an observable result — and its value here is not the syntax but the discipline of naming all three.

Five acceptance criteria examples that cannot be checked

Each of these appears on real backlogs. The replacement is not longer; it is decided.

Cannot be checkedWhat it usually meansWhat an agent can verify
Search works correctlyNobody has said which query, or what counts as a resultGET /api/search?q=lime returns 200 and at least one item whose name contains "lime"
The page loads quicklyQuickly compared to what, measured howLCP under 2.5s on the throttled mobile profile already in the Lighthouse CI run
Errors are handled gracefullySome errors, somewhere, somehowPosting an empty body returns 422 with {"error":"name is required"} and writes nothing to the database
Refactor the billing moduleThe shape should change and the behaviour should notpnpm test billing passes unchanged, and no exported name or signature in src/billing changes
The UI matches the designThe author has a picture in their headThe component renders at 320px with no horizontal overflow and uses only tokens from tailwind.tokens.js

The pattern in the left column is that every one of them is a true statement of intent. Nothing there is wrong. What is missing is the decision — which query, which threshold, which error — and the decision is the part that takes the time. Leaving it out does not remove the work; it moves it to whoever picks the item up, at the moment they are least equipped to make it.

How do you write criteria for work with no test surface?

Name the artefact that changes and the property it must have, rather than reaching for a test that does not exist.

Plenty of real work has no assertion to write: a copy change, a dependency bump, a migration, a document. The mistake is concluding that such an item cannot carry acceptance criteria and writing "looks right" instead. What it cannot carry is a unit test. It can still carry an observation:

None of that requires a test surface. It requires deciding, once, what would count as evidence.

How many acceptance criteria is the right number?

Three to six for a normal item. Below three usually means the item has not been thought through; above eight usually means it is two items.

The count matters more than it looks, because criteria are also the backlog item's scope. Every one of them is a thing the work must do, and an item carrying twelve of them cannot be finished in one pass or reviewed in one sitting. When we see a long list, it is almost always because two pieces of work were merged by a shared noun — "the settings page" — rather than by a shared outcome.

A criterion that would not send the work back if it failed is not a criterion. It is a note, and notes belong in the description.

That is the test worth applying to each line before an item goes to a coding agent. Criteria are a gate, and a gate that never closes is decoration.

What should the agent return as evidence?

The command it ran, the result, and enough scope for a reviewer to re-run it rather than trust the summary.

A criterion written as an observable outcome makes this automatic: the agent ran something, and what it ran is what the criterion named. In Laimonade this comes back attached to the item — the checks that were run, each with its command and outcome, including the ones that failed. A run that is not reported is treated as a run that did not happen, which is why the format asks for the command rather than a description of it.

The part people are usually surprised by is that closure is still not automatic. An agent can move an item as far as In Review and no further; a person takes the last step. Evidence makes that step fast: verification becomes reading what was run against what was asked, rather than reconstructing intent from a diff. The sprint workflow describes where that handover sits, and the MCP tools reference lists what an agent can actually do with an item.

A definition of done for AI agents is not a different concept from the one your team already has. It is the same concept with the ambiguity removed, because the reader changed and the new reader cannot ask a follow-up question. The four defects that make a backlog an agent can actually read are mostly this defect, item by item.

Frequently asked questions

Should acceptance criteria name specific test files?

Name the command, not the file. pnpm test billing survives a file being split or renamed; billing.test.ts passes does not, and an agent that cannot find the file has to decide whether to create it. Naming a command also keeps the criterion honest about scope: it says what must pass, rather than implying that one file is the whole check.

Can a criterion be subjective, like "feels fast"?

Not as a criterion, though the intent behind it is usually real and worth keeping. Move it to the description and replace it with the measurement you actually meant — a threshold, a profile, a percentile. If no number can be agreed, that is a decision nobody has made yet, and handing it to an agent does not make it.

Who writes the criteria, the agent or the person?

Laimon drafts them and a person accepts them. Drafting is the repetitive part and the part that decays on a busy week; deciding what "done" means for this item is a product judgement and stays with a human. Where a fact is missing, the draft names the gap rather than inventing a plausible threshold, because a guessed criterion is worse than an absent one.

What happens when a criterion turns out to be wrong mid-build?

It comes back rather than being quietly reinterpreted. A criterion that cannot be met as written is a specification defect, and about half of what teams label as rework is exactly that. Sending the item back with the conflict named costs minutes; letting an agent pick the interpretation it likes costs a rebuild, and hides the defect in the next item too.