A check-in is a filled intake form or a filled recurring check-in form waiting for a coach to review it. This module is the review surface in the coach web app. It was built in three layers, which the code calls waves:
  1. The queue. List filled forms, open one, write feedback, save a draft, reassign, remind.
  2. Tracking. A per-trainee timeline of nutrition, workouts, steps, cardio, weigh-ins and forms, shown beside the form.
  3. Insights. Deterministic status per area, flags on the queue rows, and a suggested feedback message.
Files:

Data model

The queue is made of FormResponse rows whose template type is INTAKE or CHECK_IN. One-time forms and documents to sign never enter this surface. The response id is the one id the whole surface keys on. Everything the review adds lives in CheckinReview, keyed by a unique responseId: A response with no review row, or with no handledAt, is pending. When a review is completed, the handled stamp is mirrored onto the FormResponse (handledAt, handledById, handledByName, handledNote) so the inbox reads the same truth. In the API, INTAKE is called onboarding and CHECK_IN is called update.

Auth and scope

Every handler builds the caller with callerOf(req):
  • No studio or user on the request: UNAUTHORIZED.
  • Role TRAINEE: FORBIDDEN (coaches only).
The service then resolves a scope from the caller’s Coach row (resolveScope): A caller who does not see all trainees sees a form only when the trainee is assigned to them, as lead coach (Client.coachId) or through ClientCoach, or when an admin routed that specific form to them. A caller with no coach row and no all-access sees nothing. A form outside the caller’s scope answers NOT_FOUND (check-in not found), the same as an id from another studio, so the API never reveals which ids exist. The trainee-level routes apply the same rule to a trainee and answer NOT_FOUND (trainee not found).

What the queue hides

checkins-queue.ts filters two kinds of responses out of the queue and its counts, without deleting anything:
  • A form a coach took off the queue (CheckinReview.dismissedAt set).
  • Form history imported from AutoFit. Those answers carry a marker key (ANSWER_ID_KEY from the AutoFit import).
Both stay everywhere else: the trainee’s form history, the tracking timeline and the “update number N” ordinals still count them.

Queue endpoints

GET /v1/web/checkins

The review queue with its quick stats. Auth: web lane, any coach role, scoped.
string
default:"pending"
pending, handled or all.
string
default:"all"
all, update or onboarding.
string
Comma-separated coach ids. Honoured for admins only, ignored for everyone else. Matches the row’s effective coach.
string
ISO date or datetime. Only the calendar day is used. Keeps rows whose form was sent on or after that day (UTC day start). Rows with no send time are dropped.
string
Same format. Inclusive, up to the end of that UTC day.
string
Same format, on the fill time.
string
Same format, on the fill time.
string
Trimmed, minimum length 1. Case-insensitive contains on the trainee name.
string
default:"wait_desc"
wait_desc or wait_asc. Wait is the time from fill to handled, or from fill to now while pending.
There is no pagination. The service loads every in-scope response for the type and search, builds rows, then filters and sorts in memory. How a row is built:
  • coach is the admin reroute when there is one, otherwise the trainee’s lead coach, otherwise null.
  • n is the trainee’s Nth update, 1-based, ranked by fill time across all of their check-ins. Onboarding is always 1.
  • sentAt is when the form assignment was created. filledAt is submittedAt, falling back to createdAt.
  • flags holds flag keys, see Flags. Pending rows get freshly computed flags (batched, cached six hours). Handled rows only show flags that are already cached. If the flag engine fails, the queue is served without flags.
counts describes the whole queue in view (scope, type and search). It deliberately ignores the status, coachIds and date filters, so the numbers do not jump as the coach switches tabs. Response:
  • over48h counts pending rows filled at least 48 hours ago.
  • newTrainees and onboardingPending are computed the same way: pending onboarding rows.
  • flagged counts pending rows with at least one flag.
  • coaches lists the studio’s active coaches for the filter dropdown. It is empty unless the caller sees all trainees.
Errors: UNAUTHORIZED, FORBIDDEN, VALIDATION (422) for a bad query, such as a date that is not a real calendar day.

POST /v1/web/checkins/bulk-delete

Takes forms off the queue. Despite the path, it deletes no form. Auth: web lane, any coach role, scoped.
string[]
required
Between 1 and 200 response ids. Duplicates are removed.
Only in-scope intake and check-in responses of the studio are affected. Ids outside the caller’s scope are skipped silently. In one transaction the service:
  1. Sets dismissedAt on each form’s CheckinReview, creating the row when there is none.
  2. Deletes the inbox task items linked to those forms (InboxItem rows whose autoKey matches the queue task prefixes for that response).
It then writes one AuditLog row per form with action checkin.dismissed and resource type CHECKIN_REVIEW. The trainee’s filled forms, their reviews, weigh-ins and photos are all kept. The path name is the web app’s existing contract. A comment in the service notes that a real delete here once wiped hundreds of forms. Response:
deleted is the number of forms taken off the queue. It can be lower than the number of ids sent. Errors: VALIDATION for an empty list or more than 200 ids. UNAUTHORIZED, FORBIDDEN.

GET /v1/web/checkins/:id

One check-in in full: the form, the answers, the previous answers for comparison, the trainee, the draft, and plan changes. Auth: web lane, any coach role, scoped.
string
required
The FormResponse id.
What the service assembles:
  • form.pages from the response’s schemaSnapshot, falling back to the template’s current schema. Display-only field types are skipped and empty pages are dropped.
  • previousAnswers and previousFilledAt: the trainee’s previous check-in, for updates only. null on onboarding.
  • planChangesSince: the trainee’s TRAINING and NUTRITION programs that are not drafts and were updated after this form was filled. A plan held from an intake review appears here too, with its release state.
  • sessionPrograms: onboarding only. The plans assigned from this review in hold mode (Program.releaseResponseId equals this response), oldest first, in any status. The pending ones are what the release route sends, see Programs.
  • flags: full flag objects for the trainee.
  • suggestedMessage: a prefilled feedback text, only while the draft holds no trainee message. A coach’s own words always win. An autosaved internal note alone does not block the suggestion.
If the insights engine fails, flags is empty and suggestedMessage is null. The detail itself still loads. Response:
Field types and question keys in the example are illustrative. label, detail and suggestedMessage are Hebrew strings. handled is { "at": "...", "byName": "..." } once the review is complete. draft is null when nothing was saved. Errors: NOT_FOUND (check-in not found), FORBIDDEN, UNAUTHORIZED.

POST /v1/web/checkins/:id/feedback

Completes a review. It records an internal note, a message to the trainee, or both, and marks the form handled. Auth: web lane, any coach role, scoped.
string
required
The FormResponse id.
string
default:""
Internal write-up. Trimmed, maximum 4000 characters. Never sent to the trainee.
boolean
default:"false"
Whether a message goes to the trainee. When the key is missing, it is inferred: true if message is not blank or channels is not empty. This keeps older web clients working.
string
default:""
Text for the trainee. Maximum 4000 characters.
string[]
default:"[]"
Up to two of push and whatsapp.
string[]
default:"[]"
Up to 20 program ids to mention in the message.
Rules, enforced by the schema and again in the service:
  • With sendToTrainee: true, a non-blank message and at least one channel are required.
  • With sendToTrainee: false, a non-blank note is required.
What happens, in order:
  1. Attached plans. Ids are resolved strictly inside this trainee. Ids that do not belong are dropped silently. For each resolved plan, a line naming the plan and pointing to the app is appended to the message.
  2. Push, when chosen: notifier.send with type TRAINER_MESSAGE and the message as a literal body.
  3. WhatsApp, when chosen: sent through SmartSend with the studio’s own API key from Studio.settings.smartsend.apiKey.
  4. Review. CheckinReview is upserted with the note, message, channels, attached plans and the handled stamp. The draft is cleared.
  5. Mirror. The handled stamp is copied onto the FormResponse. handledNote is the internal note when there is one, otherwise the trainee message.
  6. Audit. An AuditLog row with action checkin.feedback.sent.
  7. Activity. A ClientActivity row with type CHECKIN_FEEDBACK_SENT, entity type CHECKIN, the form name, and metadata sentToTrainee and channels. It is written whether or not anything went to the trainee.
  8. Automation. formFeedbackSentHook fires the FORM_FEEDBACK_SENT task automations and flow runs for the form. A failure there is logged and does not fail the request.
A failed or skipped channel does not stop the review from being marked handled. The response says what each channel did. Response:
channelResults has one key per channel requested: Errors: VALIDATION (422) for the schema rules, with Hebrew messages on message, channels or note. BAD_REQUEST for the same three rules from the service guard. NOT_FOUND (check-in not found). FORBIDDEN, UNAUTHORIZED.

PUT /v1/web/checkins/:id/draft

Autosaves unfinished feedback. Auth: web lane, any coach role, scoped.
string
required
The FormResponse id.
The body has the same five fields as feedback: note, sendToTrainee, message, channels, attachedChangeIds, with the same defaults and limits. Only the shape is checked. A draft may have an empty message, no channel and no note, so autosave never bounces. note is not trimmed here. If the review is already handled, the route returns success and writes nothing. An autosave that lands just after the send must not bring stale text back. Response:
Errors: NOT_FOUND (check-in not found), VALIDATION, FORBIDDEN, UNAUTHORIZED.

POST /v1/web/checkins/:id/reassign

Routes one form to another coach. The trainee’s own coach assignment does not change. Auth: web lane, admins only (isAdmin).
string
required
The FormResponse id.
string
required
The Coach row id of an active coach in the studio.
Sets CheckinReview.assignedCoachId. From then on that coach sees this form in their queue and can open the trainee’s tracking and insights, even when the trainee is not assigned to them. An AuditLog row with action checkin.reassigned is written. No notification is sent to the new coach by this route. Response:
Errors: FORBIDDEN (admins only). NOT_FOUND (check-in not found). BAD_REQUEST (assignee is not an active coach here).

POST /v1/web/checkins/:id/remind

Sends the trainee a short push saying the coach is still going over their update. Auth: web lane, any coach role, scoped.
string
required
The FormResponse id.
No body. Sends a TRAINER_MESSAGE push with a fixed Hebrew sentence that addresses the trainee by first name, then stamps CheckinReview.remindedAt. There is no rate limit or cooldown in the service, and the result of the push is not reported back. Response:
Errors: NOT_FOUND (check-in not found), FORBIDDEN, UNAUTHORIZED.

Trainee endpoints

These three routes are keyed by trainee, not by form. They are registered before /:id so the literal trainee segment is never read as a response id.

GET /v1/web/checkins/trainee/:clientId/tracking

The per-trainee timeline for a date range. Auth: web lane, any coach role, scoped to the trainee.
string
required
The trainee id.
string
required
ISO date or datetime. Only the calendar day is used.
string
required
Same format. Inclusive.
The range must be between 1 and 92 days (TRACKING_MAX_DAYS). Days are civil days in the studio’s time zone (Studio.timezone, default Asia/Jerusalem). Note that this differs from Client tracking, which prefers the trainee’s own time zone. The payload has two kinds of data. Range data, which depends on from and to:
  • days: one entry per day in the range.
    • nutrition is null for an empty day: no plan meals, no snapshot, nothing logged and no water. Otherwise it carries consumed and target from the partner module’s nutritionDays (the same source client tracking reads, fetched in 62 day chunks), meal counts, the number of swaps, water, and one slot per plan meal.
    • A slot’s eaten is null when the trainee never opened that day, so eaten-ness is unknown. It is true when the meal was eaten outside, replaced, or has an eaten item.
    • swapped counts item swaps plus whole-meal replacements.
    • workout shows one session per day. A completed session outranks a partial one. Abandoned starts are not sessions. splitLetter is the letter (A, B, C) of the plan day, resolved from the program the log itself was recorded against, so old logs keep their letter after a plan change. done is true for completed, false for partial, null for no session.
    • steps.source is always manual. DailyMetric has no provenance column, so the service cannot tell device data from typed data.
    • sleep is always null. There is no sleep data model.
  • weighIns: weigh-ins inside the range. deltaKg is against the previous weigh-in ever, not the previous one in range. measurements is the numeric DailyMetric.measurements of that day, and photos are the ClientPhoto rows taken that day.
  • sessions: every session in the range with its exercises. reps is the done-set reps joined with slashes. volume is weight times reps over weighted sets.
  • planChanges: programs updated inside the range.
  • forms: the trainee’s filled forms in the range, oldest first, with their update number.
Snapshots, which do not depend on the range. The panel fetches the timeline in window chunks, and every chunk carries the same values:
  • targets: today’s nutrition target (in portions when the studio uses portions, with kcal then holding the sum of the three portion values), workouts per week, weekly cardio minutes, steps goal, goal weight and water goal, plus trainingFilePlan and nutritionFilePlan. For a file plan the related targets are null.
  • splitDays: every day of every active training plan as the coach prescribed it, with no trainee swaps. Exercise names use the studio’s override when it renamed a shared exercise.
  • latestTechniqueVideos: keyed by exercise id, the newest technique video of that exercise in any status.
  • techniqueRequests: keyed by exercise id, the timestamp of an open coach request. A request is open while no technique video of that exercise was uploaded at or after it.
Response:
The slot labels in a real response are Hebrew: a meal’s custom label, or the Hebrew name of its meal type. Errors:

POST /v1/web/checkins/trainee/:clientId/technique-request

Asks the trainee to film one exercise in their next workout. Auth: web lane, any coach role, scoped to the trainee.
string
required
The trainee id.
string
required
Trimmed, 1 to 200 characters.
The service stamps the current time as techniqueVideo.requestedAt on every strength row that prescribes the exercise, across all of the trainee’s active training plans (stampTechniqueRequest from the program templates module). It rewrites the stored content in place. Nothing else about the plan moves, and each program’s updatedAt is written back unchanged, so the edit does not show up as a plan change. The plan version is not incremented and no plan-updated push is sent. The trainee app then shows a required technique video for that exercise in the next workout. The request clears itself once the trainee uploads a newer video of the exercise. An AuditLog row is written with action checkin.technique.requested, resource type CLIENT, and a diff holding the exercise id, the stamp, the row count and the program ids. Response:
Errors: CONFLICT with a Hebrew message when the exercise is not in any active training plan. NOT_FOUND (trainee not found). VALIDATION, FORBIDDEN.

GET /v1/web/checkins/trainee/:clientId/insights

Status, numbers and text per area for a date range, plus the trainee’s flags. Auth: web lane, any coach role, scoped to the trainee.
string
required
The trainee id.
string
required
ISO date or datetime.
string
required
ISO date or datetime. Inclusive. The same 92 day cap applies, because the engine calls the tracking service.
The scope check runs first, then the cache. A cached payload is never served to a coach the tracking route would refuse. On a cache miss the engine loads the tracking timeline twice, for the range and for the equal-length range right before it, computes the flags, runs every container, derives the overall verdict, and asks the model to phrase the overall text. Response:
headline, text, recommendation, label and detail are Hebrew. The metric names inside a container’s metrics object in the example are illustrative. Each container defines its own keys. status is one of progress, stuck, decline, none. Errors: same as the tracking route. INTERNAL (insights engine is not wired) only if the controller was built without the insights service, which the router never does.

Insights engine

The engine is hybrid. Every metric, threshold, flag and per-container text is deterministic code. A model writes exactly two things: the phrasing of the overall verdict and the suggested feedback message. Both have deterministic fallback templates, so the endpoint never fails and never changes a number because of the model. The model is Anthropic, configured from ANTHROPIC_API_KEY, AGENT_MODEL and the optional ANTHROPIC_WORKSPACE_ID, with a 400 token output budget. With no key the engine always uses the fallbacks.

Nutrition adherence

dayAdherencePct mirrors the math the frontend uses, so the panel and the engine agree:
  • A day is logged when at least one meal is eaten or any consumed macro is above zero.
  • With a target, the score is the mean of min(consumed / target, 1) over protein, carbs and fat, for the macros that have a positive target. If none do, calories are used. Overshooting a macro never scores above 100%.
  • Without a target, the score is meals eaten over plan meals.
Note that this is a different definition from the adherence percentage in Client tracking, which counts days within 90% to 115% of the calorie target.

Containers

For a file plan the related containers show the fixed file plan note in place of a computed text.

Overall verdict

overallStatusOf: decline if weight, nutrition or workouts is declining. Otherwise stuck when two or more containers are stuck. Otherwise progress.

Flags

Flags mark trainees who need attention. They appear as keys on queue rows and as full objects in the detail and insights payloads.

Suggested message

suggestedMessageFor produces the feedback prefill for the detail view. It takes the trainee’s first name, the plan changes since the form, and the trainee’s signals and flags. The model writes it when configured, and a deterministic template is used otherwise or on failure.

Caches

All three caches are in Redis. A Redis failure is logged as a warning and the value is computed without the cache. The keys do not include the data, so a payload can be up to six hours behind what the trainee logged. computedAt tells the panel how old it is.