62 KiB
Intent-Driven Architecture for Healthcare Applications
Type: Architecture Reference Document Version: 1.0 Date: 2026-02-14 Audience: LLM agents, developers, architects building on this system Language: English (code and architecture), Dutch (domain examples)
How to Read This Document
This document describes an intent-driven architecture for healthcare applications. It is written to be consumed by both humans and LLMs. Each section follows a consistent pattern: what the component does, why it exists, how it works, and what trade-offs were made. Code examples illustrate concepts — they are not implementation specifications.
The architecture is domain-agnostic in its core (any application that translates natural language into structured actions) but makes specific decisions for healthcare contexts where safety, auditability, and clinical relevance are non-negotiable.
1. Problem Statement
1.1 The Core Tension
Healthcare workers think and communicate in unstructured language: "Jan refused his medication this morning." Healthcare systems require structured input: selecting the right form, the right category, the right patient, at the right time.
The gap between these two modes is where productivity is lost. Every form-fill, every menu navigation, every context switch from clinical thinking to system operation is friction that takes time away from patient care.
1.2 What This Architecture Solves
This architecture provides a pattern for building a command interface that sits between natural language input and structured system actions. It is not a chatbot (conversation is not the goal), not a search engine (retrieval is one action among many), and not an autonomous agent (the human always decides).
It does three things:
- Understand — Classify what the user wants (intent) and extract the relevant parameters (entities)
- Prepare — Open the right form with the right data pre-filled, ready for human confirmation
- Suggest — After an action completes, propose clinically relevant follow-ups based on protocols
1.3 Design Constraints from Healthcare
These constraints are non-negotiable and shape every architectural decision:
| Constraint | Implication |
|---|---|
| No autonomous writes | The system never writes to the patient record without explicit human confirmation |
| Auditability | Every classification, every action, every suggestion must be traceable |
| Graceful degradation | If AI is unavailable, the system must still function (reduced capability, not failure) |
| Speed for routine tasks | 70%+ of interactions are routine and must resolve in milliseconds, not seconds |
| Domain vocabulary is stable | Medical terms, shift patterns, report categories change slowly — this favors rule-based approaches |
| Privacy by design | Patient data in AI prompts must be minimized; logs must be sanitized |
2. Architecture Overview
2.1 The Five Building Blocks
The system consists of five components, each with a single responsibility. They are described in the order data flows through them.
User Input (text or speech)
│
▼
┌─────────────────────────┐
│ 1. INTENT REGISTRY │ Declarative definitions — the single source of truth
│ (static config) │ for all intents, patterns, entities, and metadata
└────────────┬────────────┘
│ provides definitions to ↓
┌─────────────────────────┐
│ 2. CLASSIFICATION │ Two-phase pipeline: fast local matching (Reflex)
│ PIPELINE │ with LLM fallback (Orchestrator) for complex input
└────────────┬────────────┘
│ produces classified intent + raw entities ↓
┌─────────────────────────┐
│ 3. ENTITY RESOLUTION │ Grounds raw entities against real data:
│ │ "Jan" → Patient #427, "morgen" → 2026-02-15
└────────────┬────────────┘
│ produces resolved intent ready for action ↓
┌─────────────────────────┐
│ 4. ACTION SYSTEM │ Maps intent → UI artifact (form, view, navigation)
│ │ Human confirms → system executes → result
└────────────┬────────────┘
│ after successful action ↓
┌─────────────────────────┐
│ 5. PROTOCOL ENGINE │ Evaluates clinical rules against completed action
│ (Nudge) │ Suggests follow-ups with protocol references
└─────────────────────────┘
2.2 Why This Order Matters
The pipeline is strictly sequential for a reason. Each step produces a more refined representation:
- Raw input → (Classification) → Intent + raw entities → (Resolution) → Grounded intent → (Action) → Prepared UI → (Human confirms) → Executed action → (Protocol) → Suggested follow-up
No step can be skipped. Classification without resolution produces ungrounded entities ("Jan" without knowing which Jan). Resolution without classification has nothing to resolve. Actions without human confirmation violate healthcare safety requirements. Protocol evaluation without completed actions has no trigger.
2.3 What Is NOT in This Architecture
Decisions about what to exclude are as important as what to include:
| Excluded | Reason |
|---|---|
| Custom ML model for classification | Regex + LLM fallback outperforms custom models for <50 intents in a stable domain vocabulary. No training data maintenance overhead. |
| Event bus / pub-sub | Overkill for single-tenant applications. Direct function calls and store subscriptions are simpler and equally effective. |
| Autonomous agent execution | Healthcare requires human-in-the-loop for all record modifications. The system prepares; the human decides. |
| Ambient capture | Passive listening during patient conversations is a fundamentally different input model. This architecture handles explicit commands (typed or spoken). Ambient capture is a future extension, not a foundation change. |
| FHIR server | A FHIR-inspired data model is sufficient. Building a full FHIR-compliant server is a separate project that doesn't affect the intent architecture. |
| Knowledge graph for entity resolution | Simple database lookups (fuzzy name search, date parsing) are sufficient for <1000 patients. Knowledge graphs add value at scale but premature complexity at MVP. |
3. Building Block 1: Intent Registry
3.1 Purpose
The Intent Registry is the single source of truth for all intent definitions. Every other component in the system reads from the registry — nothing defines intent behavior independently.
3.2 Why This Matters
Without a registry, intent definitions drift apart. In the current prototype, an intent is defined in four separate locations: the TypeScript type union, the Reflex pattern map, the Orchestrator system prompt, and the Chat API system prompt. Adding a new intent requires synchronized changes to all four. This is the primary source of architectural debt.
The registry eliminates this by centralizing the definition and generating derived artifacts (pattern maps, prompts, validation schemas) from a single declaration.
3.3 Anatomy of an Intent Definition
interface IntentDefinition {
// === Identity ===
id: string; // Unique identifier, e.g. "dagnotitie"
label: string; // Human-readable name (Dutch), e.g. "Dagnotitie"
description: string; // What this intent does (used in AI prompts)
priority: 'P1' | 'P2' | 'P3'; // Classification priority tier
// === Local Classification (Reflex) ===
reflexPatterns: Array<{
pattern: RegExp;
weight: number; // 0.5 (weak signal) to 1.0 (exact match)
}>;
// === Entity Schema ===
entities: {
required: string[]; // Must be present for action execution
optional: string[]; // Enriches the action if present
extractionRules: Array<{ // Regex with named capture groups
pattern: RegExp;
mapping: Record<string, string>; // capture group → entity field
}>;
};
// === AI Classification ===
aiExamples: Array<{
input: string; // Example user input
expectedEntities: Record<string, string>;
}>;
// === Action Binding ===
artifactType: string | null; // Which UI artifact to open (null = navigation only)
requiresConfirmation: boolean; // Must user confirm before execution?
// === Access Control (prepared, not enforced in MVP) ===
allowedRoles?: string[]; // Empty = everyone
// === Feature Management ===
featureFlag?: string; // Optional flag to enable/disable
enabled: boolean; // Master switch
}
3.4 What the Registry Generates
Other components do not access the registry directly for their core logic. Instead, the registry exposes generator functions:
| Function | Consumer | What it produces |
|---|---|---|
getReflexPatterns() |
Classification Pipeline (Reflex phase) | Map of intent → pattern/weight pairs |
getExtractionRules() |
Entity Resolution | Map of intent → regex extraction rules |
buildClassificationPrompt() |
Classification Pipeline (Orchestrator phase) | System prompt section listing all intents with descriptions and examples |
buildChatPrompt() |
Chat API | System prompt section for conversational context |
getEntitySchema(intentId) |
Action System | Zod validation schema for entity validation |
getArtifactMapping() |
Action System | Map of intent → artifact type |
3.5 Trade-Off: Declarative vs. Distributed
Alternative considered: Each intent as a self-contained module (file per intent, co-located patterns + prompt + component). This is the "feature folder" pattern.
Why we chose centralized registry: In a healthcare context, the relationships between intents matter. Ambiguity detection requires comparing confidence scores across intents. RBAC filtering requires iterating all intents. Prompt generation needs the full catalog. A centralized registry makes these cross-cutting concerns natural. The cost is that adding an intent touches one large file instead of creating a new file — but that one file is the only file you need to touch.
When to reconsider: If the system grows beyond ~50 intents, the registry file becomes unwieldy. At that point, split into category-based sub-registries (clinical, administrative, navigation) that merge into a single runtime registry.
4. Building Block 2: Classification Pipeline
4.1 Purpose
The Classification Pipeline takes raw user input and produces a classified intent with confidence score and raw entities. It is the single entry point for all classification — both direct API calls and chat interactions use the same pipeline.
4.2 Two-Phase Design
Input + Context
│
▼
┌──────────────────────────────────────────────────────┐
│ Phase 1: REFLEX (local, <20ms) │
│ │
│ 1. Check escalation triggers │
│ - Multi-intent signals ("en", "daarna", "dan") │
│ - Pronouns ("hij", "zij", "hem", "haar") │
│ - Relative time ("morgen", "volgende week") │
│ → If triggered: skip to Phase 2 with best-guess │
│ │
│ 2. Pattern matching against registry patterns │
│ - Score each intent by highest matching weight │
│ - Track top-2 for ambiguity detection │
│ → If confidence ≥ 0.7 AND gap > 0.1: return │
│ → Otherwise: escalate to Phase 2 │
│ │
│ 3. Entity extraction via registry extraction rules │
│ → Attach raw entities to result │
└──────────────────────────────────────────────────────┘
│
│ (only if escalation needed)
▼
┌──────────────────────────────────────────────────────┐
│ Phase 2: ORCHESTRATOR (AI, ~250ms-3s) │
│ │
│ 1. Build prompt from registry (intent descriptions │
│ + examples + entity schemas) │
│ 2. Include application context: │
│ - Active patient (name, recent notes) │
│ - Current shift (nacht/ochtend/middag/avond) │
│ - Today's appointments │
│ - Recent intents (for pronoun resolution) │
│ 3. Send to LLM with structured output format │
│ 4. Validate response with Zod schema │
│ 5. Build IntentChain (single or multi-action) │
│ │
│ On failure: fall back to Reflex result, │
│ cap confidence at 0.6 │
└──────────────────────────────────────────────────────┘
│
▼
ClassificationResult {
intent, confidence, rawEntities,
chain (if multi-action),
source ('reflex' | 'orchestrator' | 'fallback'),
processingTimeMs
}
4.3 Why Two Phases Instead of One
Cost: An LLM API call costs money and time. At 100 classifications per hour across an organization, pure-LLM classification becomes expensive. Reflex handles 70%+ locally at zero marginal cost.
Latency: Reflex responds in <20ms. The LLM takes 250ms-3s. For routine commands ("notitie jan medicatie"), the user should not wait for AI.
Resilience: If the LLM provider is down, Reflex still works. The system degrades from "intelligent" to "fast and reliable" instead of failing completely.
Debuggability: Regex patterns are deterministic and inspectable. When a classification is wrong, you can trace exactly which pattern matched and why. LLM reasoning is probabilistic and harder to debug.
4.4 Escalation Triggers
The Reflex phase checks for escalation triggers before doing pattern matching. This is a deliberate choice: if the input contains signals that require AI reasoning (multiple actions, pronouns, relative time), there is no point in building a local hypothesis that will be incomplete.
| Trigger | Detection | Why AI is needed |
|---|---|---|
| Multi-intent | Conjunctions: "en", "daarna", "dan", "ook" | Reflex cannot split compound sentences |
| Pronouns | "hij", "zij", "hem", "haar", "die", "deze" | Requires context (active patient, recent actions) to resolve |
| Relative time | "morgen", "overmorgen", "volgende week", weekday names | Requires current date + calendar awareness |
4.5 The Unified Pipeline Principle
Both the Classify API (direct classification) and the Chat API (conversational interaction) use the same pipeline. The difference:
- Classify API: Returns the pipeline result directly as structured data
- Chat API: Feeds the pipeline result as context to a conversational LLM, which generates a natural language response alongside the structured action
This means the Chat LLM does not need to classify — it receives the classification as input and only needs to formulate a response. This eliminates the problem of maintaining two separate classification systems with divergent intent definitions.
┌─────────────────────┐
│ Classification │
│ Pipeline │
│ (single impl.) │
└──────┬──────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Classify API │ │ Chat API │
│ Returns result │ │ Passes result as │
│ as JSON │ │ context to LLM │
└──────────────────┘ │ for conversation │
└──────────────────┘
4.6 Circuit Breaker for AI Resilience
The Orchestrator wraps AI calls with a simple circuit breaker:
State Machine:
CLOSED (normal) ──[3 consecutive failures]──▶ OPEN (bypass AI)
OPEN ──[30 seconds elapsed]──▶ HALF-OPEN (try one request)
HALF-OPEN ──[success]──▶ CLOSED
HALF-OPEN ──[failure]──▶ OPEN
When the circuit is open, all classifications are handled by Reflex alone (with confidence capped at 0.6). This prevents cascade failures when the AI provider is experiencing issues — instead of every request waiting 5 seconds for a timeout, the system instantly falls back.
4.7 Trade-Off: Regex vs. Embeddings for Phase 1
Current choice: Regex patterns with weights.
Alternative: Sentence embeddings (e.g., via a local model or API) that match input against intent descriptions by semantic similarity.
Why regex for now: The domain vocabulary in healthcare (especially GGZ — mental health) is stable and limited. "Notitie", "overdracht", "afspraak" are not ambiguous words. Regex captures these patterns with near-perfect accuracy and zero latency. Embeddings add infrastructure complexity (model hosting or API calls) without proportional accuracy improvement at <50 intents.
When to reconsider: If the Reflex hit rate drops below 60% — meaning more than 40% of inputs require AI escalation — the patterns are no longer covering the vocabulary. At that point, an embedding-based first pass (selecting top-10 candidate intents, then LLM for final classification) becomes worthwhile. This is the architecture Voiceflow benchmarked successfully in production.
5. Building Block 3: Entity Resolution
5.1 Purpose
Entity Resolution takes the raw entities from classification (strings like "jan", "morgen", "medicatie") and grounds them against real system data (Patient #427, 2026-02-15, category "medicatie").
5.2 Why This Is a Separate Step
In the current prototype, entity extraction and resolution are conflated — some happens in the Reflex, some in the Orchestrator, some in the UI components. By making it an explicit pipeline step, we gain:
- Testability: Resolution logic can be unit-tested with mock data
- Reusability: The same resolution logic serves both Classify and Chat paths
- Extensibility: Swapping a name-lookup for a FHIR Patient search requires changing only this layer
- Separation of concerns: Classification decides what the user wants; resolution grounds who/when/what they mean
5.3 Resolution Types
| Entity Type | Raw Value | Resolution Method | Resolved Value |
|---|---|---|---|
| Patient name | "jan" | Fuzzy search against patient database | { id: "427", name: "Jan de Vries" } |
| Date/time | "morgen 14:00" | Date parsing with Dutch locale + current date | { date: "2026-02-15", time: "14:00" } |
| Category | "medicatie", "med", "medicijn" | Alias mapping to canonical values | "medicatie" |
| Navigation target | "risico", "diagnose" | Enum matching | "risicotaxatie" |
| Shift | "ochtend" | Direct mapping | "ochtend" (07:00-12:00) |
5.4 Disambiguation
When resolution is ambiguous (multiple patients named "Jan"), the system produces a ClarificationRequest:
interface ClarificationRequest {
type: 'patient_disambiguation' | 'appointment_disambiguation' | 'time_disambiguation';
question: string; // "Welke Jan bedoel je?"
options: Array<{
label: string; // "Jan de Vries (kamer 12)"
value: string; // patient ID
}>;
originalIntent: string; // Preserved for retry after disambiguation
originalEntities: Record<string, unknown>; // Preserved raw entities
}
The UI presents the options. After the user selects, the pipeline resumes with the resolved entity. The original intent and entities are preserved so the user doesn't need to repeat their command.
5.5 Context-Aware Resolution
Some entities resolve differently based on application context:
- "Hij"/"zij" → resolves to
activePatientfrom the context store - "De afspraak" → resolves to the most recent or next appointment in context
- "Vandaag" → resolves to current date, but reports before 07:00 belong to previous day's shift
This context-awareness is what makes the Orchestrator (AI) necessary for some inputs — the Reflex phase cannot resolve context-dependent references.
5.6 Trade-Off: Inline vs. Explicit Resolution
Alternative considered: Letting each UI component resolve entities on mount (current approach — components fetch patient data when they receive a name string).
Why explicit resolution: In a multi-action chain ("notitie jan en verzet zijn afspraak"), the patient resolved in action 1 must be the same patient used in action 2. Inline resolution per component can produce inconsistencies. Explicit resolution at the pipeline level ensures a single resolution pass for the entire chain.
6. Building Block 4: Action System
6.1 Purpose
The Action System maps resolved intents to UI artifacts (forms, views, navigation targets) and manages the execution lifecycle. It is the bridge between "the system understands what you want" and "the system shows you the right form."
6.2 The Intent-Action Separation
An intent is what the user means. An action is what the system does. This separation matters because:
- One intent can map to different actions depending on context (e.g., "overdracht" opens a summary view during shift start but generates a new summary at shift end)
- One user utterance can produce multiple actions (IntentChain)
- Actions have lifecycle states that intents do not (pending → confirming → executing → success/failed)
6.3 Action Types
There are three types of actions, each with different UI behavior:
| Type | What it does | UI Behavior | Example |
|---|---|---|---|
| Artifact Action | Opens a form or data view | Opens artifact panel with pre-filled data | dagnotitie, create_appointment |
| Query Action | Fetches and displays data | Opens read-only artifact | risico_query, diagnose_query, agenda_query |
| Navigation Action | Redirects to a page | Client-side navigation, no artifact | intake_navigeer |
6.4 The Confirmation Principle
No action that modifies patient data executes without human confirmation. This is the foundational safety principle.
In practice, this means:
- High confidence (≥0.9), non-destructive: Open form with pre-filled data. User reviews and submits.
- Medium confidence (0.7-0.9): Open form with pre-filled data + explicit confirmation banner: "Ik begrijp: dagnotitie voor Jan, categorie medicatie. Klopt dit?"
- Low confidence (0.5-0.7): Ask clarification question before opening anything.
- Very low confidence (<0.5): "Ik begrijp het niet. Kun je het anders zeggen?"
- Destructive actions (cancel, delete): Always require explicit confirmation, regardless of confidence.
The system pre-fills but never auto-submits. The human sees what the system understood and decides whether it's correct.
6.5 IntentChain: Multi-Action Execution
When the Orchestrator detects multiple intents in one utterance ("Annuleer de afspraak van Jan en maak een notitie over zijn medicatie"), it produces an IntentChain — an ordered sequence of actions.
interface IntentChain {
id: string;
originalInput: string;
actions: IntentAction[]; // Ordered sequence
status: 'pending' | 'executing' | 'completed' | 'partial' | 'failed';
meta: {
source: 'reflex' | 'orchestrator' | 'fallback';
processingTimeMs: number;
aiReasoning?: string; // Orchestrator's explanation (for debugging)
};
}
Execution is strictly sequential: action 2 starts only after action 1 completes (or the user confirms action 1). Parallel execution is intentionally not supported — the user expects a logical order, and in healthcare, action order can matter clinically (e.g., cancel before reschedule).
6.6 Trade-Off: Pre-fill vs. Auto-Execute
Alternative considered: For very high confidence classifications (≥0.95), skip the form and execute directly (write the note, create the appointment).
Why we chose pre-fill only: In healthcare, the cost of a wrong write is high (incorrect patient record) while the cost of an extra confirmation click is low (fraction of a second). The risk-reward ratio strongly favors requiring confirmation. Additionally, displaying the pre-filled form serves as a transparency mechanism — the user sees exactly what the system understood, building trust over time.
Exception to consider for the future: Read-only queries (agenda_query, risico_query) could arguably execute without confirmation since they don't modify data. This is a reasonable optimization but not a priority.
7. Building Block 5: Protocol Engine (Nudge)
7.1 Purpose
The Protocol Engine evaluates clinical rules after a successful action and suggests relevant follow-up actions. It is the system's proactive intelligence — suggesting things the user didn't ask for but should consider based on clinical protocols.
7.2 Why This Is the Differentiator
Any chatbot can classify intents. Any form system can pre-fill data. What makes a healthcare command interface genuinely useful is domain intelligence: knowing that after wound care documentation, a follow-up wound check should be scheduled in 3 days per V&VN guidelines. This is where the system transitions from "faster data entry" to "clinical decision support."
7.3 Rule Structure
interface ProtocolRule {
id: string;
name: string; // Human-readable rule name
// When to trigger
trigger: {
intent: string; // Which completed intent triggers this rule
conditions: Condition[]; // ALL must match (AND logic)
};
// What to suggest
suggestion: {
intent: string; // The suggested follow-up intent
message: string; // User-facing suggestion text (Dutch)
prefillEntities: (completedAction: Action) => Record<string, unknown>;
};
// Clinical grounding
protocol?: {
name: string; // "V&VN Richtlijn Wondzorg"
reference: string; // "§4.2 Controlebeleid"
rationale: string; // Why this suggestion matters
};
priority: 'low' | 'medium' | 'high';
expiresAfterMs: number; // Suggestion disappears after this time
enabled: boolean;
}
interface Condition {
field: string; // Entity field to check
operator: 'equals' | 'contains' | 'matches' | 'exists';
value?: string; // Expected value (not needed for 'exists')
}
7.4 Engine Design: Pure Function
The engine is a pure function: (completedAction, rules) → suggestions[]. It has no side effects, no state, and no knowledge of where rules come from. This makes it:
- Testable: Pass mock actions and rules, assert on suggestions
- Portable: Rules can come from hardcoded TypeScript objects (MVP), a database table (v2), or FHIR PlanDefinitions (enterprise)
- Predictable: Same input always produces same output
7.5 Integration Point
The Protocol Engine runs after action completion, triggered by the Action System:
Action completes successfully
│
▼
evaluateNudge(completedAction, protocolRules)
│
▼
NudgeSuggestion[] (sorted by priority)
│
▼
UI displays suggestion toast with:
- Message ("Wondcontrole inplannen over 3 dagen?")
- Protocol reference ("V&VN Wondzorg §4.2")
- Accept button → opens new artifact with pre-filled entities
- Dismiss button → logs dismissal for analytics
7.6 Example Rules (Healthcare / GGZ)
| Rule | Trigger | Suggestion | Protocol |
|---|---|---|---|
| Wound care follow-up | dagnotitie + content contains "wond" | Schedule wound check in 3 days | V&VN Wondzorg §4.2 |
| Medication change review | dagnotitie + content contains "medicatie" + "gewijzigd" | Schedule medication evaluation in 1 week | FMS Polyfarmacie §3.1 |
| Incident follow-up | dagnotitie + category = "incident" | Create incident report | IGJ Meldcode §2 |
| Risk reassessment | risico_query viewed | Schedule risk reassessment if >30 days old | GGZ Standaarden §5.4 |
| Intake completion | intake_status viewed + missing sections | Navigate to next incomplete section | Intake protocol |
7.7 Trade-Off: Rules Engine vs. AI-Generated Suggestions
Alternative considered: Instead of rule-based suggestions, let the LLM generate contextual suggestions based on the completed action and patient history.
Why we chose rules: Clinical suggestions must be traceable to specific protocols. When a nurse sees "Schedule wound check in 3 days," she needs to know why — and the answer must be a protocol reference, not "the AI thought it was a good idea." Rule-based suggestions are deterministic, auditable, and clinically grounded. AI-generated suggestions are creative but unverifiable.
Hybrid future: The rule engine could be augmented with an AI layer that proposes new rules based on patterns in action logs. These proposed rules would then be reviewed by clinical staff and added to the rule set if valid. The engine itself remains rule-based; AI contributes to rule discovery, not rule execution.
7.8 Path Toward Standards: CDS Hooks
The Protocol Engine is conceptually aligned with the HL7 CDS Hooks specification — a standard for Clinical Decision Support in healthcare. The mapping:
| Cortex Concept | CDS Hooks Equivalent |
|---|---|
| ProtocolRule | CDS Service |
| Trigger (intent + conditions) | CDS Hook (workflow trigger) |
| NudgeSuggestion | CDS Card (suggestion type) |
| Protocol metadata | Card.source |
Migrating to CDS Hooks compatibility is not needed for MVP but is architecturally possible: express rules as FHIR PlanDefinitions, map triggers to CDS hook types, format output as CDS Cards. This enables integration with hospital CDS systems that already speak HL7.
8. Cross-Cutting Concerns
8.1 State Management
The system requires state management for five distinct domains. These should be separate stores (not one monolithic store) to maintain testability and single responsibility:
| Store | Responsibility | Key State |
|---|---|---|
| Context Store | Application context | Active patient, shift, recent patients |
| Chat Store | Conversational state | Messages, streaming status, pending action |
| Artifact Store | UI artifact management | Open artifacts (max 3), active artifact |
| Action Store | Intent chain execution | Active chain, chain history |
| Nudge Store | Protocol suggestions | Active suggestions, accept/dismiss tracking |
Stores communicate via subscriptions, not direct imports. Example: when the Action Store completes a chain, the Nudge Store's subscription triggers protocol evaluation.
8.2 Observability
Every classification produces a structured log event:
interface ClassificationEvent {
timestamp: string;
userId: string;
input: string; // Sanitized (PII removed or hashed)
intent: string;
confidence: number;
source: 'reflex' | 'orchestrator' | 'fallback';
escalationReason?: string;
processingTimeMs: number;
entities: Record<string, unknown>; // Sanitized
}
Key metrics to track:
| Metric | Why It Matters |
|---|---|
| Reflex hit rate | If <60%, patterns need expansion or embeddings should be considered |
| Orchestrator fallback rate | High rate = AI is unreliable, circuit breaker effectiveness |
| Classification latency (P95) | User experience indicator |
| Intent distribution | Reveals which features are actually used |
| Nudge acceptance rate per rule | Indicates whether suggestions are clinically useful |
8.3 Audit Trail
In healthcare, every system interaction with patient context must be auditable:
interface AuditEvent {
userId: string;
action: 'classify' | 'resolve' | 'execute' | 'nudge_shown' | 'nudge_accepted' | 'nudge_dismissed';
intent?: string;
patientId?: string; // Which patient was involved
input?: string; // Sanitized
result?: string; // Summary, not full response
timestamp: string;
sessionId: string; // For correlating events within a session
durationMs?: number;
}
Storage requirements: append-only table, no UPDATE or DELETE, minimum 5 years retention (NEN 7510 compliance for Dutch healthcare).
8.4 Privacy: PII in AI Prompts
When the Orchestrator sends context to the LLM, it includes patient names and potentially clinical information. Mitigations:
- Minimize context: Send only what's needed for classification (patient name, not full medical history)
- Data processing agreement: Ensure the AI provider's DPA guarantees no training on API data
- Log sanitization: All log events must strip or hash patient names and identifiers before storage
- Pseudonymization option: For high-security environments, replace patient names with tokens before sending to AI, resolve tokens after classification
8.5 Role-Based Access (Prepared, Not Enforced)
The Intent Registry includes an allowedRoles field per intent. This enables future RBAC filtering without architectural changes:
Input → Classification Pipeline → [RBAC Filter] → Action System
│
└─ If intent not allowed for user's role:
→ "Je hebt geen toegang tot deze functie"
The filter sits between classification and action: the system classifies the intent (so it knows what was attempted) but blocks execution if the user's role doesn't permit it. The blocked attempt is logged for audit purposes.
9. Scaling Considerations
9.1 Scaling Intents: From 11 to 50+
The current system has 11 intents. The architecture supports growth to 50+ with these considerations:
| Dimension | At 11 intents | At 50 intents | Mitigation |
|---|---|---|---|
| Reflex matching | ~88 regex evaluations (<20ms) | ~400 evaluations (~50ms) | Keyword pre-filter: match first word against index, evaluate only candidate intents |
| AI prompt size | ~2000 tokens | ~8000 tokens | Category-based prompt sections; only include relevant categories based on Reflex pre-classification |
| Registry file size | ~300 lines | ~1500 lines | Split into category sub-registries that merge at runtime |
| Ambiguity | Low (intents are distinct) | Higher (more overlapping patterns) | Increase pattern specificity; add disambiguation rules |
9.2 Scaling Users
| Component | Bottleneck | Mitigation |
|---|---|---|
| Reflex | None (stateless, in-process) | Scales with application instances |
| Orchestrator | AI provider rate limits | Circuit breaker prevents cascade; consider provider load balancing |
| Rate limiting | Currently in-memory (lost on restart) | Move to database-backed (Supabase table or Redis) |
| Audit logging | Write volume | Batch writes; async logging pipeline |
9.3 AI Provider Independence
The system is currently coupled to Anthropic Claude. To reduce vendor lock-in:
interface LLMProvider {
classify(input: string, systemPrompt: string, context: string): Promise<string>;
stream(messages: Message[], systemPrompt: string): AsyncIterable<string>;
}
Two implementations (Anthropic, OpenAI) behind a single interface. Note that prompts are NOT fully portable between providers — the system prompt is optimized for Claude's behavior. Provider switching requires prompt re-tuning. This is inherent to LLM usage and cannot be fully abstracted.
10. Relationship to Healthcare Standards
10.1 FHIR Resource Mapping
Extracted entities ultimately map to FHIR-inspired resources:
| Entity | FHIR Resource | Usage |
|---|---|---|
| patientName / patientId | Patient | Patient identification |
| category + content | Observation or DocumentReference | Clinical notes |
| datetime + appointmentType | Encounter / Appointment | Scheduling |
| diagnosis / risk | Condition | Clinical data queries |
The system does not need to be a FHIR server. It needs a clear mapping layer so that when FHIR compliance becomes required, entity resolution can produce FHIR-compliant references.
10.2 NEN 7510 (Dutch Healthcare Information Security)
| Requirement | How This Architecture Addresses It |
|---|---|
| Access control | RBAC prepared in Intent Registry; Supabase Auth + RLS on data |
| Audit logging | Append-only audit events for all interactions |
| No PII in logs | Sanitization step in logging pipeline |
| Encryption in transit | HTTPS (standard for modern web apps) |
| Encryption at rest | Database-level (Supabase managed) |
| Incident logging | Classification failures logged as potential security events |
11. Migration Path from Current Prototype
Phase 1: Foundation (Low Risk)
Goal: Intent Registry as single source of truth + entity extraction in Reflex
- Create
intent-registry.tswith all 11 current intents as declarative objects - Refactor
reflex-classifier.tsto load patterns from registry instead of hardcoded map - Add entity extraction rules to registry; implement extraction in Reflex phase
- Generate Orchestrator prompt from registry (replace hardcoded prompt)
- Verify: all existing classifications produce the same results (regression test)
Risk: Low — refactoring without behavior change (except added entity extraction).
Phase 2: Unification (Medium Risk)
Goal: Single classification pipeline serving both Classify and Chat APIs
- Extract
classifyInput()as standalone pipeline function - Modify Chat API to use pipeline for classification (remove duplicate intent logic from chat prompt)
- Simplify Chat system prompt to conversational role only (no classification responsibility)
- Fix AbortController signal propagation (currently disconnected from actual fetch)
- Add circuit breaker to Orchestrator
Risk: Medium — Chat behavior may subtly change. Requires thorough testing with existing scenarios.
Phase 3: Activation (Low Risk)
Goal: Nudge system actually running; state management cleaned up
- Split monolithic Zustand store into 5 domain stores
- Wire
evaluateNudge()to fire after action completion (via store subscription) - Add 3-5 clinical protocol rules
- Implement nudge tracking (shown/accepted/dismissed)
Risk: Low — new functionality, no breaking changes to existing features.
Phase 4: Production Readiness (Low Risk)
Goal: Observable, auditable, resilient
- Structured logging (replace console.log)
- Classification metrics tracking
- Audit events to database
- Database-backed rate limiting
- PII sanitization in logs
Risk: Low — cross-cutting infrastructure, no functionality changes.
12. Decision Log
This section records key architectural decisions with their rationale, for future reference.
D1: Three-Layer Model (Reflex → Orchestrator → Nudge)
Decision: Use a tiered classification model with local fast path and AI fallback, plus protocol-driven post-action suggestions.
Rationale: Optimizes for the common case (70%+ routine commands handled locally in <20ms) while preserving AI capability for complex input. Nudge layer adds domain value beyond classification. This pattern is validated by industry benchmarks (Voiceflow 2025) showing hybrid NLU+LLM outperforms pure approaches on both cost and accuracy.
Alternatives rejected: Pure LLM (too slow and expensive for routine commands), pure rule-based (cannot handle multi-intent, pronouns, relative time), custom ML model (training data overhead without proportional accuracy gain in stable domain).
D2: Human-in-the-Loop for All Write Operations
Decision: The system never writes to patient records without explicit human confirmation. Pre-fill forms, never auto-submit.
Rationale: Healthcare requires accountability. The cost of a wrong write (incorrect patient record) far exceeds the cost of an extra confirmation click. This also builds user trust — the system is transparent about what it understood.
Alternatives rejected: Auto-execution for high-confidence classifications (>0.95). Rejected because even at 99% accuracy, 1 wrong write per 100 interactions is unacceptable in a clinical context.
D3: Centralized Intent Registry over Feature Folders
Decision: Single registry file over one-file-per-intent module structure.
Rationale: Cross-cutting concerns (ambiguity detection, RBAC filtering, prompt generation) need the full intent catalog. A centralized registry makes these natural. The cost (one large file) is manageable at <50 intents and can be split into sub-registries later.
D4: Regex over Embeddings for Phase 1 Classification
Decision: Use regex patterns with weights for the Reflex phase.
Rationale: Domain vocabulary is stable and limited (medical terms, fixed workflows). Regex achieves near-perfect accuracy for known patterns at zero latency. Embeddings add infrastructure complexity without proportional accuracy improvement at current scale.
Reconsideration trigger: Reflex hit rate drops below 60%.
D5: Rule-Based Protocol Engine over AI-Generated Suggestions
Decision: Nudge suggestions are deterministic, based on explicitly defined protocol rules.
Rationale: Clinical suggestions must be traceable to specific protocols. Rule-based suggestions are auditable and verifiable. AI-generated suggestions are creative but clinically unverifiable.
Future hybrid: AI proposes candidate rules from action log patterns; clinical staff reviews and approves; engine remains rule-based.
D6: Sequential Multi-Intent Execution over Parallel
Decision: Actions in an IntentChain execute sequentially, never in parallel.
Rationale: Users expect logical ordering ("cancel appointment AND make note" implies cancel first). In healthcare, action order can matter clinically. Sequential execution is predictable and debuggable.
13. Platform Model: From Intent System to Healthcare Application
13.1 The Four Layers
The complete system consists of four layers. The intent system (this document) is layer 1. Three additional layers build on top to form a healthcare application.
┌─────────────────────────────────────────────────────────────────┐
│ │
│ LAYER 4: EPD APPLICATION │
│ The application the healthcare worker sees and uses │
│ │
│ Reporting │ Patient Record │ Calendar │ Handover │ Intake │
│ UI modules, block components, API routes, data model │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 3: CARE DOMAIN CONFIGURATION │
│ The knowledge of the specific care type │
│ │
│ Intent definitions (dagnotitie, overdracht, agenda, ...) │
│ Entity schemas (patient, category, shift, ...) │
│ Domain vocabulary (GGZ terms, report categories) │
│ Protocol rules (nudges specific to this care type) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 2: KNOWLEDGE LAYER │
│ External data sources that feed the Protocol Engine │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌────────────┐ ┌───────────┐ │
│ │ Laws & │ │ Treatment │ │ Quality │ │ EHR Data │ │
│ │ Regula- │ │ Protocols │ │ Documents │ │ (own │ │
│ │ tions │ │ │ │ │ │ patient │ │
│ │ │ │ │ │ │ │ data) │ │
│ └───────────┘ └──────────────┘ └────────────┘ └───────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: INTENT SYSTEM (this document) │
│ The generic engine │
│ │
│ Intent Registry │ Classification Pipeline │ Entity Resolution │
│ Action System │ Protocol Engine (Nudge) │
│ │
└─────────────────────────────────────────────────────────────────┘
13.2 Why the Knowledge Layer Matters
The Protocol Engine (Building Block 5) generates nudges based on rules. But those rules don't come from nowhere — they are derived from external knowledge sources. The quality of nudges is directly dependent on the quality and accessibility of these sources.
Without the Knowledge Layer, the Protocol Engine is an empty engine: the mechanics work, but there's no fuel. The Knowledge Layer is that fuel.
13.3 The Four Knowledge Sources
13.3.1 Laws and Regulations
Laws and standards that regulate healthcare practices. These determine what must happen.
| Source | Example | Nudge type it produces |
|---|---|---|
| Wlz (Long-term Care Act) | Care plan requirements, evaluation deadlines | "Care plan evaluation expires in 2 weeks" |
| Wvggz (Compulsory Mental Healthcare Act) | Reporting obligations, coercive measure deadlines | "Coercive measure expires in 3 days — renewal needed?" |
| Wkkgz (Quality, Complaints and Disputes in Healthcare Act) | Incident reporting obligations | "Incident registered — IGJ report required within 3 business days" |
| NEN 7510 / GDPR | Record retention periods, access logging | "Audit log: 3 staff members viewed this record" |
Characteristics: Changes slowly (legislative amendments), high reliability, publicly available, legally binding.
Technical implication: These rules are stable enough to be hardcoded protocol rules in the engine. They change perhaps 1-2 times per year.
13.3.2 Treatment Protocols
Clinical guidelines and standards describing how care should be delivered.
| Source | Example | Nudge type it produces |
|---|---|---|
| GGZ Standards | Care Standard Depression, Psychosis, etc. | "Patient with depression — ROM measurement scheduled in 6 weeks?" |
| V&VN Guidelines | Wound care, medication management, fall prevention | "Schedule wound check in 3 days (V&VN §4.2)" |
| FMS Guidelines | Polypharmacy, pain management | "Medication evaluation in 1 week (FMS Polypharmacy §3.1)" |
| Multidisciplinary guidelines | MDO frequency, treatment plan discussions | "MDO scheduled in 2 weeks — start preparation?" |
Characteristics: Changes regularly (new guideline versions), high reliability, publicly available, requires interpretation by clinical staff.
Technical implication: These sources are too extensive to manually encode as rules. This is where a RAG component (Retrieval Augmented Generation) comes in: protocol texts are indexed, and for relevant actions the LLM is asked to formulate specific nudge suggestions based on protocol text. These suggestions are then validated by clinical staff and added as rules.
13.3.3 Quality Documents
Internal documents of the healthcare organization describing how this specific organization delivers care.
| Source | Example | Nudge type it produces |
|---|---|---|
| Institution protocols | Medication distribution protocol | "Double check medication required (house protocol §2.3)" |
| Work agreements | Handover moments, reporting frequency | "Handover report not completed — shift ends in 1 hour" |
| Quality handbook | Audit moments, improvement plans | "Internal audit next month — update action plan" |
| Accreditation requirements | HKZ, PREZO, Planetree | "Client satisfaction survey — 3 patients not yet surveyed" |
Characteristics: Organization-specific (not public), changes with policy decisions, requires internal coordination.
Technical implication: Most challenging source because it differs per institution. Options: manual rule creation, semi-automatic via RAG + validation, or hybrid (standard ruleset + organization-specific additions).
13.3.4 EHR Data (Own Patient Data)
Data already in the system — patient records, reports, appointments, treatment plans.
| Data source | Example | Nudge type it produces |
|---|---|---|
| Report history | No report in 48 hours | "No daily note for Jan de Vries in 2 days" |
| Treatment plan | Evaluation date expired | "Treatment plan evaluation Jan — 1 week past deadline" |
| Medication overview | Drug interactions, changes | "New medication prescribed — interaction check needed" |
| Risk assessment | Expired risk assessment | "Risk assessment Marie — last evaluation 45 days ago" |
| Calendar | Missed appointments, no-shows | "Jan 2x no-show this month — follow up?" |
| Lab/measurements | ROM scores, vital signs | "PHQ-9 score increased from 12 to 18 — adjust treatment plan?" |
Characteristics: Real-time, patient-specific, always available (it's in your own database), privacy-sensitive.
Technical implication: The only source requiring no external integration — it's already in Supabase. The Protocol Engine can run direct queries against existing tables. This is the simplest and most impactful source to start with.
13.4 How the Knowledge Layer Feeds the Protocol Engine
┌─────────────────────────────────────────────────────────────────┐
│ KNOWLEDGE LAYER │
│ │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ Static rules │ │ Dynamic rules │ │
│ │ │ │ │ │
│ │ Laws & │ │ EHR data queries │ │
│ │ regulations │ │ (report gaps, expired │ │
│ │ (hardcoded, │ │ assessments, no-shows) │ │
│ │ rarely changes)│ │ │ │
│ │ │ │ Treatment protocols via RAG │ │
│ │ National │ │ (indexed guideline texts │ │
│ │ guidelines │ │ → AI-generated candidate │ │
│ │ (V&VN, GGZ Std) │ │ rules → human validation) │ │
│ │ │ │ │ │
│ │ Institution │ │ Quality documents │ │
│ │ protocols │ │ (uploaded per institution, │ │
│ │ (configured at │ │ indexed, AI-suggested) │ │
│ │ onboarding) │ │ │ │
│ └────────┬────────┘ └────────────┬─────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PROTOCOL RULES REGISTRY │ │
│ │ │ │
│ │ All rules — regardless of source — in the same format: │ │
│ │ trigger + conditions + suggestion + protocol metadata │ │
│ │ │ │
│ │ Each rule contains: │ │
│ │ - source: 'legislation' | 'guideline' | 'institution' │ │
│ │ | 'ehr_data' | 'ai_suggested' │ │
│ │ - validatedBy: userId (who approved the rule) │ │
│ │ - validatedAt: timestamp │ │
│ │ - version: rule version (for audit trail) │ │
│ └──────────────────────────┬───────────────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────────────┘
│
▼
┌──────────────────────┐
│ PROTOCOL ENGINE │
│ (Building Block 5) │
│ │
│ evaluateNudge() │
└──────────────────────┘
13.5 Implementation Order for the Knowledge Layer
Step 1: EHR data (own database) — Start here. No external integration needed. Supabase queries on your own tables. Direct value, low complexity.
Step 2: Laws & national guidelines — Hardcoded rules. Stable sources, publicly available. 20-30 rules that apply to every GGZ institution. One-time work reusable across all clients.
Step 3: Institution protocols — Configurable per client. At onboarding: upload their protocols, translate to rules (manually or semi-automatically).
Step 4: RAG on treatment protocols — AI-assisted. Index full text of GGZ Standards, V&VN guidelines, etc. Use RAG to retrieve protocol texts and generate candidate nudges. Clinical staff validates.
13.6 Trade-Off: Rules vs. RAG
| Aspect | Hardcoded rules | RAG-generated suggestions |
|---|---|---|
| Accuracy | 100% (human validated) | ~80-90% (AI-generated, requires validation) |
| Scalability | Low (manual per rule) | High (automatic from documents) |
| Traceability | Perfect (rule → protocol §) | Good (RAG source + AI reasoning) |
| Startup cost | Low per rule, high for completeness | High for setup, low per rule after |
| Maintenance | Manual on guideline changes | Semi-automatic (reindex + revalidate) |
Recommendation: Start with hardcoded rules (steps 1-3). Build RAG when the ruleset grows beyond what's manually manageable (~50+ rules per institution). The rule format is the same — the source changes, the engine does not.
13.7 The Knowledge Layer as Product Differentiator
This is where it becomes commercially interesting. The Knowledge Layer differentiates between:
- Generic intent system: "I understand your command and open the right form" — anyone can build this.
- Care-specific platform: "I know that after wound care, a check is needed in 3 days per V&VN §4.2, and that the Wlz requires care plan evaluation within 6 weeks" — this requires domain knowledge.
- Institution-specific system: "I know that your house protocol requires dual-person medication verification, and that your next internal audit is in 3 weeks" — this requires organizational knowledge.
Each layer adds value. The intent system (layer 1) is the engine anyone can build. The Knowledge Layer (layer 2) is the flywheel that's hard to copy.
13.8 Layer Separation and Reusability
The four layers are deliberately decoupled:
| If you replace... | Then changes... | And stays unchanged... |
|---|---|---|
| Layer 4 (EPD application) | UI, API routes, data model | Intent system, Knowledge Layer, Domain config |
| Layer 3 (Care domain) | Intents, entities, vocabulary | Intent system, Knowledge Layer, EPD application |
| Layer 2 (Knowledge Layer) | Protocol rules, data sources | Intent system, Domain config, EPD application |
| Layer 1 (Intent system) | Classification, resolution, actions | Knowledge Layer, Domain config, EPD application |
Concrete example: switching from GGZ to home care?
- Layer 1 stays identical (same engine)
- Layer 2 gets different sources (home care protocols instead of GGZ Standards)
- Layer 3 gets different intents (care_moment_registration instead of dagnotitie, route_planning instead of agenda)
- Layer 4 gets a different UI (home care app instead of EPD dashboard)
Appendix A: Glossary
| Term | Definition |
|---|---|
| Intent | A classified user intention: what the user wants to do (e.g., dagnotitie, create_appointment) |
| Entity | A parameter extracted from user input (e.g., patient name, date, category) |
| Confidence | A score (0.0–1.0) indicating how certain the classification is |
| Reflex | The fast, local classification phase using regex patterns |
| Orchestrator | The AI-powered classification phase using an LLM |
| IntentChain | An ordered sequence of actions derived from a single user utterance |
| Artifact | A UI panel (form, data view) opened in response to an intent |
| Block | A React component that renders a specific artifact type |
| Nudge | A proactive suggestion generated by the Protocol Engine after action completion |
| Protocol Rule | A declarative rule that triggers a nudge based on completed action + conditions |
| Entity Resolution | The process of grounding raw entity strings against real system data |
| Escalation | The decision to send input from Reflex to Orchestrator for AI classification |
| Circuit Breaker | A resilience pattern that bypasses AI calls after consecutive failures |
Appendix B: Current Intent Catalog
| Intent | Priority | Category | Artifact | Confirmation |
|---|---|---|---|---|
dagnotitie |
P1 | Clinical | Form (note creation) | No (pre-fill only) |
zoeken |
P1 | Navigation | Search results view | No |
overdracht |
P1 | Clinical | Handover summary | No |
agenda_query |
P2 | Administrative | Calendar view | No |
create_appointment |
P2 | Administrative | Appointment form | No (pre-fill) |
cancel_appointment |
P2 | Administrative | Confirmation dialog | Yes (destructive) |
reschedule_appointment |
P2 | Administrative | Appointment form | Yes (modifying) |
intake_status |
P3 | Clinical | Intake progress view | No |
intake_navigeer |
P3 | Navigation | Page navigation | No (no artifact) |
risico_query |
P3 | Clinical | Risk assessment view | No |
diagnose_query |
P3 | Clinical | Diagnosis list view | No |
Appendix C: References
- Voiceflow: Benchmarking Hybrid LLM Classification Systems — Production benchmarks showing hybrid NLU+LLM outperforms pure approaches
- HL7 CDS Hooks Specification — Standard for Clinical Decision Support integration
- Microsoft Dragon Copilot — Largest ambient AI clinical documentation system (capture → classify → act → review pattern)
- MediGRAF: Hybrid Graph RAG for EHR — Hybrid structured + unstructured querying of patient data
- NEN 7510 — Dutch standard for information security in healthcare