Skip to content
academia.sh

Lesson 03 / 24

User Stories and Tasks

Turning a need into a story and a story into a task; checking for solution bias in a story, and ranking the task inventory by frequency, failure rate, and failed attempts.

Contents

The previous lesson produced three personas: Known-Record Searcher, Subject Browser, and Narrow-Screen Viewer. Personas say who is coming, not what they do. The Subject Browser persona’s session lasts twenty-one and a half minutes; what steps happen in that time, which step requires which other step, and which one should the design prioritize?

This lesson turns the need into action. It first establishes the distinction among need, story, and task; then it checks how a solution seeps into a story; and finally it ranks the task inventory drawn from observation data by three separate criteria, and shows that these rankings do not agree with each other.

Need, Story, and Task

The three concepts are often used interchangeably, and wherever they are confused, the design decision becomes blurred.

A need is independent of the interface. “Knowing what has been written on a subject” is a need; it exists without the catalog interface too, and it can also be met by asking a librarian.

A user story ties the need to a role and a goal. Its format is short and carries three parts: who, what, why. A story does not propose a solution on its own; it narrows the solution space.

A task is the observable counterpart of a story: a sequence of actions with a start, an end, and a success criterion. “Browse by subject” is a task; it starts, it ends, and whether it counts as successful can be measured. Tasks break down into subtasks: within browsing by subject there are steps such as “narrow the results,” “open a record,” and “return to the list.”

The chain is one-directional. It descends from need to story, from story to task, from task to interface decision. If walked in reverse — if an interface element is thought of first and then a story is written to justify it — research data does not produce the decision; it decorates it.

Solution Bias in Stories

The most common way a story breaks down is when an interface element gets written in place of the need. The sentence “I want a publication-year filter” looks like a need but is a solution; the need is “telling recent sources apart,” and a filter is not its only counterpart. The check below scans stories at the level of format, role, and wording.

// story.mjs — checking user stories for format and solution bias

const STORIES = [
  "As an undergraduate student, I want a publication-year filter on search results, so that I can tell recent sources apart",
  "As a subject browser, I want to return to my result list after opening a record, so that I can continue comparing",
  "As a faculty member, I want to select a language from a dropdown menu",
  "As a library staff member, I want to see whether a record is on the shelf, so that I can tell the reader whether the book is available",
  "As a user, I want a better search screen, so that I can search more conveniently",
  "As an external member, I want to extend my borrowing period, so that I can keep working without returning the book",
];

// Format: As a <role>, I want <need>, so that <goal>
const FORMAT = /^As an? (.+?), I want (.+?), so that (.+)$/;
// Solution vocabulary: words naming an interface element
const SOLUTION = ["filter", "button", "dropdown menu", "tab", "checkbox", "screen", "page", "form", "slider"];
// Generic names that do not stand in for a role
const GENERIC_ROLE = ["user", "person", "everyone", "people"];

console.log("no  format      role                    finding");
STORIES.forEach((s, i) => {
  const m = s.match(FORMAT);
  const findings = [];
  if (!m) findings.push("format incomplete (goal not written)");
  const role = m ? m[1] : (s.match(/^As an? ([^,]+)/)?.[1] ?? "");
  if (GENERIC_ROLE.includes(role.toLowerCase())) findings.push("role generic");
  const leaked = SOLUTION.filter((c) => s.toLowerCase().includes(c));
  if (leaked.length) findings.push(`solution bias: ${leaked.join(", ")}`);
  console.log(
    `${String(i + 1).padStart(2)}  ${(m ? "complete" : "incomplete").padEnd(10)}  ${role.slice(0, 22).padEnd(22)}  ${findings.length ? findings.join(" | ") : "-"}`
  );
});

const clean = STORIES.filter((s) => {
  const m = s.match(FORMAT);
  const role = m ? m[1] : "";
  return m && !GENERIC_ROLE.includes(role.toLowerCase()) &&
    !SOLUTION.some((c) => s.toLowerCase().includes(c));
});
console.log(`\nstories passing the check: ${clean.length} / ${STORIES.length}`);
no  format      role                    finding
 1  complete    undergraduate student   solution bias: filter
 2  complete    subject browser         -
 3  incomplete  faculty member          format incomplete (goal not written) | solution bias: dropdown menu
 4  complete    library staff member    -
 5  complete    user                    role generic | solution bias: screen
 6  complete    external member         -

stories passing the check: 3 / 6

Three of the six stories pass the check. The check is not a measure of correctness but a review tool: because it relies on a word list, it does not catch a solution outside the list, and it can flag an innocent usage by mistake. Its function is to move the discussion from the level of wording to the level of structure.

Each of the three findings shows a different flaw. In the first story, the goal is written correctly, but a solution has taken the need’s place; its corrected form becomes “being able to see publications in order of recency,” and this leaves multiple solutions open — filter, sort, date range. In the third story, there is no goal at all: because why the language selection is needed is never written, the story cannot be tested. In the fifth story, the role is “user” — which of the three personas this is remains unclear, and “more conveniently” cannot be measured.

Task Inventory and Priority

Once stories are turned into tasks, the question becomes which one to take on first. During the observation period, the attempt and success counts were logged for each task.

// task.mjs — ranking the task inventory by frequency, failure rate, and failed attempts

// Task attempts logged during a four-week observation period (data constructed for this lesson)
const TASKS = [
  { name: "find a known record",   attempts: 148, successes: 132 },
  { name: "find the shelf location", attempts: 121, successes: 79 },
  { name: "borrow",                attempts: 94,  successes: 88 },
  { name: "browse by subject",     attempts: 86,  successes: 51 },
  { name: "renew a loan",          attempts: 37,  successes: 19 },
  { name: "reserve",               attempts: 22,  successes: 12 },
  { name: "add to reading list",   attempts: 14,  successes: 13 },
];

for (const t of TASKS) {
  t.failed = t.attempts - t.successes;
  t.failureRate = t.failed / t.attempts;
}

console.log("task                     attempts  successes  failed  failure rate");
for (const t of TASKS) {
  console.log(
    `${t.name.padEnd(24)} ${String(t.attempts).padStart(8)} ${String(t.successes).padStart(10)} ${String(t.failed).padStart(7)}  ${(t.failureRate * 100).toFixed(1)}%`
  );
}

const rankedNames = (key) =>
  [...TASKS].sort((a, b) => b[key] - a[key]).map((t) => t.name);
const RANKINGS = {
  "frequency (attempts)": rankedNames("attempts"),
  "failure rate": rankedNames("failureRate"),
  "failed attempts": rankedNames("failed"),
};

console.log("\nrank  frequency                 failure rate              failed attempts");
for (let i = 0; i < TASKS.length; i++) {
  console.log(
    `${String(i + 1).padStart(4)}  ${RANKINGS["frequency (attempts)"][i].padEnd(25)} ` +
      `${RANKINGS["failure rate"][i].padEnd(25)} ${RANKINGS["failed attempts"][i]}`
  );
}

// The rank of the same task across the three rankings
console.log("\ntask                     frequency  rate  failed  largest rank gap");
let largest = { name: "", gap: -1 };
for (const t of TASKS) {
  const r = Object.values(RANKINGS).map((l) => l.indexOf(t.name) + 1);
  const gap = Math.max(...r) - Math.min(...r);
  if (gap > largest.gap) largest = { name: t.name, gap };
  console.log(`${t.name.padEnd(24)} ${String(r[0]).padStart(9)} ${String(r[1]).padStart(5)} ${String(r[2]).padStart(7)}  ${gap}`);
}
console.log(`\ntask with the largest rank shift: ${largest.name} (${largest.gap} ranks)`);
console.log(`total failed attempts: ${TASKS.reduce((t, x) => t + x.failed, 0)}`);
const topTwo = [...TASKS].sort((a, b) => b.failed - a.failed).slice(0, 2);
console.log(
  `${((topTwo.reduce((t, x) => t + x.failed, 0) / TASKS.reduce((t, x) => t + x.failed, 0)) * 100).toFixed(1)}% of failed attempts are in two tasks: ` +
    topTwo.map((t) => t.name).join(", ")
);
task                     attempts  successes  failed  failure rate
find a known record           148        132      16  10.8%
find the shelf location       121         79      42  34.7%
borrow                         94         88       6  6.4%
browse by subject              86         51      35  40.7%
renew a loan                   37         19      18  48.6%
reserve                        22         12      10  45.5%
add to reading list            14         13       1  7.1%

rank  frequency                 failure rate              failed attempts
   1  find a known record       renew a loan              find the shelf location
   2  find the shelf location   reserve                   browse by subject
   3  borrow                    browse by subject         renew a loan
   4  browse by subject         find the shelf location   find a known record
   5  renew a loan              find a known record       reserve
   6  reserve                   add to reading list       borrow
   7  add to reading list       borrow                    add to reading list

task                     frequency  rate  failed  largest rank gap
find a known record              1     5       4  4
find the shelf location          2     4       1  3
borrow                           3     7       6  4
browse by subject                4     3       2  2
renew a loan                     5     1       3  4
reserve                          6     2       5  4
add to reading list              7     6       7  1

task with the largest rank shift: find a known record (4 ranks)
total failed attempts: 128
60.2% of failed attempts are in two tasks: find the shelf location, browse by subject

The three rankings produce three separate lists, and no task sits at the same rank in all three.

By frequency, “find a known record” ranks first; it is the most attempted task. But its failure rate is 10.8%, and only sixteen of the hundred and forty-eight attempts are failures. Frequency alone does not set design priority; a task that is done a lot but works well is not an area for improvement.

By failure rate, “renew a loan” ranks first: 48.6%. This rate correctly describes the severity of the problem but not its size; the task was attempted thirty-seven times and produced eighteen failures.

By failed attempts, “find the shelf location” ranks first: forty-two failures. This task is second by frequency, fourth by failure rate — first in neither list, but it produces the largest share of the total damage. Of the total hundred and twenty-eight failed attempts, 60.2% are concentrated in two tasks.

Priority ranking is built not by picking one of the three but by defining what the problem is. If the goal is to reduce total damage, the failed-attempts ranking is used. If the goal is to raise a task’s reliability above a threshold, the failure rate is used. Frequency alone is not a priority criterion; it is a multiplier for the other two: a high rate on a rarely attempted task can produce fewer failures than a low rate on a frequently attempted one.

A caveat is needed: these numbers count only tasks that were attempted. A task the user wanted to do but could not find the way to, and so never attempted, does not enter the inventory. The fourteen attempts at “add to reading list” may show not that this task is rare, but that it is rare to be found. This is the inventory’s blind spot, and it can only be closed by an interview.

The Ethical Limit of Task Data

The task inventory is produced from per-person session logs; this is the most identifying form of research data. The three rules apply here too. A session log is collected from consenting participants; logging everyone on a publicly open interface is not research but surveillance. The data that enters analysis is aggregated: the table above has no people in it, only tasks. Per-person records are deleted within the retention limit once aggregation is done; only the numbers remain.

A related design rule follows from this: every new field collected to enrich the inventory opens one more path back to identity. Knowing whether a task succeeded does not require knowing who attempted it.

Acceptance Criteria Make a Story Testable

Even a story that passes the check is not enough on its own; when it is satisfied must be written down. Acceptance criteria are a story’s verifiable counterpart, and they are written in terms of the task.

The second story — “being able to return to my result list after opening a record” — is tied to these criteria: on return, the search term, the applied refinements, and the position in the list are preserved; a record the user has already opened stays marked in the list; the return does not require re-running the search. All three are observable; all three are testable.

The solution is again kept out while writing the criterion. “A back button is added” is not a criterion but a decision; a criterion states what the result must be, regardless of which button achieves it.

Summary

  • A need is independent of the interface, a story ties the need to a role and a goal, and a task is the story’s observable counterpart with a start, an end, and a success criterion.
  • Solution bias is the most common way a story breaks down; the wording and format check is a review tool, not a measure of correctness, and it flagged three of the six sample stories.
  • The same task inventory produces three separate rankings by frequency, failure rate, and failed attempts; in the sample data, one task shifted by four ranks across the three rankings.
  • The priority criterion is derived from the definition of the problem: failed attempts is used for total damage, failure rate for reliability; frequency alone does not set priority.
  • The inventory counts only attempted tasks; a task that is never attempted is invisible, and this blind spot can only be closed by an interview.
  • A session log is collected from consenting participants, enters analysis in aggregated form, and per-person records are deleted within the retention limit.

Next Step

The task inventory shows each task as a separate row, yet the user performs them back to back: they find a record, check the shelf location, go to the shelf, cannot find the book, and return to the catalog. The task producing the largest share of failed attempts, “find the shelf location,” is a step that starts on screen and ends off screen. The next lesson traces this chain end to end: it shows what columns a journey map consists of, why stage duration and drop-off points do not sit in the same place, and how off-screen steps enter the map.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close