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; becomes the committed workout’s description by default.
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. The baseline for the correction delta.
final_payloadjsonb nullableThe committed set_sections-shaped payload (post-adjustment). Set on commit.
deltajsonb nullablePayloadDelta from diffSectionsPayload(auto_payload, final_payload) — the headline product metric (zeroEdit, editDistance, swap/add/remove counts). Set on commit.
workout_iduuid FK → workouts.id (set null), nullableInput workoutId for the “Structure this” flow; set on commit for the paste flow.
modelvarchar(64) nullablee.g. claude-sonnet-4-5.
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_english_only / parse_failed on terminal-failure rows.
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 ┌───────────────────┼──────────────────────┐ │ │ │ language ≠ en pipeline error success │ │ │ ▼ ▼ ▼ rejected_non_english failed draft (IR + auto_payload persisted) (422, cost still (500, error_code) │ metered) ┌────────┴────────┐ │ │ POST /commit POST /discard │ │ ▼ ▼ committed discarded (final_payload + delta + workout_id recorded)

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. delta between the two is the correction distance — computed server-side at commit through the exact same diffSectionsPayload the eval harness uses, so telemetry and eval numbers are on one scale.

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.