Template authoring guide
Clinical templates drive the document editor, validation and print/PDF output from one JSON definition. Library templates live in code; clinic admins clone, edit checked properties and publish immutable versions.
Who can do this: Owner with clinical.template.manage.
Where: Admin → Clinical Templates (/app/clinical-templates).
1. Template shape
A template version contains schema, uiSchema, computed, print, phrases, optionSets, settings and metadata such as code, specialty, version, derivedFrom and licensing notice. Published versions are immutable. Documents store templateVersionId and schemaHash.
2. Restricted schema subset
| Keyword | Applies to | Rules |
|---|---|---|
type | all | One of string, number, integer, boolean, array, object |
title, description | all | Strings only |
properties | object | Required; names match ^[A-Za-z][A-Za-z0-9_]{0,63}$; constructor/prototype banned |
required | object | Unique names that exist in properties |
additionalProperties | object | Must be false |
items | array | Required single schema |
enum | string/number/integer/boolean | 1 to 1000 unique values |
minimum, maximum | number/integer | Finite numbers |
minLength, maxLength | string | Non-negative integers |
minItems, maxItems | array | Non-negative integers |
format | string | date, date-time, time, email, phone-in, pincode-in |
Forbidden keywords include pattern, $ref, $defs, oneOf, anyOf, allOf, not, if, then, else, const, default, multipleOf, uniqueItems, contains, prefixItems, propertyNames, unevaluated* and dependent*. Unknown fields are errors; data is never silently stripped.
3. Field type reference
| Type | Stored value | Validation | |
|---|---|---|---|
text | string | string | Escaped inline text |
textarea | string | string | Escaped text, newlines preserved |
richtext | markdown-lite string | Raw HTML rejected with FORMAT | Safe paragraphs, bold, italic and lists |
number | number | finite, min, max, precision | Number plus unit |
date / datetime / time | string | Fixed date/time formats | Locale date/time |
select / radio | scalar | In options/option set | Localized label |
multiselect | scalar array | Members only, no duplicates | Comma-separated labels |
checkbox | boolean | boolean | Yes/No |
yesno | yes, no, na | enum | Yes/No/N/A |
table | row objects array | Columns and no unknown cells | HTML table |
repeater | item objects array | itemFields recursively | Numbered list |
attachment | attachment array | id, MIME, size, count | Attachment list |
tooth-chart | object | FDI tooth data | Odontogram SVG + table |
body-map | object | Region markers | Body map SVG + marker list |
audiogram | object | Ear, conduction, frequency, threshold | Audiogram + PTA |
growth-chart | object | 0 to 5 year measurements | WHO chart if data installed |
milestone-grid | array | Milestone status rows | Domain × age grid |
rom-mmt-grid | array | ROM/MMT rows | ROM/MMT table |
scale | number | VAS/NRS/Likert config | Bar or label |
sensory-quadrant | array | Quadrant items/scores | Quadrant bars + disclaimer |
photo-comparison | array | Before/after photo pairs with region + capture dates | Side-by-side thumbnails (or placeholders) + table |
4. uiSchema options
Use key, widget, label, help, width, visibleIf, requiredIf, readOnly, copyForward, vital, defaultFrom, print.hideIfEmpty, print.style, options, optionSetRef, unit, min, max, precision, columns, fixedRows and itemFields. English labels are required; Hindi labels are recommended.
5. Conditions syntax
Leaf condition:
{ "field": "smoker", "op": "eq", "value": "yes" }
Operators: eq, neq, in, nin, gt, gte, lt, lte, empty, notEmpty. Combine with all, any and not. Inside repeater item fields, use @.key for the current item. Invalid conditions lint as INVALID_CONDITION, CONDITION_UNKNOWN_FIELD or CONDITION_ITEM_REF_OUTSIDE_REPEATER.
6. Computed-function reference
| Function | Arguments | Notes |
|---|---|---|
sum, avg, min, max | values | Missing input returns null |
count | values | Non-empty count |
round | value, digits | Half-away-from-zero |
percent | part, total | null for invalid denominator |
bmi | weightKg, heightCm | BMI |
band | value, bands | First matching band; max: null catch-all |
ageYears, ageMonths | dob, optional asOf | Never reads wall clock |
egfrCkdEpi2021 | creatinine, age, sex | Adults 18 to 130 |
ldlFriedewald | TC, HDL, TG | null when TG is 400 or more or result negative |
meanArterialPressure | systolic, diastolic | MAP |
quadrantScore | items, optional quadrant | Sensory totals |
pta | audiogram, ear, conduction, frequencies | Pure-tone average |
countWhere | list, key, value | Count matching rows |
growthZScore, growthPercentile | growth, indicator, which | WHO LMS if reference data installed |
The server recomputes computed values on save/sign and sanitizes NaN, Infinity and undefined to null.
7. Prefill, smart phrases, normal macros and copy-forward
Allowed bindings are patient.fullName, patient.ageYears, patient.sex, patient.dob, patient.uhid, aliases patient.age and patient.name, encounter.*, vitals.latest.*, practitioner.*, clinic.* and previous.data.*.
Smart phrases use dot triggers such as .fever. Tokens like {{patient.age}} resolve only from the whitelist; unknown tokens stay as typed. *** marks the next cursor selection. Scope precedence is personal, clinic, then pack.
Normal macros fill empty fields from section.normal.values and optional normal text. Copy-forward excludes copyForward:false, vitals, fields with defaultFrom and per-visit facts.
8. Print options
Document hints include paper, orientation, title, hideEmptyFields, showSectionHeadings, signature.label, signature.showRegistrationNumber, layout, footer and confidential. Field hints include hideIfEmpty and style. Drafts print with DRAFT watermark; finals print signature block, document number, QR verification, page numbering and end-of-report marker.
9. Lint and validation codes
| Code | Severity | Meaning |
|---|---|---|
ROOT_NOT_OBJECT | Error | Root schema is not an object |
FORBIDDEN_KEYWORD | Error | Forbidden schema keyword |
UI_KEY_NOT_IN_SCHEMA / SCHEMA_KEY_NOT_IN_UI | Error | Schema/UI mismatch |
DUPLICATE_KEY / INVALID_KEY | Error | Bad or repeated key |
UNKNOWN_WIDGET / WIDGET_SCHEMA_MISMATCH | Error | Widget problem |
MISSING_OPTIONS, INVALID_OPTION, DUPLICATE_OPTION, OPTIONS_ENUM_MISMATCH, UNKNOWN_OPTION_SET | Error | Option problem |
MISSING_COLUMNS, INVALID_COLUMN_TYPE, MISSING_ITEM_FIELDS | Error | Table/repeater problem |
CONDITION_UNKNOWN_FIELD, INVALID_CONDITION, CONDITION_ITEM_REF_OUTSIDE_REPEATER | Error | Condition problem |
INVALID_BINDING | Error | Prefill/phrase binding not allowed |
UNKNOWN_COMPUTED_FN, UNKNOWN_COMPUTED_REF, INVALID_COMPUTED_ARG, INVALID_PRECISION | Error | Computed problem |
TOO_MANY_FIELDS | Error | More than 400 fields |
NESTING_TOO_DEEP | Error | More than 3 property levels |
MISSING_EN_LABEL | Error | English label missing |
TEXT_WITHOUT_MAX_LENGTH | Warning | Free text lacks maxLength |
MISSING_HI_LABEL | Warning | Hindi label missing |
Document validation errors include REQUIRED, TYPE, ENUM, MIN, MAX, MIN_LENGTH, MAX_LENGTH, MIN_ITEMS, MAX_ITEMS, FORMAT, UNKNOWN_FIELD, widget PRECISION and DUPLICATE.
10. Versioning and publish rules
- Clone a library template. Library templates are never edited in place.
- Edit a draft using supported transformations: labels, hide/show, required, options, reorder, normal macro, smart phrases, print hints or co-signature.
- Save draft and fix checks.
- Preview with sample data.
- Publish. A new immutable version is created; existing documents keep their old version.
- Archive templates that should not be used for new documents.
Shipped pack maintainers edit packs/<pack>/src/templates.mjs, bump version, rebuild generated JSON and run pack checks.
11. Licensing notice
Do not copy proprietary assessment instruments unless the clinic holds a valid licence and the licence permits it. Sensory Profile 2, PLS and Bayley scales are not shipped. PHQ-9 and GAD-7 are included because their published notices allow reproduction, translation, display and distribution; Hindi wording is an Achal translation and not a validated translation. WHO growth reference data is a separate non-commercial data set.
12. Testing checklist
- Save a valid draft and test empty required fields.
- Test every condition branch and computed value, including missing inputs.
- Test option labels in English and Hindi.
- Test normal macro, smart phrases and
{{patient.age}}tokens. - Test print/PDF with empty fields, long text, Hindi and attachments.
- Confirm restricted templates create restricted documents where needed.
- Confirm licensed/proprietary content is not copied without permission.
13. Tenant editor workflow in detail (limited editor, R1)
Who: users with clinical.template.manage. Where: Clinical Templates (/app/clinical-templates).
-
Clone a library template (or one of your own) and give it a unique code. Library templates are never edited in place; your clone records
derivedFromso you are told when the library publishes an update. -
Edit the draft — every change is a small, checked transformation of the template JSON:
Change What is written Rule Rename label.en/label.hiof sections, fields and optionsEnglish is required (checked on save) Hide / show hidden: true+ a never-truevisibleIfon the field (a previousvisibleIfis kept and restored)The schema property stays; hidden fields are skipped by validation, so they are never required Required the parent object's requiredlistOnly for top-level fields and list-item fields; properties are never added or removed Options optionsof the field or the sharedoptionSets[...]; the schemaenumof every field using the list is kept in syncValues are typed by the schema (text or number), unique, at least one option remains Reorder order of sections, rows/fields inside a section, list-item fields Fields never move to another section (so “All normal” targets stay valid) All normal section.normal { text, textField, values }Targets must be fields of the same section; values must match the field type Smart phrases phrases[{ trigger, text, label }]Trigger 1–32 of a–z 0–9 _ -, unique, ≤ 200 phrases Print hint field.print.hideIfEmpty— Co-signature settings.requiresCosign— -
Save draft — the server validates the draft and returns the check results (errors and warnings) shown in the Checks panel. Saving uses the template's version number; if someone else saved first you are asked to reload.
-
Preview — the Preview tab renders the draft in the real document editor with sample data.
-
Publish — blocked while there are errors; creates a new immutable version. Documents keep the version they were written with.
-
Archive — retires a custom template for new documents.
The full drag-and-drop builder (new fields, rules, import/export) is planned for R5.
14. Specialty widgets reference (detailed)
Specialty widgets are field types registered by the platform (clinical-core registerSpecialtyWidgets()); the editor
renders them with keyboard-operable, labelled controls and a text alternative, and the print renders a static SVG
(no scripts) plus a findings table. Bind each widget to a top-level key (data may nest at most 3 levels).
| Widget | Stored value | Configuration on the field | |
|---|---|---|---|
tooth-chart | { numbering: FDI|UNIVERSAL, dentition: permanent|primary|mixed, teeth: [{ tooth, status, surfaces[], procedure, notes }] } — teeth always stored as FDI codes (11–48, 51–85) | — | Odontogram (5 surfaces per tooth, crossed missing / extracted teeth), legend, findings table |
body-map | { markers: [{ region, kind: pain|lesion|injury|swelling|numbness|other, severity 0–10, notes }] } — 50 named front/back regions (f_knee_r, b_lower_back…) | — | Front + back outline with numbered markers, marker list |
audiogram | { points: [{ ear R|L, conduction AC|BC, frequency 250…8000, threshold −10…120 (5 dB steps), masked, noResponse }] } — bone conduction up to 4 kHz | — | Audiogram with ASHA symbols (O △ X □ < [ > ], NR arrow), threshold table with PTA |
growth-chart | { sex, dob, measurements: [{ date, weightKg, lengthCm, headCm, position lying|standing }] } — 0–5 years | — | One chart per indicator vs WHO −3/−2/0/+2/+3 SD curves; table with z-score and percentile |
milestone-grid | [{ key, status achieved|emerging|notYet|notAssessed, notes }] (+ custom rows c_* with label, domain, band) | milestones: [{ key, domain, band, label }], optional domains, bands, allowCustom | Domain × age-band grid with ✓ ◐ ✗ symbols and counts |
rom-mmt-grid | [{ key, romLeft, romRight, mmtLeft, mmtRight, notes }] (+ custom rows c_* with label) | rows: [{ key, joint, movement, normal: { min, max } }], showRom, showMmt, allowCustom | Table with normal range; ROM < 90 % of normal flagged ↓; MMT Oxford/MRC 0–5 (±) |
scale | number | scaleType: vas|nrs|likert, min, max, step, anchors: { min, max }, options (Likert) | Value / max with a bar and anchors, or the Likert label |
sensory-quadrant | [{ key, quadrant, score }] | items: [{ key, quadrant, label }], optional quadrants, scale, bands | Quadrant bars, totals table, item responses, "not a standardized instrument" note |
photo-comparison | [{ region, before?: { attachmentId, name, mime (image/*), size, capturedOn }, after?: { … }, notes }] — up to 12 pairs; "after" cannot be dated before "before"; a photo is used once | optional maxSizeBytes (default 10 MB) | Two-cell table per pair: validated raster thumbnails from the renderer context (attachmentThumbnails) or labelled placeholders; interval in days; text table of file names / dates |
Computed functions added with the widgets:
pta(audiogram, ear, conduction = 'AC', frequencies = [500, 1000, 2000])— pure-tone average; no value when a frequency is missing or marked no-response.countWhere(list, key, value)— e.g. milestones achieved, goals met.growthZScore(growth, indicator, which = 'latest')/growthPercentile(…)—indicatoriswfa,lhfa,hcfaorbfa; WHO LMS method with the restricted application beyond ±3 SD for weight-based indicators and the ±0.7 cm lying/standing length correction.
The WHO Child Growth Standards tables are shipped as a separate data file set (not part of the engine) because WHO
publishes them under a non-commercial licence (CC BY-NC-SA 3.0 IGO). If the files are removed, growth charts still
record measurements but z-scores and curves are not shown. See backend/packages/clinical-core/assets/who/README.md.
Pack templates shipped with the platform
Platform templates live in backend/packages/clinical-core/packs/<pack>/. They are authored as code
(src/templates.mjs, one declaration per field) and generated into templates/*.json with
node backend/packages/clinical-core/packs/_tools/build.mjs; node …/_tools/check.mjs lints every template.
A test fails when the JSON is stale. Every shipped template passes the publishing linter with no errors and no
warnings (all labels in English and Hindi, every free-text field capped).