FormTemplate.schema, DefaultFormTemplate.schema, FormResponse.schemaSnapshot and FormAssignment.signSchema all hold the same document, FormSchemaV2. FormResponse.answers holds the trainee’s answers keyed by field key.

Where the contract lives

A field type is defined in four places. Change them together. The conditions engine exists three times: backend form-conditions.ts, frontend form-conditions.ts, and mobile lib/conditions.ts. They must agree, because the app decides what to show and the backend decides which answers to keep.

Shape

normalizeFormSchema(raw) always returns schemaVersion: 2. Every read and write path calls it, so older stored forms heal themselves.

fields[]

config

One bag for all per-type parameters:

Field types

FORM_FIELD_TYPES has 25 entries. DISPLAY_ONLY_TYPES is read_more, coach_message, text_block, media_block. They never produce answers and are skipped by validation. CLIENT_SAVE_TYPES is age, height, birth_date, weight, body_measurement, goal, image_upload, israeli_id, phone, email. Retiring a field type means setting hidden: true in FIELD_DEFS. The type stays in FORM_FIELD_TYPES so existing forms that use it keep working.

Rich text

content.richText and settings.thankYou.richText are arrays of spans:
size is sm, md or lg. align is start, center or end. bold, italic and underline are optional booleans.

settings

Send cadence is not in the schema. It lives in the FormTemplate columns cadence, sendWeekday and sendDayOfMonth.

Conditions

A field or a page can be shown only when another field’s answer matches a rule.
A condition has exactly one rule. There is no AND or OR. A field is answerable when its own condition passes and its page’s condition passes (isFieldAnswerable).

Operators by source type

The operators offered depend on the kind of the source field: Types not in this table cannot be a condition source: food_preferences, signature and the display blocks.

Evaluation rules

  • empty and filled look only at whether there is an answer. An empty array counts as empty.
  • checked means the answer is exactly true. unchecked means anything else.
  • For every other operator an empty answer makes the condition false.
  • Choice comparisons are string comparisons. For a multi-select the rule matches when any picked value equals the target.
  • Dates are compared as whole days. ISO dates and day-first dd/mm/yyyy are both parsed.
  • The age_* operators compute the age in whole years from a date answer.
  • between is inclusive and accepts the two bounds in either order.

It fails open

isConditionMet returns true, meaning “show it”, whenever the condition cannot be evaluated: it is disabled, has no source, the source field was deleted, the source is not a valid condition source, or the operator does not belong to that source type. A broken rule never hides content permanently. normalizeFormSchema also runs reconcileCondition on every field and page and removes conditions that can no longer be satisfied structurally.

Hidden answers are dropped

stripHiddenAnswers(schema, answers) runs on submit. Answers for display-only fields and for fields that are not answerable under the submitted answers never reach storage. validateAnswers skips the same fields, so a required field hidden by a condition does not block submission.

FormResponse.answers

A flat object keyed by field key:
validateAnswers returns a list of { key, message } errors. Messages are in Hebrew because they are shown to the trainee. Always render a response with schemaSnapshot ?? formTemplate.schema. The form may have been edited since the trainee answered it.

Documents to sign

A PDF_SIGNATURE form uses the same schema with three additions.

settings.document

Page sizes are in PDF points with rotation applied. Limits: DOCUMENT_MAX_PAGES is 200 and DOCUMENT_MAX_PAGE_POINTS is 14400.

fields[].placement

page is the zero-based PDF page. x, y, w and h are fractions of that page as pdf.js displays it, measured from the top-left corner. The minimum size is PLACEMENT_MIN_SIZE, 0.01. A placement on a page past the end is clamped to the last page.

fields[].filledBy

coach fields are filled when the form is sent. Their values are stored on FormAssignment.prefill as an object of key to string, each up to SIGNING_TEXT_MAX_LENGTH (500) characters, with blanks dropped. validateFieldValues in forms/signing.ts checks them: only coach field keys are accepted and every required one must be present. Signature fields are always the trainee’s. Field keys on a signing form must match SIGNING_KEY_PATTERN: a lowercase letter followed by up to 39 lowercase letters, digits or underscores. enforceSigningKeys applies this during normalization. The signed PDF and its audit trail are stored on the response. See Form models.