Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWorkout ParseWorkout Parse — Data Model

Workout Parse — Data Model

One table, in libs/db/src/lib/schema/ai-parse.ts. One row per transform attempt — this is where “freeform text retained as source of truth” lives (D5), and the audit trail for cost, timings, and the commit-time correction delta.

ai_parse_jobs

ColumnTypeNotes
iduuid PK
organization_iduuid FK → organizations.id (cascade), not nullScope.
user_iduuid FK → users.id (cascade), not nullRequesting coach (internal users.id, same referencing style as ai_conversations).
sourcevarchar(20) not null'paste' | 'freeform_form' (D12 entry point).
statusvarchar(30) not null, default 'draft'Lifecycle below.
input_texttext not nullNormalized (LF) source text — every span in draft is an offset into this. Retained for audit/re-parse, never copied into the structured workout’s description.
languagevarchar(10) nullableBCP-47-ish primary tag from extraction ('en', 'he', …).
multi_workout_detectedboolean not null, default falseD4.
workout_countinteger nullableExtraction’s count of distinct workouts in the paste.
draftjsonb nullableThe persisted ParseDraft IR (parse-draft.schema.ts). Null on jobs that failed before assembly.
auto_payloadjsonb nullabledraftToSectionsPayload(draft) at parse time — auto-resolved movements only. Legacy correction baseline.
final_payloadjsonb nullableThe committed set_sections-shaped payload (post-adjustment). Set on commit.
deltajsonb nullablePayloadDelta comparing the full draft to final_payload for fidelity commits, or auto_payload for legacy commits. Set on commit; the event’s deltaBaseline identifies the comparison.
workout_iduuid FK → workouts.id (set null), nullableInput workoutId for the “Structure this” flow; set on commit for the paste flow.
modelvarchar(64) nullableThe model that served the extraction, as the API reports it (e.g. claude-sonnet-5-20260801).
input_tokens / output_tokensinteger nullableExtraction usage.
cost_usd_microsbigint nullableSame math as Spotter (computeCostUsdMicros, apps/api/src/ai/cost.util.ts); also upserted into ai_usage_daily via AgentCostTracker.recordTurn.
duration_msinteger nullableWall clock of the whole parse.
stage_timingsjsonb nullable{ extractionMs, resolutionMs, totalMs }.
error_codevarchar(64) nullableparse_failed / parse_multiple_sessions on terminal-failure rows. parse_english_only appears only on historical rejected_non_english rows and is no longer produced.
created_at / updated_attimestamp tz not nullHouse pattern.

Indexes:

  • ai_parse_jobs_org_created_idx — (organization_id, created_at DESC) (org-scoped listing / lookup).
  • ai_parse_jobs_workout_idx — partial on (workout_id) WHERE workout_id IS NOT NULL.

Relations (aiParseJobsRelations): job → organization, user, workout.

Status lifecycle

The row is inserted first (status: 'draft') before the LLM call, so even a crashed extraction leaves an audit row. Terminal states are never re-entered — commit/discard require status = 'draft' and 409 otherwise.

POST /workouts/parse │ insert row (status: draft) │ LLM extraction │ ├──────────────────────┐ │ │ pipeline error success │ │ ▼ ▼ failed draft (IR + auto_payload persisted) (500, error_code) │ ┌────────┴────────┐ │ │ POST /commit POST /discard │ │ ▼ ▼ committed discarded (final_payload + delta + workout_id recorded)

rejected_non_english is a terminal status found only on historical rows from the removed English-only gate (D3). No current path writes it; a non-English paste follows the same path as any other.

Why the payload triple (draft / auto_payload / final_payload)

  • draft is the client-facing IR — spans, candidates, confidence. It’s what the preview renders and what a future re-parse feature would compare against.
  • auto_payload freezes “what the machine got right on its own” at parse time.
  • final_payload is what the coach actually shipped. Fidelity commits compare it to all movements in draft, using validated review choices to identify unresolved slots. Accepting a suggestion is not a new movement. Auto-selected identity corrections, prescription edits, and section configuration changes still count. Legacy commits compare against auto_payload; parse.committed.deltaBaseline distinguishes these metric versions. The baseline containing unresolved placeholders exists only in memory for metrics and is never written as a workout.

Multi-org isolation & PII

  • All reads go through ParseJobsRepository.findById(orgId, jobId) — org-scoped; cross-org job ids 404.
  • input_text and draft contain coach-authored content and are never emitted to observability (scrub() blocklist in parse-observability.service.ts drops text/mentions/titles/payloads — counts and enums only).
  • Cascade on org/user delete; workout_id is set null so deleting a workout keeps the audit row.

Retention posture

No automatic deletion; rows grow with usage (one per parse attempt, including rejected/failed ones). Payload jsonb is bounded by the 10,000-char input cap and the section/movement caps. Revisit alongside the Spotter retention backlog item if volume warrants.