Skip to content

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:

MechanismLinks toAuto-fill sourceProfile sync target
profile_field_keyA patient_persons columnpatient_persons (cross-org)patient_persons
custom_field_idAn org-scoped custom fieldcustom_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_keyPatient persons columnType
date_of_birthpatient_persons.date_of_birthdate
sexpatient_persons.sextext
occupationpatient_persons.occupationtext
residencepatient_persons.residencetext
blood_typepatient_persons.blood_typetext
allergiespatient_persons.allergiestext[]
chronic_conditionspatient_persons.chronic_conditionstext[]
emergency_contact_namepatient_persons.emergency_contact_nametext
insurance_entriespatient_persons.insurance_entriesjsonb

Example form field (snapshotted in forms.fields):

json
{
  "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:

json
{
  "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:

json
{
  "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)

go
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)

go
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:

go
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 typePatient 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