The "so that" clause
Day 24: The "so that" clause is the requirement, and coding agents drop it
Hand a coding agent "As a clinic admin, I want X so that Y" and it builds X, and builds it well. It treats Y, the part that explains why the feature exists, as decoration. If your product intent lives only in that second clause, it disappears somewhere between the backlog and the merged PR. Nothing in your pipeline will tell you it's gone.
1. The hook: a CSV export that did everything except its job
Here's the ticket exactly as it sat in the backlog of a clinic-management product. It went to a coding agent word for word:
Title: Export appointments to CSV
As a clinic admin,
I want to export appointments to CSV
so that I can reconcile no-shows with billing.
Acceptance criteria:
- Admin can choose a date range
- Export downloads as .csv
- Includes patient name, practitioner, date, timeThe agent shipped it in about an hour, with a date-range picker, a clean download, the right columns, tests and a tidy PR description. Review took five minutes and it was merged. Then the first admin opened the file and looked for the column showing whether the patient had actually turned up. It wasn't there. Neither was a billing reference. Every acceptance criterion passed, but the file couldn't answer the one question it was built for: which no-shows did we bill?
2. Autopsy in three lines
- ▹What the agent read: "I want to export appointments to CSV" plus the acceptance criteria. Both were concrete and checkable, and it met both.
- ▹What it skipped: "so that I can reconcile no-shows with billing". That clause quietly requires attendance status and a link to billing. Neither one made it into anything a reviewer could check.
- ▹Why a human developer wouldn't have skipped it: they'd have caught you at the coffee machine and asked, "Reconcile how? Against what? Do you need the invoice ID in there?" That five-minute chat is where the real spec used to come from.
3. The Day 24 point: "so that" was always the requirement
A user story has two halves, and they do very different jobs. "I want X" is the customer's (or the PM's) guess at a solution. "So that Y" is the outcome they actually need. Earlier in this series we split outcomes from outputs: an output is something you ship, and an outcome is a change in what someone can do. The CSV is the output. Being able to reconcile no-shows against billing is the outcome.
None of this is new. For years teams have put outcome-shaped intent in the least enforceable part of the ticket. It mostly worked because humans read the whole sentence and filled in the gaps. Agents didn't create the problem. They expose it quickly and cheaply, and they'll repeat it on every ticket you give them.
4. Why agents in particular do this
You don't need to know anything about how models work inside to see this pattern. A coding agent such as Claude Code or Cursor is very good at meeting the most concrete instruction it's given, and "export to CSV with these four columns" is about as concrete as instructions get. Next to it, "so that I can reconcile" is vague, so the agent treats it as background rather than something to deliver. It also rarely pushes back the way a colleague would. Unless you've told it to stop and ask, it fills any gap with a sensible default and keeps going. The result looks right: it compiles, the tests pass and the diff reads cleanly. A quick review checks whether the code does what the ticket says, and it does. Nobody checks whether the ticket asked for the right thing.
5. The inversion: make Y the test and X the suggestion
The product-minded fix is to flip the story. Rewrite the "so that" as an outcome someone can check, and make it the acceptance test. Move the "I want" down to a suggested approach that the implementer, human or agent, is allowed to challenge. Here's the same ticket, inverted:
Title: Admins can reconcile no-shows against billing
Outcome (this is the acceptance test):
Given last month's appointments and billing records,
an admin can identify every no-show that was billed,
without opening another tool.
Who it's for:
Clinic admins doing month-end billing review.
Suggested approach (non-binding):
CSV export of appointments. Open to alternatives.
If the export can't meet the outcome, say why before building.
Out of scope:
Automatically refunding or voiding invoices.
Changing how no-shows are recorded.
How we'll know it worked:
An admin completes last month's reconciliation using only
this feature, and finds billed no-shows we didn't know about.6. Side by side: same feature, two tickets, two results
- ▹Headline. Old: "Export appointments to CSV". Inverted: "Admins can reconcile no-shows against billing".
- ▹What counts as done. Old: a .csv file with four named columns. Inverted: an admin can find every billed no-show.
- ▹How the solution is framed. Old: fixed, it's a CSV. Inverted: suggested, with CSV as the starting guess and alternatives welcome.
- ▹What the agent built from the old ticket: a correct CSV with no attendance data and no billing link. It passed review and was useless.
- ▹What it built from the inverted ticket: the same CSV plus an attendance-status column (attended / no-show / cancelled). It also left a note in the PR: billing IDs weren't in the appointments model, so the export couldn't be joined to invoices without a schema change. Then it asked whether to add the join or scope it out.
The second run didn't solve everything at once. It did surface the real constraint, the missing billing ID, before the merge and not after an admin complained. You want that question to come up without anyone having to ask for it.
7. The template you can copy
## Outcome (testable)
Given <starting state>, <who> can <do/know something> <constraint>.
## Who it's for
<Specific role and the moment they need this>
## Suggested approach (non-binding)
<Your best guess at a solution. Implementer may propose alternatives.
If this approach cannot satisfy the Outcome, flag it before building.>
## Out of scope
- <Things a reasonable implementer might add but shouldn't>
## How we'll know it worked
<Observable signal after release, in real usage>- ▹Outcome (testable): the actual requirement, written so you, a reviewer or an agent can say pass or fail. If you can't write it, you don't know what you want yet.
- ▹Who it's for: tells the implementer whose workflow to optimise for. An admin at month-end wants very different things from the same data than a practitioner fitting it in between patients.
- ▹Suggested approach (non-binding): keeps your useful guess without turning it into a contract. The 'flag it before building' line is what gives the agent permission to push back.
- ▹Out of scope: give an agent an open-ended goal and it will happily over-build. This field keeps the work contained and the review manageable.
- ▹How we'll know it worked: moves the definition of success from 'merged' to 'used'. It also gives you something to check after launch, which ties into the metrics days later in the series.
8. When not to bother inverting
- ▹Purely technical work with no user outcome, like bumping a dependency or renaming a module. Write a plain task and don't invent a fake 'so that'.
- ▹Cases where X really is the requirement, such as a contract or regulation that names a specific format. Then X goes in the Outcome, with a note on why.
- ▹Pitfall: an Outcome that just restates X ('Given the page, an admin can download a CSV'). If your Outcome mentions your solution, you haven't inverted anything.
- ▹Pitfall: a vague Outcome ('admins have better visibility'). If nobody could fail it, it isn't a test.
9. The 5-minute exercise
Open your current backlog and pick three tickets you'd happily hand to an agent today. For each one, try rewriting the 'so that' as a single Given / can / without sentence that someone could fail. Give yourself five minutes on a timer.
- ▹If you can write it, put it at the top of the ticket as the Outcome and move the 'I want' down to Suggested approach.
- ▹If you can't, the story isn't ready for an agent or for a human. A person would have hit the same gap and quietly made something up.
- ▹Expect at least one of the three to fail. That doesn't mean you write bad tickets. It means the template let you skip the hard part.
10. What this shows
Agents didn't break user stories. They removed the person who had been quietly fixing them: the developer who read the 'so that', asked the awkward question and built the right thing anyway. Once that person is out of the loop, the ticket has to carry the intent by itself, and inverting the story is how you make that happen. Day 25 takes on the next question. Once the Outcome is the test, how do you review an agent's output against it, not just against whether the diff looks right?
Extend your knowledge
- ▹Read Jeff Patton's 'User Story Mapping', especially the idea that stories are prompts for a conversation and not specifications. Then ask yourself what replaces that conversation when the implementer is an agent.
- ▹Look at Gherkin's Given/When/Then format from the BDD world (the Cucumber docs, or Gojko Adzic's 'Specification by Example'). It's a well-established way to write outcomes as executable tests, and it works well for agent tickets.
- ▹Revisit the Jobs To Be Done framing in Clayton Christensen's 'Competing Against Luck'. A well-written 'so that' is basically the job the feature is hired to do.
- ▹Practical step: make the five-field template the default story template in your issue tracker. Then add one line to your agent instructions file (CLAUDE.md, Cursor rules): 'Treat the Outcome section as the acceptance test; treat Suggested approach as negotiable and flag conflicts before implementing.'
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.