Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Status: Proposed Date: 2026-03-24 Decision Makers: CL Team, FW Team, Platform Architect

Context

The Clinical (CL) core produces domain events that need to flow through FW-16 fw_domain_events infrastructure for workflow automation. Currently:
  • cl_pm_events channel is active and handles cross-core CL/PM events (lab orders, note signing, prescriptions)
  • cl_events channel is reserved but unused — intended for CL-internal and CL-to-other-core events
  • FW-16 (Event-Based Workflow Triggers) provides fw_process_domain_event() for consuming events and creating automated tasks/notifications
  • CL-13 (Crisis), CL-10 (Outcomes), CL-21 (MOUD), and CL-35 (Population Health) all need to publish events that FW consumes
Without a clear decision on channel routing, payload conventions, and idempotency patterns, each CL spec will make ad-hoc choices, leading to inconsistent event handling and difficult-to-debug automation failures. Driving specs: CL-13, CL-10, CL-21, CL-35, FW-16 Related contracts: CL-PM Event Wiring (pending), CL-FW Event Automation (pending) Reference: docs/architecture/integrations/EVENT_CONTRACTS.md

Decision

1. Channel Routing Strategy

CL events use two channels based on consumer scope:
ChannelWhen to UseExamples
cl_pm_eventsEvent has PM as a primary consumer (charge capture, scheduling, eligibility)progress_note_signed, cl_lab_order_created, prescription_sent
cl_eventsEvent is consumed by FW, GR, PF, or CL-internal automation (no PM charge/scheduling impact)cl_crisis_episode_opened, cl_assessment_expired, cl_moud_adherence_risk
Rule: If an event triggers both PM charge capture AND FW automation, publish to cl_pm_events. FW subscribes to both channels. Primary vs secondary PM consumer (explicit definition):
  • PM is primary when the event directly triggers PM operations that create or modify PM billing, scheduling, or eligibility records — e.g. charge capture (pm_charge_suggestions / pm_charges), appointment create/update tied to the event, or eligibility checks that persist PM state.
  • PM is secondary when the event only informs PM-side workflows (notifications, dashboards, manual follow-up) without creating or updating those PM records.
Publisher decision examples:
ScenarioChannelRationale
Progress note signed → charge suggestion / auto-postcl_pm_eventsPM is primary (billing records).
Crisis episode opened → supervisor alert, GR awarenesscl_eventsNo PM record mutation; informational/automation only.
Lab order created → PM billing line / encounter linkcl_pm_eventsPM is primary when scheduling/billing rows are created or updated.
Assessment expired → FW reassessment taskcl_eventsPM not primary unless the handler also writes PM appointments/charges.

2. Event Naming Convention

All new CL events follow the canonical format from EVENT_CONTRACTS.md:
cl_{entity}_{action}
EntityActionsExamples
crisis_episodeopened, resolved, followup_overduecl_crisis_episode_opened
restraint_eventinitiated, terminated, overduecl_restraint_event_initiated
assessmentcompleted, expired, interval_duecl_assessment_expired
moud_monitoringoverdue, adherence_risk, phase_changedcl_moud_monitoring_overdue
care_gapidentified, closed, escalatedcl_care_gap_identified
notefinalized, cosignedcl_note_finalized
safety_planactivated, updatedcl_safety_plan_activated

3. Payload Schema Convention

All CL events conform to the DomainEvent interface from EVENT_CONTRACTS.md:
interface CLDomainEvent {
  event: string;                    // e.g. 'cl_crisis_episode_opened'
  publisher: 'CL';
  subscriber: string[];             // e.g. ['FW', 'GR', 'PF']
  payload: {
    entity_id: string;              // UUID of the entity (episode, assessment, etc.)
    entity_type: string;            // e.g. 'crisis_episode', 'restraint_event'
    chart_id: string;               // Patient chart UUID
    severity?: 'info' | 'warning' | 'critical';  // For FW priority routing
    action_required?: string;       // Human-readable action hint for FW task creation
    billing_codes?: string[];       // Only for events with billing impact
    [key: string]: unknown;         // Additional event-specific fields
  };
  metadata: {
    organization_id: string;        // REQUIRED: tenant isolation
    site_id?: string;
    user_id: string;                // Actor who triggered the event
    timestamp: string;              // ISO 8601
    correlation_id: string;         // UUID for idempotency + tracing
    event_id: string;               // Unique event UUID
  };
}
PHI safety: Payloads contain UUIDs only. No patient names, DOB, SSN, or narrative clinical text. Consumers resolve details via their own RLS-protected queries.

4. Idempotency Pattern

  • Every event includes a unique event_id (UUID v4) in metadata
  • fw_process_domain_event() checks for duplicate event_id in fw_domain_events before processing
  • Duplicate events are logged but not re-processed
  • correlation_id groups related events (e.g., all events from a single crisis episode share a correlation_id)

5. Dead-Letter Queue Pattern

  • Failed event processing (consumer throws after max retries) writes to fw_domain_events with status = 'failed'
  • Failed events include error message and retry count
  • Edge function cron (cl-event-dlq-processor, runs every 15 minutes) retries failed events up to 3 additional times
  • After final failure: creates PF-10 notification to system admin with event details
  • Retry strategy: exponential backoff (1s, 2s, 4s) on 5xx errors; no retry on 4xx except 429 (rate limit)

6. FW Subscription Registration

CL events that FW consumes are registered in fw_workflow_events (column auto_task_template references the FW task template key). Seed ordering and idempotency:
  • Insert or upsert rows in fw_workflow_events only after the corresponding CL event names are implemented (publishers exist) and each referenced auto_task_template exists in FW (e.g. pf_task_templates or equivalent). Otherwise migrations fail or produce orphaned registrations that never resolve at runtime.
  • Prefer idempotent seeds: use ON CONFLICT (event_name) DO NOTHING or ON CONFLICT ... DO UPDATE so re-running migrations or seed scripts does not fail on duplicate keys.
INSERT INTO fw_workflow_events (event_name, source_core, channel, description, auto_task_template)
VALUES
  ('cl_crisis_episode_opened', 'CL', 'cl_events', 'Crisis episode opened — notify supervisor', 'crisis_supervisor_notification'),
  ('cl_assessment_expired', 'CL', 'cl_events', 'Assessment overdue — create reassessment task', 'reassessment_reminder'),
  ('cl_moud_monitoring_overdue', 'CL', 'cl_events', 'MOUD monitoring overdue — alert care team', 'moud_care_team_alert'),
  ('cl_moud_adherence_risk', 'CL', 'cl_events', 'MOUD adherence risk — escalate to supervisor', 'moud_escalation'),
  ('cl_care_gap_identified', 'CL', 'cl_events', 'Care gap identified — create task for care manager', 'care_gap_task'),
  ('cl_restraint_event_initiated', 'CL', 'cl_events', 'Restraint initiated — notify GR + create incident', 'restraint_incident_creation')
ON CONFLICT (event_name) DO UPDATE SET
  description = EXCLUDED.description,
  auto_task_template = EXCLUDED.auto_task_template,
  channel = EXCLUDED.channel,
  source_core = EXCLUDED.source_core;
(Adjust ON CONFLICT target columns to match the actual unique constraint on fw_workflow_events in the FW-16 migration.)

Rationale

Options Considered

Option A: Single cl_events channel for all CL events
  • Pro: Simple routing, one consumer per channel
  • Con: PM charge capture events mixed with FW automation events; PM team must filter irrelevant events; higher latency for time-sensitive billing events
  • Rejected: Violates separation of concerns; PM consumers shouldn’t process crisis/MOUD events
Option B: Split by consumer (current decision)
  • Pro: PM consumers only see billing-relevant events; FW consumes both channels but primarily cl_events; clear ownership
  • Con: Publisher must decide which channel; slight complexity
  • Chosen: Best balance of performance and clarity. Aligns with existing cl_pm_events usage.
Option C: General domain_events channel for all
  • Pro: Simplest; one channel for everything
  • Con: Extremely noisy; all cores’ events in one channel; consumer filtering overhead; violates EVENT_CONTRACTS.md channel ownership model
  • Rejected: Doesn’t scale; contradicts established channel-per-core pattern

Why not a new cl_fw_events channel?

Adding a third CL channel creates confusion about which channel to publish to. Two channels with clear rules (PM-impacting vs. non-PM) is sufficient. FW already subscribes to multiple channels.

Consequences

Positive

  • Consistent event naming and payload schema across all CL specs
  • FW automation rules can be authored against a known, stable contract
  • Idempotency and DLQ prevent lost events and duplicate processing
  • PHI-safe payloads by design (UUIDs only)
  • Clear channel routing reduces consumer-side filtering

Negative

  • Publishers must make a routing decision (cl_pm_events vs cl_events) — adds minor cognitive overhead
  • FW must subscribe to two CL channels (already subscribes to multiple channels for other cores)
  • Seed data in fw_workflow_events must stay in sync with actual published events

Mitigations

  • Document routing decision tree in this ADR (section 1 above)
  • CI drift check: validate that all pg_notify('cl_events', ...) calls match registered events in fw_workflow_events
  • EVENT_CONTRACTS.md updated whenever a new CL event is added (PR template checklist item)


Constitution Reference: This ADR documents the standard integration pattern for CL events flowing to FW and other consumers, per constitution.md §1.3 and §5.2.7. No exception to architecture rules is required — this ADR establishes the canonical pattern.