Writing Acceptance Criteria in BDD¶
Acceptance Criteria (AC) state when a ticket is "done". The workflow enforces BDD format (Given / When / Then) because it forces clarity on three things every reviewer needs to know: the starting state, the action, and the observable outcome.
This file is the reference the workflow uses to validate AC quality and to teach the user (assisted mode) how to phrase scenarios well.
Exception — Epics. Epics describe the goal of a feature and are broken down into Stories/Tasks that each carry their own BDD AC. On Epics, AC is optional and not BDD: render it as a free-form bulleted list of goals/outcomes if the user provides content, or omit the section entirely. The BDD guidance below applies to Bug / Task / Story / custom types — not Epics.
The shape¶
## Scenario 1: <short title that summarizes the case>
- **Given** <preconditions / context>
- **When** <action that triggers the behavior>
- **Then** <expected outcome — what someone could verify>
- **And** <optional extra Given, When, or Then>
Each ticket needs at least one scenario. Most tickets have 2–4. If you find yourself with more than 6, the ticket is probably too big — split it.
What each clause must do¶
Given — the world before the action¶
Describe the state the system is in just before the action happens. Not history, not narrative — current state. Examples:
- "Given a user with an active session"
- "Given an issuer with KYC status pending"
- "Given the feature flag mfa-enforce is on"
If the Given is "the user opens the app", that is actually a When, not a Given. The Given would be "the app is reachable and the user is logged in".
When — the action¶
A single action that triggers the behavior under test. Usually one verb. Examples:
- "When the user clicks Submit"
- "When a webhook arrives with status confirmed"
- "When the cron job runs at 03:00 UTC"
If the When has "and then" inside it, you probably have two Whens — split into two scenarios or chain with And.
Then — the outcome¶
Something observable. A reviewer or QA engineer should be able to read the Then and know what to verify. Examples:
- "Then the user is redirected to /mfa-setup"
- "Then the issuer's status changes to active"
- "Then a Slack alert is posted to #oncall-platform"
Avoid Thens that talk about implementation ("the function returns true") — talk about observable effects.
And — extending any of the three¶
Use And to chain multiple Givens, Whens, or Thens within the same scenario. Use it sparingly: if you need three Ands in the Then, the scenario is probably doing two things and should be split.
Examples — good vs. bad¶
Good¶
## Scenario 1: MFA-expired user is redirected to setup
- **Given** a logged-in user whose MFA token expired more than 24 hours ago
- **When** the user navigates to `/dashboard`
- **Then** the user is redirected to `/mfa-setup`
- **And** the original target URL is preserved as a query param
Why it's good: clear preconditions, one action, observable outcome, the And refines the Then with another verifiable detail.
Bad — Given is actually a When¶
- **Given** the user logs in with expired MFA
- **When** they see the dashboard
- **Then** they get redirected
Why it's bad: "logs in" is the action (When). "Sees the dashboard" is not an action, it's an outcome. "Gets redirected" is too vague — redirected where?
Bad — implementation in the Then¶
- **Given** a request to `/api/users/:id`
- **When** the controller is called
- **Then** `userService.findById()` is invoked
Why it's bad: the Then is internal. A QA engineer cannot verify that without reading the code. Rewrite as observable: "Then the response body matches the user record".
Bad — multiple actions¶
- **Given** an empty cart
- **When** the user adds an item AND then removes it AND then adds it again
- **Then** the cart contains the item once
Why it's bad: the When chains three actions. Either split into three scenarios (each verifying one step) or rewrite as a sequence with explicit Ands.
Multiple scenarios per ticket¶
A ticket usually needs more than one scenario to feel complete. Common patterns:
- Happy path + main alternative. First scenario = the success path. Second = the most important failure or branch.
- Per role. If behavior differs by role (admin vs. investor vs. issuer), one scenario per role.
- Per state. If behavior differs by ticket state (KYC pending vs. approved vs. rejected), one scenario per state.
Don't add scenarios for trivial permutations. If the rule is "the same for all 50 US states", write one scenario and note "applies to all US states" — don't write 50.
Assisted mode — what the workflow does¶
In assisted mode the user describes the desired behavior in natural language and the workflow structures it into BDD. The workflow should:
- Identify scenarios. Look for "and", "or", "but if", "unless" — those usually mark scenario boundaries.
- Tag clauses. Map preconditions → Given, the trigger → When, the outcome → Then.
- Render and confirm. Show the structured scenarios and ask if the user wants to adjust, add, or remove any.
If the user's description is ambiguous (e.g. unclear When, missing Then), ask a clarifying question instead of guessing. Bad AC is worse than slow AC.
Common pitfalls¶
- Vague Thens. "It works correctly", "the user sees the right thing" — useless. Always: what is the observable change?
- Conflating Given with When. If something happens, it's a When. Givens are facts about the state, not actions.
- Mixing positive and negative cases in one scenario. "If X then A, otherwise B" should be two scenarios.
- Skipping AC because "it's obvious". It is never obvious to the person reviewing the PR three weeks from now.
- Including UI copy in AC. "Then the button text is exactly 'Submit Order'" couples the AC to copy that may change. Talk about behavior, not phrasing.
When AC are mandatory but the ticket is genuinely simple¶
Some tickets really are simple: a config bump, a copy change, a one-line dependency update. In those cases AC can be a single scenario like:
## Scenario 1: Dependency is updated
- **Given** the current version of `@securitize/foo` is `1.2.3`
- **When** the upgrade is merged
- **Then** the version in `package.json` is `1.3.0`
- **And** the test suite passes
That is enough. The workflow should still ask for AC — it just won't fight if the user keeps it minimal.