Acceptance Criteria Examples — 20 Real-World Templates You Can Copy

Acceptance Criteria Examples — 20 Real-World Templates You Can Copy
Daniel Marsh · Spec-first engineering notes

Most acceptance criteria examples online are too shallow to use on a real project. This guide gives you 20 production-grade examples across five domains — authentication, e-commerce, APIs, data processing, and notifications — all in Given/When/Then format that you can copy straight into your spec.

Published on 2026-04-08 · Updated 2026-05-06 · 26 min read · Author: Daniel Marsh · Review policy: Editorial Policy

Field note: the example that catches the hidden bug

The acceptance criterion I reach for most is not the happy path. It is the duplicate action. Double-clicked submit buttons, repeated webhook deliveries, retried payment calls, imported rows seen twice. That one scenario exposes more vague thinking than a dozen success cases.

Duplicate-action criterion:
Given a user submits the same request twice within 2 seconds
When both requests reach the server
Then exactly one state change is recorded
And the second response returns the first operation id
And the audit log marks it as a replay

What are acceptance criteria?

Acceptance criteria are the specific, testable conditions that a feature must satisfy before it can be considered done. They are not user stories — a user story describes who wants what and why, while acceptance criteria describe exactly how you verify that the story is implemented correctly. They are not test cases either — test cases are the step-by-step procedures QA follows, while acceptance criteria are the requirements those test cases are derived from.

Think of acceptance criteria as the bridge between "what we want" and "how we prove it works." A user story might say: "As a customer, I want to reset my password so I can regain access to my account." The acceptance criteria specify exactly what "reset my password" means in observable, verifiable terms: what triggers the reset, what the system sends, how long the link is valid, and what happens when the link expires.

There are two common formats. The first is Given/When/Then (also called Gherkin format), which is structured and works well for behavior with clear preconditions, actions, and outcomes. The second is a checklist format — a simple bulleted list of true/false conditions. The checklist format works for straightforward configuration requirements ("the button is blue," "the field accepts a maximum of 255 characters"), but for anything with conditional logic or state transitions, Given/When/Then is more precise.

This article focuses on Given/When/Then because it is the format that scales. When you have a feature with multiple user paths, error conditions, and edge cases, a structured format forces you to be specific about each one. That specificity is what makes the difference between a spec that an engineer can implement without questions and a spec that generates three days of back-and-forth in Slack.

The Given/When/Then format explained

Every Given/When/Then criterion has three parts, and each part does a specific job:

Here is an annotated example:

Given a registered user is on the login page
      ^ precondition: the user exists and the page is loaded

When  the user enters a valid email and correct password and clicks "Sign in"
      ^ action: specific inputs + specific trigger

Then  the user is redirected to the dashboard within 2 seconds;
      the session cookie is set with HttpOnly and Secure flags;
      the last_login_at timestamp is updated in the users table.
      ^ outcomes: three observable, testable results
    

The most important quality of a good criterion is that QA can turn it into a test without asking the author what they meant. If the criterion says "the system responds quickly," that is not testable — quickly could mean 200 milliseconds or 5 seconds depending on who is reading it. If it says "the API responds within 500ms at the 95th percentile," QA knows exactly what to measure.

Here is a side-by-side comparison to make the difference concrete:

Bad

"The login should work correctly and handle errors gracefully."

This tells QA nothing. What does "correctly" mean? What does "gracefully" look like? An engineer reading this has to invent the behavior themselves.

Good

"Given a user enters an incorrect password three times in a row, when they attempt a fourth login, then the account is locked for 15 minutes; the login form displays 'Account locked. Try again in 15 minutes.'; and no further authentication attempts are processed until the lockout expires."

QA can write an automated test from this sentence alone.

With the format clear, let's look at 20 real-world examples across five categories. Each one is written at a level of detail that an engineer could implement from directly and QA could test without clarification.

Authentication examples (1–4)

Authentication is where most applications start, and where vague acceptance criteria cause the most security-related bugs. "The user can log in" is not a criterion — it is a wish. The following four examples cover the most common auth flows with the specificity that implementation and testing require.

Example 1: Login with valid credentials

Given a registered user with email "[email protected]" and a verified account
When  the user navigates to /login,
      enters "[email protected]" in the email field,
      enters the correct password,
      and clicks "Sign in"
Then  the user is redirected to /dashboard within 2 seconds;
      a session cookie named "sid" is set with the following attributes:
        - HttpOnly: true
        - Secure: true
        - SameSite: Strict
        - Max-Age: 86400 (24 hours);
      the user's last_login_at field in the database is updated
        to the current UTC timestamp;
      the login event is written to the audit_log table
        with action="login_success" and ip_address populated.
    

Example 2: Login with invalid password — account lockout

Given a registered user with email "[email protected]"
      and the account is not currently locked
When  the user submits an incorrect password 3 times consecutively
      within a 10-minute window
Then  the account is locked for 15 minutes;
      the fourth and subsequent login attempts return HTTP 429
        with body {"error": "account_locked", "retry_after_seconds": 900};
      the login form displays "Account locked. Try again in 15 minutes.";
      a login_lockout event is written to the audit_log table;
      after 15 minutes, the account is automatically unlocked
        and the user can attempt login again with the correct password.
    

Example 3: Password reset email

Given a registered user with email "[email protected]"
When  the user navigates to /forgot-password,
      enters "[email protected]",
      and clicks "Send reset link"
Then  the system sends an email to "[email protected]" within 60 seconds
        with subject "Reset your password";
      the email contains a single-use reset link in the format
        https://app.example.com/reset-password?token={token};
      the token is valid for 1 hour from the time of issuance;
      the /forgot-password page displays
        "If an account exists for that email, we've sent a reset link."
        regardless of whether the email exists (to prevent enumeration);
      if the user clicks the reset link within 1 hour,
        they are taken to a form to enter a new password;
      if the user clicks the reset link after 1 hour,
        they see "This link has expired. Please request a new one."
        with a link back to /forgot-password;
      once the reset link is used successfully,
        the same token cannot be used again.
    

Example 4: Session expiry after inactivity

Given a user is logged in with an active session
      and the session timeout is configured to 30 minutes of inactivity
When  the user performs no actions (no API calls, no page navigations)
      for 30 consecutive minutes
Then  the next request the user makes returns HTTP 401;
      the session cookie is cleared on the server side;
      the user is redirected to /login with a query parameter
        ?reason=session_expired;
      the login page displays "Your session expired due to inactivity.";
      any unsaved form data on the client is NOT recoverable
        (this is a known limitation documented in the UI).
    

E-commerce examples (5–8)

E-commerce features involve money, inventory, and customer expectations — three things where ambiguity directly translates to lost revenue or support tickets. These examples cover cart operations, discounts, inventory edge cases, and transactional emails.

Example 5: Add to cart

Given a customer is viewing product SKU-1042 ("Wireless Keyboard")
      which is in stock with available_quantity = 35
      and the product price is $49.99
When  the customer clicks "Add to Cart" with quantity = 1
Then  the cart item count in the header increases by 1;
      the cart contains one line item for SKU-1042 with:
        - quantity: 1
        - unit_price: $49.99
        - line_total: $49.99;
      the cart subtotal is recalculated and displayed;
      a brief confirmation toast appears for 3 seconds:
        "Wireless Keyboard added to cart";
      the product's available_quantity is NOT decremented at this stage
        (inventory is reserved only at checkout);
      if the customer clicks "Add to Cart" again for the same SKU,
        the existing line item quantity increments to 2
        instead of creating a duplicate line item.
    

Example 6: Apply discount code

Given a customer has a cart with subtotal $120.00
      and a valid discount code "SAVE20" exists with:
        - type: percentage
        - value: 20%
        - minimum_order: $50.00
        - max_uses: 500 (current usage: 312)
        - expires_at: 2026-12-31T23:59:59Z
When  the customer enters "SAVE20" in the discount code field
      and clicks "Apply"
Then  the discount of $24.00 (20% of $120.00) is applied;
      the cart displays:
        - Subtotal: $120.00
        - Discount (SAVE20): -$24.00
        - Total: $96.00;
      the discount code field shows a green checkmark and the text
        "Code SAVE20 applied — you saved $24.00";
      the discount code usage count increments from 312 to 313
        only when the order is placed (not when applied to cart);
      if the customer removes items so the subtotal drops below $50.00,
        the discount is automatically removed and the customer sees
        "Discount SAVE20 requires a minimum order of $50.00."
    

Example 7: Checkout with insufficient stock

Given a customer has 3 units of SKU-2087 in their cart
      and the current available_quantity for SKU-2087 is 1
      (stock decreased after the item was added to cart)
When  the customer clicks "Place Order" on the checkout page
Then  the order is NOT placed;
      the checkout page displays an error banner:
        "Some items in your cart are no longer available
         in the requested quantity.";
      the cart line item for SKU-2087 shows a warning:
        "Only 1 available — please update quantity.";
      the quantity field for that line item is highlighted in red;
      the customer can update the quantity to 1 and retry checkout;
      if available_quantity drops to 0 between page load and submission,
        the error message reads "SKU-2087 is out of stock"
        and the line item shows a "Remove" button only;
      no payment is captured in any insufficient-stock scenario.
    

Example 8: Order confirmation email

Given a customer with email "[email protected]" places order #ORD-88421
      containing 2 line items:
        - SKU-1042 "Wireless Keyboard" x1 at $49.99
        - SKU-3001 "USB-C Cable" x2 at $12.99 each
      with discount "SAVE20" applied (-$15.19)
      and shipping method "Standard (5-7 business days)" at $5.99
When  the payment is successfully captured
Then  an order confirmation email is sent to "[email protected]"
        within 120 seconds of payment capture;
      the email subject is "Order #ORD-88421 confirmed";
      the email body contains:
        - each line item with name, quantity, unit price, and line total
        - subtotal: $75.97
        - discount: -$15.19
        - shipping: $5.99
        - order total: $66.77
        - estimated delivery date range
        - a link to the order status page: /orders/ORD-88421;
      the email is sent from "[email protected]"
        with reply-to "[email protected]";
      if the email fails to send (SMTP error),
        it is retried 3 times with exponential backoff (1min, 5min, 15min);
      if all retries fail, an alert is sent to the ops channel
        and the failure is logged in the email_delivery_log table.
    

API examples (9–12)

API acceptance criteria are where engineers most often under-specify. "The endpoint returns the data" is not a criterion. APIs have status codes, headers, pagination contracts, error formats, and rate limits — all of which need to be specified or the consuming team will discover the behavior through trial and error in production.

Example 9: GET endpoint returns paginated results

Given the database contains 250 product records
      and the API consumer sends a valid Bearer token
When  the consumer sends GET /api/v2/products?page=2&per_page=25
Then  the response status is 200 OK;
      the response body is JSON with the structure:
        {
          "data": [ ...25 product objects... ],
          "meta": {
            "current_page": 2,
            "per_page": 25,
            "total_items": 250,
            "total_pages": 10
          }
        };
      each product object includes at minimum:
        id, name, sku, price, currency, created_at;
      the products are sorted by created_at descending (newest first)
        unless an explicit ?sort= parameter is provided;
      the response includes the header:
        Link: </api/v2/products?page=1&per_page=25>; rel="prev",
              </api/v2/products?page=3&per_page=25>; rel="next";
      if page exceeds total_pages (e.g., ?page=99),
        the response is 200 with an empty "data" array
        and meta.total_pages still reflects 10;
      per_page has a maximum of 100;
        values above 100 are clamped to 100 without error.
    

Example 10: POST with invalid payload returns 422

Given the API consumer sends a valid Bearer token
When  the consumer sends POST /api/v2/products with the JSON body:
        {
          "name": "",
          "sku": "ALREADY-EXISTS-001",
          "price": -5.00
        }
Then  the response status is 422 Unprocessable Entity;
      the response body is JSON with the structure:
        {
          "error": "validation_failed",
          "message": "The request body contains invalid fields.",
          "details": [
            {"field": "name", "issue": "must not be blank"},
            {"field": "sku", "issue": "already exists"},
            {"field": "price", "issue": "must be greater than or equal to 0"}
          ]
        };
      all validation errors are returned in a single response
        (not one at a time);
      the Content-Type header is "application/json; charset=utf-8";
      no product record is created in the database;
      the error response is logged with request_id for traceability.
    

Example 11: Rate limiting — 429 after N requests

Given the API rate limit for the "standard" plan is 100 requests per minute
      and the API consumer has made 100 requests in the current
        60-second window using API key "key_abc123"
When  the consumer sends the 101st request within the same window
Then  the response status is 429 Too Many Requests;
      the response body is:
        {
          "error": "rate_limit_exceeded",
          "message": "Rate limit of 100 requests per minute exceeded.",
          "retry_after_seconds": <remaining seconds in the current window>
        };
      the response includes headers:
        X-RateLimit-Limit: 100
        X-RateLimit-Remaining: 0
        X-RateLimit-Reset: <Unix timestamp when the window resets>
        Retry-After: <seconds until reset>;
      all successful responses (non-429) also include the
        X-RateLimit-Limit and X-RateLimit-Remaining headers
        so consumers can track their usage proactively;
      rate limits are scoped per API key, not per IP address;
      requests that return 429 are NOT counted against the rate limit
        for the next window.
    

Example 12: API versioning — deprecated endpoint warning

Given the API v1 endpoint GET /api/v1/products is deprecated
      and the replacement is GET /api/v2/products
      and the v1 sunset date is 2026-09-01
When  the consumer sends GET /api/v1/products
Then  the response status is 200 OK (the endpoint still functions);
      the response body is identical in structure to the current
        v1 contract (no breaking changes before sunset);
      the response includes the headers:
        Deprecation: true
        Sunset: Sat, 01 Sep 2026 00:00:00 GMT
        Link: </api/v2/products>; rel="successor-version";
      the API logs a deprecation_warning event with:
        api_key, endpoint, timestamp;
      after the sunset date (2026-09-01T00:00:00Z),
        requests to /api/v1/products return 410 Gone with body:
        {
          "error": "endpoint_removed",
          "message": "This endpoint was removed on 2026-09-01.
                      Use /api/v2/products instead.",
          "migration_guide": "https://docs.example.com/api/v1-to-v2"
        }.
    

Data processing examples (13–16)

Data processing features — imports, batch jobs, migrations — are where missing acceptance criteria cause the most damage. When a batch job fails at row 40,000 of a 50,000-row file and no one specified what should happen, the team discovers the gap at 2 AM during an on-call incident. These examples define the behavior for success, failure, and partial failure.

Example 13: CSV import with duplicate detection

Given an admin uploads a CSV file to /admin/contacts/import
      with 10,000 rows, each containing: first_name, last_name, email
      and 200 of those rows have email addresses that already exist
        in the contacts table
When  the import job processes the file
Then  9,800 new contact records are created;
      200 rows are flagged as duplicates and skipped (not updated);
      the import result page displays:
        - Total rows: 10,000
        - Created: 9,800
        - Duplicates skipped: 200
        - Errors: 0;
      a downloadable CSV of the 200 skipped rows is available
        at /admin/contacts/import/{job_id}/skipped
        with columns: row_number, email, reason;
      email comparison for duplicates is case-insensitive
        ("[email protected]" matches "[email protected]");
      the import job processes at a rate of at least 500 rows/second;
      if the CSV contains a malformed row (missing required email column),
        that row is counted under "Errors" and the rest continue processing;
      the import is atomic per-row, not per-file:
        if the job is interrupted at row 5,000,
        the first 5,000 rows remain committed.
    

Example 14: Batch job timeout handling

Given a nightly batch job "generate-monthly-invoices" is scheduled
      to run at 02:00 UTC
      and the job timeout is configured to 45 minutes
When  the job has been running for 45 minutes without completing
Then  the job is terminated with status "timed_out";
      all invoices generated before the timeout are committed
        (partial progress is preserved);
      a job_timeout event is written to the batch_jobs table with:
        job_name, started_at, timed_out_at, rows_processed, rows_remaining;
      an alert is sent to the #ops-alerts channel with the message:
        "Batch job 'generate-monthly-invoices' timed out after 45 minutes.
         Processed: {n} of {total}. Manual intervention required.";
      the job does NOT automatically retry
        (to prevent duplicate invoice generation);
      an admin can trigger a resumable re-run via
        POST /admin/batch-jobs/{job_id}/resume
        which processes only the remaining unprocessed records.
    

Example 15: Data migration rollback

Given a data migration "migrate_users_v2" transforms the users table
      by splitting the "name" column into "first_name" and "last_name"
      and the migration has been applied to 50,000 of 80,000 rows
When  the migration encounters a row where the name field contains
      no space character (e.g., "Madonna") and the splitting logic fails
Then  the migration halts and does NOT process remaining rows;
      the 50,000 already-migrated rows remain in the new format
        (they are NOT rolled back automatically);
      the migration status is set to "failed_at_row" with metadata:
        row_id, error_message, rows_completed, rows_remaining;
      the ops team receives an alert with the failure details;
      the admin can run the rollback command:
        POST /admin/migrations/migrate_users_v2/rollback
        which reverses the 50,000 completed rows by concatenating
        first_name + " " + last_name back into the name column;
      the rollback is idempotent: running it twice produces the same result;
      after rollback, a verification query confirms that all rows
        match their pre-migration state by comparing against
        the migration_snapshots table (created before migration started).
    

Example 16: Contact deduplication

Given two contact records exist in the same account:
        Contact A: id=1001, email="[email protected]", name="Alice Smith",
                   phone=null, created_at="2026-01-15"
        Contact B: id=1002, email="[email protected]", name="A. Smith",
                   phone="+1-555-0100", created_at="2026-03-20"
When  the deduplication job runs for this account
Then  the two contacts are merged into a single record;
      the surviving record is Contact A (earlier created_at);
      the merge logic fills null fields on the surviving record
        from the duplicate:
        Contact A.phone is set to "+1-555-0100" (was null);
      fields that exist on both records are NOT overwritten:
        Contact A.name remains "Alice Smith" (not replaced by "A. Smith");
      Contact B is soft-deleted (del_flag = 1) with a reference
        to the surviving record: merged_into_id = 1001;
      a dedup_event is logged with:
        source_id=1002, target_id=1001, merged_fields=["phone"],
        trigger="scheduled_job", account_id, created_at;
      any external references to Contact B's ID (e.g., in deal records)
        are updated to point to Contact A's ID;
      if Contact B has active automations referencing its ID,
        the merge is blocked and a dedup_conflict record is created instead.
    

Notification examples (17–20)

Notification criteria are consistently under-specified because teams treat them as simple — "send a notification." But notifications involve delivery channels, user preferences, timing, failure handling, and state management. A missing criterion on retry behavior means your users either get no notification or get five of them.

Example 17: Push notification delivery

Given a user has enabled push notifications on their mobile device
      and the user's device token is registered in the push_tokens table
      and the user has not muted notifications for the "orders" category
When  the user's order status changes from "shipped" to "delivered"
Then  a push notification is sent to all registered devices for that user
        within 30 seconds of the status change;
      the notification payload includes:
        title: "Order delivered"
        body: "Your order #ORD-88421 has been delivered."
        data: {"order_id": "ORD-88421", "deep_link": "/orders/ORD-88421"};
      tapping the notification opens the app to the order detail page;
      if the device token is invalid (provider returns "unregistered"),
        the token is removed from the push_tokens table
        and no retry is attempted for that token;
      if the push provider returns a transient error (5xx),
        delivery is retried up to 3 times with 30-second intervals;
      each delivery attempt is logged in the notification_log table
        with: user_id, channel="push", status, attempt_number, timestamp.
    

Example 18: Email notification preferences — opt-out

Given a user has opted out of "marketing" emails
      via their notification preferences page at /settings/notifications
      but has NOT opted out of "transactional" emails
When  the system triggers a marketing email
      (e.g., "New features this month")
Then  the email is NOT sent;
      the suppression is logged in the email_log table with:
        user_id, email_type="marketing", status="suppressed_by_preference";
      no bounce or error is generated (this is expected behavior).

Given the same user receives a transactional email trigger
      (e.g., "Your password was changed")
When  the email service processes the trigger
Then  the email IS sent regardless of the marketing opt-out
        because transactional emails are not subject to preference opt-out;
      the email footer includes a link to /settings/notifications
        but does NOT include an "unsubscribe" link for transactional emails
        (per CAN-SPAM, transactional emails are exempt from unsubscribe);
      the delivery is logged with email_type="transactional",
        status="sent".
    

Example 19: In-app notification badge count

Given a user has 5 unread notifications in the notifications table
      (where read_at IS NULL and user_id matches)
When  the user loads any page in the application
Then  the notification bell icon in the header displays a badge with "5";
      the badge is red (#E53E3E) with white text;
      if the count exceeds 99, the badge displays "99+";
      when the user opens the notification dropdown,
        the 5 most recent unread notifications are displayed
        sorted by created_at descending;
      when the user clicks a notification,
        that notification's read_at is set to the current timestamp
        and the badge count decrements by 1;
      when the user clicks "Mark all as read,"
        all notifications for that user have read_at set
        to the current timestamp and the badge disappears;
      the badge count updates in real time via WebSocket
        (if a new notification arrives while the page is open,
         the count increments without requiring a page refresh);
      if the user has 0 unread notifications, no badge is displayed
        (the bell icon appears without a number).
    

Example 20: Notification retry on failure

Given the notification service attempts to send an email notification
      to "[email protected]" via the SMTP provider
When  the SMTP provider returns a transient error
      (connection timeout, 5xx response, or DNS resolution failure)
Then  the notification is placed in a retry queue;
      the retry schedule uses exponential backoff:
        - Retry 1: after 1 minute
        - Retry 2: after 5 minutes
        - Retry 3: after 15 minutes
        - Retry 4: after 60 minutes;
      each retry attempt is logged in the notification_log table with:
        notification_id, attempt_number, status="retry",
        error_message, next_retry_at;
      after the 4th failed attempt, the notification status is set
        to "permanently_failed";
      a permanently_failed event triggers an alert to the #ops-alerts
        channel with: notification_id, user_id, channel, error_history;
      if the SMTP provider returns a permanent error
        (550 "mailbox not found," 551 "user not local"),
        no retries are attempted;
        the notification status is set to "permanently_failed" immediately;
        the user's email_verified flag is set to false
        and a "verify your email" banner is shown on their next login.
    

5 common mistakes in acceptance criteria

Even teams that use Given/When/Then consistently make the same set of mistakes. These five anti-patterns show up in nearly every spec review I conduct. Each one is paired with a concrete fix.

1. Too vague — "the system works correctly"

Bad

"The search feature works correctly and returns relevant results."

Good

"Given the product catalog contains 5,000 items, when the user searches for 'wireless keyboard,' then the results page displays within 800ms, shows products whose name or description contains the search terms, sorted by relevance score descending, with a maximum of 20 results per page."

"Works correctly" is not a testable statement. Replace it with specific inputs, measurable outputs, and defined thresholds. If you cannot describe what "correct" looks like in observable terms, you have not finished thinking through the requirement.

2. Implementation details instead of behavior — "uses Redis cache"

Bad

"The system caches product data in Redis with a 5-minute TTL and invalidates the cache on product update."

Good

"Given a product's price is updated from $49.99 to $39.99, when a customer views that product within 5 minutes of the update, then the displayed price is $39.99. Stale data is acceptable for up to 5 minutes on non-price fields (description, images)."

Acceptance criteria describe what the user or API consumer observes, not how the system is built internally. Redis is an implementation choice — the criterion should describe the behavior the user experiences. The engineering team decides whether to use Redis, Memcached, or an in-process cache to meet the criterion.

3. Multiple behaviors crammed into one criterion

Bad

"Given a user submits a form, then the data is validated, saved to the database, an email is sent to the admin, the user sees a success page, and the analytics event is tracked."

Good

Split into separate criteria:

"Given a user submits the contact form with all required fields, when the form is submitted, then the user is redirected to /thank-you within 1 second."

"Given a contact form submission is saved, when the save completes, then an email is sent to [email protected] within 60 seconds with the submission details."

"Given a user submits the contact form, when the /thank-you page loads, then a 'form_submitted' analytics event is fired with form_id and timestamp."

Each criterion should test one behavior. When you combine five behaviors into one criterion, a test failure tells you nothing about which behavior broke. Separate criteria allow targeted testing and clearer bug reports.

4. Happy path only — no error or edge cases

Bad

"Given a user uploads a profile photo, then the photo is saved and displayed on their profile."

Good

The happy path criterion plus:

"Given a user uploads a file that is not an image (e.g., a .pdf), then the upload is rejected with 'Please upload a JPG, PNG, or WebP image.'"

"Given a user uploads an image larger than 5 MB, then the upload is rejected with 'Image must be under 5 MB.'"

"Given a user uploads a valid image but the storage service is unavailable, then the user sees 'Upload failed. Please try again.' and the failure is logged."

Happy-path-only criteria guarantee that your tests will pass for the one scenario nobody is worried about. The bugs live in the error paths, the edge cases, and the failure modes. Write criteria for those.

5. Untestable performance claims — "fast performance"

Bad

"The dashboard loads quickly even with large datasets."

Good

"Given the account has 100,000 contacts, when the user navigates to /dashboard, then the page is interactive (Time to Interactive) within 3 seconds on a 4G connection. The contacts summary widget loads within 2 seconds. The activity feed loads asynchronously and displays a skeleton loader until data arrives."

"Fast" and "quickly" are not measurements. Define the dataset size, the network condition, and the specific metric (Time to Interactive, server response time, total load time). Without numbers, performance criteria are just aspirations that nobody can test against.

Blank acceptance criteria template

Use this template when writing acceptance criteria for your own features. Copy the block below, replace the bracketed prompts with your specific preconditions, actions, and outcomes, and add as many criteria as the feature requires. A typical feature needs 4 to 8 criteria for the main flows, plus 2 to 4 for error and edge cases.

## Acceptance Criteria

### Happy path

Given [precondition — the starting state of the system or user]
When  [action — the specific thing the user or system does]
Then  [outcome — the observable, verifiable result];
      [additional outcome if needed];
      [additional outcome if needed].

Given [different precondition]
When  [action]
Then  [outcome].

### Error handling

Given [precondition that leads to an error state]
When  [the same or similar action as the happy path]
Then  [the specific error the user sees or the API returns];
      [what is NOT affected — e.g., "no data is modified"];
      [how the error is logged or reported].

### Edge cases

Given [boundary condition — empty input, maximum value, concurrent access]
When  [action]
Then  [specific behavior for this edge case].

### Performance

Given [realistic dataset size or load condition]
When  [action]
Then  [measurable performance threshold — e.g., response time, throughput].
    

If you want the tool to generate a full spec — including acceptance criteria, edge cases, and deliverables — from a short feature description, the Spec Generator does that in seconds.

Try the Free Spec Generator

Generate acceptance criteria automatically

Writing 20 acceptance criteria by hand is useful for learning the format. For day-to-day work, you want a faster starting point. The Spec Generator on this site takes a short feature description — a sentence or two about what the feature should do — and produces a structured spec with acceptance criteria already written in Given/When/Then format.

The generated criteria are a draft, not a final document. They give you a solid structure that you can edit, expand, and refine during your spec review. Most teams find that starting from a generated draft cuts their spec-writing time in half, because the hardest part is not filling in details — it is staring at a blank page and deciding where to start.

The tool is free, works in your browser, and requires no account.

Open the Spec Generator

Before/after: turning a vague criterion into a test

The most common repair I make is replacing a broad product sentence with a state, action, and observable result. Here is the difference on an account-invite flow.

Before:
- Admins can invite teammates.

After:
- Given a workspace admin enters a new teammate email
  When the invitation is submitted
  Then exactly one invite is created with status="pending"
  And the invite email contains a single-use token
  And submitting the same email again returns the existing pending invite

The second version gives QA the duplicate case, security expectation, and visible state transition without requiring a follow-up meeting.

Spec Writing Block to Copy

Use this when a ticket sounds clear but still needs acceptance language. It forces the author to name the actor, trigger, result, and evidence.

Spec writing review block: Acceptance Criteria Examples &mdash; 20 Real-World Templates You Can Copy

Decision to make:
- 20 real-world acceptance criteria examples in Given/When/Then format across authentication, e-commerce, API, data processing, and notifications.

Owner check:
- Product owner:
- Engineering owner:
- QA or operations reviewer:

Scope boundary:
- In scope:
- Out of scope:
- Assumption that still needs approval:

Acceptance evidence:
- Test or fixture:
- Log, metric, or screenshot:
- Manual review step:

Writing boundary: avoid vague verbs; every criterion needs a visible pass or fail signal.

Reviewer prompt:
- What would still be ambiguous to someone who missed the planning meeting?
- What evidence would make this safe enough to ship?

Flagship Use Path

This is one of the primary Spec Coding references for Acceptance criteria library. Use it with a real ticket, pull request, or release review instead of treating it as background reading.

Flagship review path:
- Open this page during planning or review.
- Copy the relevant artifact into the work item.
- Replace example values with your system, owner, and failure mode.
- Block implementation if the evidence line is still blank.

Second-pass reviewer note: examples must teach a pattern

I checked that the examples do more than provide copyable text. Each category should teach a reusable testing pattern: identity, state, invalid input, timing, permission, or retry.

Example review pass:
- Keep examples that name actor, trigger, and observable result.
- Prefer one sharp failure case over three generic happy paths.
- Add replay, timeout, permission, or partial-success cases where the domain is risky.
- Remove criteria that only restate the feature title.

Editorial Review Note

Reviewed Apr 29, 2026. This update added a reusable artifact, checked the article against the related topic hub, and tightened the next-step links so the page works as a practical reference rather than a standalone essay.

Keywords: acceptance criteria examples · acceptance criteria template · given when then examples · how to write acceptance criteria · user story acceptance criteria examples

Topic Path

This article belongs to the Acceptance Criteria track. Start with the hub, then use the checklist, template, or tool below on a real project.

Generate specs interactively
Fill a form, get a complete feature spec in Markdown — free, no signup.
Try the Spec Generator

Editorial note