Form Auto-Fill & Profile Sync
Forms automatically pre-populate fields from patient profile data. When a patient fills a form, values sync back to the appropriate profile store for future auto-fill.
Two types of profile-linked fields
A form template field can be linked to a profile store in one of two ways:
| Mechanism | Links to | Auto-fill source | Profile sync target |
|---|---|---|---|
profile_field_key | A patient_persons column | patient_persons (cross-org) | patient_persons |
custom_field_id | An org-scoped custom field | custom_field_values (org-scoped) | custom_field_values |
A field uses one or the other, never both. A field with neither is a one-off field — its value lives only in forms.values and never syncs anywhere.
profile_field_key: portable profile fields
Use profile_field_key for fields that represent universal facts about the patient — data that is the same regardless of which clinic they're at.
Valid values:
| profile_field_key | Patient persons column | Type |
|---|---|---|
date_of_birth | patient_persons.date_of_birth | date |
sex | patient_persons.sex | text |
occupation | patient_persons.occupation | text |
residence | patient_persons.residence | text |
blood_type | patient_persons.blood_type | text |
allergies | patient_persons.allergies | text[] |
chronic_conditions | patient_persons.chronic_conditions | text[] |
emergency_contact_name | patient_persons.emergency_contact_name | text |
insurance_entries | patient_persons.insurance_entries | jsonb |
Example form field (snapshotted in forms.fields):
{
"key": "dob",
"label": "Date of Birth",
"field_type": "date",
"profile_field_key": "date_of_birth"
}Auto-fill on form creation: reads patient_persons.date_of_birth for the patient.
Profile sync on form submit: writes the new value back to patient_persons.date_of_birth.
Cross-org effect: because patient_persons has no organization_id, a patient who updates their date of birth at Clinic A will see the updated value pre-filled at Clinic B on their next form.
custom_field_id: org-specific fields
Use custom_field_id for fields that are specific to the org's workflow — referral source, VIP status, training preference, billing notes, etc.
Example form field:
{
"key": "referral",
"label": "Referral Source",
"field_type": "select",
"custom_field_id": 42,
"version": 1,
"options": ["Physiotherapist", "GP", "Online", "Word of mouth"]
}Auto-fill on form creation: reads custom_field_values for this patient in this org.
Profile sync on form submit: upserts custom_field_values for this patient, org, and field.
Org-scoped: a value filled at Clinic A does not pre-fill the same field at Clinic B.
One-off fields (no sync)
For appointment-specific data that should not sync to any profile:
{
"key": "chief_complaint_today",
"label": "What brings you in today?",
"field_type": "textarea"
}Value lives in forms.values only. No auto-fill. No profile sync.
Auto-fill flow (form creation)
func createFormInstance(templateID, appointmentID) {
template := getTemplate(templateID)
appointment := getAppointment(appointmentID)
person := getPatientPerson(appointment.PatientPersonID)
fields := []FormField{}
values := map[string]any{}
for _, f := range template.Fields {
switch {
case f.ProfileFieldKey != nil:
// Snapshot the field definition
fields = append(fields, snapshotProfileField(f))
// Auto-fill from patient_persons column
if value := getProfileFieldValue(person, *f.ProfileFieldKey); value != nil {
values[f.Key] = value
}
case f.CustomFieldID != nil:
// Snapshot the custom field version
customField := getCustomField(*f.CustomFieldID)
fields = append(fields, snapshotCustomField(f, customField))
// Auto-fill from custom_field_values (org-scoped)
entityID := resolveEntityID(customField.EntityType, appointment)
if value := getCustomFieldValue(customField.ID, customField.EntityType, entityID); value != nil {
values[f.Key] = value
}
default:
// One-off field — no auto-fill
fields = append(fields, snapshotOneOffField(f))
}
}
return createForm(appointment, template, fields, values)
}Profile sync flow (form submit)
func saveForm(formID int64, submittedValues map[string]any) {
form := getForm(formID)
appointment := getAppointment(form.AppointmentID)
// 1. Save all values to forms.values
form.Values = mergeValues(form.Values, submittedValues)
updateForm(form)
// 2. Sync profile-linked fields
for _, field := range form.Fields {
value, submitted := submittedValues[field.Key]
if !submitted {
continue // Not modified in this save — don't overwrite
}
switch {
case field.ProfileFieldKey != nil:
// Write to patient_persons (cross-org portable profile)
updatePatientPersonField(appointment.PatientPersonID, *field.ProfileFieldKey, value)
case field.CustomFieldID != nil:
// Write to custom_field_values (org-scoped)
entityID := resolveEntityID(field.EntityType, appointment)
upsertCustomFieldValue(form.OrganizationID, *field.CustomFieldID, field.EntityType, entityID, value)
}
}
}Behaviour: last write wins
Profile values represent the current known state of the patient. If a patient fills the same field across multiple forms, the most recent submission wins:
Form A (Clinic A): occupation = "Engineer" → patient_persons.occupation = "Engineer"
Form B (Clinic B): occupation = "Architect" → patient_persons.occupation = "Architect"
Old Form A still shows "Engineer" (historical snapshot, immutable)
New forms at any clinic pre-fill "Architect"Entity type resolution for custom_field_id
Custom fields apply to different entity types. The form resolves the correct entity ID based on the field's entity_type:
func resolveEntityID(entityType string, appointment *Appointment) int64 {
switch entityType {
case "patient":
return appointment.PatientID // patients.id (org-patient link)
case "specialist":
return appointment.SpecialistID
case "appointment":
return appointment.ID
case "organization":
return appointment.OrganizationID
}
}Cross-org auto-fill comparison
| Field type | Patient books at Clinic A, then Clinic B |
|---|---|
profile_field_key: "date_of_birth" | Clinic B sees the DOB filled at Clinic A — pre-filled immediately |
profile_field_key: "blood_type" | Clinic B sees the blood type from Clinic A — pre-filled immediately |
custom_field_id (referral source) | Clinic B does NOT see Clinic A's referral source — starts empty |
Related documentation
- Patient Profile → — Portable profile model and data ownership
- Entity Profiles → — How profile reads and writes work
- Custom Fields → — Org-specific field library
- Form Lifecycle → — pending → signed state machine