
Implementation Guide: Monitor vehicle recall databases and proactively notify affected customers
Step-by-step implementation guide for deploying AI to monitor vehicle recall databases and proactively notify affected customers for Automotive clients.
Hardware Procurement
Dell PowerEdge T160 Tower Server
$1,200–$1,500 MSP cost (configured) / $1,800–$2,200 suggested resale to client
On-premises host for self-hosted n8n instance, PostgreSQL database, and the recall monitoring agent. Configured with Intel Xeon E-2434 (4-core, 3.4 GHz), 32 GB DDR5 ECC RAM, 2× 1 TB SATA SSD in RAID 1 for data redundancy. Only required if dealer prefers on-prem deployment; otherwise use cloud VM option below.
APC Smart-UPS 1500VA (SMT1500C)
$550 MSP cost / $750 suggested resale
Uninterruptible power supply for the on-premises server to prevent data corruption during power events. Provides ~15 minutes of runtime for graceful shutdown. Only required for on-prem deployments.
Cloud Virtual Machine (Alternative to On-Prem)
Cloud Virtual Machine
$75–$120/month MSP cost / $175–$250/month suggested resale
Cloud-hosted compute for the n8n agent, PostgreSQL database, and all recall monitoring logic. Preferred deployment model for MSPs managing multiple dealers — enables centralized multi-tenant management. Spec: 2 vCPU, 8 GB RAM, 100 GB GP3 SSD (AWS) or Premium SSD (Azure).
Software Procurement
n8n (Self-Hosted Community Edition)
$0/month software license; infrastructure hosting cost only
Primary agent orchestration platform. Provides visual workflow builder for the recall polling agent, VIN matching logic, LLM integration, notification dispatch, and CRM write-back. Self-hosted version allows full white-labeling and multi-tenant deployment under MSP brand.
n8n Cloud Pro (Alternative)
$50/month for 10,000 workflow executions
Managed cloud alternative to self-hosted n8n. Faster to deploy but less margin for MSP and limited white-label capability. Recommended only for single-dealer proof-of-concept.
OpenAI API (GPT-5.4 mini)
$5–$25/month per dealer
LLM inference for intelligent recall-to-VIN matching, personalized notification message generation, and recall severity assessment. GPT-5.4 mini at $0.15/1M input tokens and $0.60/1M output tokens provides the best cost-efficiency for this workload.
Twilio Programmable SMS
$0.0079/message + carrier fees (~$0.003–$0.005); plus $15/month for phone number; ~$15–$30/month total for typical dealer
Outbound SMS notifications to affected customers. Requires A2P 10DLC brand and campaign registration for compliant business messaging. Also handles inbound opt-out (STOP) keyword processing automatically.
Twilio SendGrid (Essentials Plan)
$19.95/month
Transactional email delivery for recall notification emails. Provides delivery tracking, bounce handling, and template management. Recall notification emails qualify as transactional under CAN-SPAM, reducing compliance burden.
PostgreSQL 16
$0 (included in server/VM deployment)
Primary database for storing customer-VIN records, recall campaign cache, notification logs, consent records, and agent execution history. Installed on the same server/VM as n8n.
CDK Fortellis API Access
$100–$500/month depending on API tier and call volume; partner program approval required
Integration with CDK Drive DMS to extract customer records with VINs, contact information, and service history. This is the primary data source for dealers running CDK. Approval process takes 4–8 weeks.
Nginx Reverse Proxy
$0
Reverse proxy and SSL termination for the n8n web interface and webhook endpoints. Required for Twilio webhook callbacks and secure access to the n8n dashboard.
Let's Encrypt SSL Certificates
$0
Automated SSL/TLS certificate provisioning for webhook endpoints and n8n web interface via Certbot.
Prerequisites
- Active DMS subscription (CDK Drive, Reynolds ERA/POWER, Dealertrack, or Autosoft) with API access enabled or in the process of being enabled — START DMS API ACCESS REQUEST IMMEDIATELY as CDK/Reynolds approval can take 4–8 weeks
- CRM system with API access for logging outreach activities (VinSolutions, DealerSocket, Elead, or similar)
- Dealer's customer database must include VIN, customer name, phone number, and email address for at least 60% of active service customers
- Reliable internet connectivity at the dealership (50+ Mbps recommended) with a static IP address or domain name for webhook callbacks
- Dealer must have (or be willing to obtain) proper TCPA-compliant SMS opt-in consent records for customers — if no consent program exists, a consent collection campaign must be planned as part of Phase 4
- Designated dealer point of contact: Service Manager or BDC Manager who can authorize communications and validate notification content
- An active credit card for API service sign-ups (OpenAI, Twilio, SendGrid)
- If on-premises deployment: dedicated 120V/15A electrical outlet, network drop with DHCP or assigned static IP on the dealer LAN, and physical rack or shelf space for server + UPS
- If cloud deployment: AWS or Azure account with billing configured, IAM user/role for provisioning, and VPC/network configuration
- Domain name for the agent (e.g., recalls.dealername.com or msp-recalls.yourmsp.com) with DNS management access
- Firewall must allow outbound HTTPS (port 443) to: api.nhtsa.gov, api.openai.com, api.twilio.com, api.sendgrid.com, and the dealer's DMS API endpoint
Installation Steps
Step 1: Provision Compute Infrastructure
Set up the server or cloud VM that will host the n8n agent, PostgreSQL database, and Nginx reverse proxy. For cloud deployment (recommended), provision an AWS EC2 t3.medium instance or Azure B2ms VM running Ubuntu 22.04 LTS. For on-premises, install Ubuntu Server 22.04 LTS on the Dell PowerEdge T160.
aws ec2 run-instances --image-id ami-0c7217cdde317cfec --instance-type t3.medium --key-name msp-recall-agent --security-group-ids sg-xxxxxxxx --subnet-id subnet-xxxxxxxx --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":100,"VolumeType":"gp3"}}]' --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=recall-agent-dealername}]'aws ec2 allocate-address --domain vpc
aws ec2 associate-address --instance-id i-xxxxxxxxx --allocation-id eipalloc-xxxxxxxxxssh -i msp-recall-agent.pem ubuntu@<ELASTIC_IP>sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget gnupg2 software-properties-common ufwFor multi-dealer MSP deployments, consider a larger instance (t3.large with 2 vCPU / 8 GB) and run each dealer as a separate n8n project or workflow. Ensure the security group allows inbound SSH (22) from MSP IP only, and inbound HTTPS (443) from anywhere for webhooks.
Step 2: Install Docker and Docker Compose
n8n is best deployed via Docker for reproducibility, easy updates, and isolation. Install Docker Engine and Docker Compose on the server.
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
newgrp dockersudo apt install -y docker-compose-plugindocker --version
docker compose versionLog out and back in after adding user to docker group if 'newgrp' doesn't take effect. All subsequent services (n8n, PostgreSQL, Nginx) will run as Docker containers.
Step 3: Install and Configure PostgreSQL Database
Deploy PostgreSQL 16 as a Docker container. This database stores customer-VIN records, recall campaign cache, notification logs, consent tracking, and n8n's own workflow data.
sudo mkdir -p /opt/recall-agent/postgres-datadocker network create recall-networkdocker run -d \
--name recall-postgres \
--network recall-network \
--restart unless-stopped \
-e POSTGRES_USER=recallagent \
-e POSTGRES_PASSWORD=$(openssl rand -base64 24) \
-e POSTGRES_DB=recall_db \
-v /opt/recall-agent/postgres-data:/var/lib/postgresql/data \
-p 127.0.0.1:5432:5432 \
postgres:16-alpinesleep 10
docker exec -i recall-postgres psql -U recallagent -d recall_db << 'EOF'
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
dealer_id VARCHAR(50) NOT NULL,
customer_name VARCHAR(255) NOT NULL,
phone VARCHAR(20),
email VARCHAR(255),
vin VARCHAR(17) NOT NULL,
vehicle_year INTEGER,
vehicle_make VARCHAR(100),
vehicle_model VARCHAR(100),
sms_consent BOOLEAN DEFAULT FALSE,
sms_consent_date TIMESTAMP,
email_consent BOOLEAN DEFAULT FALSE,
dms_customer_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE recall_campaigns (
id SERIAL PRIMARY KEY,
nhtsa_campaign_number VARCHAR(20) UNIQUE NOT NULL,
manufacturer VARCHAR(255),
subject TEXT,
component VARCHAR(255),
summary TEXT,
consequence TEXT,
remedy TEXT,
affected_make VARCHAR(100),
affected_model VARCHAR(100),
affected_year_start INTEGER,
affected_year_end INTEGER,
report_received_date DATE,
first_seen_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE notifications (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
recall_campaign_id INTEGER REFERENCES recall_campaigns(id),
channel VARCHAR(10) NOT NULL CHECK (channel IN ('sms', 'email')),
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'delivered', 'failed', 'opted_out')),
message_content TEXT,
sent_at TIMESTAMP,
delivery_status_updated_at TIMESTAMP,
twilio_sid VARCHAR(50),
sendgrid_message_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE consent_log (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
consent_type VARCHAR(10) NOT NULL CHECK (consent_type IN ('sms', 'email')),
action VARCHAR(10) NOT NULL CHECK (action IN ('opt_in', 'opt_out')),
source VARCHAR(50),
timestamp TIMESTAMP DEFAULT NOW(),
ip_address VARCHAR(45),
raw_record TEXT
);
CREATE INDEX idx_customers_vin ON customers(vin);
CREATE INDEX idx_customers_dealer ON customers(dealer_id);
CREATE INDEX idx_recalls_campaign ON recall_campaigns(nhtsa_campaign_number);
CREATE INDEX idx_notifications_customer ON notifications(customer_id);
CREATE INDEX idx_notifications_status ON notifications(status);
EOFSave the PostgreSQL password securely in your MSP password manager (e.g., IT Glue, Hudu, or Passportal). The database is bound to 127.0.0.1 only — it is not exposed to the network. All access is via the Docker network or localhost.
Step 4: Deploy n8n Self-Hosted via Docker
Install n8n as a Docker container connected to the PostgreSQL database. Configure it with the correct environment variables for database connection, encryption, webhook URL, and timezone.
sudo mkdir -p /opt/recall-agent/n8n-data
export N8N_HOST=recalls.yourmsp.com
export N8N_PORT=5678
export POSTGRES_PASSWORD=<password_from_step_3>
export N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
echo "N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}" >> /opt/recall-agent/.env
echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" >> /opt/recall-agent/.env
docker run -d \
--name n8n \
--network recall-network \
--restart unless-stopped \
-p 127.0.0.1:5678:5678 \
-e N8N_HOST=${N8N_HOST} \
-e N8N_PORT=${N8N_PORT} \
-e N8N_PROTOCOL=https \
-e WEBHOOK_URL=https://${N8N_HOST}/ \
-e N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} \
-e DB_TYPE=postgresdb \
-e DB_POSTGRESDB_HOST=recall-postgres \
-e DB_POSTGRESDB_PORT=5432 \
-e DB_POSTGRESDB_DATABASE=recall_db \
-e DB_POSTGRESDB_USER=recallagent \
-e DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} \
-e GENERIC_TIMEZONE=America/New_York \
-e N8N_DEFAULT_BINARY_DATA_MODE=filesystem \
-v /opt/recall-agent/n8n-data:/home/node/.n8n \
n8nio/n8n:latest
sleep 15
curl -s http://localhost:5678/healthzIMPORTANT: Store the N8N_ENCRYPTION_KEY securely — if lost, all stored credentials in n8n become unrecoverable. Set GENERIC_TIMEZONE to the dealer's local timezone. The n8n container is only accessible via localhost:5678; Nginx (next step) will handle external HTTPS access.
Step 5: Configure Nginx Reverse Proxy with SSL
Set up Nginx as a reverse proxy in front of n8n to handle SSL termination and provide a public HTTPS endpoint for webhooks (required by Twilio) and secure dashboard access.
sudo apt install -y nginx certbot python3-certbot-nginxsudo tee /etc/nginx/sites-available/recall-agent << 'EOF'
server {
listen 80;
server_name recalls.yourmsp.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name recalls.yourmsp.com;
client_max_body_size 16M;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
}
}
EOFsudo ln -s /etc/nginx/sites-available/recall-agent /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginxsudo certbot --nginx -d recalls.yourmsp.com --non-interactive --agree-tos --email admin@yourmsp.comsudo systemctl enable certbot.timerEnsure the firewall allows inbound ports 80 and 443. Restrict SSH (port 22) to MSP office IP addresses only using 'sudo ufw allow from <MSP_IP> to any port 22'. The WebSocket upgrade headers are critical for n8n's real-time UI to work correctly.
Step 6: Configure Firewall Rules
Lock down the server with UFW (Uncomplicated Firewall) to allow only necessary inbound traffic and ensure all outbound API calls can proceed.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80/tcp comment 'HTTP for cert renewal redirect'
sudo ufw allow 443/tcp comment 'HTTPS for n8n webhooks and UI'
sudo ufw allow from <MSP_OFFICE_IP> to any port 22 comment 'SSH from MSP only'
sudo ufw enable
sudo ufw status verboseReplace <MSP_OFFICE_IP> with your MSP's static IP. If multiple technicians need SSH access from different locations, add each IP. Never open port 22 to 0.0.0.0/0. Outbound HTTPS to api.nhtsa.gov, api.openai.com, api.twilio.com, and api.sendgrid.com is allowed by default with the 'allow outgoing' rule.
Step 7: Create n8n User Account and Configure Credentials
Access the n8n web interface, create the initial admin account, and configure all required API credentials (OpenAI, Twilio, SendGrid, PostgreSQL).
Use a strong, unique password for the n8n admin account. Enable n8n's built-in basic auth or configure SSO if available. All credential values are encrypted at rest using the N8N_ENCRYPTION_KEY from Step 4. Create a separate OpenAI API key specifically for this project (do not reuse keys across clients).
Step 8: Register Twilio 10DLC Brand and Campaign
Register the dealer's business identity with Twilio for A2P 10DLC compliance. This is MANDATORY for sending business SMS in the United States and typically takes 1–5 business days for approval.
CRITICAL COMPLIANCE STEP. Without 10DLC registration, SMS messages will be filtered/blocked by carriers. Brand registration requires the dealer's EIN and legal business name — obtain this from the dealer contact. Campaign approval typically takes 1–5 business days. The phone number should be a local number in the dealer's area code for better deliverability and customer trust.
Step 9: Configure SendGrid for Transactional Email
Set up SendGrid for recall notification emails. Configure sender authentication (SPF/DKIM/DMARC), create email templates, and verify the sending domain.
em1234.dealername.com CNAME u12345.wl.sendgrid.net
s1._domainkey.dealername.com CNAME s1.domainkey.u12345.wl.sendgrid.net
s2._domainkey.dealername.com CNAME s2.domainkey.u12345.wl.sendgrid.nethttps://recalls.yourmsp.com/webhook/sendgrid-eventsDomain authentication is critical for email deliverability. The dealer's IT may need to update DNS records — coordinate with them or their domain registrar. Recall notification emails are classified as transactional under CAN-SPAM, so an unsubscribe link is technically not required, but including one is best practice and improves deliverability scores.
Step 10: Import Customer-VIN Data from DMS
Extract the dealer's customer database with VIN information from their DMS and load it into the PostgreSQL database. This is the foundation for recall matching. The method varies by DMS platform.
METHOD A: CDK Fortellis API (for CDK Drive dealers)
Use the Fortellis Customer API to pull customer records. Run the following API call after OAuth2 authentication:
curl -X GET 'https://api.fortellis.io/cdkdrive/customers/v1/customers?limit=100&offset=0' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Request-Id: <UUID>' \
-H 'Content-Type: application/json'METHOD B: CSV/Excel Export (for dealers without API access or while waiting for API approval)
docker cp /path/to/customer_export.csv recall-postgres:/tmp/customer_import.csvdocker exec -i recall-postgres psql -U recallagent -d recall_db << 'IMPORT'
COPY customers(dealer_id, customer_name, phone, email, vin, vehicle_year, vehicle_make, vehicle_model, sms_consent)
FROM '/tmp/customer_import.csv'
DELIMITER ','
CSV HEADER;
IMPORTMETHOD C: Autosoft/Auto-Mate REST API
Similar to CDK but with simpler authentication — consult Autosoft API docs.
DMS integration is the HARDEST part of this project. For initial deployment, start with METHOD B (CSV export) to get the agent operational quickly while the DMS API integration is being approved and built. Schedule weekly or daily CSV re-imports until the live API connection is ready. IMPORTANT: Verify sms_consent status for each customer record — do NOT assume consent. If the dealer has no consent records, set sms_consent to FALSE for all records and plan a consent collection campaign.
Step 11: Build and Deploy the Core Recall Monitoring Agent Workflows in n8n
Import the three core n8n workflows that comprise the autonomous recall monitoring agent: (1) NHTSA Recall Poller, (2) Customer-Recall Matcher, and (3) Notification Dispatcher. These are detailed in the Custom AI Components section below. This step covers importing and activating them.
DEALER_ID: unique identifier for this dealer (e.g., 'dealer_abc_motors')
DEALER_NAME: 'ABC Motors'
DEALER_PHONE: '(555) 123-4567'
DEALER_ADDRESS: '123 Main St, Anytown, USA'
SERVICE_SCHEDULER_URL: 'https://abcmotors.com/schedule-service'The workflows reference each other via webhook triggers. The Recall Poller triggers the Matcher, which triggers the Dispatcher. Ensure all three are active. Test with a manual trigger first before relying on the cron schedule.
Step 12: Configure Monitoring and Alerting
Set up monitoring to ensure the agent runs reliably and the MSP is alerted to any failures. Use n8n's built-in execution log plus external uptime monitoring.
echo '0 2 * * * docker exec recall-postgres pg_dump -U recallagent recall_db | gzip > /opt/recall-agent/backups/recall_db_$(date +\%Y\%m\%d).sql.gz' | sudo crontab -sudo tee /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
sudo systemctl restart dockerUptimeRobot's free tier supports up to 50 monitors — ideal for MSPs managing multiple dealers. Keep 30 days of database backups. Consider shipping backups to S3 or Azure Blob Storage for off-site protection. The error workflow should include the workflow name, error message, and timestamp in the alert.
Custom AI Components
NHTSA Recall Poller Agent
Type: workflow
Autonomous scheduled workflow that polls the NHTSA Recalls API for new recall campaigns affecting vehicle makes and models present in the dealer's customer database. It queries the NHTSA API for each unique make/model/year combination in the customer database, compares results against previously cached recalls, and stores any new recall campaigns in the database. Runs on a configurable cron schedule (default: daily at 6:00 AM).
Implementation
{
"name": "01 - NHTSA Recall Poller",
"nodes": [
{
"name": "Cron Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {
"rule": { "interval": [{ "field": "cronExpression", "expression": "0 6 * * *" }] }
},
"position": [250, 300]
},
{
"name": "Get Unique Vehicles",
"type": "n8n-nodes-base.postgres",
"parameters": {
"operation": "executeQuery",
"query": "SELECT DISTINCT vehicle_make, vehicle_model, vehicle_year FROM customers WHERE dealer_id = '{{$env.DEALER_ID}}' AND vehicle_make IS NOT NULL AND vehicle_model IS NOT NULL AND vehicle_year IS NOT NULL ORDER BY vehicle_make, vehicle_model, vehicle_year;"
},
"position": [450, 300]
},
{
"name": "Loop Over Vehicles",
"type": "n8n-nodes-base.splitInBatches",
"parameters": { "batchSize": 1 },
"position": [650, 300]
},
{
"name": "Call NHTSA Recalls API",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "=https://api.nhtsa.gov/recalls/recallsByVehicle?make={{$json.vehicle_make}}&model={{$json.vehicle_model}}&modelYear={{$json.vehicle_year}}",
"method": "GET",
"options": { "timeout": 30000 }
},
"position": [850, 300]
},
{
"name": "Wait 500ms Rate Limit",
"type": "n8n-nodes-base.wait",
"parameters": { "amount": 500, "unit": "milliseconds" },
"position": [1050, 300]
},
{
"name": "Extract Recall Results",
"type": "n8n-nodes-base.set",
"parameters": {
"values": {
"string": [
{ "name": "results_json", "value": "={{JSON.stringify($json.results)}}" },
{ "name": "result_count", "value": "={{$json.Count}}" }
]
}
},
"position": [1250, 300]
},
{
"name": "Filter Has Results",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"number": [{ "value1": "={{$json.result_count}}", "operation": "larger", "value2": 0 }]
}
},
"position": [1450, 300]
},
{
"name": "Split Recalls",
"type": "n8n-nodes-base.splitInBatches",
"parameters": { "batchSize": 1 },
"position": [1650, 200]
},
{
"name": "Upsert Recall to DB",
"type": "n8n-nodes-base.postgres",
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO recall_campaigns (nhtsa_campaign_number, manufacturer, subject, component, summary, consequence, remedy, affected_make, affected_model, affected_year_start, affected_year_end, report_received_date) VALUES ('{{$json.NHTSACampaignNumber}}', '{{$json.Manufacturer}}', '{{$json.Subject}}', '{{$json.Component}}', '{{$json.Summary}}', '{{$json.Consequence}}', '{{$json.Remedy}}', '{{$json.Make}}', '{{$json.Model}}', {{$json.ModelYear}}, {{$json.ModelYear}}, '{{$json.ReportReceivedDate}}') ON CONFLICT (nhtsa_campaign_number) DO NOTHING RETURNING id;"
},
"position": [1850, 200]
},
{
"name": "Check If New",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"string": [{ "value1": "={{$json.id}}", "operation": "isNotEmpty" }]
}
},
"position": [2050, 200]
},
{
"name": "Trigger Matcher Webhook",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://{{$env.N8N_HOST}}/webhook/recall-matcher",
"method": "POST",
"body": {
"recall_campaign_id": "={{$json.id}}",
"nhtsa_campaign_number": "={{$json.NHTSACampaignNumber}}",
"affected_make": "={{$json.Make}}",
"affected_model": "={{$json.Model}}",
"affected_year": "={{$json.ModelYear}}"
}
},
"position": [2250, 100]
}
]
}- The NHTSA API is free and requires no API key
- A 500ms wait between API calls prevents rate-limiting
- The ON CONFLICT clause ensures duplicate recalls are not re-processed
- Only NEW recalls (successful INSERT) trigger the matcher workflow
- The query pulls distinct make/model/year combos from the customer table to minimize API calls
- For a dealer with 50 unique make/model/year combinations, the full poll takes ~30 seconds
Customer-Recall Matcher Agent
Type: agent When triggered by the Recall Poller (via webhook), this agent matches a new recall campaign against all customers in the dealer's database whose vehicles match the affected make, model, and year range. It uses GPT-5.4 mini to handle fuzzy matching (e.g., 'Chevy' vs 'Chevrolet', 'F-150' vs 'F150') and to assess recall severity for message prioritization. Matched customers are queued for notification.
Implementation
{
"name": "02 - Customer-Recall Matcher",
"nodes": [
{
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"parameters": {
"path": "recall-matcher",
"httpMethod": "POST",
"responseMode": "onReceived"
},
"position": [250, 300]
},
{
"name": "Get Recall Details",
"type": "n8n-nodes-base.postgres",
"parameters": {
"operation": "executeQuery",
"query": "SELECT * FROM recall_campaigns WHERE id = {{$json.body.recall_campaign_id}};"
},
"position": [450, 300]
},
{
"name": "LLM Normalize Make Model",
"type": "@n8n/n8n-nodes-langchain.openAi",
"parameters": {
"model": "gpt-5.4-mini",
"messages": [
{
"role": "system",
"content": "You are a vehicle data normalization assistant. Given a vehicle make and model from a recall notice, return a JSON object with: {\"sql_make_patterns\": [\"pattern1\", \"pattern2\"], \"sql_model_patterns\": [\"pattern1\", \"pattern2\"]}. Include common abbreviations, alternate spellings, and the official name. Use SQL ILIKE patterns with % wildcards. Examples: Make 'CHEVROLET' → ['%chevrolet%', '%chevy%']. Model 'F-150' → ['%f-150%', '%f150%', '%f 150%']."
},
{
"role": "user",
"content": "Recall Make: {{$json.affected_make}}\nRecall Model: {{$json.affected_model}}"
}
],
"options": { "temperature": 0, "maxTokens": 200 }
},
"position": [650, 300]
},
{
"name": "Parse LLM Response",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const response = JSON.parse($input.first().json.message.content);\nconst makePatterns = response.sql_make_patterns;\nconst modelPatterns = response.sql_model_patterns;\n\n// Build dynamic WHERE clause\nconst makeClauses = makePatterns.map(p => `vehicle_make ILIKE '${p}'`).join(' OR ');\nconst modelClauses = modelPatterns.map(p => `vehicle_model ILIKE '${p}'`).join(' OR ');\n\nconst yearStart = $('Get Recall Details').first().json.affected_year_start;\nconst yearEnd = $('Get Recall Details').first().json.affected_year_end;\nconst dealerId = $env.DEALER_ID;\n\nconst query = `SELECT c.* FROM customers c WHERE c.dealer_id = '${dealerId}' AND (${makeClauses}) AND (${modelClauses}) AND c.vehicle_year BETWEEN ${yearStart} AND ${yearEnd} AND NOT EXISTS (SELECT 1 FROM notifications n WHERE n.customer_id = c.id AND n.recall_campaign_id = ${$('Webhook Trigger').first().json.body.recall_campaign_id} AND n.status != 'failed');`;\n\nreturn [{ json: { query, recall_campaign_id: $('Webhook Trigger').first().json.body.recall_campaign_id } }];"
},
"position": [850, 300]
},
{
"name": "Find Affected Customers",
"type": "n8n-nodes-base.postgres",
"parameters": {
"operation": "executeQuery",
"query": "={{$json.query}}"
},
"position": [1050, 300]
},
{
"name": "LLM Assess Severity",
"type": "@n8n/n8n-nodes-langchain.openAi",
"parameters": {
"model": "gpt-5.4-mini",
"messages": [
{
"role": "system",
"content": "You are a vehicle safety expert. Assess the severity of this recall and return a JSON object: {\"severity\": \"critical|high|medium|low\", \"urgency_note\": \"one sentence explaining urgency to customer\", \"customer_action\": \"specific action the customer should take\"}. 'Critical' = immediate safety risk (fire, loss of steering/braking). 'High' = safety risk under certain conditions. 'Medium' = component failure possible. 'Low' = minor defect."
},
{
"role": "user",
"content": "Recall Subject: {{$('Get Recall Details').first().json.subject}}\nComponent: {{$('Get Recall Details').first().json.component}}\nConsequence: {{$('Get Recall Details').first().json.consequence}}\nRemedy: {{$('Get Recall Details').first().json.remedy}}"
}
],
"options": { "temperature": 0, "maxTokens": 200 }
},
"position": [1250, 300]
},
{
"name": "Enrich Customer Data",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const severity = JSON.parse($('LLM Assess Severity').first().json.message.content);\nconst recall = $('Get Recall Details').first().json;\nconst customers = $('Find Affected Customers').all();\nconst recallCampaignId = $('Parse LLM Response').first().json.recall_campaign_id;\n\nconst enriched = customers.map(c => ({\n json: {\n ...c.json,\n recall_campaign_id: recallCampaignId,\n nhtsa_campaign_number: recall.nhtsa_campaign_number,\n recall_subject: recall.subject,\n recall_component: recall.component,\n recall_consequence: recall.consequence,\n recall_remedy: recall.remedy,\n severity: severity.severity,\n urgency_note: severity.urgency_note,\n customer_action: severity.customer_action\n }\n}));\n\nreturn enriched;"
},
"position": [1450, 300]
},
{
"name": "Loop Customers",
"type": "n8n-nodes-base.splitInBatches",
"parameters": { "batchSize": 1 },
"position": [1650, 300]
},
{
"name": "Trigger Notification Dispatcher",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://{{$env.N8N_HOST}}/webhook/notification-dispatcher",
"method": "POST",
"body": "={{JSON.stringify($json)}}",
"options": { "timeout": 30000 }
},
"position": [1850, 300]
}
]
}- GPT-5.4 mini is used for TWO purposes: (1) fuzzy make/model normalization to handle data quality issues (e.g., 'CHEVY SILVERADO' vs 'CHEVROLET SILVERADO 1500'), and (2) severity assessment for message prioritization
- The NOT EXISTS subquery prevents duplicate notifications for the same customer-recall pair
- Each matched customer is sent individually to the Notification Dispatcher for per-customer message personalization
- Cost per recall match cycle: ~$0.001–$0.005 in LLM tokens (negligible)
Notification Dispatcher Agent
Type: workflow Receives enriched customer-recall match data and sends personalized notifications via SMS (Twilio) and/or email (SendGrid). Respects consent preferences, generates personalized messages using GPT-5.4 mini, logs all notifications to the database, and handles opt-out processing.
Implementation
{
"name": "03 - Notification Dispatcher",
"nodes": [
{
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"parameters": {
"path": "notification-dispatcher",
"httpMethod": "POST"
},
"position": [250, 300]
},
{
"name": "Generate Personalized SMS",
"type": "@n8n/n8n-nodes-langchain.openAi",
"parameters": {
"model": "gpt-5.4-mini",
"messages": [
{
"role": "system",
"content": "You write concise, professional vehicle recall notification SMS messages for automotive dealerships. Rules: 1) Max 300 characters. 2) Include the vehicle year, make, model. 3) Include the recall subject. 4) Include the dealer name and phone. 5) End with 'Reply STOP to opt out.' 6) Tone: urgent but not alarming. 7) Do NOT include any marketing or promotional content — this is a safety notice only."
},
{
"role": "user",
"content": "Customer: {{$json.customer_name}}\nVehicle: {{$json.vehicle_year}} {{$json.vehicle_make}} {{$json.vehicle_model}}\nRecall: {{$json.recall_subject}}\nSeverity: {{$json.severity}}\nCustomer Action: {{$json.customer_action}}\nDealer: {{$env.DEALER_NAME}}\nDealer Phone: {{$env.DEALER_PHONE}}"
}
],
"options": { "temperature": 0.3, "maxTokens": 150 }
},
"position": [450, 200]
},
{
"name": "Generate Personalized Email",
"type": "@n8n/n8n-nodes-langchain.openAi",
"parameters": {
"model": "gpt-5.4-mini",
"messages": [
{
"role": "system",
"content": "You write professional vehicle recall notification emails for automotive dealerships. Return a JSON object: {\"subject\": \"email subject line\", \"body_html\": \"HTML email body\"}. Rules: 1) Subject line must mention 'Safety Recall' and the vehicle. 2) Body must include: customer greeting, vehicle details, recall description, consequence explanation, remedy available, call-to-action to schedule service, dealer contact info. 3) Professional, caring tone. 4) No marketing content. 5) Include the NHTSA campaign number for reference. 6) Use simple HTML with inline styles suitable for email clients."
},
{
"role": "user",
"content": "Customer: {{$json.customer_name}}\nVehicle: {{$json.vehicle_year}} {{$json.vehicle_make}} {{$json.vehicle_model}}\nVIN (last 4): {{$json.vin.slice(-4)}}\nRecall Campaign: {{$json.nhtsa_campaign_number}}\nRecall Subject: {{$json.recall_subject}}\nComponent: {{$json.recall_component}}\nConsequence: {{$json.recall_consequence}}\nRemedy: {{$json.recall_remedy}}\nSeverity: {{$json.severity}}\nUrgency: {{$json.urgency_note}}\nDealer: {{$env.DEALER_NAME}}\nDealer Phone: {{$env.DEALER_PHONE}}\nDealer Address: {{$env.DEALER_ADDRESS}}\nSchedule Service: {{$env.SERVICE_SCHEDULER_URL}}"
}
],
"options": { "temperature": 0.3, "maxTokens": 1000 }
},
"position": [450, 400]
},
{
"name": "Check SMS Consent",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"boolean": [{ "value1": "={{$json.sms_consent}}", "value2": true }],
"string": [{ "value1": "={{$json.phone}}", "operation": "isNotEmpty" }]
},
"combineOperation": "all"
},
"position": [650, 200]
},
{
"name": "Send SMS via Twilio",
"type": "n8n-nodes-base.twilio",
"parameters": {
"operation": "send",
"from": "={{$env.TWILIO_PHONE_NUMBER}}",
"to": "={{$json.phone}}",
"message": "={{$('Generate Personalized SMS').first().json.message.content}}"
},
"position": [850, 150]
},
{
"name": "Log SMS Notification",
"type": "n8n-nodes-base.postgres",
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notifications (customer_id, recall_campaign_id, channel, status, message_content, sent_at, twilio_sid) VALUES ({{$('Webhook Trigger').first().json.body.id}}, {{$('Webhook Trigger').first().json.body.recall_campaign_id}}, 'sms', 'sent', '{{$('Generate Personalized SMS').first().json.message.content.replace(/'/g, \"''\")}}' , NOW(), '{{$json.sid}}');"
},
"position": [1050, 150]
},
{
"name": "Check Email Available",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"string": [{ "value1": "={{$json.email}}", "operation": "isNotEmpty" }]
}
},
"position": [650, 400]
},
{
"name": "Send Email via SendGrid",
"type": "n8n-nodes-base.sendGrid",
"parameters": {
"operation": "send",
"fromEmail": "recalls@dealername.com",
"toEmail": "={{$json.email}}",
"subject": "={{JSON.parse($('Generate Personalized Email').first().json.message.content).subject}}",
"contentHtml": "={{JSON.parse($('Generate Personalized Email').first().json.message.content).body_html}}"
},
"position": [850, 450]
},
{
"name": "Log Email Notification",
"type": "n8n-nodes-base.postgres",
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notifications (customer_id, recall_campaign_id, channel, status, message_content, sent_at) VALUES ({{$('Webhook Trigger').first().json.body.id}}, {{$('Webhook Trigger').first().json.body.recall_campaign_id}}, 'email', 'sent', 'Email sent', NOW());"
},
"position": [1050, 450]
}
]
}SMS is ONLY sent when sms_consent = TRUE and phone is not empty. This is a hard TCPA compliance gate.
- Email recall notices are transactional under CAN-SPAM and do not require opt-in, but we still check for a valid email address.
- Messages are generated by GPT-5.4 mini with strict system prompts that prohibit marketing content (mixing recall + marketing in one message triggers full TCPA/CAN-SPAM compliance requirements).
- The Twilio SID is logged for delivery tracking and audit trail.
- For high-severity recalls (critical/high), consider adding a follow-up reminder workflow that re-sends after 7 days if no service appointment is booked.
Twilio Opt-Out Webhook Handler
Type: integration
Handles inbound SMS opt-out (STOP) messages from customers. When a customer replies STOP to a recall notification, Twilio automatically suppresses future messages, but this webhook also updates the database consent record to maintain an audit trail for TCPA compliance.
Implementation
Workflow Name: 04 - SMS Opt-Out Handler
Trigger: Webhook at path /webhook/twilio-optout (POST)
Configure in Twilio Console
n8n Workflow Steps
SELECT id FROM customers WHERE phone = '{{$json.From}}' AND dealer_id = '{{$env.DEALER_ID}}';UPDATE customers SET sms_consent = FALSE, updated_at = NOW() WHERE phone = '{{$json.From}}' AND dealer_id = '{{$env.DEALER_ID}}';INSERT INTO consent_log (customer_id, consent_type, action, source, timestamp) VALUES ({{customer_id}}, 'sms', 'opt_out', 'twilio_inbound_stop', NOW());UPDATE notifications SET status = 'opted_out' WHERE customer_id = {{customer_id}} AND status = 'pending';<?xml version='1.0' encoding='UTF-8'?><Response></Response>Twilio handles the actual STOP/START keyword processing at the carrier level for 10DLC numbers. This webhook is for our database audit trail, not for blocking messages (Twilio does that automatically). However, maintaining our own opt-out records is essential for TCPA compliance documentation.
DMS Sync Agent
Type: integration Scheduled workflow that synchronizes customer-VIN data from the dealer's DMS into the PostgreSQL database. Supports multiple DMS platforms via configurable adapters. For the initial deployment, a CSV import adapter is used; API-based adapters for CDK Fortellis, Dealertrack, and Autosoft can be added as DMS API access is approved.
Implementation
Workflow Name: 05 - DMS Customer Sync
Schedule: Daily at 4:00 AM (before the Recall Poller runs at 6:00 AM)
CSV Import Adapter (Phase 1 - Interim)
This adapter watches a designated SFTP/local folder for new CSV exports from the DMS.
/opt/recall-agent/dms-imports/latest_customers.csvconst rows = $input.all();
const valid = [];
const invalid = [];
for (const row of rows) {
const d = row.json;
// Validate VIN (must be 17 chars)
const vin = (d.VIN || d.vin || '').replace(/[^A-HJ-NPR-Z0-9]/gi, '');
if (vin.length !== 17) {
invalid.push({ ...d, reason: 'Invalid VIN' });
continue;
}
// Normalize phone
let phone = (d.Phone || d.phone || d.PHONE || '').replace(/[^0-9]/g, '');
if (phone.length === 10) phone = '+1' + phone;
else if (phone.length === 11 && phone.startsWith('1')) phone = '+' + phone;
else phone = null;
valid.push({
json: {
dealer_id: $env.DEALER_ID,
customer_name: (d['Customer Name'] || d.customer_name || d.Name || '').trim(),
phone: phone,
email: (d.Email || d.email || d.EMAIL || '').trim().toLowerCase(),
vin: vin.toUpperCase(),
vehicle_year: parseInt(d['Year'] || d.vehicle_year || d.YEAR),
vehicle_make: (d['Make'] || d.vehicle_make || d.MAKE || '').trim().toUpperCase(),
vehicle_model: (d['Model'] || d.vehicle_model || d.MODEL || '').trim().toUpperCase(),
sms_consent: (d['SMS Consent'] || d.sms_consent || 'false').toString().toLowerCase() === 'true',
dms_customer_id: (d['Customer ID'] || d.customer_id || d.CUSTID || '').toString()
}
};
}
return valid;- 5. Postgres Upsert: For each valid record
INSERT INTO customers (dealer_id, customer_name, phone, email, vin, vehicle_year, vehicle_make, vehicle_model, sms_consent, dms_customer_id, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
ON CONFLICT (dealer_id, vin)
DO UPDATE SET
customer_name = EXCLUDED.customer_name,
phone = COALESCE(EXCLUDED.phone, customers.phone),
email = COALESCE(EXCLUDED.email, customers.email),
vehicle_year = EXCLUDED.vehicle_year,
vehicle_make = EXCLUDED.vehicle_make,
vehicle_model = EXCLUDED.vehicle_model,
sms_consent = CASE WHEN customers.sms_consent = FALSE AND EXCLUDED.sms_consent = TRUE THEN TRUE ELSE customers.sms_consent END,
dms_customer_id = EXCLUDED.dms_customer_id,
updated_at = NOW();Add unique constraint first: ALTER TABLE customers ADD CONSTRAINT uq_dealer_vin UNIQUE (dealer_id, vin);
CDK Fortellis API Adapter (Phase 2)
Replace the CSV reader with an HTTP Request node chain:
/cdkdrive/customers/v1/customersThe sms_consent field uses a protective merge: it will not downgrade consent from TRUE to FALSE via sync (only the opt-out handler can do that). New customers coming in with consent=TRUE from the DMS will be set to TRUE.
Weekly Recall Digest Report
Type: workflow Generates a weekly summary report for the dealer's Service Manager and the MSP, showing: new recalls detected, customers notified, delivery success rates, opt-out rates, and pending follow-ups. Sent every Monday morning via email.
Implementation
Workflow Name: 06 - Weekly Recall Digest Schedule: Every Monday at 8:00 AM
-- Step 1: Cron Trigger
0 8 * * 1SELECT COUNT(*) as new_recalls,
STRING_AGG(DISTINCT nhtsa_campaign_number || ': ' || subject, E'\n') as recall_list
FROM recall_campaigns
WHERE first_seen_at >= NOW() - INTERVAL '7 days';SELECT
COUNT(*) as total_notifications,
COUNT(*) FILTER (WHERE status = 'sent' OR status = 'delivered') as successful,
COUNT(*) FILTER (WHERE status = 'failed') as failed,
COUNT(*) FILTER (WHERE channel = 'sms') as sms_count,
COUNT(*) FILTER (WHERE channel = 'email') as email_count,
COUNT(DISTINCT customer_id) as unique_customers_notified
FROM notifications
WHERE created_at >= NOW() - INTERVAL '7 days';SELECT COUNT(*) as opt_outs_this_week
FROM consent_log
WHERE action = 'opt_out' AND timestamp >= NOW() - INTERVAL '7 days';SELECT
COUNT(*) as total_customers,
COUNT(*) FILTER (WHERE sms_consent = TRUE) as sms_consented,
ROUND(COUNT(*) FILTER (WHERE sms_consent = TRUE)::numeric / COUNT(*)::numeric * 100, 1) as consent_rate_pct
FROM customers
WHERE dealer_id = '{{$env.DEALER_ID}}';You are a data analyst for an automotive dealership recall management system. Given the weekly statistics, generate a professional HTML email report. Include: Executive summary (2 sentences), Key Metrics table, any concerns (high failure rates, rising opt-outs, critical recalls needing follow-up), and recommended actions. Use the dealership brand colors and a clean table layout. Keep it concise — this is for a busy Service Manager.Testing & Validation
- NHTSA API Connectivity Test: Manually execute a curl command and verify JSON response with recall data is returned. Expected: HTTP 200 with 'Count' field > 0 and 'results' array containing recall objects.
- Database Schema Verification: Connect to PostgreSQL and verify all 4 tables exist: customers, recall_campaigns, notifications, consent_log. Verify customer data was imported correctly.
- Customer Data Import Validation: After importing DMS data, verify all VINs are exactly 17 characters and all phones are in E.164 format.
- n8n Workflow Activation Test: In the n8n UI, verify all 6 workflows show 'Active' status (green toggle). Click 'Executions' for each workflow and verify no error executions exist.
- Recall Poller End-to-End Test: Manually trigger the '01 - NHTSA Recall Poller' workflow via the n8n UI 'Test Workflow' button. Verify: (a) the workflow completes without errors, (b) new records appear in the recall_campaigns table, (c) the matcher webhook is called for any new recalls found.
- Fuzzy Match Accuracy Test: Insert a test customer with vehicle_make='CHEVY' and vehicle_model='SILVERADO 1500'. Then trigger the matcher with a recall for make='CHEVROLET' model='SILVERADO'. Verify the LLM-generated SQL patterns match the customer record. The test customer should appear in the affected customers list.
- SMS Delivery Test: Insert a test customer record with your own phone number, sms_consent=TRUE, and a vehicle that matches a known active recall. Trigger the notification dispatcher. Verify: (a) SMS arrives on your phone within 60 seconds, (b) message content includes vehicle details and dealer info, (c) message ends with 'Reply STOP to opt out', (d) notification record is logged in the notifications table with status='sent' and a valid twilio_sid.
- Email Delivery Test: Same as SMS test but verify email arrives in inbox (not spam). Check: (a) subject line mentions 'Safety Recall', (b) HTML renders correctly, (c) recall details are accurate, (d) dealer contact info and service scheduler link are present and clickable.
- SMS Opt-Out Test: Reply 'STOP' to the test SMS from the previous test. Verify: (a) Twilio sends automatic opt-out confirmation, (b) the consent_log table has a new 'opt_out' record, (c) the customer's sms_consent field is updated to FALSE, (d) subsequent recall notifications for this customer are NOT sent via SMS.
- Consent Guard Test: Insert a test customer with sms_consent=FALSE and a valid phone number matching an active recall. Trigger the full pipeline. Verify that NO SMS is sent to this customer (check Twilio logs and notifications table). Email should still be sent if email address is present.
- Duplicate Notification Prevention Test: Run the full pipeline twice for the same recall campaign. Verify that customers who were already notified in the first run are NOT notified again in the second run. Check notifications table: each customer should have exactly ONE notification per recall campaign per channel.
- Weekly Report Generation Test: Manually trigger the '06 - Weekly Recall Digest' workflow. Verify the report email arrives with accurate statistics matching the database counts. Check that the HTML renders correctly in both Gmail and Outlook.
- Load Test for Multi-Dealer Scaling: If deploying for multiple dealers, simulate 5 dealers with 5,000 customers each (25,000 total records). Run the Recall Poller and verify it completes within 30 minutes and doesn't exceed API rate limits or memory constraints.
- SSL and Webhook Security Test: Verify SSL certificate is valid and auto-renewing. Test webhook endpoint with an invalid payload and verify it returns an appropriate error without crashing the workflow.
- Backup and Recovery Test: Stop the PostgreSQL container, restore from the latest backup, restart the container, and verify all data is intact and workflows resume normally.
curl -s "https://api.nhtsa.gov/recalls/recallsByVehicle?make=Toyota&model=Camry&modelYear=2020"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';-- confirm customer data was imported
SELECT COUNT(*) FROM customers;-- must return 0 (all VINs exactly 17 characters)
SELECT COUNT(*) FROM customers WHERE LENGTH(vin) != 17;-- must return 0 (all phones in E.164 format)
SELECT COUNT(*) FROM customers WHERE phone IS NOT NULL AND phone NOT LIKE '+1%';curl -v https://recalls.yourmsp.com/healthzClient Handoff
The client handoff meeting should be conducted with the dealer's Service Manager and/or BDC Manager, lasting approximately 90 minutes. Cover the following topics:
During Consent Management, explain the TCPA compliance model in full. Show the dealer which customers currently have SMS consent (and what percentage that represents). Discuss the consent collection strategy: adding opt-in language to service intake forms, RO sign-off sheets, and the dealer website. Provide a sample consent form with compliant language: 'By providing your mobile number, you consent to receive vehicle safety recall notifications via text message from [Dealer Name]. Message frequency varies. Message and data rates may apply. Reply STOP to opt out at any time.'
Documentation to Leave Behind
Sign-Off: Have the dealer sign an acceptance document confirming the system is operational, they understand the consent requirements, and they approve the notification message templates.
Maintenance
Ongoing MSP Responsibilities:
Daily (Automated)
- Monitor n8n workflow execution logs for errors via the Error Handler workflow (alerts sent to MSP Slack/email automatically)
- Verify the NHTSA Recall Poller ran successfully at the scheduled time
- Database backup runs at 2:00 AM automatically via cron
Weekly
- Review the Weekly Recall Digest report for anomalies: sudden drops in delivery rates, spikes in opt-outs, or zero recalls detected (which may indicate an API issue)
- Check Twilio delivery logs for carrier filtering issues (if SMS delivery rate drops below 95%, investigate 10DLC campaign status)
- Verify DMS data sync completed successfully and customer count is stable or growing
- Review n8n execution history for any workflows that took abnormally long
Monthly
- Update n8n to the latest stable version
- Update PostgreSQL if security patches are available
- Review and rotate API keys (OpenAI, Twilio, SendGrid) per MSP security policy
- Run database maintenance: VACUUM ANALYZE; on all tables
- Review LLM costs in OpenAI dashboard — typical spend should be $5–$25/month per dealer
- Generate and send a monthly executive summary to the dealer GM (can be derived from weekly reports)
docker pull n8nio/n8n:latest && docker stop n8n && docker rm n8n && [re-run docker run command from Step 4]VACUUM ANALYZE;Quarterly
- FTC Safeguards Rule compliance review: verify encryption, access controls, and audit logs are intact
- TCPA consent audit: review consent_log table for any anomalies, verify opt-out processing is working correctly
- Review and update notification message templates based on dealer feedback
- Performance review with dealer: compare recall notification → service appointment conversion rate
- Assess customer database growth and consent rate trends
- Update SSL certificates if not auto-renewing (Let's Encrypt auto-renews, but verify)
Annually
- Full security audit of the server/VM (OS patches, Docker security, network configuration)
- Review NHTSA API for any changes to endpoints or data schema (check https://api.nhtsa.gov for updates)
- Renegotiate Twilio and SendGrid plans based on actual usage volumes
- Review DMS API integration status — if still using CSV import, evaluate whether API access is now available
- Disaster recovery test: restore from backup to a separate environment and verify full functionality
Escalation Paths
- Level 1 (MSP Helpdesk): Workflow errors, delivery failures, data sync issues — resolve within 4 hours during business hours
- Level 2 (MSP Engineer): API integration failures, DMS connectivity issues, performance degradation — resolve within 1 business day
- Level 3 (MSP Architect): Architecture changes, new DMS platform integration, multi-dealer scaling issues — resolve within 1 week
- Vendor Escalation: Twilio support for carrier filtering issues, OpenAI for API outages, CDK/Reynolds for DMS API access problems
SLA Considerations
- System uptime target: 99.5% (allows ~44 hours/year of downtime for maintenance)
- Recall detection latency: new recalls detected within 24 hours of NHTSA publication
- Notification delivery: within 2 hours of recall detection
- Opt-out processing: immediate (within seconds for Twilio automatic handling; database update within 1 hour)
Alternatives
BizzyCar or Recall Masters (Turnkey SaaS)
Instead of building a custom recall monitoring agent, resell an established turnkey platform like BizzyCar ($20M funded, nearly 1,000 dealers by end of 2025) or Recall Masters (trusted by largest dealer groups and OEMs). These platforms pull data nightly from OEMs, NHTSA, DMS, CRM, and third-party sources, offering far deeper recall intelligence than a custom NHTSA-only solution.
Microsoft Copilot Studio + Power Automate (Microsoft Ecosystem)
For dealers already deeply invested in Microsoft 365 and Dynamics 365, build the recall agent using Microsoft Copilot Studio ($200/month for 25,000 credits) and Power Automate for workflow orchestration. The agent can be surfaced as a Copilot in Microsoft Teams, and integrates natively with Dynamics 365 for CRM write-back.
Make.com (Budget Cloud Workflow)
Use Make.com (formerly Integromat) instead of n8n as the workflow automation platform. Make.com offers a visual builder with 3,000+ app integrations starting at $10.59/month. The recall agent logic (NHTSA polling, matching, notification) would be built as Make.com scenarios instead of n8n workflows.
Custom Python Application (Full Code)
Build the entire recall monitoring agent as a custom Python application using libraries like requests (for NHTSA API), openai (for LLM), twilio (for SMS), sendgrid (for email), and psycopg2 (for PostgreSQL). Deploy as a systemd service or Docker container without a workflow orchestration layer.
CrewAI Multi-Agent Framework
Use CrewAI (open-source or $99/month managed) to build a more sophisticated multi-agent system where specialized agents handle different aspects: a Recall Research Agent scrapes and analyzes NHTSA data, a Customer Matching Agent handles VIN-to-recall correlation, a Communication Agent drafts messages, and a Compliance Agent validates consent before sending.
Want early access to the full toolkit?