Coaches send forms to trainees: intake questionnaires, recurring check-ins and one-time forms. Each send is a FormAssignment. The trainee app lists the pending ones, renders the form from its schema and submits the answers. Mount: /v1/trainee/forms, router traineeFormsRouter in src/modules/forms/forms.routes.ts. The service is built by createWiredFormsService in forms.wiring.ts, which attaches every submit side effect. Lane: trainee. The router mounts authenticateTrainee only. It does not use the app access guard. Instead the forms service applies its own rule: while Client.planFrozenOn is set, the list is empty and the other two endpoints return 403. Preview tokens can read forms and cannot submit. See Authentication. Documents to sign (form type PDF_SIGNATURE) are never served here. They are signed on a public web page. See Public endpoints.

Endpoints

GET /v1/trainee/forms

Lists the forms waiting for the trainee. Auth: trainee token. No parameters. An assignment is listed when all of these hold:
  • it belongs to the trainee and the token’s studio,
  • its status is PENDING,
  • it has no dueAt, or dueAt is now or earlier,
  • its template is not a document to sign.
Results are ordered by creation time, newest first. A trainee whose plan is frozen gets an empty list. Response: 200.
string
The assignment id. This is the :id used by the two endpoints below, not the template id.
string | null
A short note the coach attached when sending.
string
The form’s display name for trainees (schema.settings.displayName) when set, else the coach-facing name.
string
INTAKE, CHECK_IN or ONE_TIME.
Errors: none beyond the lane errors.

GET /v1/trainee/forms/:id

Returns one assignment with the full form schema to render. Auth: trainee token.
string
required
The assignment id.
The schema is the template’s current schema passed through normalizeFormSchema, which upgrades older stored shapes to version 2. Completed assignments can also be loaded. Check status before offering a submit. Response: 200.

Schema reference

FormSchemaV2 is defined in src/modules/forms/form-schema.ts. Field types (FORM_FIELD_TYPES): settings holds rtl, pages (each with id, optional title, ctaLabel and its own condition), and optional submitLabel, successMessage, displayName and thankYou.

Conditions

A field or a page can be shown only when another field’s answer matches. Operators by source kind (form-conditions.ts): The engine fails open: a condition that is disabled, points at a missing field or cannot be evaluated leaves the field visible. The same engine is mirrored in the web builder and the mobile app, so all three agree on what is visible. The server evaluates it again on submit and never trusts the client’s view. Errors:

POST /v1/trainee/forms/:id

Submits the trainee’s answers and completes the assignment. Auth: trainee token. Refused for preview tokens.
string
required
The assignment id.
object
required
A map of field key to answer. Unknown keys and answers to hidden fields are dropped.
Answer formats by field type: A required field with an empty answer (undefined, null, empty string or empty array) is an error. Fields hidden by a condition are skipped entirely, so a required but hidden field never blocks the submit. What the service does on a valid submit:
  1. Strips answers of hidden and display-only fields (stripHiddenAnswers).
  2. Creates a FormResponse with the answers, the assignment’s formVersion, a schemaSnapshot of the schema as answered, and submittedAt.
  3. Marks the assignment COMPLETED with completedAt.
  4. For an INTAKE form, tells the subscription starter that the intake form is done. A subscription that was waiting for it starts now.
  5. Copies saveToClient answers onto the trainee, best effort (applyClientCardSaves):
    • email, phone, israeli_id, age, height, birth_date (which also recomputes age), goal and weight update the Client profile.
    • Each weight answer also creates a WeightEntry with source: "form" and the response id.
    • body_measurement answers are merged into today’s DailyMetric.measurements under the field’s config.measurement key (chest, waist, hips, arm or thigh).
    • image_upload answers that look like image URLs create a ClientPhoto with source: "form", the response id and the field’s config.pose. A failure in this step never fails the submission.
  6. Fires the submitted hook (formSubmittedHandler), also best effort:
    • FORM_FILLED webhooks registered through the Automation API are queued for delivery.
    • A FORM_FILLED automation flow runs when the studio has one. Otherwise the legacy “form filled” task is created for the coach.
    • The same pair for low ratings: the rating-below flow, or the legacy rating task.
The AI summary of a response is generated later, when a coach opens it on the web. It is not part of the submit. Response: 201. The created FormResponse row.
schemaSnapshot is the full schema and is shortened here. The row may carry further columns of the FormResponse model. Errors: Answer errors are not Zod errors. The message is the first error in Hebrew followed by the field label in quotes, clipped to 60 characters, and details lists every failed field:
The app uses details[].key to outline the fields that need attention.

Notifications for pending forms

The trainee lane has no endpoint for “new form” events. Announcements are pushed:
  • A coach-assigned form pushes the FORM_SENT notification right away when the trainee has a device registered.
  • notifyPendingForms announces forms that became visible later (a dueAt that has now passed) and runs again whenever the app registers a push token. See POST /v1/trainee/push-token on Profile and home.
The app should refetch GET /v1/trainee/forms when it opens and when it receives a form notification.