The "As a… I want… so that…" line of a user story.
A user story is a short, plain-language description of a piece of functionality told from the point of view of the person who wants it. It fits on an index card, names a user, a desire, and a reason, and it is deliberately incomplete: the card is a placeholder for a conversation the team still owes itself.
User stories came out of Extreme Programming. Kent Beck described them in *Extreme Programming Explained* (1999) as the unit of planning in XP's planning game: customers write down the featuresFeatureProduct SpecificationA product capability or featureView reference → they want in their own words, and developers estimate and schedule them. The point was to keep requirements light enough that a business person and a programmer could discuss one face to face.
The template most teams recognise came later. In 2001 the XP team at Connextra in London settled on "As a <role>, I want <goal>, so that <benefit>", and the form spread from there. Ron Jeffries gave the practice its discipline with the Three Cs: the Card holds a token of the requirement, the Conversation fleshes it out, and the Confirmation capturesCaptureWorkspaceA dated, hashed rendition of something already in the graph: a screenshot of a surface, a PDF of a report, an export of a canvas. The subject stays a graph node; the capture records what it looked like at one moment, and the content hash says whether it still does.View reference → how everyone will know it is done. The card is the smallest part; the conversation is the work.
Mike Cohn generalised the practice in User Stories Applied (2004), still the standard reference, moving the idea past physical cards into a way of working. Bill Wake then gave teams a quality test with the INVEST mnemonic: a good story is Independent, Negotiable, Valuable, Estimable, Small, and Testable. INVEST is where the live debate sits. "Valuable" pushes back on stories that describe internal plumbing with no user-visible payoff, and "Independent" is the one teams break most often, since real backlogs are full of stories that secretly depend on each other.
A payments team wants to reduce failed renewals. Someone proposes the story: "As a subscriber whose card has expired, I want to be warned before my renewal date, so that my subscriptionSubscriptionSales & RevenueA recurring subscriptionView reference → does not lapse." It scores well on INVEST. It is small, it is testable, and the benefit clause states why it matters in revenue terms.
The conversation that follows decides the detail the card omits. How far ahead is the warning, by email or in-app, what happens to a subscriber who ignores it. Those answers become acceptance criteria: a reminder fires seven days out, a second fires on the renewal date, and a dunning flow retries the charge three times across five days. The story stays a sentence. The criteria carry the precision, and the test suiteTest SuiteQuality AssuranceA suite of related testsView reference → checks them. Six weeks on, the team can point at one shipped story and a measured drop in involuntary churn, because the story named a user and a reason, which a screen mockup never would.
In the Unified Product Graph the user story lives in the Product & Delivery region, which models the work-breakdown spine feature → epic → story → task. A story sits inside an epicEpicProduct SpecificationA large body of work that can be broken into storiesView reference → through Epicspecified byUser Storyhierarchy, is reached from below by epic_specified_by_user_storyTaskimplementsUser Storycross-domain, the edge each implementing task carries rather than one the story reaches down with, and is pinned to its definition of done through task_implements_user_storyUser Storyverified byAcceptance Criterionhierarchy. This structure keeps the Three Cs honest: the card is the node, the tasks and criteria are the conversation and the confirmation made queryable, and a story that connects to neither is visibly underspecified. The story itself is lifecycle-free and holds no status, because the promise is stable while the work under it moves. Note that the canonical spec treats user_story_verified_by_acceptance_criterionUser StoryProduct SpecificationA user's goal and the value they expect, in the "As a… I want… So that…" format. Now also a first-class plannable unit (priority, effort, assignee, due_date) that schedules into a planning_cycle, and it round-trips an external board's column via workflow_state. as a label on delivery work and folds thin, task-shaped stories into user_storyTaskProduct SpecificationA unit of work within a story or epicView reference → at the leaf; the region preserves the story role for teams who plan at story granularity. 0.35.0 added the second inbound rung. A story is reached from above by taskEpicspecified byUser Storyhierarchy where a team plans at epic granularity, and by epic_specified_by_user_storyFeaturespecified byUser Storyhierarchy where the feature goes straight to its stories, which is the ordinary case because the epic level is optional in UPG. The two share a verb pair deliberately, so the ladder reads the same at either height, and per-instance parentage is untouched: a story still carries exactly one feature_specified_by_user_storyparent_id, whichever rung wrote it.
Worked example: Trellis
One of Trellis's core user stories is: as a director, I want to see how a structural change will affect every existing record before it runs, so that I can approve it with confidence. That story sits under the Safe Change epicEpicProduct SpecificationA large body of work that can be broken into storiesView reference → and drives the acceptance criteria for the preview the director reviews before any agent change is applied.
Urgent Blocking progress or time-critical; must be addressed immediately.
Type-specific fields on BaseNode
as_astring"As a [persona], …". Free-text persona name or slug ref.
i_want_tostringCapability or action the persona wants.
so_thatstringBenefit or outcome the persona expects.
textstringFree-form story text. Used as a single-line rendered view.
assigneestringAssigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable.
effortstringEffort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team.
priorityenumRelative importance against other stories. Lifted onto user_story (0.20.0) so the story is a first-class plannable unit alongside task, matching Jira/Linear where the story/issue is the estimated-and-assigned atom.
due_datestringISO date due. Typically bounded by the release or planning_cycle the story is scheduled into.
workflow_statestringThe source tool's raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.
workflow_state_categoryenumCanonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.
It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood.
idstringrequiredUnique identifier (UUID)
typeNodeTyperequiredDiscriminator for the entity type
titlestringrequiredDisplay name
descriptionstringOptional detailed description
statusstringLifecycle status
tagsstring[]Freeform tags for filtering
5 edge types connected to this entity.
user_story_verified_by_acceptance_criterion1 framework use this entity type.