User Stories & Acceptance Criteria
User stories are one of the most useful and most abused tools in product development. When used well, they connect engineering work to user outcomes and create a shared language between PMs, designers, and engineers. When used badly, they become bureaucratic overhead that slows teams down without adding clarity.
The difference between useful and wasteful comes down to how you write them, when you use them, and whether you pair them with acceptance criteria that are actually testable.
The User Story Format
The standard format is simple:
As a [type of user], I want [capability] so that [benefit].
Each piece serves a purpose:
"As a [type of user]"
Forces you to specify WHO this is for. "As a user" is almost
always too vague. "As a first-time visitor" or "as a team admin"
or "as a free-tier user" changes the scope and priority.
"I want [capability]"
Describes the WHAT. Not a technical implementation. A capability
the user can observe and use.
"so that [benefit]"
Explains the WHY. This is the most important part and the most
often skipped. Without the "so that," a story has no purpose.
Good Stories
As a hiring manager, I want to filter candidates by years of
experience so that I can quickly identify senior-level applicants
without scrolling through the entire list.
As a free-tier user, I want to see which features require an
upgrade so that I can evaluate whether the paid plan is worth it
before being surprised by a paywall.
As a team admin, I want to deactivate a user's account without
deleting their data so that we retain their contributions when
someone leaves the company.
Bad Stories
As a user, I want a better dashboard.
(Who? Better how? Why?)
As a user, I want the system to use WebSockets.
(This is an implementation detail, not a user need.)
As a user, I want to be able to do everything faster.
(Not actionable. Not testable. Not a story.)
As the product team, I want to increase retention.
(This is a business goal, not a user story.)
The Difference Between a Story & a Task
A user story describes a user outcome. A task describes engineering work. They are related but not the same.
User story:
"As a team admin, I want to bulk-invite users by uploading a CSV
so that I can onboard a new team in minutes instead of adding
members one by one."
Tasks (derived from the story):
- Create CSV upload endpoint with validation
- Parse CSV and handle common formatting issues (BOM, encoding)
- Send invitation emails in batch (respect rate limits)
- Display upload results with success/failure per row
- Handle duplicate email addresses gracefully
- Write migration to add batch_invite_id to users table
The story lives in the PRD or the planning document. The tasks live in the project board. The PM writes the story. The engineer and PM collaborate on the tasks. This distinction matters because it keeps the PM focused on outcomes and the engineer focused on implementation.
At Linear, stories are issues with a clear user outcome. Tasks are sub-issues that break the implementation into shippable pieces. The story stays open until all tasks are complete and the outcome is verified.
Acceptance Criteria
Acceptance criteria define when a user story is done. They are the bridge between "what the user wants" and "what QA should test." Without acceptance criteria, "done" is a matter of opinion.
What Good Acceptance Criteria Look Like
Good acceptance criteria are specific, testable, and complete.
Story: As a team admin, I want to bulk-invite users by uploading a
CSV so that I can onboard a new team in minutes.
Acceptance criteria:
Given a valid CSV with email addresses:
- System accepts CSV files with columns: email, first_name,
last_name, role
- Column order does not matter as long as headers are present
- System processes up to 500 rows per upload
- Each valid email receives an invitation within 5 minutes
- Admin sees a results summary: X invited, Y failed, Z skipped
Given invalid data:
- Rows with malformed email addresses are flagged with specific
error messages
- Duplicate email addresses (already in the system) are skipped
with notification
- If the CSV has no valid rows, the upload fails with a clear
message
- Files larger than 1MB are rejected before processing
Given permission constraints:
- Only account admins can access the bulk invite feature
- Non-admin users do not see the upload option
- Admin cannot invite users to a role higher than their own
The Given-When-Then Pattern
For complex stories, the Given-When-Then format (from behavior-driven development) adds precision.
Given [precondition],
When [action],
Then [expected result].
Example:
Given a CSV file with 100 rows where 5 emails are duplicates,
When the admin uploads the file,
Then 95 invitations are sent,
And the results page shows "95 invited, 5 skipped (already exist),"
And each skipped row shows the email address and reason.
What Bad Acceptance Criteria Look Like
Bad: "The upload should work correctly."
(What does "correctly" mean? Untestable.)
Bad: "Handle errors gracefully."
(What errors? What does "gracefully" mean?)
Bad: "The feature should be fast."
(How fast? Under what conditions? 100ms? 10 seconds?)
Bad: "Users should find it intuitive."
(Cannot be tested objectively. This is a design goal, not
acceptance criteria.)
Sizing Stories
User stories need to be small enough to build in a few days and large enough to deliver observable value. If a story takes more than a week, break it down. If it takes less than a few hours, it is probably a task, not a story.
Story too large:
"As an admin, I want a complete user management system."
(This is an epic, not a story. Break it into individual capabilities.)
Story right-sized:
"As an admin, I want to deactivate a user account."
(Specific, buildable in 2-3 days, testable.)
Story too small:
"As an admin, I want the deactivate button to be red."
(This is a design detail, not a user story.)
Splitting Strategies
When a story is too large, split it by workflow step, user type, data scope, or complexity.
Original story (too large):
"As a user, I want to search for products."
Split by complexity:
Story 1: "As a user, I want to search for products by name."
Story 2: "As a user, I want to filter search results by category."
Story 3: "As a user, I want to sort search results by price
or rating."
Story 4: "As a user, I want search to suggest results as I type."
Each story is independently valuable. A user can search by name
without filters. Filters work without sorting. Each can be built,
tested, and shipped separately.
Spotify splits stories aggressively. The initial version of a feature ships with the minimum viable behavior, and each subsequent story adds a layer. This allows the team to ship, learn, and adjust without over-investing upfront.
When User Stories Help
User stories are most useful when the team needs clarity about who is being served and why.
Use stories when:
- The feature serves multiple user types with different needs
- The scope is ambiguous and needs to be made explicit
- Multiple engineers will work on different parts of the feature
- QA needs clear pass/fail criteria
- The feature will be developed over multiple sprints
- There is disagreement about what "done" means
When User Stories Are Overhead
User stories are not always the right tool. Sometimes they add process without adding value.
Skip stories when:
- The work is purely technical (performance optimization, refactoring)
- The team is small and alignment happens through conversation
- The feature is a single-engineer, single-day task with obvious scope
- You're in a rapid prototyping phase where formality slows you down
- The story format forces an awkward fit ("As a database, I want
to be indexed..." -- please do not write this)
For technical work, write tasks directly. "Optimize the search query to reduce p95 latency from 800ms to 200ms" is clearer than forcing it into story format. For small features in small teams, a verbal conversation and a one-line ticket may suffice.
The test is always: does writing a story add clarity for the people who will do the work? If the answer is no, do not write one just to follow process.
Story Mapping
When planning a large feature, individual stories are hard to organize. Story mapping, developed by Jeff Patton, arranges stories in a two-dimensional grid.
Story map for "Team Onboarding":
User activities (left to right):
Sign up -> Set up team -> Invite members -> Configure workspace
Under each activity, stories ranked by priority (top to bottom):
Sign up: Set up team: Invite members: Configure:
----------- ----------- --------------- ----------
Create account Name the team Invite by email Choose theme
Verify email Upload logo Bulk CSV invite Set timezone
Set password Choose plan Invite link Notifications
SSO login Add billing SAML/SSO Integrations
The horizontal line between rows represents your MVP.
Everything above the line ships first. Everything below ships later.
Story mapping keeps individual stories small while maintaining visibility into the overall feature. Airbnb used story mapping when rebuilding their host onboarding flow. The map helped the team agree on which stories constituted the minimum viable experience and which were enhancements.
Writing Criteria for Edge Cases
Edge cases are where acceptance criteria earn their keep. The happy path is usually obvious. The edge cases are where bugs hide.
Story: As a user, I want to upload a profile photo.
Happy path criteria:
- User can upload JPG, PNG, or WebP files
- Photo is cropped to square and resized to 256x256
- Photo appears on profile within 5 seconds
Edge case criteria:
- File larger than 5MB: rejected with message "File too large.
Maximum size is 5MB."
- Unsupported format (PDF, GIF, BMP): rejected with message
"Please upload a JPG, PNG, or WebP file."
- Corrupt file: rejected with message "This file could not be
processed. Please try a different file."
- Upload fails mid-transfer: user sees "Upload failed. Please
try again." Previous photo is preserved.
- User uploads while previous upload is processing: new upload
replaces the in-progress one.
- User has no existing photo: placeholder avatar shown until
upload completes.
Engineers appreciate edge case criteria because it saves them from making product decisions in code. Without criteria for the "corrupt file" case, the engineer has to guess: show an error? Fail silently? Retry automatically? The PM should make this decision, not the engineer.
Common Pitfalls
- Skipping the "so that" clause. Without the benefit, a story is just a feature request. The "so that" creates context for engineering decisions. An engineer who knows why a feature matters will make better implementation choices.
- Writing acceptance criteria after development. Criteria should be written before or during sprint planning, not after the code is written. Retrofitting criteria to match what was already built defeats the purpose.
- Over-specifying implementation. "The system uses a Redis queue to process CSV uploads" is an implementation detail, not acceptance criteria. Describe the observable behavior, not the architecture.
- Treating every piece of work as a story. Bug fixes, tech debt, and infrastructure work do not need to be user stories. Forcing them into the format creates awkward, useless artifacts.
- Not involving engineers in acceptance criteria. Engineers catch edge cases PMs miss. Review criteria together before development starts. A 15-minute conversation prevents days of rework.
Key Takeaways
User stories connect engineering work to user outcomes using the "As a / I want / so that" format. The "so that" clause is the most important part. Acceptance criteria make stories testable by defining specific, observable pass/fail conditions. Use Given-When-Then for complex scenarios. Split large stories by workflow, user type, or complexity. Skip stories when they add overhead without adding clarity. Write edge case criteria explicitly so engineers do not have to make product decisions in code. The goal is shared understanding, not documentation for its own sake.