---
description: Use when exploring an application to document user journeys, role-based workflows, actor surfaces, executable journey contracts, or a testable journey catalog.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent
---


# Create User Journeys

Audience: AI coding agents first.

One agent, one pass. Produce evidence-backed outcome contracts. Do not write tests.

Pipeline: `create-journeys` (contract) → `creating-journey-e2e-tests` (test) → **run the test**.

**The test is the verifier. This skill does NOT certify truth on paper.** A journey is a hypothesis precise enough to execute; running its derived test is what proves or kills it. Never add a review round to buy confidence a test run buys for free.

## Iron law

**Never assert what you did not read.** Every material claim carries `source:line`.

**Contradicted fact = defect.** Disproved route, response, or state → re-scope the journey to the real path or record an implementation defect. Never preserve an impossible happy path.

**Missing fact = `UNKNOWN — SP-NNN: reason` + `readiness: blocked` + a `scan-problems.md` row.** Never guess, never infer from a nearby file.

**Never declare something missing you did not search for.** See step 1 — this rule exists because every prior run declared "no fixture exists" while `createTestUser()` and `addToCart()` sat unread in `tests/helpers/`.

## Journey boundary

One journey = one actor goal + one terminal actor-visible outcome across time.

- Split when actor, terminal outcome, or authority differs.
- Do not create one journey per page, component, or API.
- Do not combine authenticated and anonymous outcomes when persistence differs.

## Step 1 — Find what already exists (do this FIRST)

Before any claim of absence, search and record what you found:

| Look for | Where | Never claim missing until |
|---|---|---|
| Fixtures, factories, seeds | `tests/**`, `**/helpers/**`, `**/fixtures/**` | grepped for `createTest*`, `seed*`, `factory`, `fixture` |
| Auth/session mechanism | `tests/**` configs + helpers | grepped for `storageState`, `addCookies`, `SignJWT`, `login` |
| Existing runners | `playwright.config*`, package scripts | listed them |
| Roles | schema/RBAC definition | read the enum at its source |

Tests are weak evidence for **product intent** and strong evidence for **available setup**. Mine them for fixtures; do not let them define the contract.

A blocker naming a capability you did not grep for is a false blocker. False blockers are worse than no journey — they send the reader to build what they already own.

## Step 2 — Rank by blast radius; author top ranks only

1. **Money moves** — charge, refund, payout, balance. Wrong → lost/double money.
2. **Ownership/authz boundary** — actor reaches another's data/action. Wrong → IDOR.
3. **Exactly-once transition** — idempotency, dedupe, caps, replay. Wrong → double-issue.
4. **Multi-step flow with durable side-effect.** Wrong → silent half-completion.
5. **Everything else → defer** with a `coverage-omission` row. Read-only browsing, static pages, per-page element states belong to route-sweep / ui-matrix passes.

A journey earns existence ONLY if it can FAIL for a real reason. Sole outcome "page renders" → not a journey.

Stop at budget. A short catalog of real journeys beats an exhaustive catalog of fiction.

## Step 3 — Trace the path once

Read the connected path and cite each hop. Do not substitute a sibling route, wrapper, or test for a missing hop.

| Hop | Evidence |
|---|---|
| UI trigger (route + control) | `source:line` |
| Request construction | `source:line` |
| Endpoint auth + parse | `source:line` |
| Authoritative write | `source:line` |
| Response | `source:line` |
| Terminal render | `source:line` |

Rules:

- Follow imports until the actual write/side effect. A route wrapper does not prove a durable claim.
- Read 8 lines around every cited line. A line existing is not evidence when surrounding control flow disproves the claim.
- Resolve framework defaults from the actual helper (a response helper with no explicit status usually means `200`; never infer `201` from a nearby insert).
- An awaited SDK call may resolve `{error}` instead of throwing. Check before claiming a catch path.
- A broken hop → re-scope or record a defect. An unknown hop → blocked journey.

## Step 4 — Author the contract

```yaml
id: UJ-NNN
title: Outcome-oriented title
actors: [exact-role-id]
surface: customer-portal
goal: Observable actor outcome
trigger: Exact UI event at /{locale}/real-route
fixtures: [real-fixture-name-from-step-1]
success_state:
  visible: Exact confirmation and values
  durable: Exact authoritative state and values
  persistence: Exact fresh request/reload and returned values
source_specs: [project/relative/path.ts]
readiness: draft
canonical: false
blockers: []
```

Sections, exact order:

1. `## Path` — the step-3 table
2. `## Happy path`
3. `## Alternate and failure paths`
4. `## Permissions and boundaries`
5. `## Source specs`

## Step 5 — Author branches

Stable IDs `H1`, `A1`, `P1`. Write the happy path plus the branches that can actually fail:

- one input-validation denial,
- authentication denial,
- wrong-owner denial (when the resource has an owner),
- one downstream/server failure,
- duplicate submit (when the outcome must happen exactly once).

Skip a branch the code cannot reach; say so in one clause. Do NOT enumerate every `if` in the file — enumerate the outcomes a user can observe.

```markdown
### H1
- Setup: Exact fixture and starting state.
- Action: Exact actor UI action.
- Request: `POST /api/exact-route`.
- Response: `201` with `{success:true,id:<uuid>}`.
- Visible: Exact rendered confirmation and values.
- Durable: Exact authoritative rows/fields/values.
- Fresh read: `GET /api/resource/{id}` returns `200` with exact values.
- Forbidden: Exact writes/calls/disclosures that MUST NOT occur.
- Evidence:
  - Setup: `tests/helpers/fixtures.ts:12`
  - Action: `src/component.tsx:88`
  - Request: `src/component.tsx:93`
  - Response: `src/route.ts:41`
  - Visible: `src/component.tsx:105`
  - Durable: `src/domain.ts:72`
  - Fresh read: `src/read-route.ts:33`
  - Forbidden: `src/domain.ts:64`
```

Rules:

- `Request`: exactly one method + one path. No request → `N/A — reason`.
- `Response`: exactly one status + exact body/redirect literal. UI-only request N/A requires response N/A. Transport rejection → `N/A — transport produced no HTTP response`.
- `Visible`: exact business text/value/accessibility state. CSS class, Tailwind token, or `${interpolation}` is forbidden — a missing semantic anchor is a `scan-problems` row, not permission to assert styling.
- `Durable`: name the authority (table/field/value).
- `Fresh read`: new request or full reload; never cached client state.
- `Forbidden`: required on every branch, including happy paths.
- `Evidence`: map each field separately. Unknown field → `UNKNOWN — SP-NNN: reason`.

DO NOT:

```markdown
- A1: Invalid request → API returns 400/409 → error visible → state unchanged
```

Reject: combined states, missing method/path, vague oracle, unnamed durable state, no fresh read, no evidence.

Read `references/complete-journey-example.md` before authoring. It illustrates structure only; never copy its facts.

## Step 6 — Gate

```text
node <skill-dir>/scripts/validate-journeys.mjs <journey-file> --project-root <repo-root>
node <skill-dir>/scripts/validate-journeys.mjs docs/user_journeys --project-root <repo-root>
```

The validator is deterministic and free: it proves every cited line exists, every response pins one status, every blocker maps to a real row. It does NOT prove semantics — that is the test's job.

- `INVALID` → fix and rerun. Never report a journey while the gate is red.
- `VALID` + zero blockers → `readiness: ready`, `canonical: true`.
- `VALID` + blockers → `readiness: blocked`. This is a legitimate deliverable: its blockers are the work list.

**Optional escalation — money paths only.** For a journey whose failure moves money or crosses an authz boundary, run one independent review (`references/semantic-review-protocol.md`, `--with-review`): a different agent writes a hash-bound sidecar; findings are repairable once, then ship whatever state it lands in. Do NOT run this on every journey — one review costs more than the journey and the derived test catches the same class for free.

## Step 7 — Verify by execution

Hand the journey to `creating-journey-e2e-tests`, then RUN the derived test.

- Test passes → the contract is real.
- Test fails → one of the two is wrong. Read the failure; fix the journey or file the defect. This is the finding you wanted.

Never mark a journey `canonical: true` that no test has executed, unless it is `blocked` on a fixture that does not exist yet.

## Catalog outputs

- `docs/user_journeys/00-index.md` — IDs, actors, surface, readiness, file.
- `docs/user_journeys/01-actors.md` — exact roles and ownership boundaries, read from the schema.
- `docs/user_journeys/02-format.md` — point to this skill; do not duplicate it.
- Actor/surface journey files.
- `docs/user_journeys/scan-problems.md` — blockers, conflicts, omitted outcomes.

`## Source specs` MUST match header `source_specs`.

`scan-problems.md` is a write-ahead log: append each finding when evidence surfaces it; never hold findings only in context across a compaction. Classify: `missing-evidence`, `source-conflict`, `implementation-defect`, `missing-fixture`, `missing-fault-seam`, `coverage-omission`. One blocker ID = one missing capability. Only problems inside the journey boundary may block it.

## Completion gate

- [ ] Step 1 search ran; no blocker names an ungrepped capability.
- [ ] Journeys ranked by blast radius; deferred outcomes have a `coverage-omission` row.
- [ ] Every hop in `## Path` cites a line that was read, not inferred.
- [ ] Every branch has all nine fields; every request one method/path; every response one status.
- [ ] Every persisted mutation has a durable + fresh-read oracle.
- [ ] Every blocker maps to one material `UNKNOWN` field and one real `scan-problems.md` row.
- [ ] Validator prints `VALID` for the changed file and the catalog.
- [ ] Derived test executed, or journey is `blocked` with the reason.

$ARGUMENTS
