Skip to content

DailyExecution / Workout Logs / ProgressAnalytics Read Models

PR and RPE/RIR tracking turns workout execution into useful performance feedback.

The V1 baseline captures completed sets, loads, reps, notes, and effort from client mobile, then compares submitted work against historical bests. Coaches use the resulting PR history, effort trends, and performance charts to adjust load, volume, recovery, and program progression without rewriting the assigned program snapshot.

Page status

  • Set-level effort
  • Workout log events
  • PR detection
  • Coach review

Ownership decision

Ownership decision: Program Builder stores the prescribed effort target in assignment snapshots. DailyExecution owns WorkoutSession aggregates, set logs, RPE/RIR readings, idempotent offline submission, and accepted revisions. ProgressAnalytics consumes submitted or corrected workout events to maintain personal record, strength graph, effort trend, and weekly recap read models.

V1 baseline decision

V1 baseline decision: support one active effort metric per prescribed exercise or block: RPE, RIR, or NONE. Clients can record set-level effort where enabled and optional overall session difficulty on a separate simple difficulty scale. PR detection starts with strength-oriented events: heaviest load, estimated 1RM, max reps at a comparable load, and total working-set volume for the same exercise identity.

PR policy decision

PR policy decision: load values are stored canonically and displayed in the client's unit preference, not rounded to equipment-specific decimal increments. Estimated 1RM uses the average of Brzycki, Epley, and Wathan formulas. The formula is not workspace-configurable in Public V1, but coach-facing views must label the active formula. PRs auto-confirm immediately; corrections that remove or change a displayed PR send an explicit correction notice to client and coach.

Tracking Flows

The flow starts in Program Builder, but execution and analytics stay outside mutable program drafts.

Coach Enables Effort Tracking

  1. Coach configures exercise prescriptions in Program Builder with sets, reps, load target, rest, notes, and optional effort target.
  2. Effort target can be inherited from program, block, or exercise, but the published assignment snapshot resolves it to an exercise-level value.
  3. Supported target forms are RPE range, RIR range, optional free note, and whether the client must enter effort before submission.
  4. DailyExecution materializes workout tasks with the resolved effort target and offline cache checksum.

Client Logs A Workout

  1. Client starts the workout from Today using the cached assigned workout snapshot.
  2. Client records completed sets, reps, load, bodyweight or machine-load context, skipped sets, notes, and substitutions approved by the coach.
  3. When effort is enabled, the set row captures either RPE or RIR according to the prescription. The app does not ask for both at once in Public V1.
  4. Client can add an optional overall session difficulty value on a separate simple 1-to-5 difficulty scale and submit the workout online or offline.

Workout Logging Source Boundary

  1. Workout Logging owns session start, local drafts, offline sync, submit, correction, and raw set persistence.
  2. This feature consumes accepted WorkoutSessionSubmitted, WorkoutSessionCorrected, and WorkoutSessionVoided events.
  3. Effort values are captured on WorkoutSetLog; this page defines validation, interpretation, projections, and display behavior.
  4. Projection workers must tolerate delayed offline sync and recompute when a corrected session changes the source set facts.

PR Detection And Feedback

  1. WorkoutSessionSubmitted triggers a projector that normalizes loads, reps, volume, bodyweight context, and exercise identity.
  2. Projector compares each eligible working set against the relationship's previous auto-confirmed bests for the same exercise scope.
  3. New records create PersonalRecordEvent rows with detection_status = CONFIRMED and update the current-record read model immediately.
  4. Client receives post-workout PR feedback after sync. Coach sees PRs, effort context, formula policy, and trend changes in the client profile and inbox where relevant.
  5. If a correction removes or changes a previously displayed PR, the projector emits an explicit correction notice for both client and coach.

Domain Model

Keep source-of-truth logs append-friendly and build PRs as projections that can be recomputed after corrections.

Source Facts & Projections

  • WorkoutSession, WorkoutExerciseLog, WorkoutSetLog, and WorkoutSessionRevision: source facts owned by Workout Logging.
  • PersonalRecordEvent: ProgressAnalytics event row created when submitted performance exceeds a prior best.
  • CurrentPersonalRecord: ProgressAnalytics read model for the current best per relationship, exercise, and PR type.
  • ExercisePerformancePoint: chart read model combining load, reps, volume, estimated 1RM, RPE/RIR, date, and source set id.
  • EffortTrendRollup: weekly or block-level read model for effort compliance, average effort, and fatigue signal review.
  • PersonalRecordCorrectionNotice: explicit client and coach notification created when a correction changes or removes a previously displayed PR.

Value Objects & Rules

  • EffortMetric: NONE, RPE, or RIR. A prescribed exercise resolves to one metric for V1 baseline.
  • EffortReading: RPE accepts 1.0 through 10.0 in 0.5 increments. RIR accepts integer 0 through 10.
  • EffortTarget: metric, minimum, maximum, required flag, source level, and coach note copied into the assignment snapshot.
  • SessionDifficulty: optional overall workout difficulty separate from set-level RPE/RIR, recorded on a simple 1-to-5 scale for session recap and coach context.
  • PerformedLoad: submitted value, submitted unit, canonical kilograms, load source, and bodyweight inclusion flag. Display normalizes to the client's unit preference only; V1 baseline does not round display to equipment-specific decimal increments.
  • EstimatedOneRepMaxFormula: fixed V1 baseline policy averaging Brzycki, Epley, and Wathan estimates. Coach-facing views must show this active formula, but workspaces cannot change it in Public V1.
  • PersonalRecordType: MAX_LOAD, ESTIMATED_1RM, MAX_REPS_AT_LOAD, and TOTAL_VOLUME for V1 baseline.
  • ExerciseRecordScope: relationship id plus exercise catalog item id. Substitutions count for the performed exercise, not the originally prescribed exercise, unless a future equivalence mapping explicitly links them.
  • Warmup, skipped, failed, assisted, or coach-excluded sets do not create PRs unless a later policy explicitly allows that set kind.
  • Bodyweight-only movements do not get separate V1 baseline PR types for max reps, duration, or added load. They can appear in history and charts, but dedicated bodyweight PRs are deferred until after V1 baseline.
  • PR events auto-confirm on detection for all V1 baseline exercise categories, including machine-loaded movements. Coaches can dismiss noisy PRs after the fact without blocking initial client feedback.
  • Historical workout logs reference assigned program ids and snapshot exercise ids; they never mutate program templates, drafts, or assigned snapshots.

Correction rule

Correction rule: submitted logs are corrected through revisions. ProgressAnalytics handles WorkoutSessionCorrected by invalidating affected performance points, recomputing current PRs for the impacted relationship and exercise scope, and emitting explicit client/coach notices when a previously displayed PR changes or is removed.

Table Structure

Workout Logging owns raw execution tables. This feature owns ProgressAnalytics projection tables.

Note

Source rows live in Workout Logging: workout_sessions, workout_exercise_logs, workout_set_logs, and workout_session_revisions. The tables below reference those source ids but are rebuilt from events and accepted revisions.

TablePurposeImportant ColumnsConstraints & Indexes
personal_record_eventsDetected PR events from submitted or corrected workout sessions.id, relationship_id, client_user_id, exercise_catalog_item_id, record_type, record_value, record_unit, display_unit, formula_policy, source_workout_session_id, source_set_log_id, previous_record_event_id, detection_status, detected_at, dismissed_by_user_id, dismissed_atIndex (relationship_id, exercise_catalog_item_id, record_type, detected_at desc); detected events default to CONFIRMED; keep dismissed events for audit and recompute traceability.
current_personal_recordsCurrent best read model for fast profile and chart rendering.relationship_id, exercise_catalog_item_id, record_type, record_event_id, record_value, record_unit, display_unit, formula_policy, achieved_at, updated_atPrimary key (relationship_id, exercise_catalog_item_id, record_type); rebuilt from personal_record_events after corrections.
exercise_performance_pointsChart-ready performance facts by exercise and date.id, relationship_id, exercise_catalog_item_id, workout_session_id, set_log_id, performed_at, reps, canonical_load_kg, display_load_value, display_unit, estimated_1rm_kg, e1rm_formula_policy, volume_kg, effort_metric, effort_value, session_difficulty, source_revisionIndex (relationship_id, exercise_catalog_item_id, performed_at desc); stale points are replaced when a source session is corrected.
personal_record_correction_noticesExplicit notification payload when a correction changes or removes a previously displayed PR.id, relationship_id, record_event_id, nullable replacement_record_event_id, notice_type, previous_value, new_value, display_unit, client_notified_at, coach_notified_at, created_atUnique notice per changed record event and source correction; index (relationship_id, created_at desc).
effort_trend_rollupsPeriod-level effort and fatigue summary for coach review.id, relationship_id, period_start, period_end, program_phase_ref, effort_metric, average_effort, average_session_difficulty, target_hit_rate, high_effort_set_count, low_effort_set_count, updated_atUnique (relationship_id, period_start, period_end, effort_metric); rebuilt by scheduled projector or after corrections.

Note

Use plain UUID references to source workout sessions, source set logs, relationships, users, and exercise catalog items. Projection tables never mutate DailyExecution, Program Builder, or Exercise Library aggregates.

Lifecycle Policy

Workout logs are longitudinal evidence. Preserve auditability while allowing practical corrections.

Source Fact Lifecycle

  • Workout Logging owns session statuses, accepted revisions, voiding, correction windows, and hard-purge/privacy behavior.
  • ProgressAnalytics reacts only to accepted source events and source revision numbers.
  • Submitted and corrected sessions can create or replace performance points, PR events, current PRs, and effort rollups.
  • Voided sessions are excluded from active projections without deleting source audit rows.

PR And Trend Lifecycle

  • PR events are projection outputs, not hand-edited source facts.
  • PR events auto-confirm immediately on detection. Coach review is not required before the client sees the PR.
  • Coach can dismiss a noisy auto-confirmed PR, such as a typo or machine-load mismatch, without deleting the source workout log.
  • Corrections invalidate affected PR events and rebuild current-record rows for that exercise scope.
  • If correction changes or removes a PR that was already shown to the client or coach, create an explicit correction notice for both surfaces.
  • Effort trend rollups are disposable read models and can be rebuilt from submitted set logs.
  • Changing a program prescription does not retroactively change submitted effort readings or PR history.

API Contracts

Workout Logging owns submit and sync APIs. This feature owns PR, performance, and effort read APIs.

Note

Use Workout Logging API contracts for starting, saving, syncing, submitting, and correcting workout sessions. The endpoints below expose derived performance state and PR moderation.

EndpointUseRequestResponse
GET /api/v1/client/personal-recordsClient reads recent or current PRs.Optional exercise id, record type, date range, and pagination cursor.Current records, recent record events, source workout references, formula policy for estimated 1RM, correction notices, and display units in the client's unit preference.
GET /api/v1/coach/relationships/{relationshipId}/performance/exercises/{exerciseId}Coach opens exercise-specific performance history.Date range, unit preference, include effort, include PRs.Performance points, current PRs, PR event history, effort trend summary, correction notices, formula policy label, and source session links.
GET /api/v1/coach/relationships/{relationshipId}/effort-trendsCoach reviews fatigue and target compliance across a program phase.Date range, assigned program id, metric, grouping period.Rollups by week or block, high-effort flags, target hit rate, and source workout counts.
PATCH /api/v1/coach/personal-record-events/{recordEventId}Coach dismisses or restores a detected PR event.Status, reason, expected version.Updated record event and current-record recompute status.

Key Code Snippets

Implementation sketches for schema boundaries, submission validation, and PR projection.

Projection schema shape

apps/api/src/progress-analytics/db/performance.schema.ts

ts
export const personalRecordEvents = pgTable("personal_record_events", {
  id: uuid("id").primaryKey().defaultRandom(),
  relationshipId: uuid("relationship_id").notNull(),
  clientUserId: uuid("client_user_id").notNull(),
  exerciseCatalogItemId: uuid("exercise_catalog_item_id").notNull(),
  recordType: text("record_type").$type<PersonalRecordType>().notNull(),
  recordValue: numeric("record_value").notNull(),
  recordUnit: text("record_unit").notNull(),
  displayUnit: text("display_unit").notNull(),
  formulaPolicy: text("formula_policy"),
  sourceWorkoutSessionId: uuid("source_workout_session_id").notNull(),
  sourceSetLogId: uuid("source_set_log_id").notNull(),
  previousRecordEventId: uuid("previous_record_event_id"),
  detectionStatus: text("detection_status").$type<PersonalRecordStatus>().notNull().default("CONFIRMED"),
  detectedAt: timestamp("detected_at", { withTimezone: true }).notNull(),
}, (table) => ({
  recordHistoryIdx: index("personal_record_events_history_idx")
    .on(table.relationshipId, table.exerciseCatalogItemId, table.recordType, table.detectedAt),
}));

export const currentPersonalRecords = pgTable("current_personal_records", {
  relationshipId: uuid("relationship_id").notNull(),
  exerciseCatalogItemId: uuid("exercise_catalog_item_id").notNull(),
  recordType: text("record_type").$type<PersonalRecordType>().notNull(),
  recordEventId: uuid("record_event_id").notNull(),
  recordValue: numeric("record_value").notNull(),
  recordUnit: text("record_unit").notNull(),
  displayUnit: text("display_unit").notNull(),
  formulaPolicy: text("formula_policy"),
  achievedAt: timestamp("achieved_at", { withTimezone: true }).notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
}, (table) => ({
  pk: primaryKey({ columns: [
    table.relationshipId,
    table.exerciseCatalogItemId,
    table.recordType,
  ] }),
}));

Effort validator

apps/api/src/daily-execution/domain/effort-reading.ts

ts
export class EffortReading {
  static fromInput(metric: EffortMetric | null, value: number | null): EffortReading | null {
    if (metric === null || metric === "NONE") return null;
    if (value === null) throw new DomainError("effort.value_required");

    if (metric === "RPE") {
      const isHalfStep = Number.isInteger(value * 2);
      if (value < 1 || value > 10 || !isHalfStep) {
        throw new DomainError("effort.rpe_out_of_range");
      }
    }

    if (metric === "RIR") {
      if (value < 0 || value > 10 || !Number.isInteger(value)) {
        throw new DomainError("effort.rir_out_of_range");
      }
    }

    return new EffortReading(metric, value);
  }
}

PR detection projector

apps/api/src/progress-analytics/projectors/detect-personal-records.ts

ts
const ESTIMATED_1RM_FORMULA_POLICY = "AVG_BRZYCKI_EPLEY_WATHAN";

function estimateOneRepMaxKg(loadKg: number, reps: number): number {
  const brzycki = loadKg * (36 / (37 - reps));
  const epley = loadKg * (1 + reps / 30);
  const wathan = (100 * loadKg) / (48.8 + 53.8 * Math.exp(-0.075 * reps));

  return (brzycki + epley + wathan) / 3;
}

export class DetectPersonalRecordsProjector {
  constructor(
    private readonly performance: PerformancePointRepository,
    private readonly records: PersonalRecordRepository,
    private readonly outbox: OutboxPort,
  ) {}

  async onWorkoutSessionSubmitted(event: WorkoutSessionSubmitted): Promise<void> {
    const points = ExercisePerformancePoint.fromWorkout(event.session, {
      estimateOneRepMaxKg,
      formulaPolicy: ESTIMATED_1RM_FORMULA_POLICY,
      displayUnitSource: "CLIENT_UNIT_PREFERENCE",
    });

    for (const point of points.filter((item) => item.isPrEligible())) {
      await this.performance.upsert(point);

      const comparisons = await this.records.compareAgainstCurrent(point);
      for (const comparison of comparisons) {
        if (!comparison.isNewRecord) continue;

        const record = PersonalRecordEvent.detect({
          point,
          previous: comparison.previousRecord,
          detectionStatus: "CONFIRMED",
          detectedAt: event.occurredAt,
        });

        await this.records.saveEvent(record);
        await this.records.updateCurrent(record);
        await this.outbox.publish(record.pullDomainEvents());
      }
    }
  }
}

Events & Background Jobs

Submitted execution facts feed adherence, PRs, effort rollups, coach tasks, and weekly recaps.

Events

  • WorkoutSessionSubmitted: update Today completion, adherence, performance points, PR detection, and coach activity feed.
  • WorkoutSessionCorrected: rebuild impacted performance points, PRs, effort rollups, and adherence summaries; emit correction notices when shown PRs change or disappear.
  • WorkoutSessionVoided: remove the session from analytics read models without deleting audit rows.
  • PersonalRecordDetected: auto-confirm the PR, notify client, update coach profile summary, and include formula policy where relevant.
  • PersonalRecordCorrectionNoticeCreated: notify client and coach that a previously shown PR changed or was removed after a workout correction.
  • PersonalRecordDismissed: recompute current PR if the dismissed event was active.
  • EffortTrendUpdated: feed weekly recap and risk-signal evaluation when effort deviates from target.

Jobs

  • PR detection projector processes workout events from the outbox idempotently using the fixed average Brzycki/Epley/Wathan estimated 1RM policy.
  • Effort trend rollup job rebuilds weekly or program-block summaries after submissions and corrections.
  • PR correction notice job compares previously displayed PR events against recomputed records after corrections and creates explicit client/coach notices.
  • Performance chart projector writes exercise-level points optimized for web and mobile profile views.
  • Weekly recap job uses PR and effort summaries as inputs, not as source-of-truth workout data.

Permissions & Access Rules

Derived performance reads must follow the same relationship boundary as the source workout logs.

Client

  • Client can read PRs, performance points, and effort trends only for their own active or historically permitted relationship.
  • Workout submission and correction permissions are enforced by Workout Logging before this feature receives source events.
  • Client cannot alter PR detection status directly, but dismissed PRs should no longer be highlighted to the client.

Coach, Admin & System

  • Coach can read PRs, performance charts, and effort trends only for relationships they are authorized to access.
  • Coach can dismiss or restore PR events, annotate performance history, and request source log corrections through Workout Logging.
  • System workers can project performance, PR, and effort read models with scoped service credentials and outbox idempotency.
  • Support/admin access to derived performance views must be explicit and audited separately from source workout-log access.

Web, Coach Mobile, Client Mobile

All surfaces share the same execution facts, but each surface optimizes the workflow differently.

Coach Web

  • Client profile performance tab shows exercise graph, current PRs, recent PR events, set history, and effort overlays.
  • Workout review shows prescribed target versus performed sets, including RPE/RIR target misses, separate session difficulty, and substitutions.
  • Coach can filter by exercise, program phase, date range, PR type, and effort metric.
  • Estimated 1RM cards show the active formula label: average of Brzycki, Epley, and Wathan.
  • Dismissed PRs remain available in audit/history but are hidden from default highlight cards.
  • Correction notices appear when a submitted correction changes or removes a PR previously visible to coach or client.

Coach Mobile

  • Compact review cards surface recent PRs, effort outliers, and workouts that need feedback.
  • Exercise detail view uses the same performance API as web with mobile-appropriate chart density.
  • Coach can dismiss noisy PRs and request log corrections without opening the full desktop profile.
  • Estimated 1RM values include the fixed formula label in detail views.

Client Mobile

  • Workout logger displays effort controls only when enabled by the assignment snapshot; the source interaction belongs to Workout Logging.
  • After sync, client sees PR feedback and can open personal record history by exercise.
  • PR history uses the client's unit preference for display and does not expose equipment-specific decimal increment rules.
  • PR history clearly marks pending projection refresh when a recently corrected workout has not been reprocessed yet, then shows explicit correction notices if a displayed PR changes or is removed.

Test Scenarios & Confirmed Decisions

Exercise effort and PR logic needs strict domain tests because small data errors can mislead coaching decisions.

Tests

  • Domain: RPE accepts only 1.0 to 10.0 in half-step increments and RIR accepts only integer 0 to 10.
  • Domain: required effort target blocks submission only for the prescribed exercises and sets where it is required.
  • Domain: warmup, skipped, failed, and coach-excluded sets do not create PR events.
  • Domain: unit normalization compares kg and lb submissions correctly, stores canonical kg, and displays results using the client's unit preference without equipment-specific decimal increment rules.
  • Domain: estimated 1RM is the average of Brzycki, Epley, and Wathan formula outputs and stores the active formula policy on performance points and PR events.
  • Domain: PR detection handles ties, tiny rounding differences, estimated 1RM changes, and max-reps-at-load comparisons deterministically.
  • Domain: eligible PRs auto-confirm immediately for free-weight and machine-loaded movements.
  • Domain: bodyweight-only movements do not create separate max-reps, duration, or added-load PR types in Public V1.
  • Domain: overall session difficulty uses a separate 1-to-5 scale and does not reuse set-level RPE/RIR validation.
  • API/security: client cannot read PRs for another relationship; coach cannot read performance views outside authorized relationships.
  • Contract: source workout events include enough accepted set facts to build performance points without querying mutable program drafts.
  • Integration: WorkoutSessionSubmitted updates performance points, effort rollups, and current PRs idempotently.
  • Integration: correction invalidates stale performance points, recomputes current PR records for the affected exercise scope, and emits explicit correction notices when a shown PR changes or is removed.
  • Mobile E2E: after an effort-enabled workout syncs through Workout Logging, client sees PR feedback and exercise history.
  • Web E2E: coach reviews an exercise graph with effort overlay, fixed formula label, correction notices, dismisses a noisy PR, and sees the current PR card recompute.

Confirmed Decisions

  • V1 baseline display normalizes load values to the client's unit preference only. Do not apply equipment-specific decimal load increments in PR display.
  • Estimated 1RM uses the average of Brzycki, Epley, and Wathan. Coaches cannot change this per workspace in Public V1, and coach-facing views must show the active formula label.
  • PRs auto-confirm immediately for V1 baseline, including machine-loaded movements. Coaches can dismiss noisy PRs after detection.
  • When a correction removes or changes a previously shown PR, both client and coach receive an explicit correction notice.
  • Bodyweight-only movement PR types for max reps, duration, and added load are deferred until after V1 baseline.
  • Overall session effort uses a separate simple 1-to-5 difficulty scale instead of reusing set-level RPE/RIR controls.

CoachMe internal planning documentation.