
Implementation Guide: Surface patients overdue for preventive care from ehr data
Step-by-step implementation guide for deploying AI to surface patients overdue for preventive care from ehr data for Healthcare clients.
Hardware Procurement
Data Sync Workstation (Mini PC)
$450 per unit (MSP cost) / $650 suggested resale with setup
Lightweight on-premise agent host for syncing data from on-premise PMS systems (Dentrix, Eaglesoft) to the cloud-based AI analytics engine. Runs the Python sync agent as a Windows service. Not needed if the practice uses a cloud-native PMS like Open Dental Cloud, athenahealth, or Curve Dental.
VoIP Desk Phones
VoIP Desk Phones
$0 per unit (included free with Weave Elite plan) / $150 per unit if purchased separately
IP desk phones for the front office and operatories, fully integrated with Weave's patient communication platform. Display caller ID with patient name, upcoming appointments, outstanding balance, and recall status when a patient calls in.
Patient Check-In Tablet
$329 per unit (MSP cost) / $449 suggested resale with kiosk enclosure and setup
Optional front-desk tablets for patient self-check-in, digital form completion, and self-scheduling. Pairs with Weave digital forms or NexHealth forms to capture updated contact information critical for recall outreach success.
Tablet Kiosk Enclosure
$110 per unit (MSP cost) / $175 suggested resale
Secure floor-standing kiosk enclosure for the iPad check-in tablets, preventing theft and providing a professional appearance in the waiting room.
Firewall/Security Appliance
$350 per unit hardware + $250/year FortiGuard bundle (MSP cost) / $700 suggested resale with 1-year subscription
HIPAA-compliant network perimeter security. Provides firewall, IPS, VPN tunnel for secure data sync between on-premise PMS server and cloud analytics engine, content filtering, and audit logging required for HIPAA technical safeguards.
Software Procurement
Weave (Elite Plan)
$499/month per location + $750 one-time setup fee
Primary patient engagement and recall automation platform. Provides automated recall messaging (SMS, email, voice), two-way texting, online scheduling, review management, payment processing, VoIP phone system with patient pop-ups, and call analytics. The Elite plan includes AI call intelligence and practice analytics. Integrates directly with Dentrix, Eaglesoft, Open Dental, Dentrix Ascend, athenahealth, eClinicalWorks, and 40+ other PMS/EHR systems.
AWS Account (HIPAA-Eligible Services)
$40–$80/month for a typical single-practice deployment
Cloud infrastructure for the custom AI analytics engine. Uses Lambda (serverless compute), API Gateway (REST API), RDS PostgreSQL (patient analytics database), S3 (report storage), CloudWatch (monitoring and audit logs), and SES (email reports). All services configured under a signed AWS BAA for HIPAA compliance.
Anthropic Claude API (with BAA)
$50–$100/month for moderate usage (Claude Sonnet 4.6: $3/MTok input, $15/MTok output)
AI language model API for generating personalized patient recall messages, analyzing patient visit patterns, producing natural language insights in monthly reports, and powering the conversational recall agent for two-way SMS interactions. Anthropic offers a BAA for HIPAA-covered use cases.
Twilio (HIPAA-Eligible)
$50–$150/month (phone number: $1.15/mo, SMS: $0.0079/msg, voice: $0.014/min)
HIPAA-eligible communication API for the custom AI recall system. Handles programmatic SMS sending/receiving for the AI conversational agent, voice call initiation for automated recall reminders, and webhook handling for two-way patient interactions. Used alongside (not replacing) Weave for the AI-enhanced recall conversations.
Supabase (Self-Hosted or Pro Plan)
$25/month per project
PostgreSQL database and authentication layer for the custom AI analytics dashboard. Stores synced patient recall data, risk scores, outreach history, and dashboard state. Alternatively, can use AWS RDS PostgreSQL directly at similar cost.
Retool (Business Plan)
$10/user/month
Low-code internal tool builder for creating the practice-facing analytics dashboard. Connects to the Supabase/PostgreSQL database to display overdue patient lists, recall conversion funnels, revenue recovery tracking, and AI-generated insights without custom frontend development.
$250/year
Security subscription for the FortiGate 40F firewall including IPS, web filtering, antivirus, and application control. Required for HIPAA-compliant network security posture.
Prerequisites
- Active PMS/EHR subscription with API or database access enabled (Dentrix, Eaglesoft, Open Dental, athenahealth, eClinicalWorks, or equivalent). Verify the practice has admin-level access to enable integrations.
- Patient data must be populated with: last visit dates, procedure codes (CDT for dental, CPT for medical), recall/recare due dates, and current mobile phone numbers and email addresses for at least 70% of active patients.
- Reliable internet connection: minimum 25 Mbps download / 10 Mbps upload per practice location. Verify with a speed test at the practice during business hours.
- For on-premise PMS (Dentrix, Eaglesoft): access to the PMS server with administrator credentials, the ability to install a Windows service, and outbound HTTPS (port 443) access from the server through the firewall.
- Practice must designate a HIPAA Privacy Officer and Security Officer (can be the same person). Confirm the practice has a current HIPAA Risk Assessment on file (within the last 12 months).
- Written authorization from the practice owner or office manager to access and process PHI for the recall system. This should be documented as part of the MSP's BAA with the practice.
- Signed Business Associate Agreement (BAA) between the MSP and the practice. The MSP must also have BAAs in place with Weave, AWS, Anthropic, and Twilio before any PHI is processed.
- AWS account with HIPAA BAA signed (available through AWS Artifact in the console). MSP must configure the account for HIPAA-eligible services only.
- Practice staff availability for 2–3 hours of training during Week 3 of implementation. Schedule with office manager in advance.
- Current patient recall/recare rules documented by the practice (e.g., 6-month hygiene recall, 12-month comprehensive exam, 3-month perio maintenance). If not documented, work with the lead dentist or physician to define them.
Installation Steps
...
Step 1: Execute Business Associate Agreements and Vendor Contracts
Before any technical work begins, establish the legal HIPAA compliance chain. Sign BAAs with all vendors that will touch PHI and execute the service agreement with the practice client. This step protects both the MSP and the practice from regulatory liability.
BAA execution order: (1) MSP ↔ Practice client, (2) MSP ↔ Weave, (3) MSP ↔ AWS (via AWS Artifact in console), (4) MSP ↔ Anthropic (request via sales/enterprise), (5) MSP ↔ Twilio (available in Twilio console under HIPAA settings). Keep signed copies in a secure document management system. Do NOT proceed with any data access until all BAAs are executed. Typical turnaround: Weave BAA (1–3 business days), AWS BAA (instant via Artifact), Anthropic BAA (3–5 business days), Twilio BAA (instant via console toggle).
Step 2: Assess Current Practice Environment and Document Baseline Metrics
Perform a site survey of the practice's IT infrastructure and document the current state of their patient recall process. Record baseline metrics that will be used to measure ROI after implementation. Inventory all workstations, the PMS server, network equipment, internet speeds, and current recall workflow.
# Run speed test from PMS server
Invoke-WebRequest -Uri 'https://speed.cloudflare.com' -UseBasicParsing | Select-Object StatusCode
# Check PMS server specs (Windows)
Get-ComputerInfo | Select-Object CsName, OsName, OsArchitecture, CsTotalPhysicalMemory, CsNumberOfProcessors
# Document current firewall rules
Get-NetFirewallRule -Direction Outbound -Enabled True | Select-Object DisplayName, Action, Profile | Export-Csv C:\Temp\firewall_rules.csv
# Check SQL Server / MySQL instance for PMS database access
Get-Service -Name 'MSSQL*' | Select-Object Name, Status
Get-Service -Name 'MySQL*' | Select-Object Name, StatusKey baseline metrics to document: (1) Total active patients in PMS, (2) Number of patients currently overdue for recall, (3) Current monthly recall appointment count, (4) No-show rate percentage, (5) Hours per week staff spends on manual recall calls, (6) Current software tools used for recall (if any). Take screenshots of the PMS recall module showing current overdue counts. These baselines are critical for the ROI report at 90 days post-deployment.
Step 3: Install and Configure Network Security (FortiGate 40F)
Install the FortiGate 40F firewall at the practice location if one is not already present. Configure HIPAA-compliant security policies including VPN tunnel for secure PMS data sync, intrusion prevention, content filtering, and comprehensive logging for audit trail requirements.
# FortiGate initial configuration via CLI (connect via console cable)
config system interface
edit port1
set ip 192.168.1.1 255.255.255.0
set allowaccess ping https ssh
next
edit wan1
set mode dhcp
set allowaccess ping
next
end
# Enable logging for HIPAA audit trail
config log fortianalyzer setting
set status enable
end
config log syslogd setting
set status enable
set server "syslog.yourmsp.com"
set port 514
set format default
end
# Configure SSL VPN for secure remote management
config vpn ssl settings
set servercert "Fortinet_Factory"
set tunnel-ip-pools "SSLVPN_TUNNEL_ADDR1"
set port 10443
set source-interface wan1
end
# Create firewall policy for PMS server outbound HTTPS (for data sync)
config firewall policy
edit 10
set name "PMS-Sync-Outbound"
set srcintf "internal"
set dstintf "wan1"
set srcaddr "PMS-Server"
set dstaddr "all"
set action accept
set schedule "always"
set service "HTTPS"
set logtraffic all
set utm-status enable
set ips-sensor "default"
set ssl-ssh-profile "certificate-inspection"
next
endIf the practice already has a managed firewall, skip hardware installation but still verify: (1) outbound HTTPS (443) is permitted from the PMS server, (2) logging is enabled and retained for 6+ years per HIPAA, (3) IPS/IDS is active. For practices using Dentrix or Eaglesoft on-premise, the PMS server IP should be added to a dedicated address object for granular policy control. Document the firewall configuration as part of the HIPAA technical safeguards documentation.
Step 4: Deploy Weave Platform - Account Setup and PMS Integration
Create the practice's Weave account, install the Weave desktop application on front office workstations, and configure the PMS integration connector. Weave will sync patient demographics, appointment data, and recall status from the PMS in real-time.
# Download and install Weave desktop app on front office workstations
# Navigate to https://www.getweave.com/download
# Run the installer as administrator
# Weave requires .NET 4.7.2+ on Windows
# Verify Weave sync agent connectivity (after installation)
Test-NetConnection -ComputerName sync.getweave.com -Port 443
# Check that the Weave sync service is running
Get-Service -Name 'WeaveSync*' | Select-Object Name, Status, StartTypeWeave onboarding is vendor-assisted — a Weave implementation specialist will schedule a call to walk through PMS connector setup. For Dentrix: Weave installs a sync agent that reads the Dentrix database via the Dentrix API/ODBC connector. For Eaglesoft: uses the Patterson-Weave partnership integration (announced January 2025) for seamless data flow. For Open Dental: uses the Open Dental API. Schedule the Weave onboarding call for Week 1 Day 2 or 3 after the site survey is complete. The sync typically takes 4–8 hours for initial patient data import depending on practice size.
Step 5: Configure Weave Recall Rules and Message Templates
Working with the practice manager and lead clinician, configure the recall automation rules in Weave to match the practice's specific preventive care protocols. Set up message templates for SMS, email, and voice reminders that are HIPAA-compliant and match the practice's brand voice.
Common recall rule configurations for dental practices: (1) Hygiene/Prophylaxis recall: 6 months from last D1110/D1120 procedure, (2) Comprehensive exam: 12 months from last D0150, (3) Periodic exam: 6 months from last D0120, (4) Perio maintenance: 3–4 months from last D4910, (5) Pediatric fluoride: 6 months from last D1206. For medical practices: (1) Annual physical: 12 months from last 99395-99397, (2) Diabetic screening: 6 months from last 83036, (3) Mammography: 12 months from last 77067, (4) Colonoscopy: 10 years from last 45378. Configure the outreach cadence: first SMS at 2 weeks before due date, second SMS at due date, email at 1 week overdue, voice reminder at 2 weeks overdue, then weekly texts for 4 more weeks. All messages must avoid specific PHI in the preview text — use 'You are due for your appointment' rather than 'You are due for your diabetes screening'. Enable two-way texting so patients can reply to schedule directly.
Step 6: Provision AWS Infrastructure for Custom AI Analytics Engine
Set up the AWS cloud infrastructure that will host the custom AI analytics engine. This includes creating the VPC, Lambda functions, API Gateway, RDS PostgreSQL database, and S3 bucket — all configured under the HIPAA BAA with encryption at rest and in transit.
# Install AWS CLI and configure credentials
pip install awscli
aws configure
# Enter MSP AWS access key, secret key, region (us-east-1 recommended)
# Create HIPAA-compliant VPC with private subnets
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=recall-analytics-vpc},{Key=Environment,Value=production},{Key=HIPAA,Value=true}]'
# Create RDS PostgreSQL instance with encryption
aws rds create-db-instance \
--db-instance-identifier recall-analytics-db \
--db-instance-class db.t3.micro \
--engine postgres \
--engine-version 15.4 \
--master-username recall_admin \
--master-user-password '<GENERATE_STRONG_PASSWORD>' \
--allocated-storage 20 \
--storage-encrypted \
--kms-key-id alias/aws/rds \
--vpc-security-group-ids sg-XXXXXXXXX \
--db-subnet-group-name recall-db-subnet-group \
--backup-retention-period 35 \
--multi-az \
--no-publicly-accessible \
--tags Key=HIPAA,Value=true
# Create S3 bucket for reports with encryption and versioning
aws s3api create-bucket --bucket recall-analytics-reports-<CLIENT_ID> --region us-east-1
aws s3api put-bucket-encryption --bucket recall-analytics-reports-<CLIENT_ID> --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"}},{"BucketKeyEnabled":true}]}'
aws s3api put-bucket-versioning --bucket recall-analytics-reports-<CLIENT_ID> --versioning-configuration Status=Enabled
aws s3api put-public-access-block --bucket recall-analytics-reports-<CLIENT_ID> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# Create CloudWatch log group with 6-year retention for HIPAA
aws logs create-log-group --log-group-name /recall-analytics/production
aws logs put-retention-policy --log-group-name /recall-analytics/production --retention-in-days 2192CRITICAL: Before creating any resources, verify the AWS BAA is signed in AWS Artifact (Console > AWS Artifact > Agreements > AWS Business Associate Addendum > Accept). Only use HIPAA-eligible AWS services. Enable AWS CloudTrail for API audit logging across the account. Use AWS KMS for all encryption keys. The RDS instance should be in a private subnet with no public access — Lambda functions will access it via VPC attachment. Estimated monthly cost for this infrastructure: $40–$80/month for a single practice.
Step 7: Install PMS Data Sync Agent on Practice Server
Deploy the custom Python-based data sync agent on the practice's PMS server (or the Intel NUC mini PC). This agent performs a nightly read-only extraction of patient recall data from the PMS database and securely uploads it to the AWS RDS database for AI analysis.
# Install Python 3.11 on the sync workstation (Windows)
# Download from https://www.python.org/downloads/release/python-3119/
# Ensure 'Add to PATH' is checked during installation
# Create project directory
mkdir C:\RecallSync
cd C:\RecallSync
# Create virtual environment
python -m venv venv
.\venv\Scripts\activate
# Install dependencies
pip install psycopg2-binary pyodbc cryptography schedule python-dotenv requests
# Create .env file with encrypted credentials
# (Use the configuration script from the custom AI components section)
python configure_sync.py
# Install as Windows service using NSSM
# Download NSSM from https://nssm.cc/download
nssm install RecallSyncAgent C:\RecallSync\venv\Scripts\python.exe C:\RecallSync\sync_agent.py
nssm set RecallSyncAgent AppDirectory C:\RecallSync
nssm set RecallSyncAgent Description "Patient Recall Data Sync Agent - HIPAA Compliant"
nssm set RecallSyncAgent Start SERVICE_AUTO_START
nssm set RecallSyncAgent AppStdout C:\RecallSync\logs\sync_stdout.log
nssm set RecallSyncAgent AppStderr C:\RecallSync\logs\sync_stderr.log
nssm start RecallSyncAgent
# Verify the service is running
Get-Service -Name RecallSyncAgent | Select-Object Name, StatusThe sync agent performs READ-ONLY queries against the PMS database — it never writes or modifies patient data. For Dentrix: connect via ODBC to the Dentrix database (typically SQL Server Compact or SQL Server). For Eaglesoft: connect via ODBC to the Eaglesoft SQL Anywhere database. For Open Dental: connect via MySQL on port 3306 (Open Dental uses MySQL/MariaDB). The sync runs nightly at 2:00 AM local time by default to avoid impacting practice hours. All data in transit is encrypted via TLS 1.2+ (the psycopg2 connection to AWS RDS uses SSL). Test the sync manually before enabling the scheduled service. Check logs at C:\RecallSync\logs\ to verify successful sync.
Step 8: Deploy AI Analytics Lambda Functions
Deploy the serverless Lambda functions that power the AI analytics engine. These functions run the patient risk scoring model, generate personalized recall messages using Claude API, produce monthly analytics reports, and expose the REST API for the dashboard.
# Clone the recall analytics project
git clone https://github.com/<your-msp>/recall-analytics-engine.git
cd recall-analytics-engine
# Install Serverless Framework
npm install -g serverless
npm install
# Configure environment variables
cp .env.example .env.production
# Edit .env.production with client-specific values:
# ANTHROPIC_API_KEY=sk-ant-XXXXX
# DB_HOST=recall-analytics-db.XXXXX.us-east-1.rds.amazonaws.com
# DB_NAME=recall_analytics
# DB_USER=recall_admin
# DB_PASSWORD=<password>
# TWILIO_ACCOUNT_SID=ACXXXXX
# TWILIO_AUTH_TOKEN=XXXXX
# TWILIO_PHONE_NUMBER=+1XXXXXXXXXX
# PRACTICE_NAME=<Practice Name>
# PRACTICE_TIMEZONE=America/New_York
# Deploy to AWS
serverless deploy --stage production
# Run database migrations
serverless invoke --function migrate --stage production
# Trigger initial patient analysis
serverless invoke --function analyzePatients --stage production
# Verify API endpoint
curl https://<api-id>.execute-api.us-east-1.amazonaws.com/production/healthThe Serverless Framework handles all Lambda function creation, API Gateway configuration, IAM roles, and CloudWatch log setup. The deploy creates the following functions: (1) syncProcessor - processes incoming PMS data sync, (2) analyzePatients - runs nightly risk scoring and overdue detection, (3) generateRecallMessages - uses Claude API to create personalized messages, (4) sendRecalls - dispatches messages via Twilio, (5) handleInbound - processes patient SMS replies, (6) generateReport - creates monthly practice intelligence report, (7) dashboardAPI - serves data to the Retool dashboard. Each function has a 15-minute timeout and 512MB memory allocation. The analyzePatients function is triggered by a CloudWatch Events cron schedule at 3:00 AM EST nightly (after the PMS sync completes at 2:00 AM).
Step 9: Configure Retool Analytics Dashboard
Set up the Retool-based practice analytics dashboard that the practice manager and MSP can use to view overdue patients, recall conversion rates, revenue recovery metrics, and AI-generated insights. Connect Retool to the AWS RDS PostgreSQL database.
CREATE ROLE retool_readonly WITH LOGIN PASSWORD '<STRONG_PASSWORD>';
GRANT CONNECT ON DATABASE recall_analytics TO retool_readonly;
GRANT USAGE ON SCHEMA public TO retool_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO retool_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO retool_readonly;Retool connects to the database via a secure SSL connection. The read-only user ensures the dashboard cannot modify patient data. Key dashboard views to configure: (1) Overdue Patient List - sortable by days overdue, risk score, and last visit date, (2) Recall Funnel - shows patients at each outreach stage (not yet contacted → contacted → responded → scheduled → completed), (3) Revenue Recovery - estimated revenue from scheduled recall appointments, (4) Monthly Trends - line charts of recall rate, no-show rate, and patient reactivation over time, (5) AI Insights Panel - natural language summary generated by Claude API. Share the dashboard URL with the practice manager and configure their login with MFA enabled.
Step 10: Configure Twilio for HIPAA-Compliant Patient Messaging
Set up the Twilio account for HIPAA-compliant SMS messaging used by the custom AI recall agent. Register the practice's phone number, configure webhooks, and enable HIPAA security features.
# Twilio setup via CLI
pip install twilio-cli
twilio login
# Purchase a local phone number for the practice
twilio phone-numbers:buy:local --area-code <PRACTICE_AREA_CODE> --sms-enabled --voice-enabled
# Configure webhook for inbound SMS handling
twilio phone-numbers:update <PHONE_NUMBER_SID> \
--sms-url https://<api-id>.execute-api.us-east-1.amazonaws.com/production/twilio/inbound \
--sms-method POSTA2P 10DLC registration is REQUIRED for business SMS in the US and takes 1–2 weeks for approval. Start this process on Day 1 of the project. Without registration, SMS delivery rates will be severely throttled. When configuring Twilio for HIPAA: (1) Never include PHI in message logs — enable log redaction, (2) Do not store message bodies in Twilio — use your own encrypted database, (3) Set message retention to minimum in Twilio console. The AI recall messages sent via Twilio complement (not replace) the Weave platform's recall — Twilio is used specifically for the AI conversational agent that handles complex reactivation scenarios for long-overdue patients (12+ months).
Step 11: Staff Training and Workflow Configuration
Conduct hands-on training sessions with practice staff on both the Weave platform and the custom analytics dashboard. Configure staff workflows for handling recall responses, scheduling reactivated patients, and reviewing analytics reports.
Training should cover three distinct sessions: (1) Front Desk Staff (1.5 hours): Weave desktop app, patient pop-ups, two-way texting, handling recall responses, confirming appointments from text replies. (2) Office Manager (1 hour): Retool analytics dashboard walkthrough, understanding the overdue patient list, interpreting risk scores, running and exporting reports, understanding the recall funnel metrics. (3) Lead Clinician/Doctor (30 minutes): Overview of recall rules configured, how to request changes to recall intervals, reviewing the monthly AI insights report. Create a quick-reference card (laminated, 1-page) for front desk staff showing: how to check the overdue patient list, how to respond to incoming patient texts, how to mark a patient as contacted/scheduled in the system. Leave printed copies at each front desk workstation.
Step 12: Go-Live with Phased Recall Campaign
Launch the recall system in a phased approach to catch issues early. Start with a small batch of overdue patients, validate message delivery and response handling, then scale to full automation.
- Phase 1: Test batch (Day 1 of go-live) — In Weave: Send recall messages to 50 patients overdue by 1-3 months. Monitor delivery rates, opt-outs, and responses for 48 hours.
- Phase 2: Expand to full standard recall (Day 3) — In Weave: Enable automatic recall for all patients overdue 0-6 months. Enable the custom AI agent for patients overdue 6-12 months.
- Phase 3: Full automation (Day 7) — Enable AI conversational agent for patients overdue 12+ months. Enable monthly analytics report generation.
serverless invoke --function generateReport --stage production --data '{"report_type": "monthly", "test": true}'aws cloudwatch get-metric-statistics \
--namespace RecallAnalytics \
--metric-name MessageDeliveryRate \
--start-time $(date -d '-7 days' -u +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 86400 \
--statistics AverageThe phased approach is critical for catching issues before they affect a large patient population. During Phase 1, personally monitor the first 50 messages: check for delivery confirmation, validate message content is appropriate and HIPAA-compliant, and confirm two-way replies are being routed correctly. Common issues to watch for: (1) Outdated phone numbers bouncing — flag these for staff to update, (2) Patients replying STOP — ensure opt-out is processed immediately, (3) Message content appearing too generic or too clinical — adjust templates. After 7 days of clean operation, the system can run fully autonomously with weekly MSP monitoring.
Custom AI Components
PMS Data Sync Agent
Type: integration A Python-based Windows service that performs nightly read-only extraction of patient recall data from the on-premise PMS database (Dentrix/Eaglesoft/Open Dental) and securely uploads it to the AWS RDS PostgreSQL database. Supports ODBC connections to SQL Server, SQL Anywhere, and MySQL. Includes encryption, error handling, retry logic, and audit logging.
Implementation:
# HIPAA Compliant Patient Recall Data Extraction
#!/usr/bin/env python3
"""PMS Data Sync Agent - HIPAA Compliant Patient Recall Data Extraction
Runs as a Windows service via NSSM. Performs nightly read-only sync of patient
recall data from on-premise PMS to AWS RDS PostgreSQL.
"""
import os
import sys
import json
import logging
import hashlib
import datetime
import time
import schedule
import pyodbc
import psycopg2
import psycopg2.extras
from dotenv import load_dotenv
from pathlib import Path
from cryptography.fernet import Fernet
load_dotenv()
# Configure logging with HIPAA-compliant audit trail
LOG_DIR = Path('C:/RecallSync/logs')
LOG_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(LOG_DIR / f'sync_{datetime.date.today().isoformat()}.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger('RecallSync')
# PMS Type Configuration
PMS_TYPE = os.getenv('PMS_TYPE', 'dentrix') # dentrix | eaglesoft | opendental
# PMS Connection Queries by vendor
PMS_QUERIES = {
'dentrix': """
SELECT
p.patient_id,
p.first_name,
p.last_name,
p.date_of_birth,
p.home_phone,
p.cell_phone,
p.email,
p.status AS patient_status,
r.recall_type,
r.recall_due_date,
r.recall_interval_months,
a.appointment_date AS last_visit_date,
a.procedure_code AS last_procedure_code,
ins.carrier_name AS insurance_carrier
FROM patients p
LEFT JOIN recall r ON p.patient_id = r.patient_id AND r.status = 'Active'
LEFT JOIN (
SELECT patient_id, MAX(appointment_date) as appointment_date,
procedure_code
FROM appointments
WHERE status = 'Complete'
GROUP BY patient_id
) a ON p.patient_id = a.patient_id
LEFT JOIN insurance ins ON p.patient_id = ins.patient_id AND ins.ordinal = 1
WHERE p.status = 'Active'
AND r.recall_due_date IS NOT NULL
""",
'eaglesoft': """
SELECT
p.id AS patient_id,
p.firstname AS first_name,
p.lastname AS last_name,
p.birthdate AS date_of_birth,
p.home_phone,
p.wireless_phone AS cell_phone,
p.email,
p.status AS patient_status,
rc.description AS recall_type,
r.due_date AS recall_due_date,
r.interval AS recall_interval_months,
(
SELECT MAX(a.date) FROM appointment a
WHERE a.patient_id = p.id AND a.status = 'Completed'
) AS last_visit_date,
ic.name AS insurance_carrier
FROM patient p
LEFT JOIN recall r ON p.id = r.patient_id AND r.status = 1
LEFT JOIN recall_code rc ON r.recall_code_id = rc.id
LEFT JOIN insurance_carrier ic ON p.primary_carrier_id = ic.id
WHERE p.status = 1
AND r.due_date IS NOT NULL
""",
'opendental': """
SELECT
p.PatNum AS patient_id,
p.FName AS first_name,
p.LName AS last_name,
p.Birthdate AS date_of_birth,
p.HmPhone AS home_phone,
p.WirelessPhone AS cell_phone,
p.Email AS email,
CASE p.PatStatus WHEN 0 THEN 'Active' ELSE 'Inactive' END AS patient_status,
rt.Description AS recall_type,
r.DateDue AS recall_due_date,
r.RecallInterval AS recall_interval_months,
(
SELECT MAX(pl.ProcDate) FROM procedurelog pl
WHERE pl.PatNum = p.PatNum AND pl.ProcStatus = 2
) AS last_visit_date,
cp.CarrierName AS insurance_carrier
FROM patient p
LEFT JOIN recall r ON p.PatNum = r.PatNum AND r.IsDisabled = 0
LEFT JOIN recalltype rt ON r.RecallTypeNum = rt.RecallTypeNum
LEFT JOIN patplan pp ON p.PatNum = pp.PatNum AND pp.Ordinal = 1
LEFT JOIN insplan ip ON pp.PlanNum = ip.PlanNum
LEFT JOIN carrier cp ON ip.CarrierNum = cp.CarrierNum
WHERE p.PatStatus = 0
AND r.DateDue IS NOT NULL
AND r.DateDue != '0001-01-01'
"""
}
# PMS Connection strings
PMS_CONNECTIONS = {
'dentrix': f"DRIVER={{SQL Server}};SERVER={os.getenv('PMS_SERVER', 'localhost')};DATABASE={os.getenv('PMS_DATABASE', 'DentrixDB')};UID={os.getenv('PMS_USER')};PWD={os.getenv('PMS_PASSWORD')};Encrypt=yes;TrustServerCertificate=yes;",
'eaglesoft': f"DRIVER={{SQL Anywhere 17}};SERVER={os.getenv('PMS_SERVER', 'localhost')};DATABASE={os.getenv('PMS_DATABASE', 'EaglesoftDB')};UID={os.getenv('PMS_USER', 'dba')};PWD={os.getenv('PMS_PASSWORD')};Encrypt=yes;",
'opendental': None # Uses MySQL connector
}
# AWS RDS PostgreSQL connection
RDS_CONFIG = {
'host': os.getenv('RDS_HOST'),
'port': int(os.getenv('RDS_PORT', '5432')),
'database': os.getenv('RDS_DATABASE', 'recall_analytics'),
'user': os.getenv('RDS_USER'),
'password': os.getenv('RDS_PASSWORD'),
'sslmode': 'require',
'sslrootcert': 'C:/RecallSync/certs/us-east-1-bundle.pem'
}
def get_pms_connection():
"""Establish read-only connection to the PMS database."""
if PMS_TYPE == 'opendental':
import mysql.connector
return mysql.connector.connect(
host=os.getenv('PMS_SERVER', 'localhost'),
port=int(os.getenv('PMS_PORT', '3306')),
database=os.getenv('PMS_DATABASE', 'opendental'),
user=os.getenv('PMS_USER'),
password=os.getenv('PMS_PASSWORD'),
ssl_ca='C:/RecallSync/certs/mysql-ca.pem'
)
else:
conn_string = PMS_CONNECTIONS[PMS_TYPE]
conn = pyodbc.connect(conn_string, readonly=True)
return conn
def get_rds_connection():
"""Establish secure connection to AWS RDS PostgreSQL."""
return psycopg2.connect(**RDS_CONFIG)
def hash_patient_id(patient_id):
"""Create a consistent hash of patient ID for de-duplication."""
salt = os.getenv('HASH_SALT', 'recall-sync-2025')
return hashlib.sha256(f"{salt}:{patient_id}".encode()).hexdigest()[:16]
def sync_patients():
"""Main sync function - extracts patient recall data and uploads to RDS."""
logger.info('Starting patient recall data sync...')
start_time = time.time()
records_synced = 0
errors = 0
try:
# Connect to PMS (read-only)
pms_conn = get_pms_connection()
pms_cursor = pms_conn.cursor()
logger.info(f'Connected to {PMS_TYPE} PMS database')
# Execute extraction query
query = PMS_QUERIES[PMS_TYPE]
pms_cursor.execute(query)
columns = [desc[0] for desc in pms_cursor.description]
patients = [dict(zip(columns, row)) for row in pms_cursor.fetchall()]
logger.info(f'Extracted {len(patients)} patient recall records from PMS')
# Connect to RDS
rds_conn = get_rds_connection()
rds_cursor = rds_conn.cursor()
# Upsert patient data
upsert_sql = """
INSERT INTO patient_recall (
patient_hash, first_name, last_name, date_of_birth,
cell_phone, email, patient_status, recall_type,
recall_due_date, recall_interval_months, last_visit_date,
last_procedure_code, insurance_carrier,
pms_patient_id, pms_type, sync_timestamp
) VALUES (
%(patient_hash)s, %(first_name)s, %(last_name)s, %(date_of_birth)s,
%(cell_phone)s, %(email)s, %(patient_status)s, %(recall_type)s,
%(recall_due_date)s, %(recall_interval_months)s, %(last_visit_date)s,
%(last_procedure_code)s, %(insurance_carrier)s,
%(pms_patient_id)s, %(pms_type)s, NOW()
)
ON CONFLICT (patient_hash, pms_type)
DO UPDATE SET
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
cell_phone = EXCLUDED.cell_phone,
email = EXCLUDED.email,
patient_status = EXCLUDED.patient_status,
recall_type = EXCLUDED.recall_type,
recall_due_date = EXCLUDED.recall_due_date,
last_visit_date = EXCLUDED.last_visit_date,
last_procedure_code = EXCLUDED.last_procedure_code,
insurance_carrier = EXCLUDED.insurance_carrier,
sync_timestamp = NOW()
"""
for patient in patients:
try:
patient_data = {
'patient_hash': hash_patient_id(patient.get('patient_id')),
'first_name': patient.get('first_name', ''),
'last_name': patient.get('last_name', ''),
'date_of_birth': patient.get('date_of_birth'),
'cell_phone': patient.get('cell_phone') or patient.get('home_phone', ''),
'email': patient.get('email', ''),
'patient_status': patient.get('patient_status', 'Active'),
'recall_type': patient.get('recall_type', 'General'),
'recall_due_date': patient.get('recall_due_date'),
'recall_interval_months': patient.get('recall_interval_months'),
'last_visit_date': patient.get('last_visit_date'),
'last_procedure_code': patient.get('last_procedure_code', ''),
'insurance_carrier': patient.get('insurance_carrier', ''),
'pms_patient_id': str(patient.get('patient_id')),
'pms_type': PMS_TYPE
}
rds_cursor.execute(upsert_sql, patient_data)
records_synced += 1
except Exception as e:
errors += 1
logger.error(f"Error syncing patient {hash_patient_id(patient.get('patient_id'))}: {str(e)}")
continue
rds_conn.commit()
# Log audit record
rds_cursor.execute("""
INSERT INTO sync_audit_log (sync_timestamp, records_extracted, records_synced, errors, duration_seconds, pms_type)
VALUES (NOW(), %s, %s, %s, %s, %s)
""", (len(patients), records_synced, errors, time.time() - start_time, PMS_TYPE))
rds_conn.commit()
logger.info(f'Sync complete: {records_synced} records synced, {errors} errors, {time.time()-start_time:.1f}s')
except Exception as e:
logger.error(f'Sync failed: {str(e)}')
raise
finally:
if 'pms_conn' in locals():
pms_conn.close()
if 'rds_conn' in locals():
rds_conn.close()
def main():
"""Main entry point - runs sync on schedule."""
logger.info('Recall Sync Agent starting...')
logger.info(f'PMS Type: {PMS_TYPE}')
logger.info(f'Sync schedule: Daily at 02:00')
# Schedule nightly sync
schedule.every().day.at('02:00').do(sync_patients)
# Also run once immediately on startup for initial sync
sync_patients()
while True:
schedule.run_pending()
time.sleep(60)
if __name__ == '__main__':
main()Patient Risk Scoring Engine
Type: skill A serverless Lambda function that analyzes synced patient recall data to calculate a risk score (0-100) for each overdue patient. The score considers days overdue, visit frequency history, age, insurance status, and recall type urgency. Higher-risk patients are prioritized for outreach. This replaces simple date-based recall with intelligent prioritization.
Implementation:
# AWS Lambda Function. Calculates risk scores for overdue patients to
# prioritize recall outreach. Deployed via Serverless Framework. Triggered
# nightly by CloudWatch Events.
"""Patient Risk Scoring Engine - AWS Lambda Function
Calculates risk scores for overdue patients to prioritize recall outreach.
Deployed via Serverless Framework. Triggered nightly by CloudWatch Events.
"""
import os
import json
import logging
import psycopg2
import psycopg2.extras
from datetime import datetime, date, timedelta
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Risk factor weights (tunable per practice)
WEIGHTS = {
'days_overdue': 0.30, # How far past due date
'visit_gap_trend': 0.20, # Is patient becoming less frequent?
'recall_type_urgency': 0.15,# Perio maintenance > hygiene > general
'age_risk': 0.10, # Pediatric and 65+ get higher priority
'insurance_status': 0.10, # Insured patients more likely to convert
'contact_quality': 0.15 # Has mobile phone + email = higher reachability
}
RECALL_TYPE_URGENCY = {
'Perio Maintenance': 95,
'Perio': 95,
'D4910': 95,
'Hygiene': 70,
'Prophylaxis': 70,
'Prophy': 70,
'D1110': 70,
'D1120': 65,
'Comprehensive Exam': 60,
'D0150': 60,
'Periodic Exam': 50,
'D0120': 50,
'Annual Physical': 55,
'Diabetic Screening': 80,
'Mammography': 75,
'Colonoscopy': 40,
'General': 45
}
def get_db_connection():
return psycopg2.connect(
host=os.environ['DB_HOST'],
port=int(os.environ.get('DB_PORT', '5432')),
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
sslmode='require'
)
def calculate_days_overdue_score(recall_due_date):
"""Score based on how many days past the recall due date."""
if not recall_due_date:
return 50
days_overdue = (date.today() - recall_due_date).days
if days_overdue <= 0:
return 0 # Not yet overdue
elif days_overdue <= 30:
return 30
elif days_overdue <= 90:
return 50
elif days_overdue <= 180:
return 70
elif days_overdue <= 365:
return 85
else:
return 95 # 12+ months overdue - critical
def calculate_age_risk_score(date_of_birth):
"""Higher priority for pediatric and senior patients."""
if not date_of_birth:
return 50
age = (date.today() - date_of_birth).days / 365.25
if age < 6:
return 80 # Young children - early dental habits critical
elif age < 18:
return 65 # Adolescents
elif age < 40:
return 40 # Low risk adults
elif age < 65:
return 55 # Middle age - increasing risk
else:
return 85 # Seniors - highest health risk
def calculate_recall_type_score(recall_type):
"""Score based on clinical urgency of the recall type."""
if not recall_type:
return 45
for key, score in RECALL_TYPE_URGENCY.items():
if key.lower() in recall_type.lower():
return score
return 45
def calculate_insurance_score(insurance_carrier):
"""Insured patients are more likely to schedule and have benefits to use."""
if insurance_carrier and insurance_carrier.strip():
return 75 # Has insurance - higher conversion likelihood
return 35 # No insurance - lower but still worth contacting
def calculate_contact_quality_score(cell_phone, email):
"""Better contact info = higher reachability = higher score."""
score = 0
has_phone = bool(cell_phone and cell_phone.strip() and len(cell_phone.strip()) >= 10)
has_email = bool(email and email.strip() and '@' in email)
if has_phone and has_email:
score = 90
elif has_phone:
score = 70
elif has_email:
score = 50
else:
score = 10 # No good contact method
return score
def calculate_risk_score(patient):
"""Calculate composite risk score for a patient (0-100)."""
scores = {
'days_overdue': calculate_days_overdue_score(patient.get('recall_due_date')),
'visit_gap_trend': 50, # Default; enhanced with historical data if available
'recall_type_urgency': calculate_recall_type_score(patient.get('recall_type')),
'age_risk': calculate_age_risk_score(patient.get('date_of_birth')),
'insurance_status': calculate_insurance_score(patient.get('insurance_carrier')),
'contact_quality': calculate_contact_quality_score(
patient.get('cell_phone'), patient.get('email')
)
}
composite = sum(scores[k] * WEIGHTS[k] for k in WEIGHTS)
return round(min(max(composite, 0), 100), 1), scores
def handler(event, context):
"""Lambda handler - triggered nightly by CloudWatch Events."""
logger.info('Starting patient risk scoring analysis...')
conn = get_db_connection()
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
try:
# Fetch all active patients with overdue recalls
cursor.execute("""
SELECT patient_hash, first_name, last_name, date_of_birth,
cell_phone, email, recall_type, recall_due_date,
last_visit_date, insurance_carrier, pms_patient_id
FROM patient_recall
WHERE patient_status = 'Active'
AND recall_due_date IS NOT NULL
AND recall_due_date < CURRENT_DATE
""")
overdue_patients = cursor.fetchall()
logger.info(f'Found {len(overdue_patients)} overdue patients to score')
scored_count = 0
for patient in overdue_patients:
risk_score, score_breakdown = calculate_risk_score(patient)
cursor.execute("""
INSERT INTO patient_risk_scores (
patient_hash, risk_score, score_breakdown,
days_overdue, scored_at
) VALUES (%s, %s, %s, %s, NOW())
ON CONFLICT (patient_hash)
DO UPDATE SET
risk_score = EXCLUDED.risk_score,
score_breakdown = EXCLUDED.score_breakdown,
days_overdue = EXCLUDED.days_overdue,
scored_at = NOW()
""", (
patient['patient_hash'],
risk_score,
json.dumps(score_breakdown),
(date.today() - patient['recall_due_date']).days if patient['recall_due_date'] else 0
))
scored_count += 1
conn.commit()
# Generate summary statistics
cursor.execute("""
SELECT
COUNT(*) as total_overdue,
COUNT(*) FILTER (WHERE risk_score >= 80) as critical_count,
COUNT(*) FILTER (WHERE risk_score >= 60 AND risk_score < 80) as high_count,
COUNT(*) FILTER (WHERE risk_score >= 40 AND risk_score < 60) as medium_count,
COUNT(*) FILTER (WHERE risk_score < 40) as low_count,
ROUND(AVG(risk_score)::numeric, 1) as avg_score
FROM patient_risk_scores
WHERE scored_at::date = CURRENT_DATE
""")
summary = cursor.fetchone()
logger.info(f'Scoring complete: {scored_count} patients scored')
logger.info(f'Risk distribution: Critical={summary["critical_count"]}, High={summary["high_count"]}, Medium={summary["medium_count"]}, Low={summary["low_count"]}')
logger.info(f'Average risk score: {summary["avg_score"]}')
return {
'statusCode': 200,
'body': json.dumps({
'patients_scored': scored_count,
'summary': dict(summary)
}, default=str)
}
except Exception as e:
logger.error(f'Risk scoring failed: {str(e)}')
conn.rollback()
raise
finally:
conn.close()AI Recall Message Generator
Type: agent A Claude-powered AI agent that generates personalized, HIPAA-compliant recall messages for overdue patients. Unlike template-based messages from Weave, this agent creates contextually aware messages that reference the patient's specific situation (e.g., time since last visit, type of care needed) while carefully avoiding PHI in SMS previews. It also handles two-way conversational SMS for patient responses.
Implementation:
# Claude-Powered Personalized Outreach
"""AI Recall Message Generator - Claude-Powered Personalized Outreach
Generates contextually-aware recall messages and handles two-way SMS conversations.
Deployed as AWS Lambda functions.
"""
import os
import json
import logging
import anthropic
import psycopg2
import psycopg2.extras
from datetime import date
logger = logging.getLogger()
logger.setLevel(logging.INFO)
client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
PRACTICE_NAME = os.environ.get('PRACTICE_NAME', 'Our Practice')
PRACTICE_PHONE = os.environ.get('PRACTICE_PHONE', '')
SCHEDULING_URL = os.environ.get('SCHEDULING_URL', '')
SYSTEM_PROMPT = f"""You are a friendly, professional patient communications assistant for {PRACTICE_NAME}.
Your job is to craft personalized SMS messages to encourage patients to schedule their overdue preventive care appointments.
CRITICAL RULES:
1. NEVER include specific medical conditions, diagnoses, or treatment details in SMS messages.
2. NEVER mention specific procedures by clinical name (e.g., don't say 'periodontal maintenance' — say 'cleaning appointment').
3. NEVER include insurance information in messages.
4. Keep messages under 160 characters when possible (single SMS segment).
5. Always include a way to schedule (phone number or link).
6. Be warm and friendly, not clinical or alarming.
7. Use the patient's first name.
8. If a patient responds, be conversational and helpful — offer to help them find a convenient time.
9. If a patient says they've moved, changed dentists, or wants to stop messages, acknowledge politely and flag for opt-out.
10. Never pressure or guilt-trip patients.
Examples of good messages:
- "Hi Sarah! It's been a while since your last visit to {PRACTICE_NAME}. We'd love to see you! Schedule online at [link] or call us at [phone]. 😊"
- "Hey Mike, just a friendly reminder that you're due for your checkup at {PRACTICE_NAME}. We have openings this week! Reply YES to schedule."
- "Hi Lisa, we miss seeing you at {PRACTICE_NAME}! Your next visit is overdue. Book a convenient time at [link] — we have early morning and lunch appointments available!"
"""
def get_db_connection():
return psycopg2.connect(
host=os.environ['DB_HOST'],
port=int(os.environ.get('DB_PORT', '5432')),
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
sslmode='require'
)
def generate_recall_message(patient_data):
"""Generate a personalized recall message using Claude."""
days_overdue = (date.today() - patient_data['recall_due_date']).days if patient_data.get('recall_due_date') else 0
months_overdue = days_overdue // 30
# Build context without exposing PHI to the AI unnecessarily (minimum necessary)
context = f"""Generate a recall SMS for this patient:
- First name: {patient_data['first_name']}
- Months since their visit was due: {months_overdue}
- Type of visit needed: {'cleaning' if 'hyg' in (patient_data.get('recall_type') or '').lower() or 'prophy' in (patient_data.get('recall_type') or '').lower() or 'd1110' in (patient_data.get('recall_type') or '').lower() else 'checkup' if 'exam' in (patient_data.get('recall_type') or '').lower() else 'appointment'}
- Has been a patient for: {'a long time' if months_overdue > 24 else 'a while' if months_overdue > 12 else 'several months'}
- Practice phone: {PRACTICE_PHONE}
- Online scheduling link: {SCHEDULING_URL}
- Risk score: {patient_data.get('risk_score', 50)} (higher = more urgency, but keep tone friendly)
Generate ONLY the SMS message text. No quotation marks. No explanation."""
response = client.messages.create(
model='claude-sonnet-4-6',
max_tokens=200,
system=SYSTEM_PROMPT,
messages=[{'role': 'user', 'content': context}]
)
message_text = response.content[0].text.strip()
# Safety check: ensure no PHI leaked
phi_terms = [patient_data.get('last_name', '').lower(),
str(patient_data.get('date_of_birth', '')),
(patient_data.get('insurance_carrier') or '').lower()]
for term in phi_terms:
if term and len(term) > 2 and term in message_text.lower():
logger.warning(f'PHI detected in generated message for {patient_data["patient_hash"]}. Falling back to template.')
message_text = f"Hi {patient_data['first_name']}! You're due for a visit at {PRACTICE_NAME}. Schedule at {SCHEDULING_URL} or call {PRACTICE_PHONE}. We'd love to see you!"
return message_text
def handle_patient_reply(patient_hash, inbound_message):
"""Handle an inbound SMS reply from a patient using Claude."""
conn = get_db_connection()
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
try:
# Get conversation history
cursor.execute("""
SELECT direction, message_text, sent_at
FROM message_log
WHERE patient_hash = %s
ORDER BY sent_at DESC LIMIT 10
""", (patient_hash,))
history = cursor.fetchall()
# Get patient context
cursor.execute("""
SELECT first_name, recall_type FROM patient_recall WHERE patient_hash = %s
""", (patient_hash,))
patient = cursor.fetchone()
if not patient:
return "Thank you for your message! Please call us to schedule your appointment."
# Build conversation for Claude
messages = []
for msg in reversed(history):
role = 'assistant' if msg['direction'] == 'outbound' else 'user'
messages.append({'role': role, 'content': msg['message_text']})
messages.append({'role': 'user', 'content': inbound_message})
conversation_context = f"""The patient {patient['first_name']} is replying to a recall message from {PRACTICE_NAME}.
Their message: \"{inbound_message}\"
Respond helpfully. If they want to schedule, provide the scheduling link ({SCHEDULING_URL}) or phone number ({PRACTICE_PHONE}).
If they say STOP or want to opt out, say something like: \"No problem! We've removed you from our reminder list. You're always welcome to call us when you're ready to schedule. Take care!\"
If they ask about cost/insurance, say: \"Great question! Our front desk team can help with insurance and payment details. Give us a call at {PRACTICE_PHONE} and we'll get you sorted out!\"
Keep it brief and conversational (under 160 chars if possible).
Generate ONLY the reply text."""
response = client.messages.create(
model='claude-sonnet-4-6',
max_tokens=200,
system=SYSTEM_PROMPT,
messages=[{'role': 'user', 'content': conversation_context}]
)
reply_text = response.content[0].text.strip()
# Detect opt-out intent
opt_out_keywords = ['stop', 'unsubscribe', 'opt out', 'remove me', 'moved away', 'new dentist', 'new doctor']
if any(kw in inbound_message.lower() for kw in opt_out_keywords):
cursor.execute("""
UPDATE patient_recall SET outreach_status = 'opted_out', updated_at = NOW()
WHERE patient_hash = %s
""", (patient_hash,))
conn.commit()
logger.info(f'Patient {patient_hash} opted out of recalls')
# Detect scheduling intent
schedule_keywords = ['yes', 'schedule', 'book', 'available', 'appointment', 'when', 'opening']
if any(kw in inbound_message.lower() for kw in schedule_keywords):
cursor.execute("""
UPDATE patient_recall SET outreach_status = 'engaged', updated_at = NOW()
WHERE patient_hash = %s
""", (patient_hash,))
conn.commit()
logger.info(f'Patient {patient_hash} showing scheduling intent')
return reply_text
finally:
conn.close()
def generate_messages_handler(event, context):
"""Lambda handler: Generate recall messages for high-priority overdue patients."""
conn = get_db_connection()
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
try:
# Get top priority patients not yet contacted (batch of 50)
cursor.execute("""
SELECT pr.*, prs.risk_score, prs.days_overdue
FROM patient_recall pr
JOIN patient_risk_scores prs ON pr.patient_hash = prs.patient_hash
WHERE pr.outreach_status IS NULL OR pr.outreach_status = 'pending'
AND pr.cell_phone IS NOT NULL AND pr.cell_phone != ''
AND prs.risk_score >= 40
ORDER BY prs.risk_score DESC
LIMIT 50
""")
patients = cursor.fetchall()
messages_generated = []
for patient in patients:
try:
message = generate_recall_message(dict(patient))
messages_generated.append({
'patient_hash': patient['patient_hash'],
'cell_phone': patient['cell_phone'],
'message': message,
'risk_score': patient['risk_score']
})
# Log the generated message
cursor.execute("""
INSERT INTO message_log (patient_hash, direction, message_text, channel, status, sent_at)
VALUES (%s, 'outbound', %s, 'sms', 'pending', NOW())
""", (patient['patient_hash'], message))
# Update outreach status
cursor.execute("""
UPDATE patient_recall SET outreach_status = 'message_generated', updated_at = NOW()
WHERE patient_hash = %s
""", (patient['patient_hash'],))
except Exception as e:
logger.error(f'Failed to generate message for {patient["patient_hash"]}: {e}')
continue
conn.commit()
logger.info(f'Generated {len(messages_generated)} recall messages')
return {
'statusCode': 200,
'body': json.dumps({'messages_generated': len(messages_generated)})
}
finally:
conn.close()
def inbound_handler(event, context):
"""Lambda handler: Process inbound SMS via Twilio webhook."""
import urllib.parse
body = urllib.parse.parse_qs(event.get('body', ''))
from_number = body.get('From', [''])[0]
message_body = body.get('Body', [''])[0]
conn = get_db_connection()
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
try:
# Look up patient by phone number
cursor.execute("""
SELECT patient_hash FROM patient_recall
WHERE cell_phone LIKE %s LIMIT 1
""", (f'%{from_number[-10:]}%',))
patient = cursor.fetchone()
if not patient:
reply = f"Thank you for contacting {PRACTICE_NAME}. Please call us at {PRACTICE_PHONE} for assistance."
else:
# Log inbound message
cursor.execute("""
INSERT INTO message_log (patient_hash, direction, message_text, channel, status, sent_at)
VALUES (%s, 'inbound', %s, 'sms', 'received', NOW())
""", (patient['patient_hash'], message_body))
conn.commit()
reply = handle_patient_reply(patient['patient_hash'], message_body)
# Log outbound reply
cursor.execute("""
INSERT INTO message_log (patient_hash, direction, message_text, channel, status, sent_at)
VALUES (%s, 'outbound', %s, 'sms', 'sent', NOW())
""", (patient['patient_hash'], reply))
conn.commit()
# Return TwiML response
twiml = f'<?xml version="1.0" encoding="UTF-8"?><Response><Message>{reply}</Message></Response>'
return {
'statusCode': 200,
'headers': {'Content-Type': 'text/xml'},
'body': twiml
}
finally:
conn.close()Monthly Practice Intelligence Report Generator
Type: workflow An automated monthly workflow that generates a comprehensive practice intelligence report using AI analysis. The report includes recall performance metrics, revenue recovery estimates, patient risk distribution, trend analysis, and AI-generated actionable recommendations. Delivered as a PDF via email to the practice manager and MSP account manager.
Implementation:
"""Monthly Practice Intelligence Report Generator
Generates AI-powered monthly analytics report on recall performance.
Triggered by CloudWatch Events on the 1st of each month at 6:00 AM.
"""
import os
import json
import logging
import anthropic
import psycopg2
import psycopg2.extras
import boto3
from datetime import date, datetime, timedelta
from io import BytesIO
logger = logging.getLogger()
logger.setLevel(logging.INFO)
client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
ses = boto3.client('ses')
s3 = boto3.client('s3')
PRACTICE_NAME = os.environ.get('PRACTICE_NAME', 'Practice')
REPORT_BUCKET = os.environ.get('REPORT_BUCKET', 'recall-analytics-reports')
AVG_HYGIENE_REVENUE = float(os.environ.get('AVG_HYGIENE_REVENUE', '250')) # Average revenue per hygiene visit
AVG_EXAM_REVENUE = float(os.environ.get('AVG_EXAM_REVENUE', '175')) # Average revenue per exam
def get_db_connection():
return psycopg2.connect(
host=os.environ['DB_HOST'],
port=int(os.environ.get('DB_PORT', '5432')),
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
sslmode='require'
)
def gather_metrics(cursor, report_month, report_year):
"""Gather all metrics for the reporting period."""
month_start = date(report_year, report_month, 1)
if report_month == 12:
month_end = date(report_year + 1, 1, 1)
else:
month_end = date(report_year, report_month + 1, 1)
prev_month_start = (month_start - timedelta(days=1)).replace(day=1)
metrics = {}
# Total overdue patients
cursor.execute("SELECT COUNT(*) FROM patient_recall WHERE recall_due_date < CURRENT_DATE AND patient_status = 'Active'")
metrics['total_overdue'] = cursor.fetchone()[0]
# Risk distribution
cursor.execute("""
SELECT
COUNT(*) FILTER (WHERE risk_score >= 80) as critical,
COUNT(*) FILTER (WHERE risk_score >= 60 AND risk_score < 80) as high,
COUNT(*) FILTER (WHERE risk_score >= 40 AND risk_score < 60) as medium,
COUNT(*) FILTER (WHERE risk_score < 40) as low
FROM patient_risk_scores
""")
dist = cursor.fetchone()
metrics['risk_distribution'] = {'critical': dist[0], 'high': dist[1], 'medium': dist[2], 'low': dist[3]}
# Messages sent this month
cursor.execute("""
SELECT COUNT(*) FROM message_log
WHERE direction = 'outbound' AND sent_at >= %s AND sent_at < %s
""", (month_start, month_end))
metrics['messages_sent'] = cursor.fetchone()[0]
# Response rate
cursor.execute("""
SELECT COUNT(DISTINCT patient_hash) FROM message_log
WHERE direction = 'inbound' AND sent_at >= %s AND sent_at < %s
""", (month_start, month_end))
metrics['patients_responded'] = cursor.fetchone()[0]
# Patients who engaged (showed scheduling intent)
cursor.execute("""
SELECT COUNT(*) FROM patient_recall
WHERE outreach_status = 'engaged' AND updated_at >= %s AND updated_at < %s
""", (month_start, month_end))
metrics['patients_engaged'] = cursor.fetchone()[0]
# Patients scheduled (from outreach)
cursor.execute("""
SELECT COUNT(*) FROM patient_recall
WHERE outreach_status = 'scheduled' AND updated_at >= %s AND updated_at < %s
""", (month_start, month_end))
metrics['patients_scheduled'] = cursor.fetchone()[0]
# Opt-outs
cursor.execute("""
SELECT COUNT(*) FROM patient_recall
WHERE outreach_status = 'opted_out' AND updated_at >= %s AND updated_at < %s
""", (month_start, month_end))
metrics['opt_outs'] = cursor.fetchone()[0]
# Revenue recovery estimate
metrics['estimated_revenue_recovered'] = (
metrics['patients_scheduled'] * AVG_HYGIENE_REVENUE
)
# Average days overdue
cursor.execute("""
SELECT ROUND(AVG(CURRENT_DATE - recall_due_date))
FROM patient_recall
WHERE recall_due_date < CURRENT_DATE AND patient_status = 'Active'
""")
metrics['avg_days_overdue'] = cursor.fetchone()[0] or 0
# Total active patients
cursor.execute("SELECT COUNT(*) FROM patient_recall WHERE patient_status = 'Active'")
metrics['total_active_patients'] = cursor.fetchone()[0]
# Overdue rate
if metrics['total_active_patients'] > 0:
metrics['overdue_rate_pct'] = round(
(metrics['total_overdue'] / metrics['total_active_patients']) * 100, 1
)
else:
metrics['overdue_rate_pct'] = 0
return metrics
def generate_ai_insights(metrics):
"""Use Claude to generate natural language insights and recommendations."""
prompt = f"""Analyze these monthly recall performance metrics for {PRACTICE_NAME} and provide:
1. A 2-3 sentence executive summary of recall health
2. 3-5 specific, actionable recommendations to improve recall rates
3. One highlight/win to celebrate with the practice
Metrics:
- Total active patients: {metrics['total_active_patients']}
- Total overdue for preventive care: {metrics['total_overdue']} ({metrics['overdue_rate_pct']}%)
- Average days overdue: {metrics['avg_days_overdue']}
- Risk distribution: Critical={metrics['risk_distribution']['critical']}, High={metrics['risk_distribution']['high']}, Medium={metrics['risk_distribution']['medium']}, Low={metrics['risk_distribution']['low']}
- Recall messages sent this month: {metrics['messages_sent']}
- Patients who responded: {metrics['patients_responded']}
- Patients who showed scheduling intent: {metrics['patients_engaged']}
- Patients who scheduled: {metrics['patients_scheduled']}
- Patients who opted out: {metrics['opt_outs']}
- Estimated revenue recovered: ${metrics['estimated_revenue_recovered']:,.0f}
Format your response as JSON with keys: executive_summary, recommendations (array of strings), highlight"""
response = client.messages.create(
model='claude-sonnet-4-6',
max_tokens=1000,
messages=[{'role': 'user', 'content': prompt}]
)
try:
insights = json.loads(response.content[0].text)
except json.JSONDecodeError:
insights = {
'executive_summary': response.content[0].text[:500],
'recommendations': ['Review the detailed metrics above for actionable insights.'],
'highlight': 'The recall system is actively working to bring patients back.'
}
return insights
def generate_html_report(metrics, insights, report_month, report_year):
"""Generate HTML report content."""
month_name = date(report_year, report_month, 1).strftime('%B %Y')
response_rate = round((metrics['patients_responded'] / max(metrics['messages_sent'], 1)) * 100, 1)
conversion_rate = round((metrics['patients_scheduled'] / max(metrics['patients_responded'], 1)) * 100, 1)
html = f"""
<html><head><style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; color: #333; }}
h1 {{ color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 10px; }}
h2 {{ color: #1e40af; margin-top: 30px; }}
.metric-grid {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin: 20px 0; }}
.metric-card {{ background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px; text-align: center; }}
.metric-value {{ font-size: 28px; font-weight: bold; color: #2563eb; }}
.metric-label {{ font-size: 12px; color: #64748b; margin-top: 5px; }}
.insight-box {{ background: #eff6ff; border-left: 4px solid #2563eb; padding: 15px; margin: 15px 0; border-radius: 0 8px 8px 0; }}
.recommendation {{ background: #f0fdf4; border-left: 4px solid #16a34a; padding: 10px 15px; margin: 8px 0; border-radius: 0 6px 6px 0; }}
.highlight {{ background: #fefce8; border-left: 4px solid #ca8a04; padding: 15px; margin: 15px 0; border-radius: 0 8px 8px 0; }}
.footer {{ margin-top: 40px; padding-top: 20px; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 12px; }}
</style></head><body>
<h1>📊 {PRACTICE_NAME} — Preventive Care Recall Report</h1>
<p style='color: #64748b;'>{month_name} | Generated by AI Analytics Engine</p>
<div class='insight-box'><strong>Executive Summary:</strong> {insights.get('executive_summary', 'Report generated successfully.')}</div>
<h2>Key Metrics</h2>
<div class='metric-grid'>
<div class='metric-card'><div class='metric-value'>{metrics['total_overdue']}</div><div class='metric-label'>Patients Overdue</div></div>
<div class='metric-card'><div class='metric-value'>{metrics['overdue_rate_pct']}%</div><div class='metric-label'>Overdue Rate</div></div>
<div class='metric-card'><div class='metric-value'>{metrics['avg_days_overdue']}</div><div class='metric-label'>Avg Days Overdue</div></div>
<div class='metric-card'><div class='metric-value'>{metrics['messages_sent']}</div><div class='metric-label'>Messages Sent</div></div>
<div class='metric-card'><div class='metric-value'>{response_rate}%</div><div class='metric-label'>Response Rate</div></div>
<div class='metric-card'><div class='metric-value'>{metrics['patients_scheduled']}</div><div class='metric-label'>Patients Scheduled</div></div>
<div class='metric-card'><div class='metric-value'>{conversion_rate}%</div><div class='metric-label'>Conversion Rate</div></div>
<div class='metric-card'><div class='metric-value'>${metrics['estimated_revenue_recovered']:,.0f}</div><div class='metric-label'>Est. Revenue Recovered</div></div>
<div class='metric-card'><div class='metric-value'>{metrics['opt_outs']}</div><div class='metric-label'>Opt-Outs</div></div>
</div>
<h2>Risk Distribution</h2>
<p>🔴 Critical (80+): {metrics['risk_distribution']['critical']} patients | 🟠 High (60-79): {metrics['risk_distribution']['high']} | 🟡 Medium (40-59): {metrics['risk_distribution']['medium']} | 🟢 Low (<40): {metrics['risk_distribution']['low']}</p>
<div class='highlight'><strong>🌟 Highlight:</strong> {insights.get('highlight', 'Great progress this month!')}</div>
<h2>AI Recommendations</h2>
{''.join(f"<div class='recommendation'>✅ {rec}</div>" for rec in insights.get('recommendations', []))}
<div class='footer'>
<p>This report was generated automatically by the AI Preventive Care Analytics Engine managed by your MSP partner.</p>
<p>Data synced from {PRACTICE_NAME} PMS. All patient data handled in compliance with HIPAA.</p>
<p>Questions? Contact your MSP account manager.</p>
</div>
</body></html>
"""
return html
def handler(event, context):
"""Lambda handler - generates and distributes monthly report."""
# Determine report period (previous month)
today = date.today()
if today.month == 1:
report_month, report_year = 12, today.year - 1
else:
report_month, report_year = today.month - 1, today.year
logger.info(f'Generating report for {report_month}/{report_year}')
conn = get_db_connection()
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
try:
metrics = gather_metrics(cursor, report_month, report_year)
insights = generate_ai_insights(metrics)
html_report = generate_html_report(metrics, insights, report_month, report_year)
# Save to S3
report_key = f'reports/{report_year}/{report_month:02d}/recall_report_{PRACTICE_NAME.replace(" ", "_")}.html'
s3.put_object(
Bucket=REPORT_BUCKET,
Key=report_key,
Body=html_report.encode('utf-8'),
ContentType='text/html',
ServerSideEncryption='aws:kms'
)
# Send via SES
practice_email = os.environ.get('PRACTICE_MANAGER_EMAIL', '')
msp_email = os.environ.get('MSP_ACCOUNT_MANAGER_EMAIL', '')
recipients = [e for e in [practice_email, msp_email] if e]
if recipients:
month_name = date(report_year, report_month, 1).strftime('%B %Y')
ses.send_email(
Source=os.environ.get('SES_FROM_EMAIL', 'reports@yourmsp.com'),
Destination={'ToAddresses': recipients},
Message={
'Subject': {'Data': f'{PRACTICE_NAME} — Preventive Care Recall Report — {month_name}'},
'Body': {'Html': {'Data': html_report}}
}
)
logger.info(f'Report sent to {recipients}')
return {'statusCode': 200, 'body': json.dumps({'report_key': report_key, 'metrics': metrics}, default=str)}
finally:
conn.close()Monthly Recall Performance Insights — Claude Prompt
Database Schema
Type: integration PostgreSQL database schema for the recall analytics system. Creates all tables needed for patient data, risk scores, message logging, and audit trails. Must be run during initial deployment as a database migration. Implementation:
-- run as migration during initial deployment. All tables support HIPAA audit
-- requirements.
-- Recall Analytics Database Schema
-- Run as migration during initial deployment
-- All tables support HIPAA audit requirements
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Patient recall data (synced from PMS)
CREATE TABLE patient_recall (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
patient_hash VARCHAR(16) NOT NULL,
pms_patient_id VARCHAR(50) NOT NULL,
pms_type VARCHAR(20) NOT NULL, -- dentrix, eaglesoft, opendental
first_name VARCHAR(100),
last_name VARCHAR(100),
date_of_birth DATE,
cell_phone VARCHAR(20),
email VARCHAR(200),
patient_status VARCHAR(20) DEFAULT 'Active',
recall_type VARCHAR(100),
recall_due_date DATE,
recall_interval_months INTEGER,
last_visit_date DATE,
last_procedure_code VARCHAR(20),
insurance_carrier VARCHAR(200),
outreach_status VARCHAR(30), -- NULL, pending, message_generated, sent, engaged, scheduled, completed, opted_out
sync_timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(patient_hash, pms_type)
);
CREATE INDEX idx_patient_recall_due ON patient_recall(recall_due_date) WHERE patient_status = 'Active';
CREATE INDEX idx_patient_recall_status ON patient_recall(outreach_status);
CREATE INDEX idx_patient_recall_phone ON patient_recall(cell_phone);
CREATE INDEX idx_patient_recall_hash ON patient_recall(patient_hash);
-- Patient risk scores
CREATE TABLE patient_risk_scores (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
patient_hash VARCHAR(16) NOT NULL UNIQUE,
risk_score DECIMAL(5,1) NOT NULL,
score_breakdown JSONB,
days_overdue INTEGER,
scored_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (patient_hash) REFERENCES patient_recall(patient_hash) ON DELETE CASCADE
);
CREATE INDEX idx_risk_scores_score ON patient_risk_scores(risk_score DESC);
-- Message log (all outbound and inbound messages)
CREATE TABLE message_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
patient_hash VARCHAR(16) NOT NULL,
direction VARCHAR(10) NOT NULL, -- outbound, inbound
message_text TEXT NOT NULL,
channel VARCHAR(10) NOT NULL, -- sms, email, voice
status VARCHAR(20) NOT NULL, -- pending, sent, delivered, failed, received
twilio_sid VARCHAR(50),
sent_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
delivered_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (patient_hash) REFERENCES patient_recall(patient_hash) ON DELETE CASCADE
);
CREATE INDEX idx_message_log_patient ON message_log(patient_hash, sent_at DESC);
CREATE INDEX idx_message_log_date ON message_log(sent_at);
-- Sync audit log
CREATE TABLE sync_audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
sync_timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
records_extracted INTEGER,
records_synced INTEGER,
errors INTEGER,
duration_seconds DECIMAL(10,2),
pms_type VARCHAR(20)
);
-- Monthly report snapshots
CREATE TABLE report_snapshots (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
report_month INTEGER NOT NULL,
report_year INTEGER NOT NULL,
metrics JSONB NOT NULL,
ai_insights JSONB,
generated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
s3_key VARCHAR(500),
UNIQUE(report_month, report_year)
);
-- PHI access audit log (HIPAA requirement)
CREATE TABLE phi_access_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
accessed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
accessed_by VARCHAR(100) NOT NULL, -- system component or user
action VARCHAR(50) NOT NULL, -- read, sync, export, view_dashboard
resource_type VARCHAR(50) NOT NULL, -- patient_recall, message_log, report
record_count INTEGER,
ip_address INET,
details JSONB
);
CREATE INDEX idx_phi_access_date ON phi_access_log(accessed_at);
-- Trigger to auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER patient_recall_updated
BEFORE UPDATE ON patient_recall
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- View for dashboard: overdue patients with risk scores
CREATE VIEW v_overdue_patients AS
SELECT
pr.patient_hash,
pr.first_name,
pr.last_name,
pr.cell_phone,
pr.email,
pr.recall_type,
pr.recall_due_date,
pr.last_visit_date,
pr.insurance_carrier,
pr.outreach_status,
prs.risk_score,
prs.days_overdue,
prs.score_breakdown,
CASE
WHEN prs.risk_score >= 80 THEN 'Critical'
WHEN prs.risk_score >= 60 THEN 'High'
WHEN prs.risk_score >= 40 THEN 'Medium'
ELSE 'Low'
END AS risk_level
FROM patient_recall pr
LEFT JOIN patient_risk_scores prs ON pr.patient_hash = prs.patient_hash
WHERE pr.patient_status = 'Active'
AND pr.recall_due_date < CURRENT_DATE
ORDER BY prs.risk_score DESC NULLS LAST;
-- View for dashboard: recall funnel metrics
CREATE VIEW v_recall_funnel AS
SELECT
COUNT(*) FILTER (WHERE outreach_status IS NULL OR outreach_status = 'pending') AS not_contacted,
COUNT(*) FILTER (WHERE outreach_status = 'sent') AS contacted,
COUNT(*) FILTER (WHERE outreach_status = 'engaged') AS responded,
COUNT(*) FILTER (WHERE outreach_status = 'scheduled') AS scheduled,
COUNT(*) FILTER (WHERE outreach_status = 'completed') AS completed,
COUNT(*) FILTER (WHERE outreach_status = 'opted_out') AS opted_out,
COUNT(*) AS total_overdue
FROM patient_recall
WHERE patient_status = 'Active'
AND recall_due_date < CURRENT_DATE;Serverless Framework Configuration
Type: workflow Complete serverless.yml configuration file for deploying all Lambda functions, API Gateway endpoints, CloudWatch scheduled events, and IAM permissions to AWS. This is the deployment manifest for the custom AI analytics engine. Implementation:
# Recall Analytics Engine. Deploy with: serverless deploy --stage production
# serverless.yml - Recall Analytics Engine
# Deploy with: serverless deploy --stage production
service: recall-analytics-engine
provider:
name: aws
runtime: python3.11
stage: ${opt:stage, 'dev'}
region: us-east-1
memorySize: 512
timeout: 900 # 15 minutes max for analysis jobs
environment:
DB_HOST: ${ssm:/recall-analytics/${self:provider.stage}/db-host}
DB_PORT: '5432'
DB_NAME: recall_analytics
DB_USER: ${ssm:/recall-analytics/${self:provider.stage}/db-user}
DB_PASSWORD: ${ssm:/recall-analytics/${self:provider.stage}/db-password~true}
ANTHROPIC_API_KEY: ${ssm:/recall-analytics/${self:provider.stage}/anthropic-api-key~true}
TWILIO_ACCOUNT_SID: ${ssm:/recall-analytics/${self:provider.stage}/twilio-sid~true}
TWILIO_AUTH_TOKEN: ${ssm:/recall-analytics/${self:provider.stage}/twilio-auth~true}
TWILIO_PHONE_NUMBER: ${ssm:/recall-analytics/${self:provider.stage}/twilio-phone}
PRACTICE_NAME: ${ssm:/recall-analytics/${self:provider.stage}/practice-name}
PRACTICE_PHONE: ${ssm:/recall-analytics/${self:provider.stage}/practice-phone}
SCHEDULING_URL: ${ssm:/recall-analytics/${self:provider.stage}/scheduling-url}
PRACTICE_MANAGER_EMAIL: ${ssm:/recall-analytics/${self:provider.stage}/practice-email}
MSP_ACCOUNT_MANAGER_EMAIL: ${ssm:/recall-analytics/${self:provider.stage}/msp-email}
SES_FROM_EMAIL: ${ssm:/recall-analytics/${self:provider.stage}/ses-from-email}
REPORT_BUCKET: recall-analytics-reports-${self:provider.stage}
AVG_HYGIENE_REVENUE: '250'
AVG_EXAM_REVENUE: '175'
vpc:
securityGroupIds:
- ${ssm:/recall-analytics/${self:provider.stage}/lambda-sg}
subnetIds:
- ${ssm:/recall-analytics/${self:provider.stage}/subnet-1}
- ${ssm:/recall-analytics/${self:provider.stage}/subnet-2}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
Resource: arn:aws:s3:::recall-analytics-reports-${self:provider.stage}/*
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
Resource: '*'
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource: arn:aws:ssm:${self:provider.region}:*:parameter/recall-analytics/${self:provider.stage}/*
- Effect: Allow
Action:
- kms:Decrypt
Resource: '*'
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
functions:
analyzePatients:
handler: risk_scoring.handler
description: Nightly patient risk scoring analysis
events:
- schedule:
rate: cron(0 8 * * ? *) # 3 AM EST (8 AM UTC)
enabled: true
input:
source: scheduled
generateRecallMessages:
handler: recall_messages.generate_messages_handler
description: Generate AI-powered recall messages for top priority patients
events:
- schedule:
rate: cron(0 14 * * ? *) # 9 AM EST - business hours
enabled: true
sendRecalls:
handler: send_sms.handler
description: Send pending recall messages via Twilio
memorySize: 256
timeout: 300
events:
- schedule:
rate: cron(15 14 * * ? *) # 9:15 AM EST - right after generation
enabled: true
handleInbound:
handler: recall_messages.inbound_handler
description: Process inbound patient SMS replies via Twilio webhook
memorySize: 256
timeout: 30
events:
- http:
path: twilio/inbound
method: post
cors: false
generateReport:
handler: monthly_report.handler
description: Monthly practice intelligence report
timeout: 900
events:
- schedule:
rate: cron(0 11 1 * ? *) # 6 AM EST on 1st of month
enabled: true
dashboardAPI:
handler: dashboard_api.handler
description: REST API for Retool analytics dashboard
memorySize: 256
timeout: 30
events:
- http:
path: api/{proxy+}
method: any
cors: true
authorizer:
type: TOKEN
identitySource: method.request.header.Authorization
resultTtlInSeconds: 300
migrate:
handler: migrations.handler
description: Database migration runner
timeout: 120
healthCheck:
handler: health.handler
description: Health check endpoint
memorySize: 128
timeout: 10
events:
- http:
path: health
method: get
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
slim: true
layer: true
noDeploy:
- pytest
- boto3 # Already in Lambda runtime
- botocore
package:
individually: true
patterns:
- '!node_modules/**'
- '!.git/**'
- '!tests/**'
- '!.env*'
- '!README.md'Testing & Validation
- CONNECTIVITY TEST: From the PMS server, run 'Test-NetConnection -ComputerName <RDS_ENDPOINT> -Port 5432' to verify the sync agent can reach the AWS RDS database through the firewall. Expected result: TcpTestSucceeded = True.
- PMS DATA EXTRACTION TEST: Run the sync agent manually ('python sync_agent.py') and verify it extracts patient records from the PMS database. Check the log file at C:\RecallSync\logs\ for 'Extracted N patient recall records from PMS' with N > 0. Cross-reference the count against the PMS recall report.
- DATABASE SYNC VERIFICATION: After the first sync completes, connect to the RDS database and run 'SELECT COUNT(*) FROM patient_recall;' — the count should match the PMS extraction count. Then run 'SELECT * FROM sync_audit_log ORDER BY sync_timestamp DESC LIMIT 1;' to verify the audit trail was created with zero errors.
- RISK SCORING VALIDATION: Invoke the analyzePatients Lambda function manually ('serverless invoke --function analyzePatients --stage production') and then query 'SELECT risk_score, COUNT(*) FROM patient_risk_scores GROUP BY risk_score ORDER BY risk_score;' — verify scores are distributed between 0-100 and that patients with older recall_due_dates have higher scores.
- AI MESSAGE GENERATION TEST: Invoke the generateRecallMessages function and inspect the message_log table: 'SELECT patient_hash, message_text FROM message_log WHERE direction = 'outbound' ORDER BY sent_at DESC LIMIT 10;' — verify messages are personalized, contain the patient's first name, include the practice phone number or scheduling link, and do NOT contain last names, dates of birth, insurance info, or clinical terminology.
- HIPAA PHI LEAK TEST: Review 20 generated messages manually and verify none contain: patient last names, specific procedure names (should say 'cleaning' not 'D1110' or 'prophylaxis'), dates of birth, insurance carrier names, or any clinical diagnoses. Document this review in the HIPAA compliance file.
- TWILIO SMS DELIVERY TEST: Send a test SMS to the MSP technician's phone number through the system. Verify: (1) message arrives within 30 seconds, (2) sender number matches the practice's Twilio number, (3) message content matches what was generated. Then reply to the message and verify the inbound_handler processes the reply and generates an appropriate AI response.
- WEAVE PLATFORM RECALL TEST: In Weave, manually trigger a recall batch for 10 test patients (use staff members' records or test accounts). Verify: (1) SMS messages are delivered, (2) email messages arrive, (3) patient pop-up shows recall status when test patient calls in, (4) two-way text replies are received in the Weave inbox.
- RETOOL DASHBOARD VERIFICATION: Log into the Retool dashboard and verify: (1) overdue patient list loads with correct data matching the PMS, (2) risk scores are displayed and color-coded, (3) recall funnel shows accurate counts, (4) filtering by risk level works correctly, (5) the dashboard loads within 5 seconds.
- MONTHLY REPORT GENERATION TEST: Invoke the generateReport function with test data ('serverless invoke --function generateReport --stage production --data {"test": true}') and verify: (1) HTML report is saved to S3, (2) email is delivered to both practice manager and MSP account manager, (3) report contains accurate metrics matching the database, (4) AI insights are relevant and actionable.
- OPT-OUT COMPLIANCE TEST: Send a test recall message to a test number, then reply 'STOP'. Verify: (1) the system acknowledges the opt-out politely, (2) the patient's outreach_status is updated to 'opted_out' in the database, (3) no further messages are sent to that number in subsequent recall batches.
- END-TO-END RECALL FLOW TEST: Identify 5 actual overdue patients (with practice manager approval) and run through the full flow: PMS sync → risk scoring → AI message generation → SMS delivery → patient response → conversation handling → scheduling intent detection. Document the complete flow with timestamps and verify each handoff.
- WEAVE-PMS INTEGRATION SYNC TEST: In the PMS, update a test patient's phone number. Verify that within 15 minutes, Weave reflects the updated phone number in its patient record. Then add a completed appointment for an overdue patient in the PMS and verify that Weave removes them from the recall list within the next sync cycle.
- FAILOVER AND ERROR HANDLING TEST: (1) Temporarily block outbound HTTPS on the PMS server firewall and verify the sync agent logs an error and retries, (2) Temporarily use an invalid API key for Anthropic and verify the message generator falls back to a template message, (3) Verify CloudWatch alarms trigger when Lambda function errors exceed threshold.
- HIPAA AUDIT TRAIL TEST: Verify that all PHI access is logged by querying 'SELECT * FROM phi_access_log ORDER BY accessed_at DESC LIMIT 20;' — ensure entries exist for sync operations, dashboard views, and message generation. Verify CloudTrail is logging all API calls to the AWS account.
Client Handoff
Client Handoff Checklist
Training Sessions Completed
Documentation Delivered
Success Criteria Review (with Practice Owner/Manager)
System Access Credentials Documented
Go-Live Confirmation
Maintenance
Ongoing Maintenance Responsibilities
Weekly (MSP Technician — 30 minutes/week)
- Review CloudWatch dashboard for Lambda function errors, sync failures, and delivery rate anomalies
- Check sync_audit_log table for any failed syncs (query: SELECT * FROM sync_audit_log WHERE errors > 0 AND sync_timestamp > NOW() - INTERVAL '7 days')
- Review Twilio delivery reports for bounced/failed SMS messages (indicates outdated phone numbers)
- Monitor Weave recall performance in the Weave admin dashboard
- Check AWS billing to ensure costs remain within expected range ($40–$80/month)
Monthly (MSP Account Manager — 1 hour/month)
- Review the AI-generated monthly practice intelligence report with the practice manager
- Discuss recall conversion rates, revenue recovery, and any recommended adjustments
- Review and update recall rules if the practice requests changes (e.g., changing recall intervals, adding new recall types)
- Audit opt-out list to ensure compliance with TCPA and patient requests
- Update risk scoring weights if needed based on actual conversion data (which patient segments are actually scheduling vs. ignoring outreach)
- Review Anthropic API usage and costs; adjust message generation batch sizes if needed
Quarterly (MSP — 2 hours/quarter)
- Perform HIPAA security review: verify all BAAs are current, review access logs, confirm encryption is active
- Update all software components: Python packages on sync agent, Lambda function dependencies, Serverless Framework version
- Review and update AI prompts based on message performance data (which message styles get the highest response rates)
- Test disaster recovery: verify database backups are restorable, sync agent can be redeployed, Lambda functions can be redeployed from source
- Conduct quarterly business review with practice owner showing ROI metrics (recall rate improvement, revenue recovered, time saved)
Annually
- Renew all vendor contracts and BAAs (Weave, AWS, Anthropic, Twilio)
- Conduct or update HIPAA Risk Assessment for the practice
- Review FortiGate firewall firmware and security subscription renewal
- Full audit of PHI access logs for the year
- Update recall rule configurations based on clinical guideline changes
- Renegotiate MSP service agreement pricing based on value delivered
SLA Considerations
- Recall system uptime target: 99.5% (allows for ~44 hours downtime per year for maintenance)
- Sync agent failure response: MSP alerted within 1 hour via CloudWatch alarm; remediation within 4 business hours
- Patient-facing message errors (wrong content, PHI leak): Critical severity — MSP responds within 1 hour, root cause analysis within 24 hours
- Dashboard unavailability: Standard severity — MSP responds within 4 business hours
- Monthly report delivery: Guaranteed by the 3rd business day of each month
Escalation Path
Alternatives
Turnkey SaaS Only (No Custom AI)
Deploy only Weave (or Solutionreach, RevenueWell, or Adit) as a standalone patient engagement platform without the custom AI analytics engine. The SaaS platform handles all recall automation, messaging, and basic reporting out of the box. No AWS infrastructure, no custom code, no AI API integration needed.
Solutionreach Instead of Weave
Use Solutionreach as the primary patient engagement platform instead of Weave. Solutionreach has been in the market for 23 years and integrates with over 400 PMS/EHR systems, making it the broadest integration option. Pricing is per-location (reportedly $300–$500/month) rather than feature-tiered.
RevenueWell for Budget-Conscious Dental Practices
Deploy RevenueWell as the recall platform at $189/month — the lowest-cost turnkey option. RevenueWell focuses specifically on dental practices and includes recall automation, patient marketing campaigns, digital forms, and online scheduling.
Fully Custom AI Build (No Turnkey SaaS)
Build the entire recall system from scratch using the custom AI components without any turnkey SaaS platform. Uses the PMS sync agent, Claude API for message generation, Twilio for all messaging, and the Retool dashboard — but no Weave or Solutionreach. The MSP owns and operates the complete stack.
RECOMMENDATION: Only for MSPs building a dedicated healthcare vertical with development resources, targeting 10+ dental/medical practice clients where the development cost is amortized across the portfolio. Start with the hybrid approach (SaaS + custom AI) and transition to fully custom once the MSP has proven the model.
Dental Intelligence + Outreach Platform
Use Dental Intelligence ($399/month) as the primary analytics and recall identification engine, paired with a lightweight communication tool like Doctible ($149–$250/month) or Emitrr ($149/month) for the actual patient outreach. This separates the intelligence layer from the communication layer.
Want early access to the full toolkit?