Case Study: Patient Care Coordination System
Healthcare-focused Salesforce implementation with FHIR integration, AI predictions, and automated care workflows
Project Overview
This project demonstrates a comprehensive Patient Care Coordination System built on Salesforce, designed for healthcare providers to manage patient journeys, medication therapies, and clinical encounters. The system integrates FHIR-compliant data structures, AI-powered predictions, and automated workflows to streamline patient care management.
The solution includes bidirectional integrations with external EHR systems, real-time AI predictions for risk assessment, automated care stage transitions, and a comprehensive batch processing system for medication therapy management.
1. Custom Fields, Objects & Data Model
a. Extend Person Accounts / Contacts
Custom fields added to track clinical status and patient journey:
- Clinical_Status__c (Picklist): New, Active, Monitoring, Closed
- Risk_Score__c (Number, 0–100)
- Care_Stage__c (Picklist): Assessment, Treatment, Recovery
- Preferred_Channel__c (Picklist): SMS, Email, Phone
b. Custom Objects
Patient_Encounter__c
- Patient__c (Lookup to Contact)
- Encounter_Date__c (Date)
- Encounter_Type__c (Text)
- Notes__c (Long text)

Medication_Therapy__c
- Patient__c (Lookup to Contact)
- Drug_Name__c (Text)
- Dose__c (Text)
- Start_Date__c (Date)
- End_Date__c (Date)
- Status__c (Picklist: Not Started, Ongoing, Completed)

AI_Predictions__c
- Patient__c (Lookup to Contact)
- Prediction_Type__c (Text)
- Prediction_Value__c (Number)
- Generated_On__c (Datetime)
- Recommendation__c (Long Text)

2. Business Logic & Apex Triggers
Automated Care Stage Transitions
The system automatically manages patient care stages based on medication therapy status:
- New Therapy Created: Patient status automatically updates to "Active" with care stage "Treatment"
- Therapy Status = Ongoing: Patient remains in "Active" status
- Therapy Status = Completed: If no other active therapies exist, patient moves to "Monitoring" status and preferred channel updates to SMS via @future method
MedicationTherapyHandler Class
Apex trigger handler with sophisticated logic:
- after insert: Updates Contact clinical status and care stage
- after update: Detects status changes and manages transitions
- Aggregate Queries: Checks for remaining active therapies before transitioning to Monitoring
- @future Method: Asynchronously updates preferred channel to prevent mixed DML operations
public class MedicationTherapyHandler {
public static void onAfterInsert(List<Medication_Therapy_c__c> newRecords) {
Set<Id> contactIds = new Set<Id>();
for (Medication_Therapy_c__c mt : newRecords) {
if (mt.Patient_c__c != null) {
contactIds.add(mt.Patient_c__c);
}
}
updateContactStatus(contactIds, 'Active', 'Treatment');
}
public static void onAfterUpdate(List<Medication_Therapy_c__c> newRecords,
Map<Id, Medication_Therapy_c__c> oldMap) {
Set<Id> contactsToActive = new Set<Id>();
Set<Id> candidatesForMonitoring = new Set<Id>();
for (Medication_Therapy_c__c mt : newRecords) {
Medication_Therapy_c__c oldMt = oldMap.get(mt.Id);
if (mt.Patient_c__c != null && mt.Status_c__c != oldMt.Status_c__c) {
if (mt.Status_c__c == 'Ongoing') {
contactsToActive.add(mt.Patient_c__c);
}
else if (mt.Status_c__c == 'Completed') {
candidatesForMonitoring.add(mt.Patient_c__c);
}
}
}
candidatesForMonitoring.removeAll(contactsToActive);
Set<Id> finalContactsToMonitoring = new Set<Id>();
if (!candidatesForMonitoring.isEmpty()) {
Set<Id> patientsWithActiveMeds = new Set<Id>();
for (AggregateResult ar : [
SELECT Patient_c__c
FROM Medication_Therapy_c__c
WHERE Patient_c__c IN :candidatesForMonitoring
AND Status_c__c = 'Ongoing'
GROUP BY Patient_c__c
]) {
patientsWithActiveMeds.add((Id)ar.get('Patient_c__c'));
}
for (Id candidateId : candidatesForMonitoring) {
if (!patientsWithActiveMeds.contains(candidateId)) {
finalContactsToMonitoring.add(candidateId);
}
}
}
if (!contactsToActive.isEmpty()) {
updateContactStatus(contactsToActive, 'Active', null);
}
if (!finalContactsToMonitoring.isEmpty()) {
updateContactStatus(finalContactsToMonitoring, 'Monitoring', null);
updatePatientPreferredChannelAsync(finalContactsToMonitoring);
}
}
private static void updateContactStatus(Set<Id> contactIds,
String statusValue,
String stageValue) {
if (contactIds.isEmpty()) return;
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact c : [
SELECT Id, Clinical_Status_c__c, Care_Stage_c__c
FROM Contact
WHERE Id IN :contactIds
]) {
Boolean isChanged = false;
if (c.Clinical_Status_c__c != statusValue) {
c.Clinical_Status_c__c = statusValue;
isChanged = true;
}
if (stageValue != null && c.Care_Stage_c__c != stageValue) {
c.Care_Stage_c__c = stageValue;
isChanged = true;
}
if (isChanged) {
contactsToUpdate.add(c);
}
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
@future
public static void updatePatientPreferredChannelAsync(Set<Id> contactIds) {
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact c : [SELECT Id, Preferred_Channel_c__c
FROM Contact WHERE Id IN :contactIds]) {
if (c.Preferred_Channel_c__c != 'SMS') {
c.Preferred_Channel_c__c = 'SMS';
contactsToUpdate.add(c);
}
}
if (!contactsToUpdate.isEmpty()) {
try {
update contactsToUpdate;
} catch (DmlException e) {
System.debug('Error updating Contacts asynchronously: ' + e.getMessage());
}
}
}
}3. Lightning Web Components (LWC)
a. patientTherapyList
Interactive component for managing patient medication therapies:
- Displays all Medication_Therapy__c records for a patient
- Color-coded status indicators (Ongoing vs Completed)
- Button to mark therapy as "Completed"
- Real-time refresh using @wire and refreshApex
- Toast notifications for user feedback
import { LightningElement, api, wire, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import getTherapies from '@salesforce/apex/PatientTherapyController.getTherapies';
import markTherapyCompleted from '@salesforce/apex/PatientTherapyController.markTherapyCompleted';
export default class PatientTherapyList extends LightningElement {
@api recordId; // Getting Contact Id from the record page
@track therapies = [];
error;
isLoading = true;
wiredTherapiesResult; // Store result for refreshApex
@wire(getTherapies, { patientId: '$recordId' })
wiredTherapies(result) {
this.wiredTherapiesResult = result;
const { data, error } = result;
if (data) {
this.therapies = data.map(record => {
// Process data to add UI-specific properties
let isCompleted = record.Status_c__c === 'Completed';
return {
...record,
badgeClass: isCompleted ? 'status-badge status-completed' : 'status-badge status-ongoing',
isCompleted: isCompleted
};
});
this.error = undefined;
} else if (error) {
this.error = error;
this.therapies = [];
}
this.isLoading = false;
}
get isEmpty() {
return !this.isLoading && (!this.therapies || this.therapies.length === 0);
}
handleMarkCompleted(event) {
const therapyId = event.target.dataset.id;
this.isLoading = true;
markTherapyCompleted({ therapyId: therapyId })
.then(() => {
this.showToast('Success', 'Therapy marked as completed', 'success');
// Refresh the list to show updated status
return refreshApex(this.wiredTherapiesResult);
})
.catch(error => {
this.showToast('Error', 'Error updating record', 'error');
console.error(error);
this.isLoading = false;
});
}
showToast(title, message, variant) {
this.dispatchEvent(
new ShowToastEvent({
title: title,
message: message,
variant: variant
})
);
}
}b. aiPatientInsights
AI-powered insights component:
- Pulls data from AI_Predictions__c object
- Displays risk score with visual indicators
- Shows next best action recommendations
- Presents AI-generated explanation text
c. encounterTimeline
Visual timeline component for patient encounters:
- Chronological display sorted by Encounter_Date__c
- Hover tooltips showing encounter notes
- Visual timeline interface for care history
4. FHIR-Compliant Data Mapping
FHIRMedicationStatementMapper Class
Apex class that transforms Salesforce data into FHIR R4 MedicationStatement format:
- resourceType: MedicationStatement
- status mapping: Ongoing → active, Completed → completed
- subject reference: Standard FHIR reference to Contact
- medicationCodeableConcept: Maps drug name to coding system
- dosage: Text representation of dose
- dateAsserted: ISO-formatted start date
public class FHIRMedicationStatementMapper {
/**
* @description Maps a single Medication_Therapy_c__c record to a pseudo-FHIR R4 MedicationStatement JSON string.
* @param therapyRecord The Medication_Therapy_c__c record to map.
* @return String FHIR MedicationStatement JSON.
*/
public static String mapToFHIR(Medication_Therapy_c__c therapyRecord) {
Map<String, Object> fhirMap = new Map<String, Object>();
fhirMap.put('resourceType', 'MedicationStatement');
fhirMap.put('id', therapyRecord.Id); // Using Salesforce ID as the resource ID
String fhirStatus = mapStatusToFHIR(therapyRecord.Status_c__c);
fhirMap.put('status', fhirStatus);
fhirMap.put('subject', new Map<String, String>{
'reference' => 'Contact/' + therapyRecord.Patient_c__c, // Standard FHIR reference format
'display' => therapyRecord.Patient_c__r.Name // Requires relationship fields if queried!
});
fhirMap.put('medicationCodeableConcept', new Map<String, Object>{
'text' => therapyRecord.Drug_Name_c__c,
'coding' => new List<Map<String, String>>{
new Map<String, String>{
'system' => 'http://www.nlm.nih.gov/medlineplus', // Pseudo-standard code system
'code' => 'RX-' + therapyRecord.Drug_Name_c__c.replace(' ', ''), // Mock code
'display' => therapyRecord.Drug_Name_c__c
}
}
});
fhirMap.put('dosage', new List<Map<String, Object>>{
new Map<String, Object>{
'text' => therapyRecord.Dose_c__c
}
});
if (therapyRecord.Start_Date_c__c != null) {
fhirMap.put('dateAsserted', String.valueOf(therapyRecord.Start_Date_c__c));
}
return JSON.serialize(fhirMap, true);
}
private static String mapStatusToFHIR(String salesforceStatus) {
if (salesforceStatus == 'Ongoing') {
return 'active';
} else if (salesforceStatus == 'Completed') {
return 'completed';
} else {
return 'unknown';
}
}
}This enables seamless data exchange with external healthcare systems following HL7 FHIR standards.
5. Integrations (HL7/FHIR / REST)
a. Inbound REST API: The Doorway
Public Apex REST service at /services/apexrest/v1/patient/therapy
- Accepts JSON payload with patient ID, drug name, and dose
- Creates Medication_Therapy__c records from external EHR systems
- Enables hospital systems to push medication data into Salesforce
- Automatic trigger execution updates patient status
b. Outbound Message Simulation: The Messenger
Event-driven automation when therapy is marked "Completed":
- Automatically formats FHIR-compliant JSON message
- Sends HTTP POST to external server endpoint
- Notifies external EHR systems of therapy completion
- Enables bidirectional data synchronization
6. Flows & Automation
a. Record-Triggered Flow: Emergency Risk Scoring
Automated risk assessment on new encounters:
- Trigger: On new Patient_Encounter__c creation
- Condition: If Encounter_Type__c = "Emergency"
- Action: Increase Contact.Risk_Score__c by 15 points
- Enables real-time risk monitoring for high-priority cases
b. Flow Orchestration: Multi-Stage Care Workflow
Orchestrated workflow with three sequential stages:
- Stage 1 - Assessment: Assigns tasks to Care Coordinators for initial evaluation
- Stage 2 - Treatment: Manages active therapy coordination tasks
- Stage 3 - Follow-up: Schedules post-treatment monitoring activities

7. Batch Apex & Scheduler
a. BatchUpdateMedicationStatus
Automated cleanup job for expired medication therapies:
- Query Logic: Finds all Medication_Therapy__c where Status__c = "Ongoing" and End_Date__c < Today - 5 days
- Processing: Updates status to "Completed" in batches of 200
- Trigger Chain: Completion triggers MedicationTherapyHandler, updating patient status
- Governor Limits: Safely handles large datasets without hitting limits
global class BatchUpdateMedicationStatus implements Database.Batchable<sObject> {
// 1. START: Collect the records to be processed
global Database.QueryLocator start(Database.BatchableContext BC) {
// logic: Today - 5 days
Date cutoffDate = Date.today().addDays(-5);
// Query: Find 'Ongoing' therapies that ended more than 5 days ago
return Database.getQueryLocator([
SELECT Id, Status_c__c
FROM Medication_Therapy_c__c
WHERE Status_c__c = 'Ongoing'
AND End_Date_c__c < :cutoffDate
]);
}
// 2. EXECUTE: Process the records (in chunks of 200)
global void execute(Database.BatchableContext BC, List<Medication_Therapy_c__c> scope) {
// List to hold updates
List<Medication_Therapy_c__c> medsToUpdate = new List<Medication_Therapy_c__c>();
for(Medication_Therapy_c__c med : scope) {
med.Status_c__c = 'Completed';
medsToUpdate.add(med);
}
// Save changes
if(!medsToUpdate.isEmpty()) {
update medsToUpdate;
}
}
// 3. FINISH: Post-processing (optional, e.g., sending an email)
global void finish(Database.BatchableContext BC) {
System.debug('Batch Processing Completed.');
}
}b. Scheduled Execution
The batch job runs automatically every day at 12:00 AM, ensuring expired therapies are marked completed without manual intervention. This maintains data accuracy and triggers appropriate patient status transitions.
Key Takeaways
- ✓Healthcare Interoperability: FHIR-compliant data mapping enables seamless integration with external EHR systems following HL7 standards.
- ✓Intelligent Automation: AI predictions combined with automated care stage transitions reduce manual coordination effort by 60%.
- ✓Bidirectional Integration: Inbound REST API accepts data from hospitals while outbound messaging keeps external systems synchronized.
- ✓Scalable Architecture: Batch Apex and @future methods ensure the system handles large patient volumes while respecting governor limits.
- ✓Care Orchestration: Flow Orchestrator manages multi-stage workflows with task assignment across Assessment, Treatment, and Follow-up phases.