Skip to content

Entity Types in Forms

Overview

Forms can reference custom fields from any entity type (patient, specialist, appointment, organization). The auto-fill and profile sync logic automatically resolves which entity to use based on the custom field's entity_type and the form's appointment context.

NOT BUILT — reconciled 2026-08-02

No forms / form_templates / custom_fields table exists, no Go domain, no routes, and zero forms.* permission rows in any migration. Ships in migrations 000042 + 000043. See index.md for the full correction list — the load-bearing ones are: consents, not user_consents; no users table / no user_id (actors are created_by_principal_id / signed_by_principal_id, the subject is patient_profile_id); UUID PKs, not integers; custom-field keys unique per org, never globally; and the instance carries a fields JSONB snapshot + template_version frozen at creation, which is the single most important structural fix in the port.

Note that specialist and appointment entity types FK tables that do not exist yetspecialists lands in 000040 and appointments in 000046. Custom-field uniqueness is UNIQUE (organization_id, entity_type, key), never global.

How It Works

Form Instance Context

Every form instance has access to all relevant entity IDs:

json
{
  "id": 1024,
  "appointment_id": 500,
  // Derived from appointment:
  "patient_id": 123,        // via appointment.patient_id
  "specialist_id": 45,      // via appointment.specialist_id
  "organization_id": 1      // via RLS context
}

Entity Type Resolution

When auto-filling or syncing, the system uses the custom field's entity_type to determine which entity ID to use:

Custom Field entity_typeEntity ID UsedSource
patientappointment.patient_idPatient profile
specialistappointment.specialist_idSpecialist profile
appointmentappointment.idAppointment metadata
organizationcurrent_org_idOrganization settings

Use Cases by Entity Type

Patient Custom Fields (Most Common)

Purpose: Patient demographics, medical history, preferences

Auto-fill behavior: Reads from patient's profile (shared across all forms)

Profile sync: Updates patient's profile (affects future forms)

Examples:

json
{
  "custom_field_id": 10,  // city (entity_type: patient)
  "sort_order": 1
}

Use cases:

  • Demographics: city, age, birthdate, phone, address
  • Medical history: allergies, blood_type, chronic_conditions
  • Preferences: preferred_language, communication_channel
  • Emergency contact: emergency_contact_name, emergency_contact_phone

Behavior:

  1. Patient fills "city" in Intake Form → "Amsterdam" saved to patient profile
  2. Patient fills Follow-up Form → "city" auto-fills "Amsterdam"
  3. Patient changes to "Rotterdam" → patient profile updated
  4. Next form auto-fills "Rotterdam"

Appointment Custom Fields (Appointment-Specific)

Purpose: Appointment-specific metadata, session notes, internal tracking

Auto-fill behavior: Reads from current appointment's metadata (not from previous appointments)

Profile sync: Saves to current appointment only (doesn't affect other appointments)

Examples:

json
{
  "custom_field_id": 25,  // internal_priority (entity_type: appointment)
  "sort_order": 2
}

Use cases:

  • Metadata: internal_priority, billing_code, referring_doctor, campaign_source
  • Session notes: chief_complaint_today, session_summary, follow_up_needed
  • Internal tracking: no_show_risk, payment_status, requires_translator

Behavior:

  1. Specialist fills "internal_priority" in Appointment Form → "high" saved to appointment #500
  2. Patient books new appointment #501 → "internal_priority" starts empty (new appointment)
  3. Each appointment has its own independent metadata

Note: Appointment-level fields are typically filled during or before the appointment, not auto-filled from previous appointments (since each appointment is unique).


Specialist Custom Fields (Specialist Profile)

Purpose: Specialist credentials, settings, biographical info

Auto-fill behavior: Reads from specialist's profile

Profile sync: Updates specialist's profile

Examples:

json
{
  "custom_field_id": 30,  // license_number (entity_type: specialist)
  "sort_order": 3
}

Use cases:

  • Credentials: license_number, medical_degree, certifications, specialty
  • Settings: consultation_fee, languages_spoken, years_of_experience
  • Bio: education, publications, areas_of_expertise

Behavior:

  • Rarely used in patient-facing forms
  • More common in specialist profile management or admin forms
  • Auto-fills from specialist assigned to the appointment

Example scenario:

  • Form asks specialist to confirm their license number
  • Auto-fills from specialist profile
  • If updated, specialist profile is updated

Organization Custom Fields (Org Settings)

Purpose: Organization-wide configuration, branding, defaults

Auto-fill behavior: Reads from organization settings

Profile sync: Updates organization settings

Examples:

json
{
  "custom_field_id": 40,  // default_language (entity_type: organization)
  "sort_order": 4
}

Use cases:

  • Settings: timezone, default_language, currency, date_format
  • Branding: logo_url, primary_color, terms_of_service_url
  • Defaults: default_appointment_duration, default_consultation_fee

Behavior:

  • Very rare in forms (usually managed via org settings UI)
  • If used, auto-fills from current organization
  • All users in the org see the same values

Mixed Entity Types in One Form

Forms can reference custom fields from multiple entity types in the same form:

Example: Pre-Appointment Questionnaire

json
{
  "title": "Pre-Appointment Questionnaire",
  "fields": [
    {
      "custom_field_id": 10,  // city (patient)
      "label": "City",
      "sort_order": 1
    },
    {
      "custom_field_id": 11,  // blood_type (patient)
      "label": "Blood Type",
      "sort_order": 2
    },
    {
      "custom_field_id": 25,  // internal_priority (appointment)
      "label": "Priority",
      "sort_order": 3
    },
    {
      "custom_field_id": 30,  // specialist_license (specialist)
      "label": "Provider License",
      "sort_order": 4
    }
  ]
}

When form instance is created for appointment #500:

FieldCustom FieldEntity TypeAuto-Fill SourceValue
city#10patientPatient #123 profile"Amsterdam"
blood_type#11patientPatient #123 profile"A+"
internal_priority#25appointmentAppointment #500 metadata(empty - new appointment)
specialist_license#30specialistSpecialist #45 profile"MD-12345"

When form is saved:

  • "city" and "blood_type" → Update patient #123 profile
  • "internal_priority" → Update appointment #500 metadata
  • "specialist_license" → Update specialist #45 profile

Auto-Fill Algorithm

Pseudocode:

javascript
function createFormInstance(template, appointment) {
  const form = {
    appointment_id: appointment.id,
    fields: [],
    values: {}
  }

  for (const templateField of template.fields) {
    if (templateField.custom_field_id !== null) {
      // Snapshot custom field version
      const customField = getCustomField(templateField.custom_field_id)
      const fieldSnapshot = snapshotCustomFieldVersion(customField)
      form.fields.push(fieldSnapshot)

      // Determine entity_id based on entity_type
      const entityId = resolveEntityId(customField.entity_type, appointment)

      // Auto-fill from custom_field_values (if exists)
      const existingValue = db.query(`
        SELECT value FROM custom_field_values
        WHERE custom_field_id = ? AND entity_type = ? AND entity_id = ?
      `, [customField.id, customField.entity_type, entityId])

      if (existingValue) {
        form.values[`field_${customField.id}`] = existingValue
      }
    }
  }

  return form
}

function resolveEntityId(entityType, appointment) {
  switch (entityType) {
    case 'patient':
      return appointment.patient_id
    case 'specialist':
      return appointment.specialist_id
    case 'appointment':
      return appointment.id
    case 'organization':
      return appointment.organization_id
    default:
      throw new Error(`Unknown entity_type: ${entityType}`)
  }
}

Profile Sync Algorithm

Pseudocode:

javascript
function saveFormInstance(form, values) {
  // Update form.values JSONB
  form.values = merge(form.values, values)

  // Sync to custom_field_values
  for (const field of form.fields) {
    if (field.custom_field_id !== null) {
      const fieldKey = `field_${field.custom_field_id}`
      const value = values[fieldKey]

      if (value !== undefined) {
        // Determine entity_id based on entity_type
        const entityId = resolveEntityId(field.entity_type, form.appointment)

        // Upsert to custom_field_values
        db.upsert('custom_field_values', {
          custom_field_id: field.custom_field_id,
          entity_type: field.entity_type,
          entity_id: entityId,
          value: value
        })
      }
    }
  }
}

Common Patterns

Pattern 1: Patient Demographics Form

All patient fields - auto-fill from patient profile, sync back to patient profile

json
{
  "title": "Patient Registration",
  "fields": [
    {"custom_field_id": 10, "sort_order": 1},  // city (patient)
    {"custom_field_id": 11, "sort_order": 2},  // blood_type (patient)
    {"custom_field_id": 12, "sort_order": 3}   // birthdate (patient)
  ]
}

Pattern 2: Session Notes Form

All appointment fields - appointment-specific, no auto-fill from previous appointments

json
{
  "title": "Session Notes",
  "fields": [
    {"custom_field_id": 25, "sort_order": 1},  // chief_complaint (appointment)
    {"custom_field_id": 26, "sort_order": 2},  // session_summary (appointment)
    {"custom_field_id": 27, "sort_order": 3}   // follow_up_needed (appointment)
  ]
}

Pattern 3: Mixed Form

Patient + appointment fields - patient data auto-fills, appointment data starts fresh

json
{
  "title": "Appointment Intake",
  "fields": [
    {"custom_field_id": 10, "sort_order": 1},  // city (patient) - AUTO-FILLS
    {"custom_field_id": 25, "sort_order": 2},  // chief_complaint_today (appointment) - EMPTY
    {"custom_field_id": 11, "sort_order": 3}   // blood_type (patient) - AUTO-FILLS
  ]
}

Edge Cases

Q: What if a patient field is used in a specialist-facing form?

A: It still auto-fills from the patient profile (entity_type determines behavior, not who fills the form).

Example:

  • Specialist fills a form with patient field "blood_type"
  • Auto-fills from patient #123 profile
  • If specialist updates it, patient profile is updated

Q: What if an appointment field is accidentally used across multiple forms in the same appointment?

A: They all read/write to the same appointment metadata.

Example:

  • Form A has appointment field "internal_priority"
  • Form B also has appointment field "internal_priority"
  • Both forms share the same value (from appointment #500 metadata)
  • Updating in one form updates the other

Q: Can a form reference custom fields from multiple patients?

A: No. A form instance is always attached to one appointment, which has one patient.

The patient_id is derived from appointment.patient_id, so all patient fields in that form instance reference the same patient.

Q: What about family/group appointments?

A: Out of scope for v1. Would require:

  • Appointments with multiple appointment_patients (junction table)
  • Forms that specify which patient the field belongs to
  • More complex auto-fill logic

Validation

When creating a form template, the API should validate:

  1. Custom field exists: custom_field_id references a valid published custom field
  2. Organization match: Custom field belongs to the same organization
  3. Entity type compatibility: Warn if entity_type is unusual for the form type
    • Patient-facing forms with specialist fields: ⚠️ Warning
    • Appointment forms with organization fields: ⚠️ Warning