Skip to content

Form Lifecycle

State machine and transitions for form instances.

State Machine

                    ┌─────────┐
     Create form    │ pending │
     from template  └────┬────┘

                         │ Patient/specialist starts filling

                   ┌─────────────┐
                   │ in_progress │
                   └──────┬──────┘

                          │ All required fields filled

                    ┌───────────┐
                    │ completed │
                    └─────┬─────┘

                          │ Patient/specialist confirms

                     ┌────────┐
                     │ signed │  ← IMMUTABLE from this point
                     └────────┘

States

pending

Definition: Form created, no responses yet.

Characteristics:

  • Form instance exists with snapshotted fields
  • values JSONB is empty or has auto-filled values only
  • No user interaction yet

Allowed operations:

  • Read form
  • Update values (transitions to in_progress)
  • Delete form (admin only)

Transitions to:

  • in_progress — First value saved by patient/specialist

in_progress

Definition: Patient/specialist started filling, but not all required fields are complete.

Characteristics:

  • values JSONB has partial data
  • At least one field has a user-provided value
  • Some required fields may still be empty

Allowed operations:

  • Read form
  • Update values (stays in_progress or transitions to completed)
  • Upload files
  • Delete form (admin only)

Transitions to:

  • completed — All required fields filled

completed

Definition: All required fields filled. Form is ready for signing.

Characteristics:

  • values JSONB has all required fields populated
  • completed_at timestamp set
  • Form can still be edited (not yet signed)

Allowed operations:

  • Read form
  • Update values (can revert to in_progress if required fields cleared)
  • Upload/delete files
  • Sign form (transitions to signed)
  • Delete form (admin only)

Transitions to:

  • signed — Patient/specialist confirms via /v1/forms/{id}/sign
  • in_progress — If required field is cleared

signed

Definition: Patient/specialist confirmed. Form is now IMMUTABLE.

Characteristics:

  • signed_at timestamp set
  • values frozen (cannot be modified)
  • fields frozen (cannot be modified)
  • files frozen (cannot be added/removed)
  • Audit log records signing event

Allowed operations:

  • Read form
  • NO modifications allowed (all update/delete operations return 409 Conflict)

Immutability rules:

  • values cannot be updated (409 Conflict)
  • fields cannot be modified
  • files cannot be changed
  • signed_at timestamp is the legal proof
  • Audit log records who signed and when

Why immutable? HIPAA requires that signed medical forms preserve their structure and content exactly as they were when completed. Template changes or value edits cannot retroactively affect signed forms.


End-to-End Flow

Creating a Form

1. Appointment created from template
   └─ Template has associated form templates

2. For each form template:
   a. Read form_template (fields, title, description, version)
   b. SNAPSHOT: Copy fields into form.fields (frozen from this moment)
   c. Record form.template_version = form_template.version
   d. AUTO-FILL: Read custom_field_values for fields that reference custom_field_id
      → SELECT cfv.custom_field_id, cfv.value
        FROM custom_field_values cfv
        WHERE cfv.entity_type = 'patient'
          AND cfv.entity_id = ?
          AND cf.key IN ('city', 'blood_type', ...)
   e. Build initial values: {"q1": "Amsterdam", "q2": "A+"}
   f. Status = "pending"

Loading a Form

1. SELECT * FROM forms WHERE id = ?
   → ONE query, ONE row

2. Return to frontend:
   {
     "id": 42,
     "title": "Patient Intake",
     "status": "pending",
     "fields": [...],          ← field definitions (what to render)
     "values": {...},          ← current answers
     "files": {...}            ← file attachments
   }

Saving a Form

1. Validate: form.status must NOT be "signed" (immutable after signing)
2. Validate input against form.fields (required, types, options)
3. Read existing values
4. Merge new values from request
5. Store merged values → update form.values (plain JSONB, fully queryable)
6. PROFILE SYNC: For fields that reference custom_field_id, upsert custom_field_values
   → INSERT INTO custom_field_values (custom_field_id, entity_type, entity_id, value)
     ON CONFLICT (custom_field_id, entity_type, entity_id) DO UPDATE SET value = EXCLUDED.value
7. SEGMENT EVAL: Evaluate segments that reference this form template
   → For each matching segment, add/remove patient from segment_members
8. Update status:
   - If partially filled → "in_progress"
   - If all required fields filled → "completed", set completed_at
9. File uploads: upload to S3, store reference in files JSONB

Signing a Form

1. Validate: form.status must be "completed" (all required fields filled)
2. Set status = "signed", signed_at = NOW()
3. FROM THIS POINT: form.values and form.fields are IMMUTABLE
4. Any attempt to update a signed form returns 409 Conflict
5. Audit log records the signing event

State Transition Rules

FromToTriggerValidation
pendingin_progressFirst value saved-
in_progresscompletedAll required fields filledAll required fields must have values
completedsignedExplicit /sign callForm status must be completed
completedin_progressRequired field cleared-
signed-NONE (immutable)-

Business Rules

Multi-User Filling

Forms can be filled by both patients and specialists:

  • Patient fills their fields before/during appointment
  • Specialist fills specialist-specific fields (often private: true) during appointment
  • Both write to the same form.values JSONB
  • Last write wins (standard JSONB merge)
  • form.updated_at tracks when form was last modified
  • Audit log records each save with the user_id who made it

Conflict Prevention

  • Specialist and patient rarely edit simultaneously (appointment workflow is sequential)
  • If they do: last write wins
  • Each save is a full audit event: the audit log captures who saved what when

Field-Level Authorship

Not tracked in v1. If tracking which user filled which field becomes a requirement, add a field_authors JSONB column:

json
{
  "pain_level": {
    "user_id": 100,
    "role": "patient",
    "at": "2025-01-15T10:00:00Z"
  },
  "clinical_notes": {
    "user_id": 5,
    "role": "specialist",
    "at": "2025-01-15T10:30:00Z"
  }
}

This is deferred to v2 because it adds complexity without a current business need.


Audit Trail

Every form mutation is logged to audit_log:

EventActionLogged Data
Form createdCREATEentity_type=form, entity_id=form.id, changes={...}
Values savedUPDATEentity_type=form, entity_id=form.id, changes={before, after}
File uploadedUPDATEentity_type=form, entity_id=form.id, changes={files}
Form signedUPDATEentity_type=form, entity_id=form.id, changes={status: signed}
Form deletedDELETEentity_type=form, entity_id=form.id

HIPAA compliance: Audit log captures:

  • Who made the change (user_id)
  • When (created_at)
  • What changed (changes JSONB with before/after diff)
  • IP address and user agent
  • Request path (e.g., PUT /v1/forms/42)

Error Handling

Updating a Signed Form

http
PUT /v1/forms/42
{"values": {"city": "Rotterdam"}}

409 Conflict
{
  "error": {
    "code": "form_already_signed",
    "message": "Cannot update a signed form",
    "details": {
      "form_id": 42,
      "signed_at": "2025-01-15T12:00:00Z"
    }
  }
}

Signing an Incomplete Form

http
POST /v1/forms/42/sign

→ 400 Bad Request
{
  "error": {
    "code": "form_not_completed",
    "message": "Form must be completed before signing",
    "details": {
      "missing_fields": ["pain_level", "consent_signature"]
    }
  }
}

Validation Error on Save

http
PUT /v1/forms/42
{"values": {"pain_level": "Invalid"}}

400 Bad Request
{
  "error": {
    "code": "validation_error",
    "message": "Form validation failed",
    "details": {
      "errors": [
        {
          "field": "pain_level",
          "message": "value \"Invalid\" not in allowed options"
        }
      ]
    }
  }
}