Appearance
Coach task inbox turns client activity into a prioritized work queue coaches can clear every day.
The task inbox is the coach-facing operational layer for events that need human attention: questionnaire submissions, profile readiness, missed check-ins, workouts needing review, progress alerts, nutrition exceptions, and message follow-ups. It does not own the source records. It stores actionable work items, dedupes noisy signals, and records how the coach handled them.
Tags: V1 baseline coach queue · Event driven · Priority sorted · Audited resolution
V1 baseline decisions
build a coach inbox, not a general project-management system. Tasks are created by product events or supported coach actions, scoped to a workspace and coach-client relationship, owned by the coach for that relationship, and resolved through coaching workflows. Priority uses fixed V1 baseline weights, missed check-ins roll into one open task per relationship, mobile exposes three snooze presets, future snoozes do not count in the main badge, and client profiles show compact resolved task history only.
Coach Task Inbox Flows
The inbox is fed by source contexts through events and outbox consumers, then resolved from coach web or coach mobile.
Task Created From Client Activity
- A source context emits an event such as
ClientProfileReadyForReview,FormSubmissionReceived,WorkoutSubmittedForReview, orCheckInMissed. - The inbox consumer maps the event into a normalized
CoachTaskUpsertcommand with workspace, relationship, source reference, priority inputs, and action hints. - The application service upserts one open task for the same
workspaceId,relationshipId,sourceType, andsourceId. - If an open task already exists, the system updates severity, due date, latest activity timestamp, and payload summary instead of creating duplicates.
- Coach web and coach mobile receive updated counts through query polling first; realtime updates can be added later.
Coach Works The Inbox
- Coach opens the inbox and sees active tasks sorted by urgency, due date, severity, and latest client activity.
- Coach filters by task type, client, status, due window, or source area.
- Opening a task shows a compact summary and deep link to the source workflow, such as profile review, form review, workout review, or message thread.
- Coach completes the source workflow or takes a direct task action such as message, nudge, snooze, dismiss, or resolve when the task type allows direct resolution.
- Resolution writes an audit event and optionally emits downstream events for notifications, analytics, or client-facing follow-up.
Auto Resolution
- A source context can emit a completion event, for example
ClientProfileReviewedorWorkoutReviewed. - The inbox service finds open tasks with the matching source reference and marks them
RESOLVEDusingresolutionType=SOURCE_COMPLETED. - If the task was snoozed, completion still resolves it because source truth wins over local inbox state.
- Auto resolution is idempotent by source event id and task id.
- Historical task events remain visible for support and operational reporting.
Snooze Or Dismiss
- Coach can snooze low-urgency tasks until a preset time or next check-in window. Mobile V1 baseline exposes
1 hour,tomorrow morning, andnext check-in; web can also support custom date/time. - Snoozed tasks leave the default active view but remain queryable and countable in summary metrics.
- Coach can dismiss tasks only with a supported reason, such as duplicate, not relevant, handled outside app, or client paused.
- If a dismissed source emits a new material event, the system can reopen the existing task or create a new one according to the source policy.
- Dismissal never mutates the source record; it only records the coach's inbox decision.
Domain Model
The task inbox owns coach work items and resolution history. Source contexts own the underlying coaching data.
Aggregates & Entities
CoachTask: aggregate root for one actionable item in a workspace and coach-client relationship.CoachTaskSourceRef: immutable reference to source context, source type, and source id that created or refreshed the task.CoachTaskEvent: append-only transition history for created, refreshed, viewed, snoozed, resolved, dismissed, reopened, and archived states.CoachTaskViewPreference: user-level default filters and sort choice for coach web and coach mobile.CoachTaskPolicy: domain service for priority score, dedupe, dismissal, reopening, and required permission checks.
Value Objects & Rules
CoachTaskType:PROFILE_REVIEW,FORM_REVIEW,WORKOUT_REVIEW,MISSED_CHECK_IN,PROGRESS_ALERT,NUTRITION_REVIEW,MESSAGE_FOLLOW_UP,PLAN_UPDATE.CoachTaskStatus:OPEN,IN_PROGRESS,SNOOZED,RESOLVED,DISMISSED,ARCHIVED.CoachTaskPriority: computed from severity, due date, client status, risk signal, source freshness, and manual pinning.ResolutionType:SOURCE_COMPLETED,MESSAGE_SENT,PLAN_UPDATED,NUDGE_SENT,HANDLED_OUTSIDE_APP,NOT_RELEVANT,DUPLICATE.DirectResolutionPolicy:MESSAGE_FOLLOW_UP,MISSED_CHECK_IN, andPLAN_UPDATEcan be resolved directly from the inbox with a supported action. Profile, form, workout, nutrition, and progress review tasks require source workflow completion, except dismissing duplicate or not relevant tasks.MissedCheckInPolicy: use one rolling open missed-check-in task per relationship. Repeated missed occurrences increment occurrence count, update latest occurrence, and refresh priority instead of creating one task per missed date.BadgeCountPolicy: default dashboard and mobile badges count open, in-progress, overdue, and snoozed tasks whosesnoozed_untilhas passed. Snoozed tasks due later today stay out of the main badge until they wake, but appear in snoozed filters.- Only one unresolved task can exist for a dedupe key unless the source explicitly permits multiple instances.
- Task payloads store summaries and deep links only; sensitive answers, photos, nutrition details, and private notes stay in their source contexts.
| Model | Owned By | Important Methods | Emits |
|---|---|---|---|
CoachTask | CoachWorkflow | createFromSource(), refreshFromSource(), start(), snooze(), resolve(), dismiss(), reopen() | CoachTaskCreated, CoachTaskResolved, CoachTaskDismissed |
CoachTaskSourceRef | CoachWorkflow | dedupeKey(), deepLink(), assertSourceVisible() | None |
CoachTaskPolicy | CoachWorkflow | calculatePriority(), canDismiss(), shouldReopen(), requiresResolutionNote() | None |
CoachClientRelationship | CoachClientManagement | Referenced by id for workspace, client, coach, and relationship status. | CoachClientRelationshipPaused, CoachClientRelationshipEnded |
Source Aggregate | Forms, Programs, Nutrition, Communication, Analytics | Owns the actual review, measurement, message, workout, or alert record. | Source-specific events consumed by inbox projectors. |
Table Structure
Persist task state and audit history. Keep source data in source tables and use explicit reference columns for cross-context links.
V1 baseline Priority Weights
- Base task weights:
PROGRESS_ALERT=90,MESSAGE_FOLLOW_UP=75,PROFILE_REVIEW=70,NUTRITION_REVIEW=65,FORM_REVIEW=55,WORKOUT_REVIEW=50,PLAN_UPDATE=45,MISSED_CHECK_IN=40. - Severity modifiers:
LOW=-10,NORMAL=0,HIGH=15,URGENT=30. - Due modifiers: overdue
+20, due today+10, no due date0. - Client status modifiers: onboarding
+10, active0, paused-30. Ended relationships do not receive actionable tasks. - Manual pinning can add
+25to display order but cannot suppress urgent system tasks.
Resolution & History Policy
- Direct inbox resolution is allowed for message follow-ups, missed check-ins, and plan update reminders when the coach sends a linked message/nudge, records handled outside app, or completes the supported direct action.
- Profile, form, workout, nutrition, and progress review tasks close through source workflow completion so source review state stays authoritative.
- Client profile timeline shows compact resolved/dismissed task history for the last 30 days, capped at 20 items, with task type, date, resolution label, and source link only.
- Detailed task event history stays in the inbox/support view and is not exposed to clients.
| Table | Purpose | Important Columns | Constraints & Indexes |
|---|---|---|---|
coach_tasks | Current work item state for inbox queries and source dedupe. coach_user_id stores the coach responsible for the relationship. | id, workspace_id, relationship_id, client_user_id, coach_user_id, task_type, status, priority_score, severity, source_context, source_type, source_id, dedupe_key, deep_link_route, due_at, snoozed_until, occurrence_count, last_occurrence_at, last_source_event_at, payload jsonb, created_at, updated_at, resolved_at | Partial unique (workspace_id, dedupe_key) where status is OPEN, IN_PROGRESS, or SNOOZED; index (workspace_id, coach_user_id, status, priority_score desc, due_at); index (source_context, source_type, source_id); missed check-ins dedupe by relationship and roll occurrence fields forward; default query excludes resolved, dismissed, archived, and active snoozes. |
coach_task_events | Append-only task transition history. | id, task_id, event_type, actor_user_id, source_event_id, metadata jsonb, created_at | Index (task_id, created_at); idempotency unique on (task_id, source_event_id, event_type) when source event is present. |
coach_task_view_preferences | Per-coach inbox defaults. | id, workspace_id, user_id, default_status_filter, default_type_filter, sort_mode, show_snoozed, updated_at | Unique (workspace_id, user_id); safe to recreate from defaults if deleted. |
outbox_events | Transactional handoff to notification, analytics, mobile badge counts, and external consumers. | id, aggregate_type, aggregate_id, event_type, payload jsonb, status, available_at | Standard outbox worker with retries; payload references task IDs and source IDs only. |
Bounded-context references
Use plain UUID references across bounded contexts. Do not make the inbox domain navigate or mutate Forms, Programs, Nutrition, Communication, or Analytics aggregates.
Lifecycle Policy
Inbox state should explain what needed attention, who saw it, and how it was handled without becoming the source of truth for the underlying event.
Task Lifecycle
OPENafter a source event creates or reopens a task.IN_PROGRESSwhen a coach opens or starts the linked workflow and the source still needs action.SNOOZEDwhen the coach defers the item untilsnoozed_until.RESOLVEDwhen source workflow completes or coach records a supported resolution action.DISMISSEDwhen coach closes the task as not actionable with a required reason.ARCHIVEDafter retention or relationship-ended policy removes it from normal operational history.
Retention & Visibility
- Resolved and dismissed tasks remain visible on the client profile timeline for 30 days, capped to the latest 20 compact items.
- Ended or revoked coach-client relationships hide active tasks from the default inbox and prevent new coach actions.
- Paused clients keep active tasks queryable, but reminder jobs and urgency escalation can be suspended.
- Hard deletion follows workspace/account retention policy and must preserve required audit or anonymize actor/source references.
- Source deletion or redaction keeps task metadata minimal and removes sensitive payload summary fields.
API Contracts
Coach-facing APIs query and mutate inbox state. Source event ingestion stays inside application services and outbox consumers.
| Endpoint | Purpose | Request | Response |
|---|---|---|---|
GET /api/v1/workspaces/{workspaceId}/coach-tasks | List tasks for inbox views. | Query: status, taskType, relationshipId, dueBefore, includeSnoozed, sort, cursor. | Cursor page of CoachTaskListItemDto with safe summary, priority, client card, source route, and available actions. |
GET /api/v1/workspaces/{workspaceId}/coach-tasks/summary | Return counts for dashboard badges and mobile tabs. | Optional type and due window filters. | Open, in-progress, overdue, due today, high-severity, and snoozed counts. Main badge excludes snoozed tasks until snoozed_until has passed. |
GET /api/v1/workspaces/{workspaceId}/coach-tasks/{taskId} | Read task detail and event history. | Authorized coach workspace session. | Task detail, source reference, timeline, safe payload summary, and action availability. |
POST /api/v1/workspaces/{workspaceId}/coach-tasks/{taskId}/start | Mark task as in progress and return source deep link. | Idempotency key and optional client platform context. | Updated task and route target for web or coach mobile. |
POST /api/v1/workspaces/{workspaceId}/coach-tasks/{taskId}/resolve | Resolve after a supported coach action. | resolutionType, optional note, optional linked action id, idempotency key. | Resolved task, compact profile timeline event when eligible, updated summary counts. |
POST /api/v1/workspaces/{workspaceId}/coach-tasks/{taskId}/dismiss | Dismiss non-actionable item with audit reason. | dismissReason, required note for selected reasons, idempotency key. | Dismissed task and updated counts. |
POST /api/v1/workspaces/{workspaceId}/coach-tasks/{taskId}/snooze | Defer a task until a later time. | snoozePreset of ONE_HOUR, TOMORROW_MORNING, NEXT_CHECK_IN, or web-only custom snoozedUntil; optional reason; idempotency key. | Snoozed task and summary counts with main badge excluding future snoozes. |
PUT /api/v1/workspaces/{workspaceId}/coach-tasks/preferences | Save inbox defaults for the current coach. | Default filters, sort mode, show snoozed flag. | Saved preferences. |
Key Code Snippets
Implementation sketches for the planned TypeScript/NestJS/Drizzle stack. Keep source ownership in Forms, Programs, Nutrition, Communication, and Analytics; the inbox stores coach work state and audit history.
Drizzle schema shape
apps/api/src/coach-workflow/db/coach-tasks.schema.ts
ts
export const coachTaskStatus = pgEnum("coach_task_status", [
"OPEN",
"IN_PROGRESS",
"SNOOZED",
"RESOLVED",
"DISMISSED",
"ARCHIVED",
]);
export const coachTasks = pgTable("coach_tasks", {
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(),
taskType: text("task_type").$type<CoachTaskType>().notNull(),
status: coachTaskStatus("status").notNull().default("OPEN"),
severity: text("severity").$type<CoachTaskSeverity>().notNull().default("NORMAL"),
priorityScore: integer("priority_score").notNull().default(0),
sourceContext: text("source_context").$type<CoachTaskSourceContext>().notNull(),
sourceType: text("source_type").notNull(),
sourceId: uuid("source_id").notNull(),
dedupeKey: text("dedupe_key").notNull(),
deepLinkRoute: text("deep_link_route").notNull(),
payload: jsonb("payload").$type<CoachTaskPayload>().notNull(),
dueAt: timestamp("due_at", { withTimezone: true }),
snoozedUntil: timestamp("snoozed_until", { withTimezone: true }),
occurrenceCount: integer("occurrence_count").notNull().default(1),
lastOccurrenceAt: timestamp("last_occurrence_at", { withTimezone: true }),
lastSourceEventAt: timestamp("last_source_event_at", { withTimezone: true }).notNull(),
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
unresolvedDedupeUnique: uniqueIndex("coach_tasks_unresolved_dedupe_uq")
.on(table.workspaceId, table.dedupeKey)
.where(sql`${table.status} in ('OPEN', 'IN_PROGRESS', 'SNOOZED')`),
inboxSortIdx: index("coach_tasks_inbox_sort_idx")
.on(table.workspaceId, table.coachUserId, table.status, table.priorityScore, table.dueAt),
sourceLookupIdx: index("coach_tasks_source_lookup_idx")
.on(table.sourceContext, table.sourceType, table.sourceId),
}));
export const coachTaskEvents = pgTable("coach_task_events", {
id: uuid("id").primaryKey().defaultRandom(),
taskId: uuid("task_id").notNull().references(() => coachTasks.id),
eventType: text("event_type").$type<CoachTaskEventType>().notNull(),
actorUserId: uuid("actor_user_id"),
sourceEventId: uuid("source_event_id"),
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
taskTimelineIdx: index("coach_task_events_task_timeline_idx").on(table.taskId, table.createdAt),
sourceEventUnique: uniqueIndex("coach_task_events_source_event_uq")
.on(table.taskId, table.sourceEventId, table.eventType),
}));Source event upsert use case
apps/api/src/coach-workflow/use-cases/upsert-coach-task.ts
ts
@Injectable()
export class UpsertCoachTaskFromSourceUseCase {
constructor(
private readonly relationships: CoachClientRelationshipRepository,
private readonly tasks: CoachTaskRepository,
private readonly policy: CoachTaskPolicy,
private readonly outbox: OutboxPort,
private readonly tx: TransactionRunner,
private readonly clock: Clock,
) {}
async execute(command: CoachTaskUpsertCommand): Promise<CoachTaskDto> {
return this.tx.run(async () => {
const relationship = await this.relationships.getById(command.relationshipId);
this.policy.assertSourceCanCreateTask(command, relationship);
const existing = await this.tasks.lockOpenByDedupeKey(
command.workspaceId,
command.source.dedupeKey,
);
const priorityScore = this.policy.calculatePriority(command, relationship, this.clock.now());
const task = existing
? existing.refreshFromSource({
sourceEventId: command.source.sourceEventId,
occurredAt: command.occurredAt,
severity: command.severity,
dueAt: command.dueAt,
priorityScore,
incrementOccurrence: command.taskType === "MISSED_CHECK_IN",
payload: command.summary,
})
: CoachTask.create({
workspaceId: command.workspaceId,
relationshipId: relationship.id,
clientUserId: relationship.clientUserId,
coachUserId: relationship.coachUserId,
taskType: command.taskType,
source: command.source,
severity: command.severity,
dueAt: command.dueAt,
priorityScore,
payload: command.summary,
occurredAt: command.occurredAt,
});
await this.tasks.save(task);
await this.outbox.publish(task.pullDomainEvents());
return CoachTaskDto.fromDomain(task);
});
}
}Resolve task use case
apps/api/src/coach-workflow/use-cases/resolve-coach-task.ts
ts
@Injectable()
export class ResolveCoachTaskUseCase {
constructor(
private readonly tasks: CoachTaskRepository,
private readonly relationships: CoachClientRelationshipRepository,
private readonly policy: CoachTaskPolicy,
private readonly outbox: OutboxPort,
private readonly tx: TransactionRunner,
private readonly clock: Clock,
) {}
async execute(input: ResolveCoachTaskInput, actor: Actor): Promise<CoachTaskDto> {
return this.tx.run(async () => {
const task = await this.tasks.lockById(input.taskId);
const relationship = await this.relationships.getById(task.relationshipId);
this.policy.assertCanActOnTask(actor, task, relationship);
this.policy.assertResolutionAllowed(task, input.resolutionType, input.note);
task.resolve({
resolutionType: input.resolutionType,
note: input.note,
linkedActionId: input.linkedActionId,
actorUserId: actor.userId,
resolvedAt: this.clock.now(),
idempotencyKey: input.idempotencyKey,
});
await this.tasks.save(task);
await this.outbox.publish(task.pullDomainEvents());
return CoachTaskDto.fromDomain(task);
});
}
}Policy boundary
apps/api/src/coach-workflow/policies/coach-task.policy.ts
ts
export class CoachTaskPolicy {
assertCanReadTask(actor: Actor, task: CoachTask, relationship: CoachClientRelationship): void {
actor.requireRole("COACH");
actor.requireWorkspaceMembership(task.workspaceId, ["OWNER", "COACH"]);
relationship.assertVisibleToCoach(actor.userId);
}
assertCanActOnTask(actor: Actor, task: CoachTask, relationship: CoachClientRelationship): void {
this.assertCanReadTask(actor, task, relationship);
if (!relationship.isActiveOrOnboarding()) {
throw new ForbiddenException("Inactive relationships cannot receive coach task actions.");
}
}
assertCanOpenSource(actor: Actor, task: CoachTask): void {
const permissionBySource: Record<CoachTaskSourceContext, string> = {
PROFILE: "client.profile.read",
FORMS: "forms.submissions.read",
PROGRAMS: "programs.review",
NUTRITION: "nutrition.review",
COMMUNICATION: "messages.read",
ANALYTICS: "client.risk_flags.read",
};
actor.requirePermission(permissionBySource[task.sourceContext]);
}
assertResolutionAllowed(task: CoachTask, resolutionType: ResolutionType, note?: string): void {
if (task.isTerminal()) throw new ConflictException("Task is already closed.");
const directResolvableTaskTypes: CoachTaskType[] = [
"MESSAGE_FOLLOW_UP",
"MISSED_CHECK_IN",
"PLAN_UPDATE",
];
if (!directResolvableTaskTypes.includes(task.taskType) && resolutionType !== "DUPLICATE" && resolutionType !== "NOT_RELEVANT") {
throw new BadRequestException("This task must be resolved from the source workflow.");
}
if (resolutionType === "HANDLED_OUTSIDE_APP" && !note?.trim()) {
throw new BadRequestException("Handled outside app requires a note.");
}
if (resolutionType === "SOURCE_COMPLETED") {
throw new BadRequestException("Source completion must come from a trusted source event.");
}
}
calculatePriority(command: CoachTaskUpsertCommand, relationship: CoachClientRelationship, now: Date): number {
const taskWeight: Record<CoachTaskType, number> = {
PROGRESS_ALERT: 90,
MESSAGE_FOLLOW_UP: 75,
PROFILE_REVIEW: 70,
NUTRITION_REVIEW: 65,
FORM_REVIEW: 55,
WORKOUT_REVIEW: 50,
PLAN_UPDATE: 45,
MISSED_CHECK_IN: 40,
};
const severityModifier = { LOW: -10, NORMAL: 0, HIGH: 15, URGENT: 30 }[command.severity];
const dueModifier = command.dueAt && command.dueAt < now
? 20
: command.dueAt && isSameLocalDay(command.dueAt, now, relationship.timezone)
? 10
: 0;
const clientStatusModifier = relationship.status === "ONBOARDING"
? 10
: relationship.status === "PAUSED"
? -30
: 0;
const pinModifier = command.manualPin ? 25 : 0;
return taskWeight[command.taskType] + severityModifier + dueModifier + clientStatusModifier + pinModifier;
}
}Events & Background Jobs
The inbox consumes source events, publishes task lifecycle events, and drives notification or badge-count side effects.
Consumed Events
ClientProfileReadyForReview: create profile review task.ClientProfileSectionUpdated: refresh or reopen dependent review tasks.FormSubmissionReceived: create questionnaire or check-in review task.OnboardingQuestionnaireSkipped: create low-priority follow-up or visibility task.WorkoutSubmittedForReview: create workout review task when coach review policy requires it.MeasurementEntrySubmitted: create progress review task when entry needs coach review.CheckInMissed: create or refresh one rolling missed-check-in task per relationship after grace period.ProgressTrendDetected: create alert task from future analytics/risk modules.ClientMessageNeedsReply: create message follow-up task based on reply SLA policy.
Emitted Events
CoachTaskCreated: update dashboard counts and optional coach notification.CoachTaskRefreshed: update priority, due date, latest activity summary, and occurrence count when applicable.CoachTaskViewed: audit that a coach opened the item.CoachTaskSnoozed: schedule wake-up and remove from default active counts.CoachTaskResolved: update metrics, client timeline, and source read-model references.CoachTaskDismissed: record non-actionable outcome for reporting and dedupe decisions.
Jobs
- Outbox consumer maps source events into
CoachTaskUpsertcommands with retries and dead-letter logging. - Overdue sweep recalculates priority scores and emits notifications for high-severity due tasks.
- Snooze wake-up job returns tasks to the active inbox when
snoozed_untilpasses. - Source completion projector auto-resolves matching open or snoozed tasks.
- Daily digest job prepares optional coach summary notifications by workspace timezone; digest excludes future snoozes unless they wake before the digest window.
- Retention job archives old resolved/dismissed tasks and redacts sensitive payload summaries when source records are deleted.
Priority Inputs
- Task type base weight using the fixed V1 baseline table: progress alert, message follow-up, profile review, nutrition review, form review, workout review, plan update, missed check-in.
- Severity from source event or policy engine.
- Due date proximity and overdue duration.
- Client status: onboarding adds priority, paused lowers priority, ended relationships do not receive active tasks.
- Recent activity frequency, such as repeated missed logs or repeated client messages.
- Manual pinning can lift display order but should not hide urgent system tasks.
Permissions & Access Rules
Inbox reads can expose sensitive operational context, so every query is workspace, relationship, role, and source-permission aware.
Coach
- Must have active membership in the task workspace and visibility into the coach-client relationship.
- Coach can list tasks for relationships they can see in their workspace.
- Can start, snooze, resolve, or dismiss visible tasks when the relationship is active or onboarding.
- Needs source-specific permissions to open detailed health, nutrition, photo, private note, or message content.
- Cannot resolve a task by claiming source completion unless the source context confirms it or the resolution type supports coach assertion.
Client & System
- Clients do not read coach inbox tasks directly.
- Client-facing nudges, messages, or requests are created through source workflows and communication APIs.
- System workers can create, refresh, auto-resolve, archive, and redact tasks through internal service accounts.
- System workers must use idempotency keys based on source event id.
Web, Coach Mobile, Client Mobile
Both coach surfaces share API contracts but optimize task handling for different contexts.
Coach Web
- Primary dense inbox with table/list view, filters, search, multi-select actions, and side panel preview.
- Shows client status, task type, due date, severity, and source summary without exposing sensitive details until opened.
- Supports deep links into profile review, form review, workout review, nutrition review, and message threads.
- Can save view preferences for common filters and sort modes.
- Client profile timeline shows the latest compact resolved/dismissed inbox history for the last 30 days, capped at 20 items.
Coach Mobile
- Action-first card list grouped by urgent, due today, and snoozed.
- Supports start, message, nudge, snooze presets, and simple resolve/dismiss actions for directly resolvable task types.
- Complex source workflows can open mobile-native review screens or defer to web when not yet supported.
- Badge counts refresh on foreground and push notification receipt.
Client Mobile
- Does not show coach inbox state or internal coach decisions.
- Receives only explicit client-facing actions such as messages, nudges, clarification requests, or plan changes.
- Client completions and submissions emit source events that can create or resolve coach tasks.
- Offline client actions are reflected in the coach inbox only after sync reaches the backend.
Test Scenarios & V1 Baseline Decisions
Validate idempotent event ingestion, permissions, sorting, source-boundary behavior, badge counts, and direct-resolution rules before implementation.
Tests
- Domain: repeated source event creates one open task and refreshes priority instead of duplicating.
- Domain: repeated missed check-ins refresh one rolling task per relationship and increment occurrence count.
- Domain: priority score uses V1 baseline task-type, severity, due date, client status, and manual pin weights.
- Domain: review task types cannot be directly resolved from the inbox except duplicate or not relevant dismissal.
- Domain: task cannot be resolved or dismissed after relationship is ended unless support/admin policy allows it.
- Domain: source completion auto-resolves open and snoozed tasks idempotently.
- API: coach cannot list tasks outside their workspace or relationships they cannot see.
- API: source-sensitive task details are filtered when coach lacks health, nutrition, media, or message permissions.
- API: dismiss requires a supported reason and required note for handled outside app.
- API: summary badge excludes snoozed tasks due later today until
snoozed_untilhas passed. - API: mobile snooze accepts
ONE_HOUR,TOMORROW_MORNING, andNEXT_CHECK_INpresets. - Integration: profile-ready, form-submitted, missed-check-in, and workout-submitted events create expected task types.
- Integration: source deletion redacts task payload summary without deleting audit events.
- Integration: client profile timeline shows compact resolved/dismissed task history for the last 30 days capped at 20 items.
- Web E2E: coach filters inbox, opens profile review task, marks profile ready, and task resolves.
- Coach mobile E2E: coach opens urgent task, sends message, and resolves with linked message action.
Resolved V1 Baseline Decisions
- Priority weights: fixed V1 baseline scoring uses task type base weight, severity modifier, due-date modifier, client-status modifier, and optional manual pin boost.
- Direct resolution: message follow-ups, missed check-ins, and plan update reminders can be resolved from the inbox; review tasks close through the source workflow except duplicate/not relevant dismissal.
- Missed check-ins: one rolling open task per relationship, with occurrence count and latest missed date refreshed on repeat misses.
- Mobile snooze presets:
1 hour,tomorrow morning, andnext check-in. Web can offer custom date/time. - Badge counts: main dashboard/mobile badge excludes snoozed tasks due later today until they wake; snoozed filter/count still shows them separately.
- Client profile timeline: show compact resolved/dismissed task history for 30 days, capped at 20 items, with no private notes or sensitive payload details.