
Implementation Guide: Analyze no-show patterns and recommend intervention outreach for high-risk patients
Step-by-step implementation guide for deploying AI to analyze no-show patterns and recommend intervention outreach for high-risk patients for Allied & Mental Health clients.
Hardware Procurement
Next-Generation Firewall
$700–$900 per unit (MSP cost with 1-year UTP bundle) / $1,000–$1,200 suggested resale
HIPAA-compliant network perimeter security with IPS, web filtering, SSL inspection, SD-WAN, and ZTNA. Protects all PHI in transit between practice endpoints and cloud-based AI/analytics platforms. Required for HIPAA Security Rule compliance.
Next-Generation Firewall
$1,400–$1,800 per unit (MSP cost with 1-year UTP bundle) / $1,900–$2,400 suggested resale
Higher-throughput NGFW for multi-provider clinics with more endpoints. Same security feature set as FortiGate 40F but with greater capacity for concurrent connections and VPN tunnels.
Managed Network Switch
$250–$350 per unit (MSP cost) / $400–$500 suggested resale
Managed Layer 2/3 switch for VLAN segmentation between clinical workstations, reception/front-desk systems, guest Wi-Fi, and IoT devices. Integrates with FortiGate Security Fabric for unified management.
Reception Dashboard Tablet
$350 per unit (MSP cost) / $450–$500 suggested resale
Wall-mounted or counter-mounted tablet at reception for real-time display of daily no-show risk dashboard. Staff can see color-coded risk scores for upcoming appointments and trigger manual outreach with one tap.
Tablet Enclosure/Mount
$80 per unit (MSP cost) / $120 suggested resale
Secure wall-mount or counter-mount enclosure for iPad tablets at reception. Prevents theft, provides clean installation, and includes cable management for power.
Standard Workstation (if client needs refresh)
Dell OptiPlex 3000 Micro (i5-12500T, 16GB RAM, 256GB SSD)
$650–$800 per unit (MSP cost) / $950–$1,100 suggested resale
Standard office workstation for clinical and administrative staff. Browser-based access to analytics dashboards and patient engagement platforms. No GPU or specialized hardware required — this is a cloud-first analytics workload.
Software Procurement
healow Genie
$249/seat/month
Primary AI-powered no-show prediction and patient outreach platform. Provides: (1) ML-based no-show risk scoring using appointment history, demographics, and behavioral patterns; (2) automated multi-channel outreach — voice calls, SMS, chat — to high-risk patients; (3) 24/7 availability with multilingual support; (4) HIPAA-compliant with BAA available. This is the core intelligence engine of the solution.
Weave
$250/month per location (Pro plan)
Alternative/complementary patient communication platform providing VoIP phones, HIPAA-compliant 2-way texting, automated appointment reminders, review management, and AI voicemail transcriptions. Use as a fallback if healow Genie does not integrate with the client's specific EHR, or as a simpler communications-first approach.
Curogram
Contact vendor for tier pricing; typically $150–$300/month per location
Budget-friendly HIPAA-compliant 2-way texting and patient engagement platform. Useful for smaller practices that need outreach capabilities at lower cost. Includes appointment reminders, patient intake forms, and reputation management.
Microsoft Power BI Pro
$10/user/month
Business intelligence dashboard for visualizing no-show risk trends, provider-level analytics, day-of-week and time-of-day patterns, and ROI reporting. Connects to Azure data services or can ingest exported data from healow/EHR systems. Used by practice managers and MSP for reporting.
FortiGate Unified Threat Protection (UTP) Subscription
Included in hardware bundle pricing above; renewal ~$300–$500/year for FG-40F, ~$600–$900/year for FG-60F
Ongoing security subscription providing IPS signatures, antivirus, web filtering, and FortiCare support. Required for maintaining HIPAA-compliant network security posture.
Twilio Programmable Messaging (HIPAA-eligible)
$0.0079/SMS segment outbound; $150/month Twilio HIPAA environment fee
HIPAA-eligible SMS API for custom outreach workflows if building custom intervention logic. Used only in the custom ML approach (Approach B) or for supplemental outreach not covered by the primary platform. Requires BAA execution with Twilio.
Azure Machine Learning (Custom ML approach only)
~$70–$140/month for D-series VM training; storage ~$0.018/GB/month; inference ~$50–$100/month
Cloud ML platform for training and deploying custom no-show prediction models when the turnkey SaaS approach is insufficient. HIPAA-eligible with signed BAA. Only needed for Approach B (custom ML).
Cliniko EHR
$45–$395/month depending on practitioner count
Recommended EHR/practice management system for allied health practices requiring API-based integration. Cliniko offers a full REST API that enables programmatic extraction of appointment data for custom no-show analytics. Only relevant if client is evaluating EHR migration or already uses Cliniko.
Prerequisites
- Active EHR or Practice Management System — The practice must have a functioning EHR/PM system (SimplePractice, TherapyNotes, Cliniko, Jane App, Valant, or similar) with at minimum 6 months of appointment history data including show/no-show/cancellation records
- Minimum appointment data volume — At least 500–1,000 historical appointment records (including no-shows and cancellations) for the prediction model to have sufficient training signal. Practices with fewer than 500 records should accumulate data for 2–3 months before enabling predictive features
- Internet connectivity — Minimum 50 Mbps symmetric broadband (100+ Mbps recommended for multi-provider practices). All AI and analytics processing occurs in the cloud
- Business email system — Active business email (Microsoft 365 or Google Workspace) for staff accounts, outreach campaigns, and platform administration
- SMS-capable communication channel — Either an existing practice phone number capable of sending/receiving SMS or willingness to provision a new number through the outreach platform (healow, Weave, or Curogram)
- Designated project champion — One staff member (practice manager or lead clinician) who will serve as the internal champion, make configuration decisions, and drive adoption among clinical staff
- HIPAA Security Risk Assessment — A current (within 12 months) HIPAA Security Risk Assessment must be completed or scheduled. If the practice has never had one, this should be performed as a pre-project engagement ($2,000–$5,000)
- 42 CFR Part 2 assessment — Determine whether the practice provides any substance use disorder (SUD) diagnosis, treatment, or referral. If yes, a specialized compliance review is required before including SUD patient data in any predictive model
- Modern web browsers — All workstations must run current versions of Chrome, Edge, or Safari for dashboard access. Internet Explorer is not supported
- Admin credentials and access — MSP must have or obtain admin-level access to the EHR/PM system, practice email system, DNS records, and network equipment for configuration
Installation Steps
Step 1: Pre-Implementation Compliance and Data Audit
Before any technical work begins, conduct a compliance and data readiness audit. This step is critical for mental health practices due to enhanced privacy protections under HIPAA, 42 CFR Part 2, and state mental health laws. Meet with the practice owner/manager to: (1) Document whether the practice provides any SUD treatment (triggers 42 CFR Part 2 requirements); (2) Identify the EHR/PM system in use and its data export/API capabilities; (3) Inventory all appointment data fields available; (4) Review existing HIPAA policies and BAA inventory; (5) Assess state-specific mental health privacy requirements; (6) Confirm that psychotherapy notes will be EXCLUDED from all data pipelines.
This step is NON-NEGOTIABLE. Do not proceed with technical implementation until compliance boundaries are clearly defined. If the practice treats SUD patients, engage a healthcare compliance attorney before including their scheduling data in any predictive model. Document all findings in a Pre-Implementation Compliance Checklist and obtain practice owner signature.
Step 2: Network Security Deployment — FortiGate NGFW
Install and configure the FortiGate next-generation firewall as the practice's network perimeter security device. This ensures all PHI transmitted to cloud-based AI platforms traverses a secured, monitored connection.
# hostname, WAN interface, VLANs, firewall policy, and DNS
# Initial access — connect laptop to FortiGate port1, navigate to:
# https://192.168.1.99 (default management IP)
# Default credentials: admin / (blank password)
# CLI: Set hostname
config system global
set hostname "CLIENTNAME-FG40F"
set timezone 12
end
# CLI: Configure WAN interface (adjust for ISP settings)
config system interface
edit "wan1"
set mode dhcp
set allowaccess ping https ssh
next
end
# CLI: Create VLAN for clinical workstations
config system interface
edit "clinical-vlan"
set vdom "root"
set ip 10.10.10.1 255.255.255.0
set allowaccess ping https ssh
set interface "internal"
set vlanid 10
next
end
# CLI: Create VLAN for reception/IoT (tablets)
config system interface
edit "reception-vlan"
set vdom "root"
set ip 10.10.20.1 255.255.255.0
set allowaccess ping https
set interface "internal"
set vlanid 20
next
end
# CLI: Enable IPS, AV, Web Filter on LAN-to-WAN policy
config firewall policy
edit 1
set name "clinical-to-internet"
set srcintf "clinical-vlan"
set dstintf "wan1"
set srcaddr "all"
set dstaddr "all"
set action accept
set schedule "always"
set service "ALL"
set utm-status enable
set av-profile "default"
set ips-sensor "default"
set webfilter-profile "default"
set ssl-ssh-profile "certificate-inspection"
set nat enable
set logtraffic all
next
end
# CLI: Configure DNS to use FortiGuard
config system dns
set primary 208.91.112.53
set secondary 208.91.112.52
endFor HIPAA compliance, ensure: (1) All firewall policies log traffic ('set logtraffic all'); (2) SSL inspection is enabled for outbound HTTPS (use certificate-inspection profile to avoid breaking healthcare applications); (3) FortiGuard security subscriptions are active and updating. Save the FortiGate configuration backup to a secure, encrypted location. Document the network topology including VLAN assignments in the client's IT documentation.
Step 3: Workstation and Endpoint Preparation
Prepare all staff workstations and reception tablets that will interact with the no-show analytics platform.
# Windows: Verify OS version
winver
# Windows: Update Chrome via command line
winget upgrade Google.Chrome
# Windows: Set DNS to FortiGate (if not using DHCP from FortiGate)
netsh interface ip set dns "Ethernet" static 10.10.10.1For iPad reception dashboards, use Apple's Guided Access feature to lock the device to the dashboard browser tab. This prevents staff or patients from navigating away. Set the iPad to never auto-lock (Settings > Display & Brightness > Auto-Lock > Never) and keep it plugged into power in the Compulocks enclosure. If the practice uses Microsoft Intune or Jamf for MDM, enroll all devices.
Step 4: Execute Business Associate Agreements (BAAs)
Before transmitting any PHI to any vendor platform, execute BAAs with every vendor in the technology stack. This is a HIPAA legal requirement, not optional.
CRITICAL: Do not transmit, upload, or connect ANY patient data until all relevant BAAs are fully executed and stored securely. Key BAA clauses to verify: (1) Data usage restrictions — vendor must not use PHI for model training without explicit written consent; (2) Breach notification obligations — vendor must notify within contractually specified timeframe (aim for 24–72 hours); (3) Data return/destruction upon contract termination; (4) Subcontractor obligations. Maintain a BAA register document listing all vendors, execution dates, and renewal dates.
Step 5: EHR Data Assessment and Export Setup
Connect to or extract appointment data from the practice's EHR/PM system. The approach varies significantly by EHR platform. This data will feed the no-show prediction engine.
For Cliniko (API-based): Use the REST API to programmatically pull appointment data. For SimplePractice/TherapyNotes (no API): Set up scheduled CSV exports. For eClinicalWorks practices: healow Genie integrates natively.
Required data fields: appointment_id, patient_id, appointment_datetime, appointment_type, provider_id, booking_datetime (for lead-time calculation), status (attended/no-show/cancelled/late-cancel), patient_age, patient_zip, insurance_type, previous_no_show_count.
# Install requests library
pip install requests
# Test API connectivity
import requests
API_KEY = 'YOUR_CLINIKO_API_KEY'
SHARD = 'api2' # Check your Cliniko shard
BASE_URL = f'https://{SHARD}.cliniko.com/v1'
headers = {
'User-Agent': 'YourMSPName (your@email.com)',
'Accept': 'application/json',
'Authorization': f'Basic {API_KEY}' # Base64 encode API_KEY:''
}
# Fetch recent appointments
response = requests.get(
f'{BASE_URL}/individual_appointments',
headers=headers,
params={'page': 1, 'per_page': 100, 'sort': 'appointment_start:desc'}
)
print(response.status_code)
print(response.json()['total_entries'])IMPORTANT DATA HANDLING: (1) All exported CSV files containing PHI must be stored on encrypted drives (BitLocker on Windows, FileVault on macOS). (2) Never email unencrypted CSV files containing PHI. (3) After importing data into the platform, securely delete local copies using a secure deletion tool. (4) For 42 CFR Part 2 practices: Flag or exclude SUD patient records before export unless proper consent and compliance framework is in place. (5) Apply the HIPAA Minimum Necessary standard — only extract the data fields needed for no-show prediction, never full clinical notes or psychotherapy content.
Step 6: Deploy and Configure healow Genie Platform
Set up the healow Genie AI platform as the primary no-show prediction and outreach engine.
healow Genie Key Configuration Reference
MENTAL HEALTH PRIVACY CRITICAL: All automated outreach messages must be carefully crafted to NEVER reveal the nature of the practice or treatment type. Do not use words like 'therapy,' 'counseling,' 'psychiatry,' 'mental health,' or any diagnosis-related terms in any automated communication. Messages should only reference 'your appointment' at 'Practice Name.' Voicemails should be equally generic — assume someone other than the patient may hear the message. Review all message templates with the practice owner before going live. This is both a HIPAA requirement and a patient safety/trust concern.
Step 7: Configure Power BI Analytics Dashboard
Set up Microsoft Power BI to provide practice managers and the MSP with visual analytics on no-show patterns, prediction accuracy, and outreach effectiveness. This dashboard supplements healow Genie's built-in reporting with custom views.
# No-Show Rate by Provider
NoShowRate_Provider =
DIVIDE(
COUNTROWS(FILTER(Appointments, Appointments[Status] = "No-Show")),
COUNTROWS(Appointments),
0
)
# No-Show Rate by Day of Week
NoShowRate_DayOfWeek =
DIVIDE(
CALCULATE(COUNTROWS(Appointments), Appointments[Status] = "No-Show"),
COUNTROWS(Appointments),
0
)
# Average Lead Time for No-Shows vs Shows
AvgLeadTime_NoShow =
CALCULATE(
AVERAGE(Appointments[LeadTimeDays]),
Appointments[Status] = "No-Show"
)
# Outreach Effectiveness (requires outreach data)
OutreachConversionRate =
DIVIDE(
COUNTROWS(FILTER(Outreach, Outreach[PatientResponded] = TRUE())),
COUNTROWS(Outreach),
0
)Power BI dashboards should include these key views: (1) Overall no-show rate trend (weekly/monthly); (2) No-show rate by provider; (3) No-show rate by day-of-week and time-of-day; (4) No-show rate by appointment type; (5) Lead time analysis (how far in advance was appointment booked); (6) Outreach effectiveness (response rates by channel); (7) Financial impact estimate (no-shows × average appointment value). Use Power BI Row-Level Security (RLS) to ensure providers only see their own patient data if needed. Publish to Power BI Service with appropriate workspace permissions.
Step 8: Configure Outreach Message Templates and Workflow Rules
Build the intervention outreach workflows that activate when healow Genie identifies a high-risk patient. This step defines WHAT messages are sent, WHEN they are sent, and through WHICH channels. The workflow must comply with mental health privacy requirements — messages must never reveal the treatment specialty.
Set up three outreach tiers:
- Tier 1 (Low Risk <40%): Standard 24-hour SMS reminder
- Tier 2 (Medium Risk 40-70%): SMS at 72 hours + SMS at 24 hours + email at 48 hours
- Tier 3 (High Risk >70%): Personal phone call from staff at 72 hours + SMS at 48 hours + SMS at 24 hours + SMS at 2 hours
# SMS Template: Standard Reminder (Tier 1)
# ----------------------------------------
# Hi [FIRST_NAME], this is a reminder from [PRACTICE_NAME] that you have
# an appointment on [DATE] at [TIME]. Reply YES to confirm, CHANGE to
# reschedule, or call us at [PHONE]. We look forward to seeing you!# SMS Template: Medium Risk Follow-Up (Tier 2 - 72hr)
# ----------------------------------------
# Hi [FIRST_NAME], [PRACTICE_NAME] here. Your appointment is coming up on
# [DATE] at [TIME]. We want to make sure this still works for you. Reply
# YES to confirm or CHANGE if you need a different time. We're happy to help!SMS Template: High Risk Personal Touch — Tier 3 (48hr)
Staff Call Script: High Risk — Tier 3 (72hr phone call)
Email Template: Appointment Confirmation Request — Tier 2 (48hr)
MESSAGE PRIVACY RULES (enforce strictly): (1) NEVER include the words therapy, counseling, therapist, psychiatrist, psychologist, mental health, behavioral health, or any diagnosis in any automated message; (2) NEVER leave a voicemail that reveals practice specialty — assume a family member or employer could hear it; (3) Use only the generic practice name, never a specialty descriptor; (4) SMS messages should not include links that reveal the practice type in the URL path; (5) Configure the platform to STOP outreach after a patient responds (do not over-message); (6) Maintain an opt-out mechanism for all SMS communications (required by TCPA/carrier regulations). Have the practice owner review and approve ALL templates before activation.
Step 9: Historical Data Import and Model Calibration
Import historical appointment data into the prediction platform to calibrate the no-show model for this specific practice's patterns. Every practice has unique no-show patterns influenced by patient population, geography, appointment types, and provider styles.
import pandas as pd
import numpy as np
from datetime import datetime
# Load exported appointment data
df = pd.read_csv('appointments_export.csv')
# Standardize column names
df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
# Parse dates
df['appointment_date'] = pd.to_datetime(df['appointment_date'])
df['booking_date'] = pd.to_datetime(df['booking_date'])
# Calculate lead time (days between booking and appointment)
df['lead_time_days'] = (df['appointment_date'] - df['booking_date']).dt.days
# Standardize status to binary outcome
status_map = {
'Attended': 0, 'Completed': 0, 'Showed': 0, 'Arrived': 0,
'No-Show': 1, 'No Show': 1, 'Missed': 1, 'DNA': 1,
'Cancelled': np.nan, 'Late Cancel': np.nan, # Exclude cancellations from no-show model
'Rescheduled': np.nan
}
df['no_show'] = df['status'].map(status_map)
df_model = df.dropna(subset=['no_show'])
# Feature engineering
df_model['day_of_week'] = df_model['appointment_date'].dt.dayofweek
df_model['hour_of_day'] = df_model['appointment_date'].dt.hour
df_model['is_monday'] = (df_model['day_of_week'] == 0).astype(int)
df_model['is_first_appointment'] = (df_model.groupby('patient_id').cumcount() == 0).astype(int)
# Calculate patient-level historical no-show rate
patient_history = df_model.groupby('patient_id')['no_show'].agg(['sum','count']).reset_index()
patient_history.columns = ['patient_id', 'total_no_shows', 'total_appointments']
patient_history['historical_no_show_rate'] = patient_history['total_no_shows'] / patient_history['total_appointments']
df_model = df_model.merge(patient_history[['patient_id','historical_no_show_rate']], on='patient_id', how='left')
# Summary statistics
print(f'Total appointments: {len(df_model)}')
print(f'No-show rate: {df_model["no_show"].mean():.1%}')
print(f'Unique patients: {df_model["patient_id"].nunique()}')
print(f'Date range: {df_model["appointment_date"].min()} to {df_model["appointment_date"].max()}')
print(f'\nNo-show rate by day of week:')
print(df_model.groupby('day_of_week')['no_show'].mean().round(3))
# Export cleaned data for import into prediction platform
df_model.to_csv('appointments_cleaned.csv', index=False)
print(f'\nCleaned data exported: {len(df_model)} records')DATA QUALITY CHECKS: (1) Verify the no-show rate looks reasonable (typically 15–40% for mental health); if it's below 5% or above 60%, investigate data quality issues; (2) Check for duplicate records; (3) Ensure patient IDs are consistent across the export period; (4) If the practice changed EHR systems during the export period, data may be inconsistent — use only data from the current system; (5) Exclude COVID-era data (March 2020 – June 2021) from model training if telehealth patterns differ significantly from current operations; (6) All data handling must occur on encrypted drives and be securely deleted after import.
Step 10: Pilot Testing Period (2 Weeks)
Run the system in shadow mode for 2 weeks before activating automated outreach. During this period: (1) The prediction engine scores all upcoming appointments with no-show risk; (2) Staff review risk scores daily but do NOT act on them yet; (3) After each day, compare predictions to actual outcomes; (4) Track prediction accuracy, false positive rate, and false negative rate; (5) Adjust risk thresholds if needed; (6) Verify all message templates render correctly; (7) Test outreach delivery to staff members' personal phones (not patients) to verify message content and timing.
# Daily pilot tracking spreadsheet columns:
# Date | Patient_ID | Appointment_Time | Predicted_Risk | Risk_Tier | Actual_Outcome | Correct_Prediction?
# Calculate pilot accuracy metrics (Python)
import pandas as pd
pilot = pd.read_csv('pilot_tracking.csv')
# Accuracy: How often was the prediction correct?
# For binary: High Risk (>70%) predicted no-show, Low Risk (<40%) predicted show
pilot['predicted_noshow'] = pilot['predicted_risk'] > 0.5
pilot['actual_noshow'] = pilot['actual_outcome'] == 'No-Show'
from sklearn.metrics import classification_report, confusion_matrix
print(classification_report(pilot['actual_noshow'], pilot['predicted_noshow']))
print(confusion_matrix(pilot['actual_noshow'], pilot['predicted_noshow']))
# Target metrics for go-live:
# - Sensitivity (recall for no-shows) > 70%
# - Specificity (recall for shows) > 60%
# - Overall accuracy > 70%
# - False positive rate < 30% (we don't want to over-message patients who would have come)CRITICAL GO/NO-GO CRITERIA: Do NOT activate automated outreach until: (1) Prediction accuracy exceeds 70% overall; (2) The practice owner has reviewed and approved all message templates; (3) Staff have been trained on how to interpret risk scores and handle the phone outreach script; (4) At least one test message has been successfully sent to a staff member's phone through each channel (SMS, voice, email); (5) BAAs are confirmed executed for all vendors. If accuracy is below 70%, extend the pilot period and review data quality.
Step 11: Go-Live: Activate Automated Outreach
config webfilter urlfilter
edit 1
set name "allowed-healthcare-saas"
config entries
edit 1
set url "*.healow.com"
set type wildcard
set action allow
next
edit 2
set url "*.powerbi.com"
set type wildcard
set action allow
next
end
next
endFIRST WEEK MONITORING CHECKLIST: (1) Check SMS delivery reports daily — any failed messages? (2) Monitor patient opt-out rates — if >5% opt out in the first week, review message frequency and content; (3) Ask front-desk staff if any patients complained about messages; (4) Verify that messages are correctly personalized (no placeholder text showing); (5) Confirm Tier 3 phone calls are actually being made by assigned staff; (6) Review the day's risk scores vs. actual outcomes to track ongoing accuracy; (7) Check that no messages inadvertently reveal practice specialty.
Step 12: Post-Go-Live Optimization (Weeks 2–4)
Fine-tune the system based on real-world performance data.
baseline_noshow_rate = 0.28 # 28% pre-implementation (example)
current_noshow_rate = 0.18 # 18% post-implementation (example)
avg_appointment_value = 150 # Average revenue per appointment
weekly_appointments = 120 # Total weekly appointments across all providers
weekly_recovered_appointments = weekly_appointments * (baseline_noshow_rate - current_noshow_rate)
weekly_recovered_revenue = weekly_recovered_appointments * avg_appointment_value
monthly_recovered_revenue = weekly_recovered_revenue * 4.33
annual_recovered_revenue = monthly_recovered_revenue * 12
monthly_system_cost = 249 * 5 # 5 provider seats at $249/mo = $1,245
monthly_net_benefit = monthly_recovered_revenue - monthly_system_cost
print(f'Baseline no-show rate: {baseline_noshow_rate:.0%}')
print(f'Current no-show rate: {current_noshow_rate:.0%}')
print(f'Weekly recovered appointments: {weekly_recovered_appointments:.1f}')
print(f'Monthly recovered revenue: ${monthly_recovered_revenue:,.0f}')
print(f'Monthly system cost: ${monthly_system_cost:,.0f}')
print(f'Monthly NET benefit: ${monthly_net_benefit:,.0f}')
print(f'Annual NET benefit: ${monthly_net_benefit * 12:,.0f}')
print(f'ROI: {(monthly_net_benefit / monthly_system_cost * 100):.0f}%')Typical results: Most practices see a 10–15 percentage point reduction in no-show rates within the first month of active outreach. For a 5-provider practice with 120 weekly appointments and a $150 average appointment value, a 10-point reduction recovers approximately $7,800/month or $93,600/year in revenue — a significant ROI against the ~$1,245/month platform cost. Present these numbers to the practice owner at the 30-day review meeting.
Custom AI Components
No-Show Risk Scoring Model
Type: skill
A machine learning model that predicts the probability of a patient missing a scheduled appointment. This model analyzes historical appointment data, patient behavior patterns, appointment characteristics, and temporal features to produce a risk score from 0 to 1 for each upcoming appointment. This component is only needed if building a custom ML solution instead of using healow Genie's built-in prediction. The model uses gradient-boosted decision trees (XGBoost) which have been shown to outperform logistic regression and neural networks for structured tabular healthcare data of this type.
Implementation:
# No-Show Risk Scoring Model for Allied & Mental Health Practices
# no_show_prediction_model.py
# No-Show Risk Scoring Model for Allied & Mental Health Practices
# Requirements: pip install pandas numpy scikit-learn xgboost joblib
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import classification_report, roc_auc_score, precision_recall_curve
from xgboost import XGBClassifier
import joblib
import json
from datetime import datetime, timedelta
class NoShowPredictor:
"""
Predicts probability of patient no-show for scheduled appointments.
Designed for allied & mental health practices.
"""
def __init__(self, model_path=None):
self.model = None
self.feature_columns = None
self.label_encoders = {}
self.scaler = StandardScaler()
self.risk_thresholds = {'high': 0.70, 'medium': 0.40}
if model_path:
self.load_model(model_path)
def prepare_features(self, df):
"""Engineer features from raw appointment data."""
features = pd.DataFrame()
# Temporal features
features['day_of_week'] = pd.to_datetime(df['appointment_date']).dt.dayofweek
features['hour_of_day'] = pd.to_datetime(df['appointment_date']).dt.hour
features['is_monday'] = (features['day_of_week'] == 0).astype(int)
features['is_friday'] = (features['day_of_week'] == 4).astype(int)
features['is_morning'] = (features['hour_of_day'] < 12).astype(int)
features['is_last_slot'] = (features['hour_of_day'] >= 16).astype(int)
features['month'] = pd.to_datetime(df['appointment_date']).dt.month
# Lead time (days between booking and appointment)
if 'booking_date' in df.columns:
features['lead_time_days'] = (
pd.to_datetime(df['appointment_date']) - pd.to_datetime(df['booking_date'])
).dt.days
features['lead_time_days'] = features['lead_time_days'].clip(0, 365)
else:
features['lead_time_days'] = 7 # Default if not available
# Patient history features
if 'historical_no_show_rate' in df.columns:
features['historical_no_show_rate'] = df['historical_no_show_rate'].fillna(0)
if 'total_appointments' in df.columns:
features['total_appointments'] = df['total_appointments'].fillna(1)
features['is_new_patient'] = (df['total_appointments'] <= 1).astype(int)
if 'total_no_shows' in df.columns:
features['total_no_shows'] = df['total_no_shows'].fillna(0)
if 'days_since_last_visit' in df.columns:
features['days_since_last_visit'] = df['days_since_last_visit'].fillna(30)
# Appointment type (encode categorical)
if 'appointment_type' in df.columns:
if 'appointment_type' not in self.label_encoders:
self.label_encoders['appointment_type'] = LabelEncoder()
features['appointment_type_encoded'] = self.label_encoders['appointment_type'].fit_transform(
df['appointment_type'].fillna('Unknown')
)
else:
# Handle unseen categories
known = set(self.label_encoders['appointment_type'].classes_)
features['appointment_type_encoded'] = df['appointment_type'].fillna('Unknown').apply(
lambda x: x if x in known else 'Unknown'
)
features['appointment_type_encoded'] = self.label_encoders['appointment_type'].transform(
features['appointment_type_encoded']
)
# Provider (encode categorical)
if 'provider_id' in df.columns:
if 'provider_id' not in self.label_encoders:
self.label_encoders['provider_id'] = LabelEncoder()
features['provider_encoded'] = self.label_encoders['provider_id'].fit_transform(
df['provider_id'].astype(str)
)
else:
known = set(self.label_encoders['provider_id'].classes_)
features['provider_encoded'] = df['provider_id'].astype(str).apply(
lambda x: x if x in known else list(known)[0]
)
features['provider_encoded'] = self.label_encoders['provider_id'].transform(
features['provider_encoded']
)
# Insurance type
if 'insurance_type' in df.columns:
if 'insurance_type' not in self.label_encoders:
self.label_encoders['insurance_type'] = LabelEncoder()
features['insurance_encoded'] = self.label_encoders['insurance_type'].fit_transform(
df['insurance_type'].fillna('Unknown')
)
else:
known = set(self.label_encoders['insurance_type'].classes_)
features['insurance_encoded'] = df['insurance_type'].fillna('Unknown').apply(
lambda x: x if x in known else 'Unknown'
)
features['insurance_encoded'] = self.label_encoders['insurance_type'].transform(
features['insurance_encoded']
)
self.feature_columns = features.columns.tolist()
return features
def train(self, df, target_column='no_show'):
"""Train the no-show prediction model."""
print(f'Training on {len(df)} records...')
print(f'No-show rate in training data: {df[target_column].mean():.1%}')
X = self.prepare_features(df)
y = df[target_column].values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Scale features
X_train_scaled = self.scaler.fit_transform(X_train)
X_test_scaled = self.scaler.transform(X_test)
# Calculate scale_pos_weight for class imbalance
n_positive = sum(y_train == 1)
n_negative = sum(y_train == 0)
scale_pos_weight = n_negative / n_positive if n_positive > 0 else 1
# Train XGBoost model
self.model = XGBClassifier(
n_estimators=200,
max_depth=5,
learning_rate=0.1,
scale_pos_weight=scale_pos_weight,
subsample=0.8,
colsample_bytree=0.8,
random_state=42,
eval_metric='auc',
use_label_encoder=False
)
self.model.fit(
X_train_scaled, y_train,
eval_set=[(X_test_scaled, y_test)],
verbose=False
)
# Evaluate
y_pred_proba = self.model.predict_proba(X_test_scaled)[:, 1]
y_pred = (y_pred_proba > 0.5).astype(int)
print('\n--- Model Performance ---')
print(classification_report(y_test, y_pred, target_names=['Show', 'No-Show']))
print(f'AUC-ROC: {roc_auc_score(y_test, y_pred_proba):.3f}')
# Feature importance
importance = dict(zip(self.feature_columns, self.model.feature_importances_))
print('\n--- Feature Importance ---')
for feat, imp in sorted(importance.items(), key=lambda x: x[1], reverse=True):
print(f' {feat}: {imp:.3f}')
# Cross-validation
cv_scores = cross_val_score(
self.model, self.scaler.transform(X), y, cv=5, scoring='roc_auc'
)
print(f'\n5-Fold CV AUC: {cv_scores.mean():.3f} (+/- {cv_scores.std():.3f})')
return {
'auc_roc': roc_auc_score(y_test, y_pred_proba),
'cv_auc_mean': cv_scores.mean(),
'feature_importance': importance
}
def predict_risk(self, df):
"""Predict no-show risk for upcoming appointments."""
X = self.prepare_features(df)
X_scaled = self.scaler.transform(X)
probabilities = self.model.predict_proba(X_scaled)[:, 1]
results = df.copy()
results['no_show_probability'] = probabilities
results['risk_tier'] = pd.cut(
probabilities,
bins=[0, self.risk_thresholds['medium'], self.risk_thresholds['high'], 1.0],
labels=['Low', 'Medium', 'High'],
include_lowest=True
)
results['recommended_action'] = results['risk_tier'].map({
'Low': 'Standard 24hr SMS reminder',
'Medium': 'SMS at 72hr + SMS at 24hr + Email at 48hr',
'High': 'Staff phone call at 72hr + SMS at 48hr + SMS at 24hr + SMS at 2hr'
})
return results[['patient_id', 'appointment_date', 'no_show_probability',
'risk_tier', 'recommended_action']]
def save_model(self, path='noshow_model'):
"""Save trained model and preprocessing artifacts."""
joblib.dump({
'model': self.model,
'scaler': self.scaler,
'label_encoders': self.label_encoders,
'feature_columns': self.feature_columns,
'risk_thresholds': self.risk_thresholds
}, f'{path}.joblib')
print(f'Model saved to {path}.joblib')
def load_model(self, path='noshow_model'):
"""Load a trained model."""
artifacts = joblib.load(f'{path}.joblib')
self.model = artifacts['model']
self.scaler = artifacts['scaler']
self.label_encoders = artifacts['label_encoders']
self.feature_columns = artifacts['feature_columns']
self.risk_thresholds = artifacts['risk_thresholds']
print(f'Model loaded from {path}.joblib')
# --- USAGE EXAMPLE ---
if __name__ == '__main__':
# Load cleaned historical data
df = pd.read_csv('appointments_cleaned.csv')
# Train model
predictor = NoShowPredictor()
metrics = predictor.train(df, target_column='no_show')
predictor.save_model('practice_noshow_model')
# Score upcoming appointments
upcoming = pd.read_csv('upcoming_appointments.csv')
risk_scores = predictor.predict_risk(upcoming)
print('\n--- Upcoming Appointment Risk Scores ---')
print(risk_scores.to_string(index=False))
# Export for outreach system
high_risk = risk_scores[risk_scores['risk_tier'] == 'High']
print(f'\nHigh-risk appointments requiring intervention: {len(high_risk)}')
risk_scores.to_csv('risk_scores_today.csv', index=False)Daily Risk Scoring Pipeline
Type: workflow
An automated daily workflow that runs every morning at 6:00 AM, pulls upcoming appointments for the next 3 days from the EHR, scores them through the no-show prediction model, and routes high-risk patients to the appropriate outreach tier. This workflow orchestrates the connection between the EHR data source, the prediction model, and the outreach communication platform.
Implementation:
# Azure Logic App / Power Automate workflow definition (can also be
# implemented as a cron-triggered Python script)
# daily_risk_scoring_pipeline.yaml
# Azure Logic App / Power Automate workflow definition
# Can also be implemented as a cron-triggered Python script
name: daily-noshow-risk-scoring
schedule: "0 6 * * 1-6" # 6:00 AM Monday through Saturday
timezone: "America/New_York" # Adjust to practice timezone
steps:
- id: extract_appointments
name: "Extract Upcoming Appointments"
description: "Pull appointments for next 72 hours from EHR"
# For Cliniko API:
action: http_request
config:
method: GET
url: "https://api2.cliniko.com/v1/individual_appointments"
headers:
Authorization: "Basic ${CLINIKO_API_KEY_BASE64}"
User-Agent: "MSPName (contact@msp.com)"
Accept: "application/json"
params:
q: "appointment_start:>=${TODAY}&appointment_start:<=${TODAY_PLUS_3}"
per_page: 100
sort: "appointment_start:asc"
output: raw_appointments
- id: enrich_patient_history
name: "Enrich with Patient History"
description: "Calculate historical no-show rate per patient"
action: python_script
config:
script: |
import pandas as pd
# Load raw appointments from previous step
appointments = pd.DataFrame(raw_appointments['individual_appointments'])
# Query patient attendance history from local cache/database
# (maintained by nightly sync from EHR)
history = pd.read_csv('/data/patient_history_cache.csv')
enriched = appointments.merge(
history[['patient_id', 'historical_no_show_rate',
'total_appointments', 'total_no_shows',
'days_since_last_visit']],
on='patient_id',
how='left'
)
enriched['historical_no_show_rate'] = enriched['historical_no_show_rate'].fillna(0.15)
output: enriched_appointments
- id: score_risk
name: "Score No-Show Risk"
description: "Run prediction model on all upcoming appointments"
action: python_script
config:
script: |
from no_show_prediction_model import NoShowPredictor
predictor = NoShowPredictor(model_path='practice_noshow_model')
risk_scores = predictor.predict_risk(enriched_appointments)
# Log scoring run
print(f"Scored {len(risk_scores)} appointments")
print(f"High risk: {(risk_scores['risk_tier']=='High').sum()}")
print(f"Medium risk: {(risk_scores['risk_tier']=='Medium').sum()}")
print(f"Low risk: {(risk_scores['risk_tier']=='Low').sum()}")
output: risk_scores
- id: route_outreach
name: "Route to Outreach Tiers"
description: "Send high-risk patients to intervention workflows"
action: python_script
config:
script: |
import json
import requests
OUTREACH_API_URL = "${HEALOW_OUTREACH_API_URL}"
OUTREACH_API_KEY = "${HEALOW_API_KEY}"
for _, appt in risk_scores.iterrows():
payload = {
'patient_id': appt['patient_id'],
'appointment_date': str(appt['appointment_date']),
'risk_score': float(appt['no_show_probability']),
'risk_tier': appt['risk_tier'],
'recommended_action': appt['recommended_action']
}
if appt['risk_tier'] == 'High':
# Queue for staff phone call + automated SMS sequence
payload['outreach_type'] = 'tier3_high_touch'
payload['channels'] = ['phone_staff', 'sms_48hr', 'sms_24hr', 'sms_2hr']
elif appt['risk_tier'] == 'Medium':
# Queue for automated multi-touch
payload['outreach_type'] = 'tier2_multi_touch'
payload['channels'] = ['sms_72hr', 'email_48hr', 'sms_24hr']
else:
# Standard reminder
payload['outreach_type'] = 'tier1_standard'
payload['channels'] = ['sms_24hr']
# Send to outreach platform
resp = requests.post(
f"{OUTREACH_API_URL}/outreach/queue",
headers={'Authorization': f'Bearer {OUTREACH_API_KEY}',
'Content-Type': 'application/json'},
json=payload
)
print(f"Patient {appt['patient_id']}: {appt['risk_tier']} -> {resp.status_code}")
output: outreach_results
- id: update_dashboard
name: "Update Reception Dashboard"
description: "Push today's risk scores to the live dashboard"
action: python_script
config:
script: |
# Export today's scores for Power BI / dashboard consumption
today_scores = risk_scores[
risk_scores['appointment_date'].str[:10] == str(pd.Timestamp.today().date())
]
today_scores.to_csv('/data/dashboard/today_risk_scores.csv', index=False)
today_scores.to_json('/data/dashboard/today_risk_scores.json', orient='records')
print(f"Dashboard updated with {len(today_scores)} appointments for today")
- id: generate_staff_report
name: "Generate Staff Action Report"
description: "Create a simple report for front desk staff with high-risk patients needing calls"
action: python_script
config:
script: |
high_risk_today = risk_scores[
(risk_scores['risk_tier'] == 'High') &
(risk_scores['appointment_date'].str[:10] <= str(
(pd.Timestamp.today() + pd.Timedelta(days=3)).date()
))
].sort_values('no_show_probability', ascending=False)
report = "=== HIGH-RISK NO-SHOW PATIENTS - ACTION REQUIRED ===\n"
report += f"Generated: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}\n\n"
for _, row in high_risk_today.iterrows():
report += f"Patient: {row['patient_id']}\n"
report += f" Appointment: {row['appointment_date']}\n"
report += f" Risk Score: {row['no_show_probability']:.0%}\n"
report += f" Action: Personal phone call required\n\n"
report += f"Total high-risk patients needing calls: {len(high_risk_today)}\n"
# Email report to front desk
# (Use practice email system or HIPAA-compliant internal messaging)
with open('/data/reports/daily_action_report.txt', 'w') as f:
f.write(report)
print(report)
- id: log_audit
name: "HIPAA Audit Log"
description: "Log all PHI access for HIPAA compliance"
action: python_script
config:
script: |
import json
from datetime import datetime
audit_entry = {
'timestamp': datetime.utcnow().isoformat(),
'action': 'daily_risk_scoring',
'system': 'noshow_prediction_pipeline',
'user': 'automated_system',
'patients_accessed': len(risk_scores),
'data_elements': ['patient_id', 'appointment_date', 'appointment_type',
'historical_attendance', 'demographics'],
'purpose': 'treatment_operations_no_show_prediction',
'hipaa_basis': 'TPO_operations',
'output': 'risk_scores_and_outreach_queue'
}
with open('/data/audit/audit_log.jsonl', 'a') as f:
f.write(json.dumps(audit_entry) + '\n')
print(f"Audit entry logged: {audit_entry['timestamp']}")# Cron job setup (Linux/macOS server or Azure VM)
# Add to crontab: crontab -e
0 6 * * 1-6 cd /opt/noshow-pipeline && python daily_pipeline.py >> /var/log/noshow-pipeline.log 2>&1{
"bindings": [
{
"name": "timer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 0 10 * * 1-6"
}
]
}Patient Outreach Orchestrator
Type: agent An intelligent agent that manages the multi-channel outreach workflow for high-risk patients. It determines optimal timing, channel selection, and message personalization based on patient preferences and prior outreach response patterns. The agent respects do-not-contact preferences, manages opt-outs, and ensures all communications comply with mental health privacy requirements by never revealing treatment type in messages.
Implementation
# Patient Outreach Orchestrator Agent
# outreach_orchestrator.py
# Patient Outreach Orchestrator Agent
# Requirements: pip install requests schedule twilio
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from enum import Enum
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('OutreachOrchestrator')
class Channel(Enum):
SMS = 'sms'
EMAIL = 'email'
PHONE_STAFF = 'phone_staff'
VOICE_AUTO = 'voice_auto'
class OutreachStatus(Enum):
PENDING = 'pending'
SENT = 'sent'
DELIVERED = 'delivered'
RESPONDED = 'responded'
CONFIRMED = 'confirmed'
RESCHEDULED = 'rescheduled'
OPTED_OUT = 'opted_out'
FAILED = 'failed'
@dataclass
class OutreachAction:
channel: Channel
hours_before_appointment: int
template_id: str
status: OutreachStatus = OutreachStatus.PENDING
sent_at: Optional[datetime] = None
response_at: Optional[datetime] = None
@dataclass
class PatientOutreachPlan:
patient_id: str
appointment_id: str
appointment_datetime: datetime
risk_score: float
risk_tier: str
actions: List[OutreachAction] = field(default_factory=list)
patient_confirmed: bool = False
patient_opted_out: bool = False
# --- PRIVACY-SAFE MESSAGE TEMPLATES ---
# CRITICAL: These templates must NEVER mention therapy, counseling,
# mental health, psychiatry, or any treatment type.
MESSAGE_TEMPLATES = {
'sms_standard_24hr': {
'channel': Channel.SMS,
'body': 'Hi {first_name}, reminder from {practice_name}: you have an appointment on {date} at {time}. Reply YES to confirm or CHANGE to reschedule. Call {phone} with questions.',
'max_length': 160
},
'sms_medium_72hr': {
'channel': Channel.SMS,
'body': 'Hi {first_name}, {practice_name} here. Your appointment is on {date} at {time}. We want to make sure this works for you. Reply YES to confirm or CHANGE to reschedule.',
'max_length': 160
},
'sms_high_48hr': {
'channel': Channel.SMS,
'body': 'Hi {first_name}, {provider_name} at {practice_name} wants to check in about your appointment on {date}. We know schedules get busy - reply CHANGE if you need to adjust.',
'max_length': 160
},
'sms_high_2hr': {
'channel': Channel.SMS,
'body': 'Hi {first_name}, just a reminder your appointment at {practice_name} is in 2 hours at {time}. See you soon! Reply CHANGE if needed.',
'max_length': 160
},
'email_confirmation': {
'channel': Channel.EMAIL,
'subject': 'Appointment Reminder - {practice_name}',
'body': 'Dear {first_name},\n\nThis is a friendly reminder about your upcoming appointment:\n\nDate: {date}\nTime: {time}\nLocation: {address}\n\nPlease click below to confirm or reschedule:\n{confirm_link}\n{reschedule_link}\n\nIf you have questions, call us at {phone}.\n\nBest,\nThe {practice_name} Team'
},
'phone_staff_script': {
'channel': Channel.PHONE_STAFF,
'script': 'Hi, may I speak with {first_name}? This is {staff_name} calling from {practice_name}. I\'m calling to confirm your appointment on {date} at {time}. We just want to make sure this time still works for you.',
'if_hesitant': 'I completely understand. Would you like to reschedule? We have availability on {available_slots}. We\'d love to keep you on the schedule.',
'do_not': 'Do NOT reference therapy, counseling, diagnosis, mental health, or any treatment details.'
}
}
class OutreachOrchestrator:
"""
Manages multi-channel outreach workflows for high-risk no-show patients.
Ensures HIPAA compliance and mental health privacy protections.
"""
def __init__(self, practice_config: Dict):
self.practice_name = practice_config['practice_name']
self.practice_phone = practice_config['phone']
self.practice_address = practice_config['address']
self.sms_provider = practice_config.get('sms_provider', 'healow') # or 'twilio'
self.opt_out_list = set() # Patient IDs who opted out
self.outreach_plans = {} # appointment_id -> PatientOutreachPlan
self.audit_log = []
def create_outreach_plan(self, patient_id: str, appointment_id: str,
appointment_datetime: datetime, risk_score: float,
risk_tier: str) -> PatientOutreachPlan:
"""Create an outreach plan based on risk tier."""
# Check opt-out list
if patient_id in self.opt_out_list:
logger.info(f'Patient {patient_id} has opted out - no outreach plan created')
return None
plan = PatientOutreachPlan(
patient_id=patient_id,
appointment_id=appointment_id,
appointment_datetime=appointment_datetime,
risk_score=risk_score,
risk_tier=risk_tier
)
if risk_tier == 'High':
plan.actions = [
OutreachAction(Channel.PHONE_STAFF, 72, 'phone_staff_script'),
OutreachAction(Channel.SMS, 48, 'sms_high_48hr'),
OutreachAction(Channel.EMAIL, 48, 'email_confirmation'),
OutreachAction(Channel.SMS, 24, 'sms_standard_24hr'),
OutreachAction(Channel.SMS, 2, 'sms_high_2hr'),
]
elif risk_tier == 'Medium':
plan.actions = [
OutreachAction(Channel.SMS, 72, 'sms_medium_72hr'),
OutreachAction(Channel.EMAIL, 48, 'email_confirmation'),
OutreachAction(Channel.SMS, 24, 'sms_standard_24hr'),
]
else: # Low
plan.actions = [
OutreachAction(Channel.SMS, 24, 'sms_standard_24hr'),
]
self.outreach_plans[appointment_id] = plan
self._audit('plan_created', patient_id, appointment_id, risk_tier)
logger.info(f'Outreach plan created: patient={patient_id}, '
f'risk={risk_tier} ({risk_score:.0%}), '
f'actions={len(plan.actions)}')
return plan
def execute_due_actions(self):
"""Check all plans and execute any actions that are due."""
now = datetime.now()
for appt_id, plan in self.outreach_plans.items():
if plan.patient_confirmed or plan.patient_opted_out:
continue
for action in plan.actions:
if action.status != OutreachStatus.PENDING:
continue
trigger_time = plan.appointment_datetime - timedelta(hours=action.hours_before_appointment)
if now >= trigger_time:
self._execute_action(plan, action)
def _execute_action(self, plan: PatientOutreachPlan, action: OutreachAction):
"""Execute a single outreach action."""
template = MESSAGE_TEMPLATES[action.template_id]
if action.channel == Channel.SMS:
success = self._send_sms(plan, template)
elif action.channel == Channel.EMAIL:
success = self._send_email(plan, template)
elif action.channel == Channel.PHONE_STAFF:
success = self._queue_staff_call(plan, template)
else:
success = False
action.status = OutreachStatus.SENT if success else OutreachStatus.FAILED
action.sent_at = datetime.now()
self._audit('action_executed', plan.patient_id, plan.appointment_id,
f'{action.channel.value}:{action.template_id}:{action.status.value}')
def _send_sms(self, plan: PatientOutreachPlan, template: Dict) -> bool:
"""Send SMS via configured provider. Returns True on success."""
# Implementation depends on provider (healow native or Twilio)
logger.info(f'SMS sent to patient {plan.patient_id} '
f'for appointment {plan.appointment_id}')
return True # Replace with actual API call
def _send_email(self, plan: PatientOutreachPlan, template: Dict) -> bool:
"""Send email via configured provider."""
logger.info(f'Email sent to patient {plan.patient_id}')
return True # Replace with actual API call
def _queue_staff_call(self, plan: PatientOutreachPlan, template: Dict) -> bool:
"""Add to staff call queue (displayed on reception dashboard)."""
logger.info(f'Staff call queued for patient {plan.patient_id} '
f'(risk: {plan.risk_score:.0%})')
return True # Replace with dashboard/queue integration
def handle_patient_response(self, appointment_id: str, response: str):
"""Process patient response to outreach."""
plan = self.outreach_plans.get(appointment_id)
if not plan:
logger.warning(f'No plan found for appointment {appointment_id}')
return
response_upper = response.strip().upper()
if response_upper in ['YES', 'CONFIRM', 'Y', 'C']:
plan.patient_confirmed = True
# Cancel remaining pending actions
for action in plan.actions:
if action.status == OutreachStatus.PENDING:
action.status = OutreachStatus.RESPONDED
logger.info(f'Patient {plan.patient_id} CONFIRMED appointment')
elif response_upper in ['CHANGE', 'RESCHEDULE', 'R', 'CANCEL']:
# Flag for staff to follow up with reschedule
for action in plan.actions:
if action.status == OutreachStatus.PENDING:
action.status = OutreachStatus.RESPONDED
logger.info(f'Patient {plan.patient_id} requested RESCHEDULE')
elif response_upper in ['STOP', 'OPTOUT', 'OPT OUT', 'UNSUBSCRIBE']:
plan.patient_opted_out = True
self.opt_out_list.add(plan.patient_id)
for action in plan.actions:
if action.status == OutreachStatus.PENDING:
action.status = OutreachStatus.OPTED_OUT
logger.info(f'Patient {plan.patient_id} OPTED OUT of communications')
self._audit('patient_response', plan.patient_id, appointment_id, response_upper)
def _audit(self, action: str, patient_id: str, appointment_id: str, detail: str):
"""HIPAA audit log entry."""
entry = {
'timestamp': datetime.utcnow().isoformat(),
'action': action,
'patient_id': patient_id,
'appointment_id': appointment_id,
'detail': detail,
'system': 'outreach_orchestrator'
}
self.audit_log.append(entry)
# In production: write to persistent audit log (database or file)
def get_daily_summary(self) -> Dict:
"""Generate daily outreach summary for practice manager."""
today = datetime.now().date()
today_plans = [
p for p in self.outreach_plans.values()
if p.appointment_datetime.date() == today
]
return {
'date': str(today),
'total_appointments': len(today_plans),
'high_risk': sum(1 for p in today_plans if p.risk_tier == 'High'),
'medium_risk': sum(1 for p in today_plans if p.risk_tier == 'Medium'),
'low_risk': sum(1 for p in today_plans if p.risk_tier == 'Low'),
'confirmed': sum(1 for p in today_plans if p.patient_confirmed),
'pending': sum(1 for p in today_plans if not p.patient_confirmed and not p.patient_opted_out),
'opted_out': sum(1 for p in today_plans if p.patient_opted_out)
}
# --- USAGE ---
if __name__ == '__main__':
config = {
'practice_name': 'Wellness Center', # Generic name - no mental health reference
'phone': '(555) 123-4567',
'address': '123 Main St, Suite 200, Anytown, ST 12345',
'sms_provider': 'healow'
}
orchestrator = OutreachOrchestrator(config)
# Create plans from daily risk scoring output
orchestrator.create_outreach_plan(
patient_id='P001',
appointment_id='A001',
appointment_datetime=datetime.now() + timedelta(days=3),
risk_score=0.82,
risk_tier='High'
)
# Execute due actions
orchestrator.execute_due_actions()
# Handle patient response
orchestrator.handle_patient_response('A001', 'YES')
# Daily summary
print(json.dumps(orchestrator.get_daily_summary(), indent=2))HIPAA-Compliant Data Extraction Integration
Type: integration
A secure integration layer that extracts appointment data from various Allied and Mental Health EHR systems (Cliniko, SimplePractice, TherapyNotes, Jane App) while enforcing HIPAA Minimum Necessary standard and excluding psychotherapy notes. Supports both API-based extraction (Cliniko) and file-based extraction (CSV export from SimplePractice/TherapyNotes). Includes data validation, PHI field filtering, and audit logging.
Implementation:
# HIPAA-Compliant EHR Data Extraction for No-Show Prediction
# ehr_data_extractor.py
# HIPAA-Compliant EHR Data Extraction for No-Show Prediction
# Requirements: pip install requests pandas cryptography
import os
import json
import logging
import hashlib
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import pandas as pd
import requests
from base64 import b64encode
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('EHRExtractor')
# --- HIPAA MINIMUM NECESSARY: Only these fields are extracted ---
ALLOWED_FIELDS = [
'appointment_id', 'patient_id', 'appointment_date', 'appointment_time',
'appointment_type', 'provider_id', 'provider_name', 'booking_date',
'status', # attended, no-show, cancelled, late-cancel
'patient_age', 'patient_zip', 'insurance_type',
'appointment_duration_minutes'
]
# --- FIELDS TO NEVER EXTRACT (psychotherapy notes, clinical content) ---
BLOCKED_FIELDS = [
'notes', 'clinical_notes', 'psychotherapy_notes', 'therapy_notes',
'diagnosis', 'diagnosis_code', 'icd_code', 'treatment_plan',
'medication', 'prescription', 'substance_use', 'sud_history',
'ssn', 'social_security', 'credit_card', 'bank_account',
'full_address', 'email', 'phone' # PII not needed for prediction
]
class BaseEHRExtractor(ABC):
"""Base class for EHR data extraction with HIPAA safeguards."""
def __init__(self, practice_id: str, audit_log_path: str = '/data/audit/'):
self.practice_id = practice_id
self.audit_log_path = audit_log_path
os.makedirs(audit_log_path, exist_ok=True)
@abstractmethod
def extract_appointments(self, start_date: datetime, end_date: datetime) -> pd.DataFrame:
pass
def filter_to_minimum_necessary(self, df: pd.DataFrame) -> pd.DataFrame:
"""HIPAA Minimum Necessary: Keep only allowed fields, block prohibited ones."""
# Remove any blocked fields
blocked_present = [col for col in df.columns if col.lower() in BLOCKED_FIELDS]
if blocked_present:
logger.warning(f'BLOCKED FIELDS DETECTED AND REMOVED: {blocked_present}')
df = df.drop(columns=blocked_present)
# Keep only allowed fields that exist
allowed_present = [col for col in df.columns if col.lower() in ALLOWED_FIELDS]
df = df[allowed_present]
logger.info(f'Minimum Necessary filter applied. Retained fields: {list(df.columns)}')
return df
def pseudonymize_patient_ids(self, df: pd.DataFrame, salt: str) -> pd.DataFrame:
"""Hash patient IDs for additional de-identification (optional layer)."""
if 'patient_id' in df.columns:
df['patient_id'] = df['patient_id'].apply(
lambda x: hashlib.sha256(f'{salt}:{x}'.encode()).hexdigest()[:16]
)
return df
def audit_extraction(self, record_count: int, fields: List[str], source: str):
"""Write HIPAA audit log entry."""
entry = {
'timestamp': datetime.utcnow().isoformat(),
'action': 'data_extraction',
'practice_id': self.practice_id,
'source_system': source,
'records_extracted': record_count,
'fields_extracted': fields,
'purpose': 'no_show_prediction_model_input',
'hipaa_basis': 'TPO_health_care_operations',
'user': os.environ.get('MSP_TECHNICIAN_ID', 'system')
}
log_file = os.path.join(self.audit_log_path, 'extraction_audit.jsonl')
with open(log_file, 'a') as f:
f.write(json.dumps(entry) + '\n')
logger.info(f'Audit logged: {record_count} records from {source}')
class ClinikoExtractor(BaseEHRExtractor):
"""Extract appointment data from Cliniko via REST API."""
def __init__(self, practice_id: str, api_key: str, shard: str = 'api2'):
super().__init__(practice_id)
self.base_url = f'https://{shard}.cliniko.com/v1'
self.headers = {
'User-Agent': 'MSPNoShowPredictor (contact@yourmsp.com)',
'Accept': 'application/json',
'Authorization': f'Basic {b64encode(f"{api_key}:".encode()).decode()}'
}
def extract_appointments(self, start_date: datetime, end_date: datetime) -> pd.DataFrame:
"""Pull appointments from Cliniko API with pagination."""
all_appointments = []
page = 1
while True:
params = {
'page': page,
'per_page': 100,
'sort': 'appointment_start:asc',
'q[]': [
f'appointment_start:>={start_date.strftime("%Y-%m-%dT00:00:00Z")}',
f'appointment_start:<={end_date.strftime("%Y-%m-%dT23:59:59Z")}'
]
}
response = requests.get(
f'{self.base_url}/individual_appointments',
headers=self.headers,
params=params
)
response.raise_for_status()
data = response.json()
appointments = data.get('individual_appointments', [])
if not appointments:
break
all_appointments.extend(appointments)
if not data.get('links', {}).get('next'):
break
page += 1
if not all_appointments:
logger.warning('No appointments found in date range')
return pd.DataFrame()
# Normalize to flat DataFrame
df = pd.json_normalize(all_appointments)
# Map Cliniko fields to standard schema
field_map = {
'id': 'appointment_id',
'patient.links.self': 'patient_id',
'appointment_start': 'appointment_date',
'appointment_type.name': 'appointment_type',
'practitioner.links.self': 'provider_id',
'created_at': 'booking_date',
'did_not_arrive': 'did_not_arrive',
'cancellation_reason': 'cancellation_reason'
}
df = df.rename(columns={k: v for k, v in field_map.items() if k in df.columns})
# Derive status
if 'did_not_arrive' in df.columns:
df['status'] = df.apply(
lambda row: 'No-Show' if row.get('did_not_arrive') == True
else ('Cancelled' if pd.notna(row.get('cancellation_reason'))
else 'Attended'),
axis=1
)
# Apply Minimum Necessary filter
df = self.filter_to_minimum_necessary(df)
self.audit_extraction(len(df), list(df.columns), 'Cliniko_API')
return df
class CSVExtractor(BaseEHRExtractor):
"""Extract appointment data from CSV exports (SimplePractice, TherapyNotes, etc.)."""
def __init__(self, practice_id: str, ehr_system: str = 'SimplePractice'):
super().__init__(practice_id)
self.ehr_system = ehr_system
def extract_appointments(self, csv_path: str,
field_mapping: Optional[Dict[str, str]] = None) -> pd.DataFrame:
"""Load and standardize CSV export."""
if not os.path.exists(csv_path):
raise FileNotFoundError(f'CSV file not found: {csv_path}')
df = pd.read_csv(csv_path)
logger.info(f'Loaded {len(df)} records from {csv_path}')
logger.info(f'Source columns: {list(df.columns)}')
# Apply custom field mapping if provided
if field_mapping:
df = df.rename(columns=field_mapping)
# Apply Minimum Necessary filter
df = self.filter_to_minimum_necessary(df)
self.audit_extraction(len(df), list(df.columns), f'{self.ehr_system}_CSV')
return df
# --- Field mapping templates for common EHR CSV exports ---
SIMPLEPRACTICE_FIELD_MAP = {
'Client Name': 'patient_id', # Will need hashing
'Appointment Date': 'appointment_date',
'Appointment Time': 'appointment_time',
'Service Type': 'appointment_type',
'Clinician': 'provider_id',
'Status': 'status',
'Date Created': 'booking_date'
}
THERAPYNOTES_FIELD_MAP = {
'Patient': 'patient_id',
'Date': 'appointment_date',
'Time': 'appointment_time',
'Type': 'appointment_type',
'Therapist': 'provider_id',
'Attendance': 'status',
'Created': 'booking_date'
}
# --- USAGE ---
if __name__ == '__main__':
# Example: Cliniko API extraction
extractor = ClinikoExtractor(
practice_id='practice_001',
api_key=os.environ['CLINIKO_API_KEY'],
shard='api2'
)
appointments = extractor.extract_appointments(
start_date=datetime.now() - timedelta(days=365),
end_date=datetime.now()
)
print(f'Extracted {len(appointments)} appointments from Cliniko')
print(appointments.head())
# Example: SimplePractice CSV extraction
csv_extractor = CSVExtractor(
practice_id='practice_002',
ehr_system='SimplePractice'
)
appointments_csv = csv_extractor.extract_appointments(
csv_path='/data/exports/simplepractice_appointments.csv',
field_mapping=SIMPLEPRACTICE_FIELD_MAP
)
print(f'Extracted {len(appointments_csv)} appointments from CSV')Compliance Verification Prompt
Instructions
Review the following patient outreach message template against ALL criteria below. A message MUST pass ALL checks to be approved for production use.
Message Under Review
[PASTE MESSAGE TEMPLATE HERE]
Channel: [SMS / Email / Voice Script / Voicemail]
MANDATORY PRIVACY CHECKS
Check 1: Treatment Type Disclosure
Does the message contain ANY of these words or their variants?
- therapy, therapist, therapeutic
- counseling, counselor
- psychiatry, psychiatrist, psychiatric
- psychology, psychologist
- mental health, behavioral health
- substance abuse, substance use, addiction, recovery
- diagnosis, treatment, medication
- anxiety, depression, PTSD, bipolar, ADHD, OCD, eating disorder
- any DSM-5 diagnostic term
Check 2: Practice Name Neutrality
Does the practice name used in the message reveal the treatment specialty?
- Examples that FAIL: "Sunrise Therapy Center", "Mind & Body Counseling", "Behavioral Wellness Clinic"
- Examples that PASS: "Wellness Center", "Sunrise Health", "Oak Street Practice"
Check 3: URL/Link Neutrality
Do any links in the message contain treatment-revealing paths?
- Fails: healthpractice.com/therapy-appointment-confirm
- Passes: healthpractice.com/confirm
Check 4: Voicemail Safety (Voice/Phone channels only)
Could this message be heard by someone other than the patient (family member, employer, roommate) without revealing the nature of treatment?
RESULT: [ ] PASS [ ] FAIL [ ] N/A (not voice)
Check 5: Minimum Information
Does the message contain ONLY:
- Patient first name
- Practice name (neutral)
- Appointment date and time
- Action options (confirm/reschedule)
- Practice phone number
Does it AVOID including:
- Provider credentials or title (e.g., "Dr. Smith, Psychiatrist")
- Appointment type (e.g., "your therapy session")
- Clinical details of any kind
Check 6: Opt-Out Mechanism (SMS only)
Does the SMS include or support a STOP/opt-out mechanism per TCPA requirements?
Check 7: 42 CFR Part 2 Compliance (If practice treats SUD)
If this practice provides ANY substance use disorder services:
- Does this message comply with 42 CFR Part 2 consent requirements?
- Has the patient provided specific written consent for this type of outreach?
OVERALL VERDICT
Reviewer: ___________
Date: ___________
Practice: ___________
Testing & Validation
- NETWORK CONNECTIVITY TEST: From a clinical workstation on the clinical VLAN, open a browser and navigate to the healow Genie dashboard URL. Verify the page loads within 5 seconds. Then try accessing from the reception VLAN with an iPad. Both should have full access. Attempt access from the guest Wi-Fi VLAN — this should be BLOCKED by the FortiGate firewall policy.
- FIREWALL SECURITY SCAN: Run an external vulnerability scan (using a tool like Qualys FreeScan or Nmap from an external IP) against the practice's public IP. Verify that no unnecessary ports are open and that the FortiGate IPS is actively inspecting traffic. Check the FortiGate threat log for any blocked intrusion attempts.
- HIPAA ENCRYPTION VERIFICATION: Use a browser's developer tools (F12 > Security tab) to verify that all connections to healow Genie, Power BI, and any other cloud platforms use TLS 1.2 or higher. Verify certificate validity. On the FortiGate, confirm SSL inspection is active on the clinical-to-internet policy.
- BAA DOCUMENTATION AUDIT: Review the BAA register and confirm signed BAAs are on file for: (1) healow/eClinicalWorks, (2) Microsoft (if using Power BI/Azure), (3) Fortinet/FortiCloud (if using cloud management), (4) Twilio (if applicable), (5) MSP-to-client BAA. Each BAA must include breach notification clauses and data use restrictions.
- EHR DATA EXTRACTION TEST: Execute the data extraction process (API call for Cliniko or CSV export for SimplePractice/TherapyNotes). Verify that: (1) At least 500 appointment records are retrieved; (2) Only ALLOWED_FIELDS are present in the output — no clinical notes, psychotherapy notes, or blocked fields; (3) The data includes appointment_date, patient_id, status, and provider_id at minimum; (4) An audit log entry was created documenting the extraction.
- PREDICTION MODEL ACCURACY TEST: Using the 2-week pilot data, calculate and verify: (1) Overall accuracy > 70%; (2) AUC-ROC > 0.65; (3) Sensitivity (recall for no-shows) > 60%; (4) The model correctly identified at least 6 out of 10 actual no-shows. If any metric falls below threshold, do NOT proceed to live outreach — investigate data quality and retrain.
- MESSAGE TEMPLATE PRIVACY TEST: Run every outreach message template (SMS, email, voice script, voicemail) through the Compliance Verification Prompt checklist. Verify ALL templates pass ALL seven checks. Have the practice owner sign off on each approved template. Test by sending each template to an MSP staff member's personal phone/email to verify content renders correctly with no placeholder text visible.
- OUTREACH DELIVERY TEST: Send a test SMS, test email, and make a test phone call to an MSP staff member's personal phone using the production outreach system. Verify: (1) SMS arrives within 60 seconds; (2) SMS displays correct practice name and appointment placeholder data; (3) Email arrives and is not caught by spam filters; (4) Reply functionality works (reply YES, reply CHANGE, reply STOP); (5) Opt-out (STOP) is processed and prevents further messages.
- RECEPTION DASHBOARD TEST: On the iPad at reception, verify the daily risk dashboard: (1) Shows today's appointments color-coded by risk tier (green=low, yellow=medium, red=high); (2) Updates when new risk scores are generated; (3) Guided Access prevents navigating away from the dashboard; (4) The display is readable from 3–5 feet away; (5) No excessive PHI is visible to passing patients (show first name and appointment time only, not risk scores).
- END-TO-END WORKFLOW TEST: Create a test appointment in the EHR for a fictitious test patient. Manually assign it a high risk score. Verify the complete workflow fires: (1) Risk score appears on the dashboard; (2) Tier 3 outreach plan is created (staff call queue + SMS sequence); (3) SMS is sent at the configured time intervals; (4) When the test patient replies YES, all pending outreach actions are cancelled; (5) The confirmation is logged; (6) HIPAA audit trail shows all actions taken.
- 42 CFR PART 2 SEGREGATION TEST (if applicable): If the practice treats SUD patients, verify that: (1) SUD patient records are flagged in the data pipeline; (2) SUD patient data is either excluded from the prediction model or has proper consent documentation; (3) Outreach messages to SUD patients follow additional consent requirements; (4) The system correctly identifies SUD appointments based on appointment type or provider assignment.
- ROI BASELINE MEASUREMENT: Before go-live, record the practice's baseline no-show rate over the prior 3 months. Calculate: total appointments, total no-shows, no-show rate by provider, no-show rate by day of week, and estimated revenue lost to no-shows. This baseline is essential for measuring the system's impact at the 30-day, 60-day, and 90-day reviews.
Client Handoff
The client handoff should be conducted as a 90-minute in-person or video meeting with the practice owner, office manager, front-desk lead, and at least one clinician representative. The meeting should cover the following:
1. System Overview (15 min)
Walk through how the no-show prediction system works at a high level — data flows from the EHR to the AI engine, which scores each appointment and triggers appropriate outreach. Show the reception dashboard and explain the red/yellow/green risk tier system.
2. Daily Workflow Training (20 min)
Train the front-desk staff on their daily responsibilities. Walk through 2–3 real scenarios using actual upcoming appointments.
- Check the reception iPad dashboard each morning for high-risk patients
- Make personal phone calls to Tier 3 (high-risk) patients using the approved script
- Document call outcomes
- Monitor for patient responses to automated SMS/email and process reschedule requests
3. Message Privacy Rules (15 min)
Review the mental health privacy requirements with ALL staff. Emphasize that automated messages never reveal practice specialty. Train phone staff on the call script and what NOT to say. Provide printed copies of the approved call script and the DO NOT reference list.
4. Power BI Dashboard Training (15 min)
Show the practice manager how to access and interpret the analytics dashboard: no-show rate trends, provider comparisons, outreach effectiveness, and ROI metrics. Show how to export reports.
5. Escalation Procedures (10 min)
Explain how to contact the MSP for support. Define SLA expectations (e.g., critical issues responded to within 2 hours during business hours).
- MSP help desk phone number and email
- Escalation path for urgent issues (e.g., messages not sending)
- After-hours emergency contact
6. Documentation Handoff (10 min)
Leave behind printed and digital copies of the following materials:
- System Architecture Diagram showing all components
- Approved Message Templates binder
- HIPAA Compliance Checklist (completed and signed)
- BAA Register with all vendor BAAs
- Quick Reference Card for front-desk staff (laminated, placed at each reception station)
- Login credentials document (sealed envelope to practice owner)
- FortiGate network documentation
7. Success Criteria Review (5 min)
Review the agreed-upon success metrics with the practice owner. Schedule 30-day, 60-day, and 90-day review meetings.
- Target no-show rate reduction of 10–15 percentage points within 90 days
- Patient confirmation rate >50% for outreach messages
- Staff adoption — Tier 3 phone calls being made daily
- Zero privacy incidents — no messages revealing treatment type
Maintenance
Ongoing Maintenance Responsibilities:
Daily (Automated)
- Risk scoring pipeline runs at 6:00 AM and scores all appointments for the next 72 hours
- Outreach orchestrator sends messages according to tier schedules
- HIPAA audit logs capture all PHI access and outreach actions
- FortiGate threat logs monitored via FortiCloud (automated alerts to MSP NOC)
Weekly (MSP Technician — 30 minutes)
- Review outreach delivery reports: check for failed SMS/email deliveries and investigate
- Monitor patient opt-out rates: if >3% weekly opt-out, review message frequency and content
- Check FortiGate firmware and signature update status
- Verify EHR data sync is functioning (API calls succeeding or CSV exports being processed)
- Review prediction accuracy: compare last week's risk scores against actual attendance outcomes
Monthly (MSP Account Manager — 1 hour)
- Generate and deliver monthly performance report to practice owner via Power BI
- Review no-show rate trend vs. baseline — is the 10–15 point improvement holding?
- Calculate monthly ROI (recovered revenue vs. system cost)
- Review and respond to any patient complaints about outreach communications
- Verify all software subscriptions (healow, Power BI, FortiGuard) are active and paid
- Check for platform updates from healow and apply any new features or improvements
Quarterly (MSP + Practice Owner — 1 hour meeting)
- Comprehensive review of system performance, ROI, and patient satisfaction
- Model retraining assessment: if prediction accuracy has dropped below 70%, retrain the model with updated data. Retraining triggers include: (a) accuracy drop >5 points from baseline; (b) practice adds new providers or appointment types; (c) significant patient population change; (d) seasonal patterns not captured in original training data
- Review and update outreach message templates if needed
- HIPAA compliance check: verify all BAAs are current, review access logs for anomalies
- Update risk thresholds if the practice's no-show patterns have changed
Annually
- Full HIPAA Security Risk Assessment (can be delivered by MSP for $2,000–$5,000)
- FortiGate hardware and subscription renewal assessment
- healow Genie contract renewal review — negotiate pricing based on demonstrated ROI
- Review 42 CFR Part 2 compliance (if applicable) against any regulatory updates
- Full model retrain with 12 months of new data
- Staff refresher training on privacy rules and system usage (1-hour session)
SLA Considerations
- Critical issues (outreach system down, data breach suspected): 2-hour response, 4-hour resolution target
- High issues (dashboard not updating, prediction errors): 4-hour response, 8-hour resolution
- Medium issues (report formatting, minor config changes): Next business day response
- Low issues (feature requests, cosmetic changes): Within 5 business days
Escalation Path
- Level 1: MSP Help Desk (basic troubleshooting, password resets, connectivity)
- Level 2: MSP Senior Technician (platform configuration, data pipeline issues)
- Level 3: MSP Solutions Architect / healow Support (model performance, complex integrations)
- Level 4: Vendor Engineering (healow, Fortinet, Microsoft — for platform bugs or outages)
Alternatives
Custom ML Pipeline with Cliniko API + Twilio
Instead of using healow Genie as a turnkey solution, build a custom machine learning pipeline using Python (scikit-learn/XGBoost), deployed on Azure Machine Learning, with data extracted via the Cliniko REST API. Patient outreach is handled through Twilio's HIPAA-eligible SMS API and a custom outreach orchestrator. This approach gives the MSP and practice full control over the prediction model, outreach logic, and data pipeline.
- COST: Higher upfront ($15,000–$30,000 development) but potentially lower ongoing ($70–$200/month cloud + $0.0079/SMS vs. $249/seat/month for healow). Break-even at ~6–12 months for a 5-provider practice.
- COMPLEXITY: Requires a data engineer or ML specialist — not standard MSP skill set. Timeline is 12–20 weeks vs. 4–8 weeks for turnkey.
- CAPABILITY: Full customization of model features, risk thresholds, and outreach logic. Can incorporate practice-specific features that turnkey platforms cannot.
- RISK: Higher — model accuracy depends on data quality and engineering skill. No vendor support for the ML components.
- RECOMMEND WHEN: The practice has 10+ providers, uses Cliniko (API access), has unique needs not met by turnkey platforms, or the MSP has in-house data science capability.
Weave Communications Platform (Communications-First Approach)
Deploy Weave ($250/month per location) as a unified communications and patient engagement platform. Weave provides VoIP phones, HIPAA-compliant 2-way texting, automated appointment reminders, AI voicemail transcription, and review management. While Weave does not include a dedicated no-show prediction engine, its automated reminder system and 2-way texting significantly reduce no-shows through proactive communication alone.
- COST: Lower — $250/month flat per location vs. $249/seat/month (per provider) for healow. For a 5-provider practice, Weave is $250/month vs. $1,245/month for healow.
- COMPLEXITY: Very low — Weave is a plug-and-play communications platform with minimal configuration. 1–2 week deployment.
- CAPABILITY: No predictive AI — relies on standardized reminders and 2-way texting rather than risk-scored interventions. Typical no-show reduction is 5–10 percentage points vs. 10–15 points for predictive-based approaches.
- ADDITIONAL VALUE: Includes VoIP phone system, review management, and payment processing.
- RECOMMEND WHEN: The practice is cost-sensitive, has a relatively low no-show rate already (<20%), wants a broader communications upgrade, or is a solo/small practice where per-seat AI pricing is prohibitive.
ClosedLoop.ai Enterprise Platform
Deploy ClosedLoop.ai, the #1 KLAS-rated healthcare AI platform, which includes pre-built no-show prediction model templates along with dozens of other healthcare predictive models (readmission risk, chronic disease onset, etc.). ClosedLoop provides an end-to-end ML platform that can go from raw EHR/claims data to production-deployed models in 24 hours with minimal data science expertise.
- COST: Significantly higher — enterprise pricing typically $50,000–$150,000+/year. Only justifiable for health systems, large group practices, or multi-site organizations.
- COMPLEXITY: Medium — ClosedLoop handles the ML complexity, but requires dedicated analytics staff and robust data infrastructure (data warehouse, HL7/FHIR feeds).
- CAPABILITY: Best-in-class — purpose-built for healthcare with pre-trained models, explainable AI, and clinical validation. Expandable to many other use cases beyond no-shows.
- RECOMMEND WHEN: The client is a multi-location behavioral health organization with 20+ providers, has existing data infrastructure, wants to expand AI capabilities beyond no-show prediction, or is part of a larger health system already evaluating ClosedLoop.
EHR-Native Reminder System (No AI)
Use the built-in appointment reminder features of the practice's existing EHR system (SimplePractice, TherapyNotes, Jane App, etc.) without adding any external AI platform. Most modern EHR systems include basic SMS and email appointment reminders. This approach maximizes simplicity and minimizes cost but provides no predictive intelligence — all patients receive the same reminders regardless of no-show risk.
Tradeoffs
- COST: Minimal to zero additional cost — most EHR plans include basic reminders.
- COMPLEXITY: Near-zero — reminders are already built into the EHR. No integration, no data pipeline, no model training.
- CAPABILITY: Very limited — same reminder cadence for all patients. No risk stratification, no personalized outreach, no staff call queue for high-risk patients. Typical no-show reduction is 3–5 percentage points — the least effective option.
- MSP REVENUE: Minimal — no hardware, software resale, or managed service opportunity beyond basic IT support.
- RECOMMEND WHEN: The practice has extremely limited budget, is very small (solo practitioner), has a low no-show rate (<15%), or wants to start with the simplest approach before investing in AI. Can serve as Phase 1 before upgrading to a predictive platform.
SonicWall TZ270 Alternative (Network Security)
Replace the recommended Fortinet FortiGate 40F with a SonicWall TZ270 as the network security appliance. The SonicWall provides equivalent NGFW capabilities including IPS, gateway anti-virus, anti-spyware, content filtering, and application control.
- COST: Slightly lower — SonicWall TZ270 starts at ~$330 appliance-only vs. ~$500 for FortiGate 40F appliance-only. With security subscriptions, total cost is comparable.
- CAPABILITY: SonicWall TZ270 offers 2 Gbps firewall throughput and 750 Mbps threat prevention — adequate for most small practices. FortiGate 40F has superior SD-WAN and ZTNA capabilities.
- MANAGEMENT: SonicWall uses Network Security Manager (NSM) for cloud management; Fortinet uses FortiCloud/FortiManager. Both are capable.
- MSP ECOSYSTEM: Choose based on which vendor the MSP already partners with — margin protection and deal registration are similar between Fortinet and SonicWall partner programs.
- RECOMMEND WHEN: The MSP is already a SonicWall SecureFirst partner with existing expertise, has SonicWall deployed at other clients for operational consistency, or the client specifically requests SonicWall.
Want early access to the full toolkit?