Segments
Auto-updating patient groups based on rules—find "high pain patients" or "inactive 6+ months" without manual lists.
What this enables
Smart patient cohorts: Group patients by conditions—"Pain level = high", "From Bucharest", "Hasn't booked in 6 months"—without manually dragging names into folders.
Auto-updates: As patients fill forms or get appointments, segments refresh automatically. A patient's pain level goes from "low" to "high"? They move to the high-pain segment instantly.
Targeting campaigns: Send emails to "patients with active pain" or "inactive patients due for checkup" automatically.
Multi-rule cohorts: Combine conditions—"Age 65+, from Bucharest, AND less than 2 appointments this year" → complex cohorts without code.
Audit trail: Track when segments change, when rules are updated, keep historical records.
How it works
- Admin creates a segment: "High Pain Patients"
- Admin defines rules:
- Form response: pain_level = "Big pain"
- AND custom field: age >= 65
- AND appointment metric: < 2 appointments in last year
- System evaluates: Automatically finds all patients matching all rules
- Patient updates: Patient fills form answering "Big pain" → automatically added to segment
- Patient changes: Pain level updates to "Low" → automatically removed from segment
- Use the segment: Send campaign to all patients in segment, run reports, bulk actions
Technical Reference
Overview
Segments are rules-based patient grouping that automatically evaluates membership based on form responses, profile data, and appointment history. They enable dynamic patient cohorts that stay up-to-date as data changes.
Core Concepts
Segment: A named group of patients defined by rules rather than manual assignment. Rules are evaluated automatically when data changes.
Rule: A condition that a patient must meet to belong to a segment. Rules can query:
- Form responses (values from form instances)
- Profile data (custom_field_values for patient entity)
- Appointment metrics (count, dates, status)
Match Mode: How multiple rules combine:
all(AND) — patient must match ALL rulesany(OR) — patient must match ANY rule
Auto-Evaluation: Segment membership is updated automatically:
- When forms are saved (real-time or async)
- When profile data is updated
- On-demand via API (manual refresh)
Use Cases
| Segment | Rules |
|---|---|
| High Pain Patients | Form response: pain_level = "Big pain" |
| Bucharest Patients | Profile: city = "Bucharest" |
| At-Risk Seniors | Profile: age >= 65 AND Appointments: count < 2 in last 6 months |
| Active Patients | Appointments: last_date >= now-30d |
| Complex Cohorts | Multi-source rules with nested groups (see operators.md) |
Key Features
Real-Time Membership Updates
When a patient fills a form or updates their profile, segments referencing that data are automatically re-evaluated. No manual refresh needed for most use cases.
Multi-Source Rules
A single segment can combine:
- Form responses: "answered X in survey Y"
- Profile data: "blood type A+, from Bucharest"
- Appointment history: "had 2+ appointments in last year"
Tiered Evaluation Strategy
- Tier 1 (Inline): Simple segments (≤3 rules, single source) evaluate synchronously during form save
- Tier 2 (Async): Complex segments (multi-source, >3 rules) evaluate via background worker
- Tier 3 (Batch): Full rebuild on-demand via API
See performance.md for detailed evaluation strategy.
Queryable History
Segment membership changes are tracked via matched_at timestamp. Rule changes are versioned for audit and rollback.
Architecture
┌──────────────┐
│ Segments │ Rules definition (JSONB)
└──────┬───────┘
│
│ Evaluates against
▼
┌─────────────────────────────────┐
│ Patient Data (3 sources) │
│ 1. forms.values (JSONB) │
│ 2. custom_field_values (TEXT) │
│ 3. appointments (aggregates) │
└─────────────────────────────────┘
│
│ Membership stored in
▼
┌──────────────────┐
│ segment_members │ patient_id + segment_id + matched_at
└──────────────────┘Why This Design
| Problem | Solution |
|---|---|
| Manual patient groups don't scale | Rules auto-evaluate on data changes |
| Can't filter by form responses | Forms store values as queryable JSONB |
| Need complex multi-condition filtering | Support nested rule groups with AND/OR logic |
| Real-time updates too expensive at scale | Tiered evaluation: inline for simple, async for complex |
| Historical forms skew current state | Multi-instance resolution: latest completed form wins |
Quick Start
Create a Simple Segment
POST /v1/segments
{
"name": "High Pain Patients",
"description": "Patients reporting big pain in intake survey",
"match_mode": "all",
"rules": [
{
"source": "form",
"template_id": 5,
"custom_field_id": 11,
"op": "eq",
"value": "Big pain"
}
]
}Create a Multi-Source Segment
POST /v1/segments
{
"name": "Bucharest High Pain Seniors",
"match_mode": "all",
"rules": [
{"source": "profile", "custom_field_id": 10, "op": "eq", "value": "Bucharest"},
{"source": "profile", "custom_field_id": 12, "op": "gte", "value": 50},
{"source": "form", "template_id": 5, "custom_field_id": 15, "op": "eq", "value": "Big pain"},
{"source": "appointments", "metric": "count", "op": "gte", "value": 2, "filters": {"status": "done"}}
]
}Note: Rules reference custom_field_id (from the field library) rather than arbitrary keys. This ensures:
- Type-safe queries (field type is known)
- Consistent lookups across forms and profiles
- Compatibility with versioned field definitions
Query Segment Members
GET /v1/segments/{id}/members
GET /v1/segments/{id}/members?fresh=true # Force re-evaluation first
GET /v1/patients/{id}/segments # Which segments does this patient belong to?Manual Rebuild
POST /v1/segments/{id}/evaluate # Full rebuild from scratchDocumentation Structure
- README.md (this file) — Overview and concepts
- schema.sql — Tables, indexes, RLS policies
- api.md — HTTP endpoints and request/response formats
- rule-engine.md — Rule evaluation logic and multi-instance resolution
- performance.md — Tiered evaluation, scaling, benchmarks
- operators.md — All supported operators and rule sources
- examples.md — Complex query examples and real-world use cases
Related Features
- Forms (../forms/) — Form instances provide response data (forms.values JSONB)
- Custom Fields (../custom-fields/) — Versioned field library + profile data (custom_field_values)
- Appointments (../appointments/) — Appointment history and metrics
Data Flow:
- Patient fills form → data saved to
forms.valuesAND synced tocustom_field_values - Segments query BOTH sources:
source: "form"→ queriesforms.values(historical/all responses)source: "profile"→ queriescustom_field_values(current patient state)
- Both sources reference
custom_field_idfor type-safe, consistent queries
Constraints
- Max nesting depth: 3 levels for nested rule groups (prevents runaway query complexity)
- Relative dates: Supported via
now-30d,now-6M,now-1ysyntax (see operators.md) - Form instance resolution: Latest completed/signed form wins when multiple instances exist
- Evaluation SLOs: <50ms inline, <5s async, <30s full rebuild (10K patients)
Migration Notes
From legacy system: No direct equivalent exists. Segments replace manual filtering and ad-hoc SQL queries for patient cohorts. Existing workflows that use "find patients where X" should be migrated to segments for automatic updates.
Data requirements:
- Forms must store values as plaintext JSONB (not encrypted) for queryability
- Custom field values must be TEXT (not BYTEA) for profile queries
- Appointments table must support status and date filtering
Version History
Segment rules are versioned. When rules change, the current version is archived to segment_versions table. This enables:
- Audit trail of rule changes
- Rollback to previous rule definitions
- Understanding why membership changed over time
See schema.sql for version table structure.