58 min readIntelligence & insights

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

FortinetFortiGate 40F (FG-40F-BDL-950-12)Qty: 1

$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

FortinetFortiGate 60F (FG-60F-BDL-950-12)Qty: 1

$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

FortinetFortiSwitch 108F (FS-108F)Qty: 1

$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

AppleiPad 10th Generation (64GB Wi-Fi, MK2K3LL/A)Qty: 2

$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

CompulocksSpace Enclosure Wall Mount (102IPDSW)Qty: 2

$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)

DellOptiPlex 3000 MicroQty: 5

$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

healow (eClinicalWorks)per-seat SaaSQty: per provider seat

$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

Weave Communicationsper-location SaaSQty: per location

$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

Curogram Inc.per-location SaaSQty: per location

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

Microsoftper-user SaaS

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

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.

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

~$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

Clinikoper-practice SaaS

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

Note

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.

1
Rack or desk-mount the FortiGate 40F/60F
2
Connect WAN port to ISP modem/ONT
3
Connect LAN port(s) to FortiSwitch 108F
4
Perform initial setup via browser (https://192.168.1.99)
5
Register device on FortiCloud and activate UTP license
6
Configure interfaces, VLANs, and firewall policies
FortiGate initial configuration
bash
# 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
end
Note

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

1
Verify all workstations run current OS versions (Windows 11 or macOS 13+)
2
Install/update Chrome or Edge browser to latest version
3
Configure workstations to use the clinical VLAN
4
Set up iPads for reception dashboard display
5
Enroll all devices in the practice's endpoint management solution (if applicable)
6
Verify each workstation can reach the healow Genie portal URL
Windows workstation verification and configuration commands
shell
# 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.1
1
iPad Setup: Sign in with practice-managed Apple ID
2
Install Safari or Chrome from App Store
3
Enable Guided Access (Settings > Accessibility > Guided Access) to lock the iPad to the dashboard URL
4
Connect to reception-vlan Wi-Fi SSID
5
Navigate to healow Genie dashboard URL and bookmark
Note

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

1
healow/eClinicalWorks — for the AI prediction and outreach platform
2
Fortinet/FortiCloud — if using cloud-based FortiGate management or logging
3
Microsoft — if using Power BI, Azure, or Microsoft 365
4
Twilio — if using for custom SMS outreach
5
Any cloud backup provider
6
The MSP itself must have a BAA with the client practice
Critical

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.

Cliniko API Example — fetch recent appointments
python
# 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'])
1
Log into SimplePractice as admin
2
Navigate to Reports > Appointments
3
Set date range to maximum available history
4
Export as CSV
5
Save to encrypted local storage (BitLocker/FileVault)
6
Schedule monthly re-export or set calendar reminder
Warning

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.

1
Contact healow sales to provision the practice's account (reference MSP partner agreement if applicable).
2
Complete healow BAA execution.
3
Provision provider seats (one per clinician who sees patients).
4
Configure EHR data connection — for eClinicalWorks practices this is native; for others, work with healow implementation team on supported integration methods.
5
Configure practice hours, appointment types, and provider schedules.
6
Set up outreach channel preferences (voice, SMS, email).
7
Configure no-show risk thresholds and intervention triggers.

healow Genie Key Configuration Reference

1
healow Genie is a fully managed SaaS platform — no CLI installation needed
2
Configuration is done through the healow admin portal
3
Key configuration checklist:
4
1. PRACTICE PROFILE - Practice name, address, phone, NPI - Operating hours (M-F, weekends if applicable) - Timezone setting - Appointment types (individual therapy, group therapy, psych eval, etc.)
5
2. PROVIDER SETUP - Add each provider: name, credentials, NPI, schedule template - Map providers to appointment types
6
3. OUTREACH CONFIGURATION - SMS templates: Generic, non-identifying messages Example: 'Reminder: You have an appointment on [DATE] at [TIME]. Reply C to confirm or R to reschedule. Call [PHONE] with questions.' - Voice call scripts: Do NOT mention practice specialty Example: 'This is a reminder from [PRACTICE NAME] about your upcoming appointment.' - Email templates: Similar non-identifying language
7
4. RISK THRESHOLD CONFIGURATION - High risk (>70% no-show probability): Trigger personal phone outreach + SMS + email - Medium risk (40-70%): Trigger SMS + email reminders at 72hr, 24hr, 2hr - Low risk (<40%): Standard SMS reminder at 24hr
8
5. DATA CONNECTION - Follow healow implementation team's guidance for EHR integration - Provide API credentials or data export schedule - Verify bidirectional data flow (appointment data in, outreach status back)
Critical

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.

1
Provision Power BI Pro licenses for practice manager and MSP admin
2
Connect to data sources (healow Genie exports, EHR data)
3
Import the custom no-show analytics Power BI template
4
Configure scheduled data refresh
5
Share dashboard with authorized users only
1
Download Power BI Desktop: https://powerbi.microsoft.com/desktop/
2
Sign in with Microsoft 365 account
3
Connect to data sources: Import CSV/Excel from scheduled EHR exports, or connect to Azure SQL if using custom data pipeline
Power BI DAX measures for no-show analytics
dax
# 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
)
Note

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 (24hr)
text
# 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)
text
# 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)

1
SMS Template: High Risk Personal Touch (Tier 3 - 48hr)
2
----------------------------------------
3
Hi [FIRST_NAME], [PROVIDER_FIRST_NAME] at [PRACTICE_NAME] wanted to
4
check in about your appointment on [DATE]. We know schedules get busy —
5
if you need to adjust, just reply CHANGE and we'll find a time that works.
6
Your [PROVIDER_FIRST_NAME] is looking forward to connecting with you.

Staff Call Script: High Risk — Tier 3 (72hr phone call)

1
Staff Call Script: High Risk (Tier 3 - 72hr phone call)
2
----------------------------------------
3
'Hi, may I speak with [FIRST_NAME]? This is [STAFF_NAME] calling from
4
[PRACTICE_NAME]. I'm calling to confirm your appointment on [DATE] at
5
[TIME]. We just want to make sure this time still works for you.'
6
IF PATIENT INDICATES THEY MAY NOT ATTEND:
7
'I completely understand. Would you like to reschedule? We have
8
[AVAILABLE_TIMES] open. We'd love to keep you on the schedule.' DO NOT: Reference therapy, counseling, diagnosis, mental health, or
9
any treatment details during the call.

Email Template: Appointment Confirmation Request — Tier 2 (48hr)

1
Email Template: Appointment Confirmation Request
2
---------------------------------------- Subject: Appointment Reminder - [PRACTICE_NAME] Body: Dear [FIRST_NAME], This is a friendly reminder about your upcoming appointment: Date: [DATE] Time: [TIME] Location: [ADDRESS] Please click below to confirm or reschedule: [CONFIRM_LINK] [RESCHEDULE_LINK]
3
If you have questions, call us at [PHONE]. Best, The [PRACTICE_NAME] Team
Warning

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.

1
Export appointment history (minimum 6 months, ideally 12–24 months) from EHR
2
Clean and validate data — remove test appointments, verify status codes
3
Import into healow Genie or custom ML pipeline
4
Run initial model calibration
5
Review initial risk score distribution
6
Adjust risk thresholds based on practice-specific patterns
Data cleaning script (Python) for CSV-exported appointment data
python
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')
Note

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 and accuracy metrics
python
# 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

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

1
Switch healow Genie from shadow mode to active outreach mode
2
Enable all three outreach tiers
3
Assign staff member(s) responsible for making Tier 3 phone calls
4
Brief all clinical and front-desk staff on the go-live date
5
Monitor closely for the first 5 business days — check daily for any message delivery failures, patient complaints, or unexpected behaviors
6
Set up the reception tablet dashboards to display today's appointment risk view
1
Navigate to Settings > Accessibility > Guided Access > ON
2
Set passcode for Guided Access exit
3
Open Safari, navigate to dashboard URL: [healow Genie Dashboard URL]/today-view
4
Triple-click Home/Side button to start Guided Access
5
Tap 'Start' — iPad is now locked to dashboard view
FortiGate: Add dashboard URL to allowed sites (if web filtering blocks it)
fortios
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
end
Note

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

1
Analyze first 2 weeks of outreach data — which channels drove the most confirmations?
2
Review no-show rate change vs. pre-implementation baseline.
3
Adjust risk thresholds based on observed patterns.
4
Refine message timing (some practices find 48-hour reminders more effective than 72-hour).
5
Identify appointment types with persistently high no-show rates for targeted interventions.
6
Generate first ROI report for practice owner.
ROI Calculation Script
python
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}%')
Note

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_prediction_model.py
python
# 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:

daily_risk_scoring_pipeline.yaml
yaml
# 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 for Linux/macOS server or Azure VM
bash
# 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
Azure Functions timer trigger (serverless) — function.json
json
{
  "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

outreach_orchestrator.py
python
# 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:

ehr_data_extractor.py
python
# 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"

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?