# User story template with acceptance criteria

> A user story names who wants something, what they want and why, and its acceptance criteria say exactly when it is done. Copy the example below, keep one story per behavior a user notices, and write each criterion as Given, When, Then so a tester or a coding agent can check it.

Updated November 8, 2026. Canonical: https://sniptide.com/templates/user-stories
Open it in the Sniptide editor: https://app.sniptide.com/new?template=user-stories
Files as JSON: https://sniptide.com/templates/user-stories.json

## Files

### docs/stories/team-invites.md

```markdown
# Team invites

Epic: Workspaces for teams
Updated: 2026-11-08

## Story 1: Invite a teammate by email

As a workspace owner, I want to invite a teammate by email, so that they can see and edit our
invoices without sharing my login.

### Acceptance criteria

- Given I am an owner on Settings > Members, when I enter a valid email and choose a role, then
  the invite appears under "Pending" and the person receives an email with a link.
- Given the email already belongs to a member, when I send the invite, then I see "This person
  is already in the workspace" and no email is sent.
- Given the workspace is at its seat limit, when I open the invite form, then the button is
  disabled and links to the billing page.

### Notes

- Roles: Owner, Editor, Viewer. Viewer is the default.
- Invite links expire after 7 days.

## Story 2: Accept an invite

As an invited teammate, I want to join with one click, so that I can start working without
setting up a separate workspace.

### Acceptance criteria

- Given I have an account, when I open a valid invite link, then I land in the workspace with
  the role I was invited with.
- Given I have no account, when I open the link, then I sign up with the invited email filled in
  and join the workspace after verifying it.
- Given the link has expired, when I open it, then I see "This invite has expired. Ask the
  owner for a new one."

## Story 3: Revoke a pending invite

As a workspace owner, I want to revoke an invite I sent by mistake, so that the wrong person
can't join.

### Acceptance criteria

- Given an invite is pending, when I choose Revoke, then it disappears from the list and its
  link shows the expired message.

## Out of scope

- Inviting by link without an email.
- Custom roles.
```

## What each part does [#what-each-part-does]

| Part | What it settles | How to write it |
| --- | --- | --- |
| As a ... | Whose problem this is, and which permissions apply. | A real role in your product, such as "workspace owner", never "user". |
| I want ... | The capability, in the user's words. | One action. If you need "and", split the story. |
| So that ... | The reason, which settles the small choices the story doesn't cover. | The outcome the user cares about, not the feature again. |
| Acceptance criteria | When the story is done. | Given a starting state, when the user acts, then a visible result. |
| Notes | Facts the team would otherwise ask about. | Defaults, limits and names. |
| Out of scope | What the story deliberately leaves out. | The features people will assume come along. |

The "so that" clause is the one teams skip and the one that matters most. "So that they can see our invoices without sharing my login" tells the developer that a shared password is the real problem, which rules out a cheaper fix that misses it.

## Write criteria someone can check [#write-criteria-someone-can-check]

Each criterion should pass or fail with no discussion. "Invites work well" can't fail. "The invite appears under Pending and the person receives an email with a link" can. Cover the unhappy paths too, such as an existing member, a full workspace or an expired link, because those are the cases that ship broken.

Exact copy belongs in the criteria when it matters, as in the expired-link message above. A developer or an agent will otherwise write its own.

## Keep stories small [#keep-stories-small]

A story should fit in a few days of work. When one grows, split it by behavior the user notices (invite, accept, revoke) rather than by layer (database, API, UI), so each piece is still something a person can try.

## Variant: a story in an issue [#variant-a-story-in-an-issue]

When stories live in your tracker instead of the repository, the same parts fit in an issue body:

```md title="Issue body"
**As a** workspace owner, **I want** to revoke a pending invite, **so that** the wrong person
can't join.

**Done when**
- [ ] A revoked invite disappears from Pending.
- [ ] Its link shows "This invite has expired. Ask the owner for a new one."
```

For a feature larger than a few stories, write a [PRD](/templates/prd-for-agents) and keep the stories inside it.
