> ## 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.

# ADR-004: CL-FW Event Patterns for Clinical Workflow Automation

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

**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:

| Channel        | When to Use                                                                                 | Examples                                                                      |
| -------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `cl_pm_events` | Event has PM as a **primary consumer** (charge capture, scheduling, eligibility)            | `progress_note_signed`, `cl_lab_order_created`, `prescription_sent`           |
| `cl_events`    | Event 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:**

| Scenario                                               | Channel        | Rationale                                                              |
| ------------------------------------------------------ | -------------- | ---------------------------------------------------------------------- |
| Progress note signed → charge suggestion / auto-post   | `cl_pm_events` | PM is primary (billing records).                                       |
| Crisis episode opened → supervisor alert, GR awareness | `cl_events`    | No PM record mutation; informational/automation only.                  |
| Lab order created → PM billing line / encounter link   | `cl_pm_events` | PM is primary when scheduling/billing rows are created or updated.     |
| Assessment expired → FW reassessment task              | `cl_events`    | PM 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}
```

| Entity            | Actions                                      | Examples                       |
| ----------------- | -------------------------------------------- | ------------------------------ |
| `crisis_episode`  | `opened`, `resolved`, `followup_overdue`     | `cl_crisis_episode_opened`     |
| `restraint_event` | `initiated`, `terminated`, `overdue`         | `cl_restraint_event_initiated` |
| `assessment`      | `completed`, `expired`, `interval_due`       | `cl_assessment_expired`        |
| `moud_monitoring` | `overdue`, `adherence_risk`, `phase_changed` | `cl_moud_monitoring_overdue`   |
| `care_gap`        | `identified`, `closed`, `escalated`          | `cl_care_gap_identified`       |
| `note`            | `finalized`, `cosigned`                      | `cl_note_finalized`            |
| `safety_plan`     | `activated`, `updated`                       | `cl_safety_plan_activated`     |

### 3. Payload Schema Convention

All CL events conform to the `DomainEvent` interface from EVENT\_CONTRACTS.md:

```typescript theme={null}
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.

```sql theme={null}
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)

***

## Related Documents

* [EVENT\_CONTRACTS.md](../integrations/EVENT_CONTRACTS.md) — Canonical event schema and channel registry
* [FW-16 Event-Based Workflow Triggers](../integrations/event-based-workflow-triggers-integration.md) — FW automation infrastructure
* [CL-13 Crisis Intervention](../../../specs/cl/specs/CL-13-crisis-intervention-documentation.md) — Primary driver for crisis events
* [CL-21 MOUD Tracking](../../../specs/cl/specs/CL-21-medication-assisted-treatment-moud-tracking.md) — MOUD monitoring events
* [CL-35 Population Health](../../../specs/cl/specs/CL-35-population-health-care-gap-management.md) — Care gap events
* [ADR-002 CL-PM Cross-Core Foreign Keys](ADR-002-cl-pm-cross-core-foreign-keys.md) — Related CL-PM integration decision
* [constitution.md](../../../constitution.md) §1.3 — Integration pattern exceptions require ADR

***

**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.
