- The queue. List filled forms, open one, write feedback, save a draft, reassign, remind.
- Tracking. A per-trainee timeline of nutrition, workouts, steps, cardio, weigh-ins and forms, shown beside the form.
- Insights. Deterministic status per area, flags on the queue rows, and a suggested feedback message.
Files:
Data model
The queue is made ofFormResponse 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 withcallerOf(req):
- No studio or user on the request:
UNAUTHORIZED. - Role
TRAINEE:FORBIDDEN(coaches only).
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.dismissedAtset). - Form history imported from AutoFit. Those answers carry a marker key (
ANSWER_ID_KEYfrom the AutoFit import).
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.coachis the admin reroute when there is one, otherwise the trainee’s lead coach, otherwisenull.nis the trainee’s Nth update, 1-based, ranked by fill time across all of their check-ins. Onboarding is always 1.sentAtis when the form assignment was created.filledAtissubmittedAt, falling back tocreatedAt.flagsholds 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:
over48hcounts pending rows filled at least 48 hours ago.newTraineesandonboardingPendingare computed the same way: pending onboarding rows.flaggedcounts pending rows with at least one flag.coacheslists the studio’s active coaches for the filter dropdown. It is empty unless the caller sees all trainees.
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.
- Sets
dismissedAton each form’sCheckinReview, creating the row when there is none. - Deletes the inbox task items linked to those forms (
InboxItemrows whoseautoKeymatches the queue task prefixes for that response).
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.form.pagesfrom the response’sschemaSnapshot, falling back to the template’s current schema. Display-only field types are skipped and empty pages are dropped.previousAnswersandpreviousFilledAt: the trainee’s previous check-in, for updates only.nullon onboarding.planChangesSince: the trainee’sTRAININGandNUTRITIONprograms 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.releaseResponseIdequals 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.
flags is empty and suggestedMessage is null. The detail itself still loads.
Response:
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.
- With
sendToTrainee: true, a non-blankmessageand at least one channel are required. - With
sendToTrainee: false, a non-blanknoteis required.
- 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.
- Push, when chosen:
notifier.sendwith typeTRAINER_MESSAGEand the message as a literal body. - WhatsApp, when chosen: sent through SmartSend with the studio’s own API key from
Studio.settings.smartsend.apiKey. - Review.
CheckinReviewis upserted with the note, message, channels, attached plans and the handled stamp. The draft is cleared. - Mirror. The handled stamp is copied onto the
FormResponse.handledNoteis the internal note when there is one, otherwise the trainee message. - Audit. An
AuditLogrow with actioncheckin.feedback.sent. - Activity. A
ClientActivityrow with typeCHECKIN_FEEDBACK_SENT, entity typeCHECKIN, the form name, and metadatasentToTraineeandchannels. It is written whether or not anything went to the trainee. - Automation.
formFeedbackSentHookfires theFORM_FEEDBACK_SENTtask automations and flow runs for the form. A failure there is logged and does not fail the request.
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.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:
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.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:
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.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:
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.
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.nutritionisnullfor an empty day: no plan meals, no snapshot, nothing logged and no water. Otherwise it carriesconsumedandtargetfrom the partner module’snutritionDays(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
eatenisnullwhen the trainee never opened that day, so eaten-ness is unknown. It istruewhen the meal was eaten outside, replaced, or has an eaten item. swappedcounts item swaps plus whole-meal replacements.workoutshows one session per day. A completed session outranks a partial one. Abandoned starts are not sessions.splitLetteris 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.doneistruefor completed,falsefor partial,nullfor no session.steps.sourceis alwaysmanual.DailyMetrichas no provenance column, so the service cannot tell device data from typed data.sleepis alwaysnull. There is no sleep data model.
weighIns: weigh-ins inside the range.deltaKgis against the previous weigh-in ever, not the previous one in range.measurementsis the numericDailyMetric.measurementsof that day, andphotosare theClientPhotorows taken that day.sessions: every session in the range with its exercises.repsis the done-set reps joined with slashes.volumeis 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.
targets: today’s nutrition target (in portions when the studio uses portions, withkcalthen holding the sum of the three portion values), workouts per week, weekly cardio minutes, steps goal, goal weight and water goal, plustrainingFilePlanandnutritionFilePlan. For a file plan the related targets arenull.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.
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.
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:
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.
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 fromANTHROPIC_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.
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.