Appearance
Coach Workflow / Daily Triage / Client Ranking
Priority view ranks coach-client relationships so coaches know who needs attention first.
The Priority view is a derived client-level triage surface. It does not replace the coach task inbox or risk flags. It aggregates open tasks, risk state, overdue commitments, client activity freshness, message reply gaps, onboarding blockers, and manual follow-up pins into an explainable ranked list of clients.
Page status
- Post-V1 coach workflow
- Client-level ranking
- Explainable score
- Inbox integrated
Not in Public V1
The Public V1 scope contract defers the full Priority view as a separate ranked-client product surface to Post-V1. Public V1 covers coach triage with task inbox sorting (coach_tasks.priority_score) and deterministic risk signals instead.
This page stays the approved design for the surface when it is built. Public V1 coach web and coach mobile must not depend on client-level rankScore, PriorityTier, or the explainable reason stack — but they should leave room for it, because Priority view is additive over data V1 already owns. Section terminology below still reads as if this shipped in V1 and needs a pass when the page is scheduled.
Design decision
Treat Priority view as an operational read model, not a separate source of truth. Source contexts own the events and data; coach_tasks owns individual actionable items; client_risk_flags owns risk state; Priority view owns the current ranking, reason summaries, and daily snapshots for the coach's visible relationships. Use deterministic thresholds: any critical signal becomes CRITICAL, scores of 75+ are HIGH, 45-74 are MEDIUM, 1-44 are LOW, and 0 is CLEAR. Critical crossings can push; high crossings update badges and daily digest only.
Priority View Flows
The view is built by projectors and scoring policy, then used by coach web and coach mobile as a start-of-day triage list.
System Builds The Ranking
- Source contexts emit operational events such as
CoachTaskCreated,ClientRiskFlagRaised,ClientMessageNeedsReply,DailyLogMissed,WorkoutSubmittedForReview, andCoachClientRelationshipPaused. - Priority projectors normalize those events into
CoachPrioritySignalrecords keyed by workspace, coach-client relationship, signal type, and source reference. - The scoring service recalculates one current
CoachPriorityItemper visible coach-client relationship. - The item stores score breakdown, top reasons, priority tier, next due timestamp, primary action, and source deep-link target.
- Coach web and coach mobile read the ranked list from the projection; they do not calculate ranking in the client.
Coach Starts Daily Review
- Coach opens Priority view and sees clients sorted by
rankScore, then due time, then latest client activity. - Each row shows client status, tier, top reasons, highest-risk flag, oldest overdue task, unread message status, and a primary action.
- Coach filters by priority tier, due today, onboarding, high risk, or unanswered messages.
- Opening a row shows the compact priority detail and links to task inbox, risk flag, profile, form review, workout review, nutrition review, or message thread.
- When the coach resolves a task, handles a risk flag, messages the client, or updates the plan, source events update the ranking automatically.
Manual Focus Or Pin
- Coach can pin a client to the top of their personal Priority view for a short follow-up window.
- Manual pins default to 24 hours and offer preset expirations of 24 hours, 3 days, and 7 days. Web can allow a custom expiration capped at 7 days.
- A coach can suppress a low-priority client until a later date when the current reason is acknowledged but no action is needed.
- Manual pinning can lift display order, but cannot hide critical system risks or overdue client-facing commitments.
- Manual changes are stored as priority events and included in the score breakdown as coach-specific modifiers.
Daily Snapshot
- Daily snapshots preserve the order the coach saw at the start of the day for support and reporting.
- Snapshot payloads store safe reason summaries and source references, not raw message, health, nutrition, or photo content.
- Snapshots are append-only for 90 days, then reduced to monthly aggregate counts retained for 12 months.
Domain Model
Priority view belongs to CoachWorkflow as a projection and ranking policy over coach-client relationships.
Aggregates & Entities
CoachPriorityItem: current ranked projection for one workspace and coach-client relationship.CoachPrioritySignal: normalized active or expired signal contributing to a relationship score.CoachPriorityReason: explainable reason shown in the UI, derived from one or more signals.CoachPrioritySnapshot: immutable daily ranking snapshot for reporting, digest generation, and support review.CoachPriorityViewPreference: per-coach filters, default tier filter, and suppression display behavior.CoachPriorityPolicy: domain service for scoring, tier thresholds, reason selection, suppression rules, and manual pin handling.
Value Objects & Rules
PriorityTier:CRITICAL,HIGH,MEDIUM,LOW,CLEAR.PrioritySignalType:OPEN_TASK,RISK_FLAG,O1ERDUE_ACTION,MESSAGE_REPLY_GAP,ONBOARDING_BLOCKER,ACTI1ITY_DROP,PROGRESS_STALL,MANUAL_PIN.PriorityAction:OPEN_TASK,RE1IEW_RISK,MESSAGE_CLIENT,RE1IEW_CHECK_IN,UPDATE_PLAN,RE1IEW_PROFILE,NO_ACTION.ScoreBreakdown: explainable numeric contribution by tasks, risk, due dates, communication, activity, onboarding, and manual modifiers.PriorityTierThresholds:CRITICALif any active signal severity is critical;HIGHat 75-100;MEDIUMat 45-74;LOWat 1-44;CLEARat 0.PriorityRowSummary: list rows may show source type, count, age, severity, due window, safe reason code, and primary action; raw message text, medical answers, photos, nutrition details, and private notes require opening the source workflow with source permission.- Only one current priority item exists per visible workspace and coach-client relationship.
- Score inputs must be deterministic and explainable. Do not ship opaque model scores in the first version.
- Package tier affects expected response cadence and applicable signals only. It never multiplies a client's score downward because they are on a lower-paid package.
- Priority payloads store safe summaries and source references only. Sensitive answers, photos, nutrition details, and private notes stay in source contexts.
| Model | Owned By | Important Methods | Emits |
|---|---|---|---|
CoachPriorityItem | CoachWorkflow | recalculate(), applyManualPin(), suppressUntil(), clearWhenNoSignals(), markViewed() | CoachPriorityItemChanged, CoachPriorityItemCleared |
CoachPrioritySignal | CoachWorkflow | createFromSource(), refresh(), expire(), weightContribution() | None |
CoachPriorityPolicy | CoachWorkflow | calculateScore(), deriveTier(), selectPrimaryAction(), selectReasons(), canSuppress() | None |
CoachTask | CoachWorkflow | Provides unresolved task count, oldest due task, severity, coach ownership, and deep-link target. | CoachTaskCreated, CoachTaskResolved, CoachTaskSnoozed |
ClientRiskFlag | CoachWorkflow | Provides active risk severity, evidence summary, recovery status, and intervention action. | ClientRiskFlagRaised, ClientRiskFlagResolved |
CoachClientRelationship | CoachClientManagement | Referenced by id for workspace, client, coach, lifecycle status, and visibility. | CoachClientRelationshipPaused, CoachClientRelationshipEnded |
Table Structure
Persist current ranking, normalized signals, daily snapshots, and coach-specific preferences. Keep raw source data in owning contexts.
V1 baseline Scoring Defaults
- Risk flags: critical risk is a hard
CRITICALoverride; high contributes 70; medium 45; low 20; recovering 15. - Open tasks: urgent contributes 45; high 30; normal 15; low 5, capped at 80 total task points.
- Due dates: due today contributes 10; overdue under 24 hours 20; overdue 1-3 days 35; overdue over 3 days 50, capped at 70.
- Message reply gaps: 24 hours contributes 15; 48 hours 30; 72 hours or more 45.
- Onboarding blockers: skipped blocking questionnaire 55; profile ready for review 35; onboarding stalled 25.
- Activity and progress: adherence drift or missed logs contribute 25-45 based on severity; progress stall contributes 35 when risk rules deem the data fresh.
- Manual pin adds a temporary 20-point display boost, cannot create
CRITICAL, and expires within 7 days.
Visibility & Notifications
- Default list shows
CRITICAL,HIGH,MEDIUM,LOW, andWATCHLIST;CLEARclients are hidden unless the coach filters or searches for them. - High threshold crossings update dashboard badges, mobile tabs, cached list pages, and daily digest entries. They do not send push notifications in Public V1.
- Critical threshold crossings can create a rate-limited coach push notification when the underlying source policy allows push.
- Package tier can tighten expected reply windows or add package-specific signals, but lower-tier clients are never automatically ranked below higher-tier clients.
- Daily snapshots retain safe row summaries for 90 days, then keep monthly aggregate counts for 12 months and delete row-level snapshot details.
| Table | Purpose | Important Columns | Constraints & Indexes |
|---|---|---|---|
coach_priority_items | Current client-level ranking read model. coach_user_id is the V1 baseline ownership field. | id, workspace_id, relationship_id, client_user_id, coach_user_id, status, priority_tier, rank_score, rank_position, top_reason, reason_codes jsonb, score_breakdown jsonb, primary_action, primary_source_context, primary_source_type, primary_source_id, next_due_at, latest_activity_at, manual_pin_until, suppressed_until, last_scored_at, created_at, updated_at | Unique (workspace_id, relationship_id); index (workspace_id, status, rank_score desc, next_due_at); index (workspace_id, coach_user_id, status, rank_score desc); default query excludes CLEAR, archived, ended relationships, and future suppressions unless requested. |
coach_priority_signals | Normalized score inputs from tasks, risk flags, communication, activity, onboarding, and manual actions. | id, workspace_id, relationship_id, signal_type, status, severity, weight, source_context, source_type, source_id, dedupe_key, reason_code, payload jsonb, occurred_at, expires_at, resolved_at, created_at, updated_at | Partial unique (workspace_id, relationship_id, dedupe_key) where status is ACTI1E; index (workspace_id, relationship_id, status); index (source_context, source_type, source_id); payload contains safe summary only. |
coach_priority_events | Append-only history for scoring transitions, manual pins, suppressions, views, and projector activity. | id, priority_item_id, event_type, actor_user_id, source_event_id, metadata jsonb, created_at | Index (priority_item_id, created_at); idempotency unique on (priority_item_id, source_event_id, event_type) when source event is present. |
coach_priority_snapshots | Immutable daily ranking rows for digest, reporting, and support review. | id, workspace_id, snapshot_date, timezone, relationship_id, rank_position, priority_tier, rank_score, top_reasons jsonb, created_at | Unique (workspace_id, snapshot_date, relationship_id); index (workspace_id, snapshot_date, rank_position); no application soft delete. |
coach_priority_view_preferences | Per-coach default filters. | id, workspace_id, user_id, default_tier_filter, show_suppressed, show_clear_clients, updated_at | Unique (workspace_id, user_id); safe to recreate from defaults if deleted. |
Note
Use plain UUID references across bounded contexts. Priority view may validate relationship visibility through application services and read models, but it must not navigate or mutate Profile, Forms, Programs, Nutrition, Communication, or Analytics aggregates.
Lifecycle Policy
Priority state is derived, recalculable, and explainable. Operational history is retained separately from current ranking.
Current Item Lifecycle
ACTI1Ewhen one or more active signals, overdue commitments, unresolved tasks, active risk flags, or manual pins require attention.WATCHLISTwhen signals are weak but still useful for monitoring, such as early adherence drift or recent onboarding delay.SUPPRESSEDwhen a coach intentionally hides a low-priority item until a future timestamp.CLEARwhen no active signals remain and no manual pin is active.ARCHI1EDwhen the coach-client relationship is ended, revoked, deleted, or hidden by retention policy.
Signal Lifecycle
- Signals are created or refreshed from source events and are idempotent by source dedupe key.
- Signals resolve when source tasks, risks, messages, or activity gaps resolve.
- Time-windowed signals expire through a cleanup job and trigger score recalculation.
- Manual pin signals expire at
manual_pin_until; suppression expires atsuppressed_until. - Source deletion or privacy redaction removes sensitive payload fields but keeps minimal score history when retention policy allows it.
Retention & Visibility
- Current priority items are recalculable and can be rebuilt from active tasks, active flags, recent activity summaries, and manual events.
- Priority events are retained for 90 days for operational support and scoring audit, then trimmed to minimal support metadata unless workspace policy is stricter.
- Daily snapshots preserve only safe reason summaries, not raw medical, photo, nutrition, or message content; row-level snapshots are retained for 90 days and monthly aggregates for 12 months.
- Ended relationships are removed from default views immediately and archived by relationship status projector.
Score Stability
- Incremental recalculation updates the affected relationship after each source event.
- Workspace-level rank positions can be recomputed asynchronously after item scores change.
- Daily snapshots freeze the morning ranking used for digest and comparison.
- Tie-breakers are deterministic: score desc, next due asc, critical risk first, latest source event desc, relationship id asc.
API Contracts
Coach-facing APIs read the projection and record coach-specific priority actions. Projector ingestion stays internal.
| Endpoint | Purpose | Request | Response |
|---|---|---|---|
GET /api/v1/workspaces/{workspaceId}/coach-priority | List current priority items for the coach's visible relationships. | Query: tier, status, hasRisk, dueBefore, includeSuppressed, cursor. | Cursor page of CoachPriorityListItemDto with safe client card, tier, score, reasons, due summary, primary action, and source route. |
GET /api/v1/workspaces/{workspaceId}/coach-priority/summary | Return counts for dashboard widgets, mobile tabs, and daily digest headers. | Optional tier and due-window filters. | Counts by tier, overdue, high risk, unanswered message, onboarding blocker, suppressed, and clear. |
GET /api/v1/workspaces/{workspaceId}/coach-priority/{relationshipId} | Read detail for one priority item. | Authorized coach workspace session and visible relationship. | Priority detail, score breakdown, active signals, related tasks, related risk flags, source routes, and available actions. |
POST /api/v1/workspaces/{workspaceId}/coach-priority/{relationshipId}/viewed | Audit that a coach opened a priority item. | Idempotency key and optional surface context. | Updated viewed timestamp and item summary. |
POST /api/v1/workspaces/{workspaceId}/coach-priority/{relationshipId}/pin | Add or update a coach-specific manual follow-up pin. | pinUntil, optional reason, idempotency key. | Updated item with manual-pin reason and recalculated display order. |
POST /api/v1/workspaces/{workspaceId}/coach-priority/{relationshipId}/suppress | Temporarily hide a low-priority item from default views. | suppressedUntil, reason, idempotency key. | Suppressed item and updated summary counts. |
POST /api/v1/workspaces/{workspaceId}/coach-priority/{relationshipId}/recalculate | Force rebuild for one relationship for support/admin diagnostics. | Support/admin actor, optional reason. | Recalculated item and score diagnostics. |
PUT /api/v1/workspaces/{workspaceId}/coach-priority/preferences | Save default filters for the current coach. | Tier filter, show suppressed, show clear clients. | Saved preferences. |
Key Code Snippets
Implementation sketches for a TypeScript, NestJS, and Drizzle modular monolith. Priority view reads from source-owned state and writes only ranking projections, preference rows, and audit events.
Drizzle schema shape
apps/api/src/coach-workflow/db/coach-priority.schema.ts
ts
export const priorityItemStatus = pgEnum("coach_priority_item_status", [
"ACTI1E",
"WATCHLIST",
"SUPPRESSED",
"CLEAR",
"ARCHI1ED",
]);
export const priorityTier = pgEnum("coach_priority_tier", [
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
"CLEAR",
]);
export const coachPriorityItems = pgTable("coach_priority_items", {
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id").notNull(),
relationshipId: uuid("relationship_id").notNull(),
clientUserId: uuid("client_user_id").notNull(),
coachUserId: uuid("coach_user_id").notNull(),
status: priorityItemStatus("status").notNull().default("CLEAR"),
priorityTier: priorityTier("priority_tier").notNull().default("CLEAR"),
rankScore: integer("rank_score").notNull().default(0),
rankPosition: integer("rank_position"),
topReason: text("top_reason"),
reasonCodes: jsonb("reason_codes").$type<string[]>().notNull().default([]),
scoreBreakdown: jsonb("score_breakdown").$type<PriorityScoreBreakdown>().notNull(),
primaryAction: text("primary_action").$type<PriorityAction>().notNull().default("NO_ACTION"),
primarySourceContext: text("primary_source_context"),
primarySourceType: text("primary_source_type"),
primarySourceId: uuid("primary_source_id"),
nextDueAt: timestamp("next_due_at", { withTimezone: true }),
latestActivityAt: timestamp("latest_activity_at", { withTimezone: true }),
manualPinUntil: timestamp("manual_pin_until", { withTimezone: true }),
suppressedUntil: timestamp("suppressed_until", { withTimezone: true }),
lastScoredAt: timestamp("last_scored_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
relationshipUnique: uniqueIndex("coach_priority_items_relationship_uq")
.on(table.workspaceId, table.relationshipId),
workspaceRankIdx: index("coach_priority_items_workspace_rank_idx")
.on(table.workspaceId, table.status, table.rankScore, table.nextDueAt),
coachRankIdx: index("coach_priority_items_coach_rank_idx")
.on(table.workspaceId, table.coachUserId, table.status, table.rankScore),
}));
export const coachPrioritySignals = pgTable("coach_priority_signals", {
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id").notNull(),
relationshipId: uuid("relationship_id").notNull(),
signalType: text("signal_type").$type<PrioritySignalType>().notNull(),
status: text("status").$type<"ACTI1E" | "RESOL1ED" | "EXPIRED">().notNull().default("ACTI1E"),
severity: text("severity").$type<PrioritySignalSeverity>().notNull().default("NORMAL"),
weight: integer("weight").notNull().default(0),
sourceContext: text("source_context").notNull(),
sourceType: text("source_type").notNull(),
sourceId: uuid("source_id").notNull(),
dedupeKey: text("dedupe_key").notNull(),
reasonCode: text("reason_code").notNull(),
payload: jsonb("payload").$type<PrioritySignalPayload>().notNull(),
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }),
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
activeSignalUnique: uniqueIndex("coach_priority_signals_active_uq")
.on(table.workspaceId, table.relationshipId, table.dedupeKey)
.where(sql`${table.status} = 'ACTI1E'`),
relationshipSignalIdx: index("coach_priority_signals_relationship_idx")
.on(table.workspaceId, table.relationshipId, table.status),
sourceLookupIdx: index("coach_priority_signals_source_idx")
.on(table.sourceContext, table.sourceType, table.sourceId),
}));Signal projector use case
apps/api/src/coach-workflow/use-cases/refresh-priority-signal.ts
ts
@Injectable()
export class RefreshCoachPrioritySignalUseCase {
constructor(
private readonly relationships: CoachClientRelationshipRepository,
private readonly signals: CoachPrioritySignalRepository,
private readonly priority: CoachPriorityItemRepository,
private readonly policy: CoachPriorityPolicy,
private readonly outbox: OutboxPort,
private readonly tx: TransactionRunner,
private readonly clock: Clock,
) {}
async execute(command: RefreshPrioritySignalCommand): Promise<CoachPriorityItemDto> {
return this.tx.run(async () => {
const relationship = await this.relationships.getById(command.relationshipId);
this.policy.assertRelationshipCanBeRanked(relationship);
const signal = CoachPrioritySignal.fromSource({
workspaceId: relationship.workspaceId,
relationshipId: relationship.id,
signalType: command.signalType,
severity: command.severity,
source: command.source,
reasonCode: command.reasonCode,
payload: command.safeSummary,
occurredAt: command.occurredAt,
expiresAt: command.expiresAt,
});
await this.signals.upsertActive(signal);
const activeSignals = await this.signals.listActiveForRelationship(
relationship.workspaceId,
relationship.id,
);
const existing = await this.priority.getOrCreateForRelationship(relationship);
const scored = this.policy.calculateScore({
relationship,
signals: activeSignals,
previousItem: existing,
now: this.clock.now(),
});
existing.recalculate(scored);
await this.priority.save(existing);
await this.outbox.publish(existing.pullDomainEvents());
return CoachPriorityItemDto.fromDomain(existing);
});
}
}Scoring policy
apps/api/src/coach-workflow/policies/coach-priority.policy.ts
ts
const PRIORITY_TIER_THRESHOLDS = {
HIGH: 75,
MEDIUM: 45,
LOW: 1,
} as const;
const SIGNAL_SCORE_CAPS: Record<PrioritySignalType, number> = {
OPEN_TASK: 80,
RISK_FLAG: 100,
O1ERDUE_ACTION: 70,
MESSAGE_REPLY_GAP: 45,
ONBOARDING_BLOCKER: 55,
ACTI1ITY_DROP: 45,
PROGRESS_STALL: 35,
MANUAL_PIN: 20,
};
export class CoachPriorityPolicy {
calculateScore(input: PriorityScoreInput): PriorityScoreResult {
const taskScore = this.sumSignals(input.signals, "OPEN_TASK", 0, SIGNAL_SCORE_CAPS.OPEN_TASK);
const riskScore = this.sumSignals(input.signals, "RISK_FLAG", 0, SIGNAL_SCORE_CAPS.RISK_FLAG);
const dueScore = this.sumSignals(input.signals, "O1ERDUE_ACTION", 0, SIGNAL_SCORE_CAPS.O1ERDUE_ACTION);
const messageScore = this.sumSignals(input.signals, "MESSAGE_REPLY_GAP", 0, SIGNAL_SCORE_CAPS.MESSAGE_REPLY_GAP);
const activityScore = this.sumSignals(input.signals, "ACTI1ITY_DROP", 0, SIGNAL_SCORE_CAPS.ACTI1ITY_DROP);
const progressScore = this.sumSignals(input.signals, "PROGRESS_STALL", 0, SIGNAL_SCORE_CAPS.PROGRESS_STALL);
const onboardingScore = this.sumSignals(input.signals, "ONBOARDING_BLOCKER", 0, SIGNAL_SCORE_CAPS.ONBOARDING_BLOCKER);
const manualScore = this.activeManualPinBoost(input.previousItem, input.now);
const rawScore = taskScore + riskScore + dueScore + messageScore +
activityScore + progressScore + onboardingScore + manualScore;
const rankScore = Math.min(100, rawScore);
const reasons = this.selectReasons(input.signals, input.previousItem);
const tier = this.deriveTier(rankScore, input.signals);
const primary = this.selectPrimaryAction(input.signals);
return {
rankScore,
tier,
status: this.deriveStatus(rankScore, input.signals, input.previousItem, input.now),
reasons,
primaryAction: primary.action,
primarySource: primary.source,
scoreBreakdown: {
tasks: taskScore,
risk: riskScore,
due: dueScore,
communication: messageScore,
activity: activityScore,
progress: progressScore,
onboarding: onboardingScore,
manual: manualScore,
},
};
}
deriveTier(score: number, signals: CoachPrioritySignal[]): PriorityTier {
if (signals.some((signal) => signal.severity === "CRITICAL")) return "CRITICAL";
if (score >= PRIORITY_TIER_THRESHOLDS.HIGH) return "HIGH";
if (score >= PRIORITY_TIER_THRESHOLDS.MEDIUM) return "MEDIUM";
if (score >= PRIORITY_TIER_THRESHOLDS.LOW) return "LOW";
return "CLEAR";
}
canSuppress(item: CoachPriorityItem): boolean {
return item.priorityTier !== "CRITICAL" && item.rankScore < 75;
}
private sumSignals(
signals: CoachPrioritySignal[],
type: PrioritySignalType,
min: number,
max: number,
): number {
const value = signals
.filter((signal) => signal.signalType === type && signal.status === "ACTI1E")
.reduce((sum, signal) => sum + signal.weightContribution(), 0);
return Math.max(min, Math.min(max, value));
}
}List query service
apps/api/src/coach-workflow/queries/list-coach-priority.ts
ts
@Injectable()
export class ListCoachPriorityQuery {
constructor(
private readonly priority: CoachPriorityReadRepository,
private readonly relationships: CoachClientRelationshipReadRepository,
private readonly policy: CoachPriorityAccessPolicy,
) {}
async execute(query: ListCoachPriorityRequest, actor: Actor): Promise<CursorPage<CoachPriorityListItemDto>> {
actor.requireRole("COACH");
actor.requireWorkspaceMembership(query.workspaceId, ["OWNER", "COACH"]);
const visibility = await this.relationships.getVisibilityScope({
workspaceId: query.workspaceId,
actorUserId: actor.userId,
requestedScope: "COACH",
});
this.policy.assertCanList(actor, query, visibility);
const page = await this.priority.list({
workspaceId: query.workspaceId,
relationshipIds: visibility.relationshipIds,
tiers: query.tiers,
statuses: query.statuses,
includeSuppressed: query.includeSuppressed,
dueBefore: query.dueBefore,
cursor: query.cursor,
limit: query.limit,
});
return page.map((item) => CoachPriorityListItemDto.fromReadModel(item, actor.permissions));
}
}Events & Background Jobs
Priority view consumes task, risk, relationship, activity, and communication events. It emits ranking events for dashboards, badges, and digest jobs.
Consumed Events
CoachTaskCreated,CoachTaskRefreshed,CoachTaskResolved,CoachTaskDismissed,CoachTaskSnoozed: update open task and overdue signals.ClientRiskFlagRaised,ClientRiskFlagRefreshed,ClientRiskFlagResolved: update risk signals and top reason selection.ClientMessageNeedsReply,ClientMessageReplied: update communication gap signals.CheckInMissed,DailyLogMissed,WorkoutSkipped,MeasurementTrendUpdated: update adherence and progress signals.ClientProfileReadyForReview,OnboardingQuestionnaireSkipped,ClientOnboardingCompleted: update onboarding and profile readiness signals.CoachClientRelationshipPaused,CoachClientRelationshipEnded: update visibility and archive behavior.
Emitted Events
CoachPrioritySignalCreated: audit normalized signal creation.CoachPriorityItemChanged: update dashboard counts, mobile badges, and cached list pages.CoachPriorityTierRaised: update badges, cache, and digest entries for high crossings; critical crossings may push only when source policy allows it.CoachPriorityItemViewed: record that a coach reviewed the item.CoachPriorityItemPinnedandCoachPriorityItemSuppressed: record coach-specific triage decisions.CoachPriorityDailySnapshotCreated: feed digest and reporting jobs.
Jobs
- Incremental projector processes source events and refreshes the affected relationship.
- Hourly overdue sweep updates due-date and message-reply-gap signals.
- Daily workspace snapshot job freezes ranking by workspace timezone for digest and reporting, retaining row-level snapshots for 90 days.
- Monthly snapshot aggregation job keeps count-level metrics for 12 months and deletes expired daily row details.
- Expired signal cleanup resolves stale time-windowed signals and recalculates affected items.
- Relationship lifecycle projector archives priority items for ended or revoked relationships.
- Backfill job rebuilds current items from active tasks, active flags, recent activity summaries, and manual events.
Priority Inputs
- Open task count, highest task severity, and oldest overdue task from the coach task inbox.
- Active risk flag severity, recovery status, and evidence freshness from risk flags.
- Unanswered message age and client reply SLA from communication.
- Recent missed logs, skipped workouts, check-in gaps, and progress drift from activity summaries.
- Onboarding status, profile readiness, package-derived SLA expectations, and relationship status.
- Manual pin and suppression state from coach-specific priority actions.
Permissions & Access Rules
Priority view can reveal sensitive operational context even when it uses safe summaries. Every query must enforce workspace, role, relationship, and source-level visibility.
Coach
- Must have active workspace membership and visibility into the coach-client relationship.
- Coach can list priority items for relationships they can see in their workspace.
- Can open source workflows only when they also have source-specific permission such as forms, nutrition, media, messages, or risk flags.
- Can pin or suppress only visible relationships and only within Public V1 limits: pin defaults to 24 hours, custom pin max is 7 days, and suppression cannot hide critical items.
- Cannot suppress critical items.
Client & System
- Clients never read priority score, rank, or internal coach reasons.
- Client-facing actions are created through source workflows such as messages, nudges, check-in requests, or plan updates.
- System projectors can create, refresh, clear, archive, snapshot, and rebuild priority items through internal service accounts.
- System workers must use idempotency keys based on source event id and source dedupe key.
Web, Coach Mobile, Client Mobile
Coach surfaces share API contracts but optimize the triage interaction for their context.
Coach Web
- Primary dense triage table with priority tier, score, reasons, due summary, client status, and primary action.
- Supports saved filters, search, reason drilldown, source deep links, and side-panel detail.
- Can open task inbox filtered to the selected client or source category.
- Can pin, suppress, mark viewed, and navigate to message, risk, profile, form, workout, nutrition, or program workflows.
Coach Mobile
- Action-first ranked list grouped by critical, high, due today, and watchlist.
- Shows safe reasons and one primary action per client; deeper review opens mobile-native source screens where available.
- Supports message, nudge, pin, suppress, and open task actions. Complex review can defer to web when no mobile workflow exists yet.
- Refreshes on foreground, critical/source-approved push notification receipt, and manual pull-to-refresh. Offline mode can show cached rankings as read-only.
Client Mobile
- Does not display coach priority rank, score, internal reasons, or suppression state.
- Receives only explicit client-facing actions such as messages, nudges, questionnaire requests, plan changes, or check-in prompts.
- Offline client actions update priority only after sync reaches the backend and source events are projected.
- Client privacy settings and data deletion policies must redact priority summaries when source records are redacted.
Test Scenarios & V1 Baseline Decisions
Validate deterministic scoring, projection idempotency, permissions, source-boundary behavior, notifications, and retention before implementation.
Tests
- Domain: scoring combines task, risk, due-date, communication, activity, onboarding, and manual-pin inputs into expected tiers.
- Domain: seeded score weights produce
HIGHat 75+,MEDIUMat 45-74,LOWat 1-44, andCLEARat 0. - Domain: critical signals cannot be suppressed and always produce
CRITICALtier regardless of manual state. - Domain: manual pins default to 24 hours, cannot exceed 7 days, and add only the 20-point manual display boost.
- Domain: expired signals clear the item when no other active signals remain.
- Projection: repeated source event refreshes one active signal and one priority item instead of duplicating rows.
- Projection: task resolution, risk recovery, message reply, and onboarding completion lower or clear the affected relationship.
- Projection: package tier changes adjust applicable SLA signals without lowering any lower-tier client by default.
- API: coach cannot list relationships outside workspace or relationship visibility.
- API: reason summaries redact source-sensitive detail when actor lacks source permission.
- API: pin, suppress, viewed, and recalculate endpoints enforce idempotency keys and audit events.
- API: default list excludes clear clients, but explicit filters and search can include them.
- Notification: high crossings update badges and daily digest without push; critical crossings push only through source-approved rate limits.
- Integration: daily snapshot freezes order by workspace timezone and remains unchanged after later same-day events.
- Retention: daily snapshots purge row details after 90 days and preserve monthly aggregate counts for 12 months.
- Web E2E: coach opens Priority view, filters high priority, opens a client, resolves related task, and sees rank update.
- Coach mobile E2E: coach opens critical item, sends message, pins follow-up, and sees cached read-only ranking while offline.
- Performance: list endpoint returns first page under target latency with thousands of active relationships in a workspace.
Resolved Decisions
- Initial tiers: any active critical signal is
CRITICAL; score 75+ isHIGH; 45-74 isMEDIUM; 1-44 isLOW; 0 isCLEAR. - Manual pins: default to 24 hours, offer 24-hour, 3-day, and 7-day presets, allow custom web expiration up to 7 days, and cannot hide critical risks.
- Snapshots: keep daily row-level safe summaries for 90 days, then retain monthly aggregate counts for 12 months.
- Default visibility: hide
CLEARclients by default for all coaches; make them available through search or an explicit filter. - Safe row details: show source type, age, severity, counts, due window, safe reason code, and primary action only. Raw source content stays behind source-specific permissions.
- Notifications: high crossings update dashboard badges, mobile tabs, cached lists, and daily digests. Push is reserved for critical source-approved events.
- Package influence: package tier can define expected SLA and applicable signals, but it never creates a blanket paid-plan priority multiplier.