Requirements: prose for humans versus criteria for agents
A ticket written for a colleague and a ticket written for an agent look almost identical and behave completely differently. The gap between them is where most agentic rework comes from.
Here is a ticket. It is a perfectly good ticket. I have written hundreds like it and so have you.
Add rate limiting to the public API. We are seeing abuse from a handful of clients hammering the search endpoint. Add sensible rate limits so this stops. Should return a proper error when limits are exceeded. Talk to Dana if you need context on the current traffic patterns.
Hand that to a mid-level engineer who has been on the team six months and you will get something reasonable back. They will look at the traffic, notice that search is the problem but that the export endpoint is the actually expensive one, pick a limit that does not break the mobile client, return a 429 with a Retry-After header because that is what the ecosystem expects, add the limit to the docs, and mention in the pull request that they left the internal service tokens exempt because otherwise the nightly sync would break.
Almost none of that was in the ticket.
Hand the same ticket to an agent and you will get rate limiting on the public API, sensible limits, and a proper error. Which is exactly what you asked for, and possibly not remotely what you wanted. Maybe it picks 100 requests per minute globally rather than per client. Maybe it returns a 400. Maybe it rate-limits the internal token too and the nightly sync fails at 2am on Thursday.
The engineer did not read the ticket better. The engineer read a different document: the ticket plus six months of context, plus knowledge of who Dana is, plus an instinct that internal tokens are usually exempt, plus a memory of the last time somebody broke the mobile client. The ticket was a pointer into a shared body of understanding. It was never the specification. We just never had to notice that, because there was always a human standing behind it holding the rest.
The engineer read a different document: the ticket, plus six months of context, plus knowing who Dana is, plus an instinct that internal traffic should be exempt.
Requirements written for humans are deliberately underspecified, and that was a feature. Full specification is expensive; it was cheaper to write "sensible limits" and rely on a reader who would supply the rest.
Prose is a pointer, not a spec
This is the thing to internalise, and it explains most of what feels weird about writing work for agents.
Requirements written for humans are deliberately underspecified, and that is a feature. Full specification is enormously expensive. It was cheaper to write “sensible rate limits” and rely on a colleague’s judgement than to enumerate every decision, and in a human team that trade was correct. The cost of the ambiguity was paid at review time by someone who could tell good judgement from bad, and mostly it was fine.
The economics of that trade have inverted. Specification is still expensive. But the cost of ambiguity has gone up sharply, because the entity filling the gap has enormous fluency and no stake, no memory of Thursday, and no instinct that something feels off. It will fill the gap plausibly. Plausible-and-wrong is much more expensive to catch than obviously-wrong, and it arrives faster and in larger quantities.
So the question is not “should we write better tickets.” Everybody has always said yes to that and nobody has ever done it, because the incentive was not there. The question is what specifically changes about the artefact.
Three things a criterion has that prose does not
I have come to think there are exactly three properties that separate a requirement that works on an agent from one that does not. Not style. Not length. These three.
It is decidable. Someone or something can look at the result and say yes or no without needing to interpret. “The checkout works correctly” is not decidable. “A checkout with an expired card returns HTTP 402 and does not create an order record” is decidable. The test is simple: could two reasonable people, or one reasonable person and one machine, disagree about whether it was met? If yes, it is prose.
It is bounded. It says what is out of scope as clearly as what is in it. Human requirements almost never do this, because the reader’s judgement supplies the boundary. Agents have no boundary instinct at all, which is why under-scoped tickets come back with three unrelated refactors attached. “Do not change the response shape of any existing endpoint” is a sentence that costs you eight seconds and saves you a review cycle.
It carries its own evidence. It names how you would know. A criterion that says “the nightly sync continues to work” is decidable in principle but expensive to check. A criterion that says “the integration test sync_uses_internal_token passes” is decidable and cheap, because the evidence is an artefact that already exists in the pipeline. This is the property that turns acceptance criteria from documentation into an interface.
Notice that none of these require a formal language. You do not need Gherkin, you do not need a DSL, you do not need to turn your backlog into a proof assistant. You need sentences that are decidable, bounded, and attached to evidence. Most teams could rewrite their next ten tickets to that standard this afternoon.
Rewriting the rate limit ticket
Take the example above and do the work. This is what it looks like when you actually apply the three properties, and I want to show it at full length because the abstract version always sounds easier than it is.
Add per-client rate limiting to the public API.
Context: a small number of external clients are issuing high-volume requests to
/v1/search, degrading latency for everyone. Current p99 on that endpoint is roughly ten times its median during those bursts.In scope
- Rate limiting applies per API key, not globally and not per IP.
- Limits: 60 requests per minute on
/v1/search, 10 per minute on/v1/export, 600 per minute on everything else under/v1.- Exceeding a limit returns HTTP 429 with a
Retry-Afterheader in seconds and a JSON body matching the existing error envelope.- API keys with the
internalscope are exempt from all limits.- Limit state is shared across application instances (we run four).
Out of scope
- No changes to authentication, to the error envelope shape, or to any endpoint outside
/v1.- No admin UI for configuring limits. Values may be hardcoded or config-driven; do not build a management surface.
Acceptance
test_rate_limit_per_key_search: 61st request within a minute from one key returns 429; a request from a second key in the same window returns 200.test_rate_limit_internal_exempt: 200 requests in a minute with aninternalscoped key all return 200.test_retry_after_header: the 429 response includesRetry-Afterwith an integer value between 1 and 60.- Existing test suite passes with no modifications to existing test files.
- p99 latency on
/v1/searchunder the standard load test does not regress by more than 5%.
That took maybe twelve minutes to write. It is roughly five times longer than the original. And it is, I would argue, the single highest-leverage twelve minutes anyone on that team will spend that day, because it converts a work item that would have produced two rounds of “no, not like that” into one that either comes back correct or comes back visibly incorrect in under an hour.
Notice something about the rewrite: almost every line in it is a decision that a human engineer would have made silently. Per key, not per IP. Internal exempt. Shared state across instances. Do not build an admin UI. The specification did not create new work. It surfaced work that was always happening, just invisibly, inside one person’s head, where nobody could review it and nobody could disagree with it before the code got written.
That is the actual argument for writing this way, and it is worth separating from the agent argument entirely. Even on a fully human team, moving those decisions from implementation time to specification time is a straightforward win, because a bad decision costs eight seconds to fix in the ticket and half a day to fix in the diff. Agents did not create this benefit. They just raised the price of not taking it high enough that teams finally do.
Almost every line in the rewrite is a decision a human engineer would have made silently. Per key, not per IP. Internal exempt. Shared state across instances.
Even on a fully human team, moving those decisions from implementation time to specification time is the win. The agent argument is a reason to finally do it, not the reason it is right.
The thing nobody warns you about
Here is the part that catches teams by surprise: writing decidable criteria will expose that your product decisions are not actually made.
You will sit down to write “what does done mean for this” and you will find you do not know whether the limit should be per key or per IP, and you will discover that nobody knows, and that the reason nobody knows is that this question has never been asked out loud, and that it has been silently resolved a different way in three different parts of the codebase. The specification did not create that problem. It found it.
Teams experience this as the new process being slow. It is not the process being slow. It is the accumulated ambiguity of the last two years arriving all at once, because for the first time something is refusing to paper over it with judgement. This gets better. The first month is genuinely painful and the third month is not, because most of the recurring ambiguities are structural and once you have decided them they stay decided.
Who writes them
An organisational point that matters more than it sounds. The person who writes machine-checkable acceptance criteria needs product context and technical judgement in the same head. That is not typically the product manager, who has the first and not the second, and it is not typically the engineer picking up the ticket, who has the second and is downstream of the first.
The pattern that seems to work is a pairing: product states the outcome and the constraints, engineering converts them into decidable criteria with named evidence, and the conversion itself is where the disagreement surfaces. It looks a bit like the old three amigos practice from BDD, which is unsurprising, because BDD was right about the mechanism and wrong about the market. It asked teams to pay a large specification cost at a time when a competent human would fill the gap for free. Nobody sustained it. The cost of the gap has now changed, and the practice suddenly pays for itself.
If you already tried BDD and abandoned it, you are not being asked to admit you were wrong. You were right then. The trade moved.
Where this breaks down
Several honest limits.
Not all work decomposes. Research spikes, exploratory prototyping, performance investigations where the goal is to learn rather than deliver, design work where the criterion is genuinely “does this feel right”: none of these produce decidable acceptance criteria without becoming a parody of themselves. A team that insists everything be machine-checkable will either write fake criteria for this work or, worse, stop doing it. Both outcomes are bad. Explicitly mark exploratory work as exempt and let a human own the judgement.
Over-specification is its own failure mode. There is a version of this that goes too far, where the ticket describes the implementation rather than the outcome, and you have written the code in prose and then asked something to transcribe it. That is slower than writing the code, and it locks out better approaches. The line is roughly: specify observable behaviour and constraints, not internal structure. If your criteria mention class names, you have crossed it.
Decidable criteria can still be wrong criteria. This is the deepest limit. Everything above makes it much harder to build the thing incorrectly and does nothing whatsoever to stop you building the incorrect thing. A perfectly specified, fully verified, elegantly delivered feature that no user wanted is still waste, and the specification discipline can actively hide this by making the process feel rigorous. Rigour about the wrong question is a well-documented way to fail confidently.
Evidence can rot. Criteria that name tests are only as honest as the tests. A criterion pointing at a test that has been quietly weakened, or that never asserted what its name implies, gives you a green check and no information. The next piece in this series about generated tests deals with this at length, and it is a genuinely unsolved problem rather than one with a tidy answer.
And this is more work. Not “feels like more work” but is. Twelve minutes a ticket, times a lot of tickets, is a real cost to a real budget, and the payoff shows up as absence: rework that did not happen, review cycles that were not needed. Absence is politically weak. It never shows up in a chart, and the person who spent the twelve minutes gets no credit while the person who shipped the fast wrong thing gets to demo it. That asymmetry kills more specification practices than any technical limitation.
The takeaway
Requirements written for humans were pointers into shared context, and they worked because a colleague held the rest. Agents have the fluency and none of the context, so the pointer gets dereferenced into something plausible and unowned.
The fix is not more documentation. It is criteria with three properties: decidable, bounded, and carrying their own evidence. Written that way, acceptance criteria stop being documentation and become the interface between intent and implementation, and most of the decisions that used to happen invisibly during coding move somewhere they can be reviewed.
That relocation has a consequence downstream. If most of the meaningful decisions now happen at specification time, and the volume of code arriving for review has gone up tenfold, then reading every diff line by line stops being either possible or the best use of anyone’s attention. The next piece is about what code review becomes when you can no longer read it all: the shift from gatekeeper to sampler, and why that is less of a surrender than it sounds.