Docs / Admin

Feedback, bug, and friction intake

Summary — Staff and leads capture bugs, friction, blocks, and other operational gaps as feedback reports. Each report moves through a fixed status machine, gets an automated triage suggestion, and — once triaged — is manually routed by a lead into a change request, an incident, or a workflow block.

Purpose — Understand the feedback report lifecycle, the fields it captures, how the automated triage suggestion is computed, and what each routing outcome actually creates downstream.

Audience — Staff can create and view reports. Lead (or admin) roles perform all triage, classification, and resolution actions.

Prerequisites — None. This is a platform-level governance tool, not tied to a specific business module (though every report is tagged to one via moduleScope).


Overview

Key concepts:

  • Feedback report — A single intake record: a bug, friction point, workflow block, missing requirement, automation gap, data-quality issue, or privacy risk.
  • Feedback status machine — 10 statuses, transitions centralized in state-machine.ts (never duplicated in the UI, per project rule 15).
  • Automated triage suggestion — A read-only recommendation (suggestTriage()) shown while a report awaits routing. Nothing auto-executes; a lead must click the corresponding action.
  • Workflow block — A separate record type for something actively blocking an operational flow (e.g., a stuck consultation), with its own 6-status lifecycle.
  • Friction signal — An aggregated counter (abandoned forms, repeated validation errors, etc.) surfaced on the dashboard, not an individually actionable record.
  • Routing outcomes — Triaged reports link to a real change_requests or ops_incidents row in the ops-control module, not just a status label.

Screenshot: Admin → Feedback dashboard showing "Open by type", "Aging by severity", "Affected modules", and "Validation failures" stat tiles above the filterable report list.


Feedback report fields

Captured via createFeedbackReportSchema (Zod, src/lib/modules/feedback/validators.ts):

FieldConstraintNotes
feedbackTyperequired, enumSee Feedback types table below
titlerequired, 1–500 chars
summaryrequired, 1–5000 chars
sourceoptional, max 100 charsFree text — where the report came from
environmentoptional, max 100 charse.g. production
moduleScoperequired, 1–100 charsWhich module/area is affected
routePathoptional, max 500 charsThe URL where it happened
entityType / entityIdoptional (entityId must be a UUID)Links the report to a specific record
workflowStepoptional, max 200 chars
severityoptional, enum (low/medium/high/critical)
reproducibilityoptional, max 2000 chars
privacyClassificationoptional, max 100 chars
evidenceLinksoptional, array of strings (max 1000 chars each)Split from newline-separated textarea input in the create form

Feedback types

ValueLabel shown in UI
bugBug
performancePerformance
frictionFriction
blockBlock
missing_requirementMissing Requirement
automation_gapAutomation Gap
data_qualityData Quality
privacy_riskPrivacy Risk

The feedback status machine

Statuses (FeedbackStatus): new, classified, triaged, linked_to_change_request, linked_to_incident, linked_to_workflow_block, mitigated, resolved, closed, rejected.

Allowed transitions (FEEDBACK_TRANSITIONS in state-machine.ts):

FromAllowed to
newclassified
classifiedtriaged
triagedlinked_to_change_request, linked_to_incident, linked_to_workflow_block
linked_to_change_requestmitigated, resolved
linked_to_incidentmitigated, resolved
linked_to_workflow_blockresolved
mitigatedresolved
resolvedclosed
closed— (terminal)
rejected— (terminal)

rejected is reachable from any non-terminal status — any report in new through resolved can be rejected (ACTIVE_FEEDBACK_STATUSES includes everything except closed/rejected). closed and rejected are the only two terminal statuses.

The detail page (/admin/feedback/[id]) only renders an action button for a next status if TRANSITION_ACTIONS has an entry for it — every status in the table above has a corresponding button ("Classify", "Mark Triaged", "Create Change Request", "Open Incident", "Mark Mitigated", "Resolve", "Close", "Reject"), except linked_to_workflow_block, which opens an inline form instead of a single-click button.


Automated triage suggestion

Shown as a blue info box on the detail page while report.status is new, classified, or triaged. Computed by suggestTriage() (src/lib/modules/feedback/triage-rules.ts) — a suggestion only; a lead must still click the matching action. auto_mitigate is deliberately never suggested — fast mitigations require a human to choose them.

ConditionSuggested outcomeReason shown
An open report with the same title + module scope already existsreject_as_duplicate"An open report with the same title and module scope exists"
feedbackType = privacy_riskcreate_incident"Privacy risks always route to incident handling"
feedbackType is bug or performance, severity is high/critical, and environment = productioncreate_incident"{severity} severity with live production impact"
feedbackType is bug or performance (otherwise)create_change_request"Routes to governed engineering work"
feedbackType = blockcreate_workflow_block"Blocks route to operational owner resolution"
feedbackType = missing_requirementrequest_client_input"Missing requirements need phase-input review and client input"
feedbackType = friction, severity lowroute_to_backlog"Low-severity friction routes to the product backlog"
feedbackType = friction (otherwise)create_change_request"Recurring or impactful friction routes to engineering work"
feedbackType = automation_gapcreate_change_request"Automation gaps route to an automation change request"
feedbackType = data_qualitycreate_change_request"Data quality issues route to the data correction queue"

Note: request_client_input and route_to_backlog are suggestion-only outcomes — there is no corresponding status transition or downstream record creation for them in the current code; they surface as guidance text only.


What each routing action actually creates

Link to change request

Action: "Create Change Request" button (available from triaged).

Only allowed for feedbackType in bug, performance, friction, automation_gap, data_quality (CHANGE_REQUEST_TYPES). Attempting it on any other type throws Feedback type '{type}' does not route to a change request.

Calls createChangeRequest() in the ops-control module, creating a real change_requests row with requestType set to performance if the feedback type is performance, otherwise bug — carrying over title, summary, severity, moduleScope, privacyClassification, and requestSource: "feedback". The feedback report then transitions to linked_to_change_request with linkedChangeRequestId set to the new change request's ID.

Link to incident

Action: "Open Incident" button (available from triaged).

Only allowed if feedbackType = privacy_risk, or severity is high/critical. Otherwise throws Incident linking requires privacy risk or high/critical severity.

Calls createOpsIncident() in ops-control — incidentType is set to privacy if the feedback type is privacy_risk, otherwise reliability; detectedFrom: "feedback". Transitions to linked_to_incident with linkedIncidentId set.

Link to workflow block

Action: "Create Workflow Block" (inline form, available from triaged) — requires blockedEntityType, moduleScope, workflowStep, blockReason (all required text inputs, pre-filled with values from the report where available), and an optional requiredInput.

Creates a WorkflowBlock record scoped to the report and transitions the feedback report to linked_to_workflow_block.


Workflow blocks

A workflow block represents something actively stopping an operational flow — separate lifecycle from the feedback report that spawned it (a block can also exist without a linked report).

Statuses (WorkflowBlockStatus): open, acknowledged, waiting_for_input, in_progress, resolved, closed.

Allowed transitions (WORKFLOW_BLOCK_TRANSITIONS):

FromAllowed to
openacknowledged
acknowledgedwaiting_for_input, in_progress
waiting_for_inputin_progress
in_progressresolved
resolvedclosed
closed— (terminal)

Open blocks (status not closed/resolved) show on the feedback dashboard under "Open Workflow Blocks", with block reason, module scope, workflow step, owner role, and — if set — the required input needed to unblock it.

transitionWorkflowBlockAction requires staff role (lower bar than the lead role required for feedback report transitions), reflecting that operational owners resolving a block may not be leads.


Friction signals

A separate, lighter record type: an aggregated counter of a recurring UX friction pattern, not an individually triaged report.

Signal types (FrictionSignalType): abandoned_form, repeated_validation_error, manual_override, backtrack, long_idle_time, support_request, unclear_step.

Each signal tracks moduleScope, routePath, workflowStep, signalType, a running count, sampleActorRole, firstSeenAt, lastSeenAt. The dashboard's "Top Friction Signals" list shows up to 10, sorted as returned by listFrictionSignals(), and a dedicated "Validation failures" tile filters specifically for repeated_validation_error signals.


Viewing and filtering feedback

Path: Admin → Feedback (/admin/feedback)

Dashboard stat tiles (computed client-side over open/non-terminal reports, where terminal = resolved, closed, rejected):

  • Open by type — count of open reports per feedbackType, sorted descending.
  • Aging by severity — oldest open report's age in days, per severity level.
  • Affected modules — top 5 moduleScope values by open-report count.
  • Validation failures — top 5 repeated_validation_error friction signals by count.

Filters: Quick-filter links for All, and status shortcuts new, triaged, resolved (via ?status=), plus one link per feedback type (via ?type=). Filtering is server-side through listFeedbackReportsAction/listFeedbackReports, driven entirely by URL search params — there is no combined status+type filter UI beyond what the query string supports.


Roles and permissions

ActionRequired role
Create feedback reportstaff
List feedback reportsstaff
Classify / Triage / Link to change request / Link to incident / Link to workflow block / Mark mitigated / Resolve / Close / Rejectlead
Transition a workflow blockstaff

All checks use requireRole(role, { module: "feedback" }). Admin implicitly satisfies any lower-role check per the role hierarchy.


Best practices

  1. Fill in entityType/entityId and routePath when reporting a bug tied to a specific record. These fields drive pre-filled workflow-block forms and make the report actionable without re-asking the reporter for context.
  2. Trust the triage suggestion but verify severity first. The incident-routing threshold (high/critical + production) is a hard gate on the "Open Incident" action itself — if severity doesn't qualify, the action throws regardless of what the suggestion box says.
  3. Use friction signals for volume, feedback reports for individual action. A single friction report per user session is noise; the friction-signal counters exist specifically so recurring UX pain shows up in aggregate instead.
  4. Close the loop. A report linked to a change request or incident isn't done until it reaches resolvedclosed — the linked ID alone doesn't reflect downstream completion.

Warnings

Routing actions are hard-gated, not just suggested. "Create Change Request" throws if the feedback type isn't in the allowed list; "Open Incident" throws if severity/type doesn't meet the threshold. The blue suggestion box does not override these checks.

closed and rejected are both terminal. Neither has any further allowed transition — there is no "reopen" path in the state machine.

request_client_input and route_to_backlog have no dedicated action or status. They are triage-suggestion labels only; acting on them today means a lead manually following up outside the feedback UI (e.g., contacting the client, or adding an item to a separate backlog tool).


Troubleshooting

"Feedback transition '{from}' → '{to}' not allowed"

Symptom: Server error when submitting a transition action.

Cause: The target status isn't in the allowed-transitions list for the report's current status (canTransitionFeedback returned false).

Fix: Check the current status against the transition table above. The detail page only renders buttons for legal transitions, so this typically only happens if the report's status changed between page load and submission — reload the page and retry.


"Feedback type '{type}' does not route to a change request"

Symptom: Clicking "Create Change Request" fails with this message.

Cause: The report's feedbackType is not one of bug, performance, friction, automation_gap, data_quality (e.g., it's block, missing_requirement, or privacy_risk).

Fix: Use the routing outcome that matches the type instead — block → workflow block, privacy_risk → incident. missing_requirement has no dedicated action; follow up manually per the "Request client input" suggestion.


"Incident linking requires privacy risk or high/critical severity"

Symptom: Clicking "Open Incident" fails with this message.

Cause: The report is neither privacy_risk type nor high/critical severity.

Fix: If the underlying issue genuinely warrants incident handling, first reclassify the severity (Classify action) to high or critical, then retry.


FAQ

Who can submit a feedback report? Any user with staff role or higher.

What is the difference between a feedback report and a workflow block? A feedback report is the intake record describing what happened. A workflow block is a separate, focused record for something actively stopping an operational flow right now — it can be created from a feedback report or independently, and has its own status lifecycle.

Does creating a change request or incident from feedback actually build anything? It creates a real row in the change_requests or ops_incidents tables (ops-control module) — not just a label on the feedback report. See Ops control and shift readiness for what happens to that record next.

Can a rejected report be reopened? No. rejected (like closed) has no allowed forward transitions in the current state machine.

Why does the dashboard show a report as "open" differently from its literal status? "Open" on the dashboard means any status that isn't resolved, closed, or rejected — it's a computed grouping for the stat tiles, not a status value stored in the database.


Related articles