User journeys
A User Journey is an ordered sequence of Gherkin scenarios — possibly drawn from multiple features — that describes how a single persona moves through the app to accomplish an outcome. Journeys exist so the team can see and reason about end-to-end flows that scenario lists alone don't make obvious, without anyone having to author them from scratch.
Concepts at a glance
- The AI agent drafts journeys (autonomously or from a one-line user intent). First-pass drafts are deliberately imperfect — the value is giving the user something to react to instead of a blank page.
- Users iterate via structured feedback (reorder / remove / replace / insert) which applies optimistically AND records a feedback row. Run Refinement opens a feedback dialog, records that free-text feedback, and queues a
USER_JOURNEY_REFINEMENTagent run. - Scenario proposals are opt-in. Set
ENABLE_SCENARIO_PROPOSALS=truefor server/agent writes andNEXT_PUBLIC_ENABLE_SCENARIO_PROPOSALS=trueto show proposal UI. When disabled (the default), agents must build journeys from existing scenarios only and cannot writeproposalIdsteps. - When enabled, the agent may propose new or modified scenarios mid-discovery (
ScenarioProposalrows). Nothing inFeature.gherkinScenariomutates until a human accepts the proposal.
Data model
All models live in prisma/schema.prisma.
| Model | Role |
|---|---|
Scenario | Canonical, FK-able store of a Gherkin scenario block. Synced from Feature.gherkinScenario — never written directly. Soft-deleted (deletedAt) when its name disappears from the source Gherkin. |
UserJourney | Project-scoped sequence of steps. status: DRAFT / APPROVED / STALE / ARCHIVED. version increments on every refinement run. |
UserJourneyStep | A single ordered step. Has exactly one of scenarioId (FK to a real Scenario) or, when scenario proposals are enabled, proposalId (when the step depends on a pending ScenarioProposal). |
UserJourneyFeedback | Edits captured against a journey or step. kind: structured (REMOVE_STEP, REORDER, REPLACE_WITH, ADD_AFTER, ADD_BEFORE, RENAME) — these are resolved=true on insert because the UI applied them. FREE_TEXT and RESCOPE insert resolved=false and require a refinement run. |
ScenarioProposal | Agent-emitted CREATE / UPDATE / DELETE of a Scenario. status: PENDING / ACCEPTED / REJECTED / SUPERSEDED. |
A journey cannot be APPROVED while any of its steps depends on a PENDING ScenarioProposal.
The mutation gate
Feature.gherkinScenario has a single sanctioned write path:
writeFeatureGherkin(featureId, gherkin, { expectedUpdatedAt? })
↳ src/lib/repositories/feature-gherkin-repository.ts
It wraps the update + syncScenarios() in a transaction and supports optimistic locking via expectedUpdatedAt. The accept-proposal flow uses this lock to detect concurrent edits.
Callers that bypass the chokepoint and write Feature.gherkinScenario through prisma.feature.update(...) directly are caught by a Prisma $extends backstop registered in src/lib/db.ts (applyScenarioSyncExtension), which best-effort re-runs syncScenarios after the commit. The chokepoint is preferred because it's atomic; the backstop is for stragglers.
syncScenarios(featureId) in src/lib/services/scenario-sync.ts parses Feature.gherkinScenario via parseScenarioBlocks in src/lib/gherkin-parser.ts, upserts Scenario rows by (featureId, name), soft-deletes rows whose names vanished, and revives rows whose names reappear.
For legacy/as-is features before To-Be generation, Feature.gherkinScenario and Feature.asIsGherkinScenario are the same artifact and must be updated together. Once To-Be generation completes (toBeGeneratedAt, toBeGenerationStatus = COMPLETED, or behaviorMapping present), asIsGherkinScenario is the frozen legacy snapshot and only gherkinScenario remains the active/current contract.
Agents
Two agent job types, both registered in AI_ENABLED_JOB_TYPES:
USER_JOURNEY_DISCOVERY—prompt/user-journey-discovery.md→UserJourneyDiscoveryService. Two-stage process:- Stage 1 (survey):
repave features list,repave personas list,repave scenarios search— names only. Agent sketches candidate journey skeletons. No scenario proposals allowed in this stage. - Stage 2 (validate):
repave scenarios readfor each candidate; agent confirms ordering, may emitScenarioProposalrows for genuine gaps only whenENABLE_SCENARIO_PROPOSALSorNEXT_PUBLIC_ENABLE_SCENARIO_PROPOSALSis true, then writes journeys viarepave user-journeys save.
- Stage 1 (survey):
USER_JOURNEY_REFINEMENT—prompt/user-journey-refinement.md→UserJourneyRefinementService. Operates on a single journey, consumes unresolved feedback, may emit scenario proposals only when the scenario proposal flag is enabled, replaces the step list viarepave user-journeys save --journey-id <id>, and bumpsUserJourney.version. The refine API enforces one in-flight refinement per journey (debounce).
A third dispatched job type, USER_JOURNEY_STALENESS_CHECK, is not an agent — it's a plain DB flag-flip run from runStalenessCheck (src/lib/services/user-journey-staleness-service.ts). It's enqueued by acceptProposal after a CREATE/UPDATE/DELETE accept and marks every journey containing a step that references the changed scenario as STALE. Deliberately omitted from AI_ENABLED_JOB_TYPES.
A fourth agent job type, USER_JOURNEY_TEST_ALIGNMENT, is auto-enqueued on the transition into APPROVED. It harmonises the approved journey's ordered scenario Gherkin for UJ test execution only, especially shared Scenario Outline examples that represent the same entity across create/read/update scenarios. The aligned feature is stored as a UserJourneyTestAlignment row and does not mutate the source Scenario.gherkinText. If the alignment job fails, the Tests tab surfaces the error and lets editors restart the failed alignment job.
Accept-proposal cascade
acceptProposal(proposalId, userId) in src/lib/services/scenario-proposal-service.ts, called from PATCH /api/projects/[id]/scenario-proposals/[proposalId] with { status: "ACCEPTED" }:
- Loads the proposal + feature with current
updatedAt. - Builds the new Gherkin text via
appendScenarioBlock/replaceScenarioBlock/deleteScenarioBlockinsrc/lib/gherkin-block-editor.ts. - Calls
writeFeatureGherkin(featureId, newText, { expectedUpdatedAt }). On lock conflict throwsProposalConflictError. - Repoints any
UserJourneySteprows that depended on the proposal at the resultingScenariorow (clearsproposalId, setsscenarioId). - Flips the proposal to
ACCEPTED, recordsreviewedBy/reviewedAt. - Auto-
SUPERSEDEDs any siblingPENDINGproposals on the same(featureId, scenarioName), withsupersededByIdpointing at the winner. - For UPDATE/DELETE only: enqueues a
USER_JOURNEY_STALENESS_CHECKjob for the changed scenario.
CLI
repave user-journeys save <journey-json>(or--journey-id <journey-id>) — creates / replaces a journey via the Repave CLI API.proposalIdsteps are rejected while scenario proposals are disabled.repave scenario-proposals create|update|delete ...— emits aScenarioProposalrow via the Repave CLI API when scenario proposals are enabled.
UI
/projects/[id]/user-journeys— list with status / persona filters, an All scenarios implemented filter, and the "Discover Journeys" entry point./projects/[id]/user-journeys/[journeyId]— timeline with structured edit affordances (↑↓ reorder, ↔ replace, 🗑 remove, 💬 comment), inline proposal diff on any step that has aproposalId, side feedback rail showing unresolved and resolved items./projects/[id]/proposals— project-level proposal queue with status filter; clicking a row shows the diff and accept / reject controls. Hidden unlessNEXT_PUBLIC_ENABLE_SCENARIO_PROPOSALS=true.discover-user-journeys-dialog.tsx— autonomous vs seeded mode toggle, extracted-persona dropdown filter,AgentButtonfor model selection.
Structured edits (REORDER, REMOVE_STEP, REPLACE_WITH, ADD_*) apply optimistically client-side AND PATCH the journey with the new step list; the corresponding UserJourneyFeedback rows are inserted with resolved=true. Step-level comments still insert FREE_TEXT rows with resolved=false. The main Run Refinement action opens a dialog for journey-level feedback, inserts that feedback as unresolved FREE_TEXT, and enqueues USER_JOURNEY_REFINEMENT; any existing unresolved feedback is included in the same run.
Tests
src/lib/__tests__/gherkin-parser.test.ts— parser round-trip across tags, Background, Rule, Scenario Outline + Examples, CRLF.src/lib/__tests__/user-journey-prompts.test.ts— Handlebars compilation + variable substitution + verification-protocol toggle.src/lib/__tests__/user-journey-workflow-integration.test.ts— Testcontainers-backed integration: sync, optimistic lock, accept UPDATE / CREATE, auto-supersede, staleness cascade, Prisma backstop.src/lib/__tests__/user-journeys-playwright-integration.test.ts— full browser flow: list → detail → accept proposal → reorder → free-text comment → refine → approve. Trace saved totest-results/user-journeys-browser/<journeyId>/trace.zip.
Out of scope (v1)
- Migration of legacy text-based scenario references (
ScenarioComment.scenarioName,TestScenario.scenarioName, etc.) toScenarioFKs. Will happen opportunistically in later phases. - Auto-refinement on staleness —
STALEis a flag, not an action. - Revert flow for already-accepted proposals.
Generating a User Journey Test
Once a journey is APPROVED, the Tests tab on the journey details page lets users generate an end-to-end Playwright/Cucumber test directly from the approved steps.
What makes a UJ test different from a regular CUJ
| Regular CUJ | User Journey Test | |
|---|---|---|
| Scenario source | Agent selects from all project scenarios | Approved journey steps, in exact order |
| Alignment | N/A | Alignment agent harmonises data across scenarios |
| Browser state | Each scenario can reset | Shared — no reset between scenarios |
| Data seed/teardown | Per-scenario Before/After hooks | Shared for one UJ feature execution; cleanup reset happens after that feature, not between scenarios |
Flow
- Approve the journey (all pending
ScenarioProposalrows must be resolved first). - Approval enqueues
USER_JOURNEY_TEST_ALIGNMENT, which stores the UJ-only aligned feature inUserJourneyTestAlignment. If this job fails, the Tests tab shows Restart alignment for editors. - Implement every scenario referenced by the journey. The Tests tab shows exactly which journey steps are still missing implementation, with links to the feature scenario (
/projects/[id]/features/[featureId]?tab=gherkin&scenario=...). The User Journeys list can be filtered to journeys whose referenced scenarios are all implemented, regardless of journey status. - Open the Tests tab → click Generate Test (model selector available).
- The backend rejects generation unless the journey is
APPROVEDand every referenced scenario is implemented. - A scenario is considered implemented when its exact scenario name appears in
Feature.implementedScenarios; legacy rows with no scenario list fall back to fully implemented when the feature status iscompletedormerged.
- The backend rejects generation unless the journey is
- A
USER_JOURNEY_TEST_GENERATIONjob is enqueued (status: PENDING). - The job processor reads the latest
UserJourneyTestAlignmentfor the currentUserJourney.versionand writes it to the UJ Cucumber feature output path (user-journey-tests/featuresby default). If no alignment exists yet, generation runs the same alignment agent on demand and stores the result before continuing.- Agents can refresh this generated artifact without creating a normal
Featurerow by runningrepave user-journeys test-feature regenerate --journey-id <journey-id> --cuj-id <cuj-id>. By default this re-aligns from the current source scenarios;--reuse-alignmentonly rewrites the latest stored alignment to disk.
- Agents can refresh this generated artifact without creating a normal
- The Cucumber implementation agent (
prompt/cuj-cucumber-implementation.mdwithsharedStateBetweenScenarios: true) implements dedicated UJ step definitions underuser-journey-tests/step_definitions. The initializedrun-uj-tests.shharness usesuser-journey-tests/supporthooks/world so the browser page and seeded data persist across scenarios in a feature. When a directory contains multiple UJ feature files, the runner executes each feature in a separate Cucumber process so data is reset after every feature execution. - After a passing run, the UJ test review agent (
prompt/user-journey-test-review.md) inspects the generated feature, selected source scenario Gherkin, step definitions, support hooks/world, JSON report, and trace artifact for false-positive risk, journey alignment, source outcome coverage, state continuity, and maintainability. If review fails, the same Cucumber implementation session is resumed with review feedback, then the test is re-run and re-reviewed before commit. A failed or malformed review result blocks acceptance instead of silently passing. - The Tests tab shows the latest generated UJ test result only; older generation attempts remain backend history.
- View Trace opens the latest generated test's Playwright trace via
TraceViewerDialog. The trace panel's Report Issue action logs aBugReportwith the selected action, picked locator, CUJ execution id, and user journey id; it does not immediately run a fix agent. - Any passing UJ-linked execution with a Playwright trace is marked ready for review. This includes the initial generated run, manual reruns from the Tests tab, and passing full post-fix UJ replays.
- When a reviewer fixes a bug report that came from a UJ trace, the bug-fix workflow re-runs the UJ test after the fix. It first attempts a targeted
--nameverification when one scenario can be identified, then always runs the full UJ feature. The passing full post-fix execution is the review-ready replay. - The User Journeys list and Tests tab derive one primary UJ test state: not ready, ready for review, reviewed, bug reported, or fix failed. Generation, running, interrupted, and failed-run details are shown as secondary context.
- The Tests tab shows a ready trace until an editor opens the trace and clicks Mark reviewed. The same item appears in the notification bell with an Open review action that links back to the journey Tests tab.
- If tests fail or review fails after retries, Regenerate Test re-enqueues the job. If the journey goes STALE, resolve and approve the journey before generating a fresh test.
Data model
Cuj.userJourneyId— nullable FK linking a CUJ back to its sourceUserJourney.UserJourney.cujs— back-relation listing all tests generated from this journey.CujExecution.reviewRequestedAt— marks a passing traced UJ-linked execution as ready for explicit replay review. The notification bell and User Journeys list hide the item once aCujExecutionReviewexists.CujExecution.sourceBugReportId,sourceAnalysisJobId,verificationKind— metadata linking post-fix UJ replay executions to the bug report/job that triggered them and identifying whether they are targeted or full re-review runs.CujExecution.runnerId,runnerRunId,runnerHeartbeatAt— DB-backed liveness fields refreshed while a CUJ/UJ execution is actively running. Astarting/runningrow without a fresh heartbeat is treated as interrupted instead of leaving the UI in limbo after a server restart.UserJourneyTestAlignment— versioned, UJ-only aligned Gherkin feature content used for UJ test generation. This preserves source scenario Gherkin unchanged.Cuj.scenarioSelectionJson— for UJ-generated tests, contains{ source: "user-journey", journeyId, alignmentId, alignmentNotes, steps }.
Initialized UJ harness
New modernized projects include a dedicated UJ Cucumber harness alongside the normal BDD harness:
run-uj-tests.sh— shell runner for aligned User Journey features. It preserves data between scenarios in the same feature, executes multiple feature files serially as separate Cucumber processes, resets after every feature execution by default, and supports--reset-beforeonly when recovering from an aborted previous run.cucumber.user-journey.js— Cucumber config that loads only UJ support and UJ step definitions.user-journey-tests/features/— generated aligned UJ feature files.user-journey-tests/step_definitions/— dedicated UJ step definitions written by the implementation agent.user-journey-tests/support/— shared-state UJworld.tsandhooks.ts; these reuse the existing BDD infrastructure helpers, avoid normal BDD step-definition registration, keep one browser/data state across scenarios in a feature, and reset data only after that feature execution completes.user-journey-tests/reports/— Cucumber JSON reports, screenshots, and trace artifacts produced by the UJ runner.
API
| Route | Method | Purpose |
|---|---|---|
/api/projects/[id]/user-journeys/[journeyId]/generate-test | POST | Validate APPROVED and all referenced scenarios implemented, create Cuj + AnalysisJob, return 202 { cujId, jobId } |
/api/projects/[id]/user-journeys | GET | Return journey list rows with reviewSummary, testStateSummary, and implementationReadiness for filtering and badges |
/api/projects/[id]/user-journeys/[journeyId]/tests | GET | Return { cujs, inFlightJob, inFlightAlignmentJob, latestAlignment, implementationReadiness } plus execution review metadata for polling |