Acceptance Criteria (Given/When/Then)
Overview
Acceptance criteria are the contract QA tests the product against and the PM verifies at UAT. If a criterion isn't executable, it can't be verified — and unverifiable criteria are where defects hide.
Core principle: Every criterion is an observable behavior QA can execute with a command or a click path. No aspirational adjectives.
The shape
Given [precondition], When [action], Then [measurable result].
Where they go
Criteria live in the board task's own acceptance_criteria field — an array of strings, one criterion per item, passed to create_board_task (or update_board_task to replace the whole list). Each item becomes a checkbox the implementer ticks with set_criterion_completed, and that QA (in ready_for_qa/in_qa) and you (in pm_uat) then rule on independently with review_criterion. The tick is the implementer's claim — QA and PM never call set_criterion_completed; your verdict is review_criterion.
Never write them into description. A criteria block pasted as markdown leaves the task's checklist empty: nothing to tick, nothing the move guard can check, and the prose copy goes stale the moment the real criteria are edited.
Testability gate
For each criterion, before you save it, write down:
- who executes it — QA in in_qa, and the PM in pm_uat with browser or mobile tools (or
http_request against the task's own preview when the surface is an API, not a screen);
- the action — a click path, a URL, or a request;
- the exact observable — text, a count, an element state, or a file.
If the behaviour is user-facing, the Then must be visible in the UI — not a status code standing in for what the user sees. If nobody outside the code can observe it, it belongs in technical_description, not in a criterion.
Declarative When, concrete Then
Write the When as what happens, not the click-by-click path — the cucumber test: "Will this wording need to change if the implementation changes?" If yes, it's too imperative. One behaviour per criterion. Put exact copy strings in quotes in the Then; "an error appears" is not testable, "'Title must be at most 200 characters' appears under the field" is.
Never a criterion: board actions
A criterion states what the finished WORK is, never what the board does around
it. These are not criteria and the tools drop them:
| Not a criterion | Why | Write instead |
|---|
| "Task moved to ready_for_qa / analiz_review / done" | a column move is workflow; the agent's role already tells it where the card goes | the behaviour that makes the task ready |
| "Implementation tasks are created" | they are opened AFTER this task is approved — a criterion nobody can tick before the hand-off keeps the card out of done forever | what the plan those tasks come from must contain |
| "Spec attached with add_task_document" / "questions asked with add_task_comment" | naming the tool describes the mechanics, and an attached document is not a good one | what the document must say |
| "Presented for human approval" / "assigned to the developer" | hand-offs are the flow's job | nothing — the flow does it |
The test: could a reviewer check it by looking only at the product and the
documents, without opening the board's history? If not, it is not a criterion.
Rules
- Observable + measurable. "Then a 422 with message 'title too long'" — not "Then it handles it gracefully."
- At least one negative criterion where the feature accepts input or has permissions: invalid input, unauthorized access, empty state. Not required for a pure copy or styling change.
- 3–7 criteria per task. More means the task should be split.
- No 'fast'/'user-friendly'/'robust' — replace with a number or a concrete behavior.
Mid-flight edits
update_board_task(acceptance_criteria=…) replaces the whole array and resets every tick on it — use it only before work starts, or when the human explicitly asked for the change. To drop one criterion from a task already in progress (descoped, superseded, covered elsewhere), use cancel_criterion with a reason instead of rewriting the list.
Worked Example
Feature: reject task titles over 200 chars, in the New Task dialog.
❌ Given a create-task form, When I submit a 201-char title, Then I get 422.
(status code stands in for what the user sees — fine for an API-only task, not for a dialog)
✅ AC1 (happy) Given the New Task dialog,
When I enter a 200-character title and press Create,
Then the dialog closes and a card with that title appears on the board.
✅ AC2 (boundary/negative)
Given the New Task dialog,
When I enter a 201-character title and press Create,
Then the dialog stays open, shows "Title must be at most 200 characters"
under the field, and no card is added to the board.
✅ AC3 (auth/negative)
Given a user without write access to the project,
When they open the board,
Then the "New Task" button is not shown.
Keep one API-surface example only when the API itself is the product (usually a task_type: "technical" task):
Given a member of the project, When they GET /api/v1/projects/:id/tasks/export,
Then they get 200 + text/csv with header "id,title,status,created_at"
Common Mistakes
- A board action as a criterion (see above) — the most common one is "the task is moved to X".
- An HTTP status as the only Then on a user-facing task — say what the user sees, not the wire protocol underneath it.
- Aspirational wording ("intuitive", "fast").
- Only happy-path criteria, no negative, on a task with input or permissions.
- A "Then" with no exact text, count or state — "an error appears" instead of the exact copy.
- 12 criteria on one task → split it.
- Criteria pasted into
description as a markdown list instead of passed as acceptance_criteria.
- Rewriting
acceptance_criteria on a task already in progress instead of cancel_criterion.
Red Flags
- QA asks "how do I test this?" → the AC isn't executable; run it through the testability gate again.
- No negative/auth criterion on a task that takes input or checks permission.
- An adjective in a "Then".
- A criterion that names a board tool or a column the card must reach.