46 min readAutonomous agents

Implementation Guide: Monitor subcontractor insurance expirations and request updated certificates autonomously

Step-by-step implementation guide for deploying AI to monitor subcontractor insurance expirations and request updated certificates autonomously for Construction & Contractors clients.

Hardware Procurement

Business Workstation for Dashboard Access

Business Workstation for Dashboard Access

DellOptiPlex 7020 Micro (i5-14500T, 16GB, 256GB SSD)Qty: 1

$850 per unit (MSP cost) / $1,100 suggested resale

Primary workstation for the office administrator who monitors the compliance dashboard, reviews escalated certificates, and manages the subcontractor database. Only needed if the client does not already have a suitable modern PC.

Document Scanner

Document Scanner

Fujitsu (Ricoh)ScanSnap iX1600Qty: 1

$400 per unit (MSP cost) / $550 suggested resale

Scans paper COI certificates that arrive via mail or are handed over at job sites. Converts to searchable PDF for ingestion into the AI pipeline. Only required if the client still receives a meaningful volume of paper certificates — skip for clients who receive COIs exclusively via email.

Cloud Virtual Machine (n8n Agent Host)

Cloud Virtual Machine (n8n Agent Host)

Microsoft AzureAzure B2s VM (2 vCPU, 4 GB RAM, 30 GB SSD) — East US regionQty: 1

$38/month (MSP cost) / $65/month suggested resale

Hosts the self-hosted n8n Community Edition instance that runs the autonomous agent workflows. Provides dedicated, always-on compute for scheduled expiration checks, email monitoring, document processing pipelines, and LLM orchestration.

Software Procurement

TrustLayer — COI Tracking Platform

TrustLayerSaaS — free Starter tier (up to 50 vendors), paid tiers for 50+ vendors

$0/month (Starter, ≤50 subs) or $300–$800/month (Growth/Pro, 50–500 subs) per client

Core compliance database and vendor portal. Provides AI-powered COI parsing, subcontractor self-service upload portal, compliance status dashboard, expiration date tracking, and ACORD form recognition. Serves as the system of record for all insurance compliance data.

bcs COI Tracking (Alternative Primary Platform)

Business Credentialing Services (bcs)SaaS — free tier (up to 25 vendors), $0.95/vendor/month paid tierQty: 50–500 subs

$0/month (≤25 subs) or $48–$475/month (50–500 subs at $0.95/vendor) per client

Alternative to TrustLayer with transparent per-vendor pricing, 78,000+ pre-qualified vendor network, and a broker resale program that enables the MSP to white-label the service. Preferred for clients with large subcontractor rosters where per-vendor pricing is more economical.

$0/month (self-hosted) or $50/month (Cloud, 10K executions) per client

Core autonomous agent orchestration engine. Runs all scheduled checks, email automations, document processing workflows, LLM calls, and integration logic. Self-hosted on the Azure VM for maximum control and zero per-execution costs.

OpenAI API — GPT-5.4 mini

OpenAIGPT-5.4 miniQty: Usage-based API

$5–$25/month per client (estimated 50–200 COI verifications/month)

LLM backbone for intelligent certificate verification. Extracts structured data from OCR'd certificates, verifies coverage amounts against project requirements, identifies additional insured endorsements, checks waiver of subrogation clauses, and generates natural-language escalation summaries.

Google Document AI — Form Parser

Google CloudForm Parser

$1.50/page; estimated $15–$75/month per client

High-accuracy OCR engine specifically trained on structured forms like ACORD 25 and ACORD 28 certificates. Extracts text, tables, and form fields from scanned or photographed COI documents before passing to GPT-5.4 mini for semantic verification.

SendGrid — Transactional Email

Twilio (SendGrid)SaaS — free tier (100 emails/day), paid from $19.95/month

$0–$19.95/month per client

Sends automated renewal request emails to subcontractors on escalating schedules. Provides delivery tracking, open/click analytics, bounce handling, and unsubscribe management. Templates branded with client's company logo and contact information.

Microsoft 365 Business Standard

MicrosoftPer-seat SaaS (CSP)

$12.50/user/month (MSP CSP cost ~$10.50)

Client's business email (Exchange Online) for receiving incoming COI emails and for the n8n agent to monitor a shared mailbox (e.g., insurance@clientdomain.com). SharePoint Online used for COI document archive with 10-year retention policy.

Procore — Construction Project Management (Client Existing)

Procore TechnologiesSaaS — annual subscription

$4,500–$50,000/year (typically already owned by client)

Integration target — the agent syncs subcontractor lists, project assignments, and compliance status with Procore. Not an MSP procurement item; the client typically already owns this. The MSP configures the API integration.

Prerequisites

  • Client has an active business email system (Microsoft 365 or Google Workspace) with the ability to create a shared/service mailbox (e.g., insurance@clientdomain.com or coi@clientdomain.com)
  • Client has a current master list of active subcontractors with company name, primary contact name, email address, phone number, trade/specialty, and current project assignments — even if in spreadsheet format
  • Client has documented insurance requirements (minimum coverage amounts for GL, WC, Auto, Umbrella) per project type or risk tier, or is willing to define these during discovery
  • Client has existing certificates of insurance (COIs) in digital format (PDF/image) or is willing to scan existing paper certificates during onboarding
  • Standard business internet connection (25+ Mbps download) at the office location where the dashboard will be accessed
  • Client has a Procore, Buildertrend, or similar project management platform with API access enabled — or is willing to manage subcontractor data through the COI platform directly
  • A designated client stakeholder (office manager, project coordinator, or risk manager) who will serve as the compliance escalation contact and system administrator
  • Client has an active Azure subscription or the MSP provisions one under their CSP tenant for hosting the n8n VM
  • MSP has an active OpenAI API account with billing configured and a Google Cloud account with Document AI API enabled
  • MSP has access to the client's DNS records to configure SPF, DKIM, and DMARC for SendGrid email deliverability from the client's domain

Installation Steps

Step 1: Discovery & Data Collection

Conduct a 2-hour on-site or video discovery session with the client's office manager/project coordinator and CFO/owner. Audit the current COI tracking process (spreadsheets, filing cabinets, email threads). Collect the master subcontractor list, current COI documents, insurance requirement templates from contracts, and identify the project management software in use. Document the client's escalation preferences (who gets notified, when to hold payments, when to stop work).

Note

Prepare a standardized intake questionnaire before the meeting. Key data to collect: number of active subs, number of projects, insurance requirement tiers, current software stack, who currently handles COI tracking, pain points. Export the sub list from Procore if available (Procore > Directory > Company Directory > Export CSV).

Step 2: Provision Azure VM for n8n

Deploy an Azure B2s virtual machine to host the self-hosted n8n Community Edition. This VM will run 24/7 as the autonomous agent's execution environment. Use Ubuntu 22.04 LTS as the base OS.

bash
az login
az group create --name rg-coi-agent-<clientname> --location eastus
az vm create --resource-group rg-coi-agent-<clientname> --name vm-n8n-<clientname> --image Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest --size Standard_B2s --admin-username n8nadmin --generate-ssh-keys --public-ip-sku Standard --nsg-rule SSH
az vm open-port --resource-group rg-coi-agent-<clientname> --name vm-n8n-<clientname> --port 5678 --priority 1001
az vm open-port --resource-group rg-coi-agent-<clientname> --name vm-n8n-<clientname> --port 443 --priority 1002
Note

Replace <clientname> with the client's short identifier (e.g., 'acmebuilders'). After deployment, note the public IP address. For production, configure a DNS A record (e.g., n8n.clientdomain.com) and set up an Nginx reverse proxy with Let's Encrypt SSL. Consider restricting port 5678 access to MSP and client office IP ranges via NSG rules for security.

Step 3: Install Docker and n8n on Azure VM

SSH into the Azure VM and install Docker, then deploy n8n as a Docker container with persistent storage. Configure n8n to start automatically on boot and to use a PostgreSQL database for workflow data persistence.

1
SSH into the Azure VM
2
Update and upgrade system packages
3
Install Docker, Docker Compose, Nginx, Certbot, and the Nginx Certbot plugin
4
Enable and start the Docker service
5
Add n8nadmin user to the docker group
6
Create the n8n-docker working directory and navigate into it
7
Write the docker-compose.yml file with n8n and PostgreSQL configuration
8
Start the Docker Compose stack in detached mode
9
Run Certbot to provision an SSL certificate for n8n via Nginx
Install Docker, deploy n8n with PostgreSQL via Docker Compose, and provision SSL with Certbot
bash
ssh n8nadmin@<VM_PUBLIC_IP>
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose-v2 nginx certbot python3-certbot-nginx
sudo systemctl enable docker && sudo systemctl start docker
sudo usermod -aG docker n8nadmin
mkdir -p /home/n8nadmin/n8n-docker && cd /home/n8nadmin/n8n-docker
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
  n8n-db:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_DB: n8n
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: <GENERATE_STRONG_PASSWORD>
    volumes:
      - n8n_db_data:/var/lib/postgresql/data
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - '5678:5678'
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: n8n-db
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: <GENERATE_STRONG_PASSWORD>
      N8N_HOST: n8n.<clientdomain>.com
      N8N_PORT: 5678
      N8N_PROTOCOL: https
      WEBHOOK_URL: https://n8n.<clientdomain>.com/
      N8N_ENCRYPTION_KEY: <GENERATE_32_CHAR_KEY>
      GENERIC_TIMEZONE: America/New_York
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - n8n-db
volumes:
  n8n_data:
  n8n_db_data:
EOF
sudo docker compose up -d
sudo certbot --nginx -d n8n.<clientdomain>.com --non-interactive --agree-tos -m msp-admin@mspdomain.com
Note

Generate strong passwords with: openssl rand -base64 24. The N8N_ENCRYPTION_KEY is critical — it encrypts all credential storage. Back it up securely in your MSP password vault (e.g., IT Glue, Hudu). After starting, access n8n at https://n8n.<clientdomain>.com:5678 and create the initial admin account. Set timezone to the client's local timezone.

Step 4: Configure TrustLayer (or bcs) COI Platform

Sign up for the COI tracking platform, configure the client's account, define insurance requirement templates, and import the subcontractor master list. This platform serves as the compliance database and subcontractor-facing portal.

Note

For TrustLayer: Go to https://app.trustlayer.io/signup → Create organization with client's company name → Set up requirement templates (create separate templates for: High-Risk Trades requiring $2M GL/$1M WC/$1M Auto/$5M Umbrella, Standard Trades requiring $1M GL/$500K WC/$1M Auto/$2M Umbrella, Low-Risk Trades requiring $1M GL/statutory WC/$1M Auto) → Import subcontractor CSV (Name, Email, Trade, Phone, EIN) → Enable automated reminder emails at 60/30/14/7/1 day intervals → Upload existing COI PDFs for each sub → Configure the vendor self-service portal URL. For bcs: Similar setup at https://app.abordsoftware.com. The key configuration fields are: coverage types (GL, WC, Auto, Umbrella, Professional Liability), minimum limits per template, additional insured requirements, waiver of subrogation requirements, and per-project vs. blanket tracking.

Step 5: Configure SendGrid for Automated Email

Set up SendGrid as the transactional email provider for automated renewal requests that go beyond the COI platform's built-in emails. This allows the n8n agent to send highly customized, escalating request emails with the client's branding.

1
In SendGrid dashboard (https://app.sendgrid.com), create API key: Settings > API Keys > Create API Key (Full Access) > save key securely
2
Authenticate domain: Settings > Sender Authentication > Authenticate Your Domain
3
Create Dynamic Templates for: initial_request (60 days before expiration), second_request (30 days before expiration), urgent_request (14 days before expiration), final_warning (7 days before expiration), expired_notice (day of expiration), compliance_hold (post-expiration — work stoppage warning)
DNS records to add to clientdomain.com for SendGrid domain authentication
dns
CNAME: em1234.clientdomain.com → u1234567.wl.sendgrid.net
CNAME: s1._domainkey.clientdomain.com → s1.domainkey.u1234567.wl.sendgrid.net
CNAME: s2._domainkey.clientdomain.com → s2.domainkey.u1234567.wl.sendgrid.net
Note

Email deliverability is critical for this system. Ensure SPF, DKIM, and DMARC records are properly configured. Use the client's domain (not a generic MSP domain) as the sender. Create a dedicated sender identity: insurance@clientdomain.com or compliance@clientdomain.com. Store the API key in n8n credentials, never in workflow nodes directly.

Step 6: Configure OpenAI and Google Document AI API Credentials

Set up API access for the AI services that power intelligent certificate verification. Google Document AI handles OCR extraction from PDF/image COIs, and OpenAI GPT-5.4 mini performs semantic verification of coverage compliance.

1
Google Cloud — Document AI Setup: Log in and create a new project
Authenticate and create Google Cloud project with Document AI enabled
bash
gcloud auth login
gcloud projects create coi-agent-<clientname> --name='COI Agent <ClientName>'
gcloud config set project coi-agent-<clientname>
gcloud services enable documentai.googleapis.com
1
Create a Form Parser processor in the Google Cloud Console: Navigation > Document AI > Create Processor > Form Parser > Select US region
2
Note the Processor ID (e.g., 'abc123def456')
3
Create a service account and grant Document AI access
Create service account, bind Document AI role, and export key file
bash
gcloud iam service-accounts create n8n-docai --display-name='n8n Document AI'
gcloud projects add-iam-policy-binding coi-agent-<clientname> --member='serviceAccount:n8n-docai@coi-agent-<clientname>.iam.gserviceaccount.com' --role='roles/documentai.apiUser'
gcloud iam service-accounts keys create ~/docai-key.json --iam-account=n8n-docai@coi-agent-<clientname>.iam.gserviceaccount.com
1
OpenAI Setup: Go to https://platform.openai.com/api-keys → Create new secret key
2
Set usage limits: $50/month hard cap to prevent runaway costs
3
Note the API key (sk-...)
Note

Store all API keys in n8n's built-in credential store (Settings > Credentials). The Google service account key JSON file should be uploaded to n8n and then deleted from the VM filesystem. Set OpenAI spending alerts at $25 and a hard cap at $50/month — typical usage for 200 COI verifications/month is $10–$25. For Document AI, the Form Parser is preferred over the basic OCR processor because it understands table structures in ACORD forms.

Step 7: Create Shared Mailbox for Incoming COIs

Set up a dedicated shared mailbox in the client's Microsoft 365 tenant that the n8n agent will monitor for incoming certificate submissions from subcontractors. This mailbox receives forwarded COIs and direct submissions.

1
Connect to Exchange Online PowerShell
2
Create shared mailbox
3
In Azure AD > App Registrations > New Registration: Name: n8n-COI-Agent | Redirect URI: https://n8n.<clientdomain>.com/rest/oauth2-credential/callback | API Permissions: Microsoft Graph > Application > Mail.Read, Mail.ReadWrite | Grant admin consent
4
Note the Application (Client) ID, Tenant ID, and Client Secret
Connect to Exchange Online and create the shared mailbox
powershell
Connect-ExchangeOnline -UserPrincipalName admin@clientdomain.com

New-Mailbox -Shared -Name 'Insurance Certificates' -DisplayName 'Insurance Certificates' -Alias insurance
Note

The shared mailbox approach is preferred over monitoring individual user mailboxes for security and separation of concerns. Configure a mail flow rule in Exchange Admin Center to auto-forward any emails containing 'certificate of insurance', 'COI', or 'ACORD' in subject/body to this shared mailbox. Also set up the subcontractor email templates to have a reply-to address of insurance@clientdomain.com.

Step 8: Deploy n8n Agent Workflows

Import and configure the core autonomous agent workflows in n8n. There are four primary workflows: (1) Expiration Monitor & Notification Scheduler, (2) Incoming COI Email Monitor, (3) Document OCR & AI Verification Pipeline, and (4) Escalation & Reporting Engine. These workflows are defined in detail in the Custom AI Components section of this guide.

1
Go to Settings > Credentials and add: SendGrid API (API key from Step 5), OpenAI API (API key from Step 6), Google Service Account (JSON key from Step 6), Microsoft Graph OAuth2 (App registration from Step 7), TrustLayer API (if available) or HTTP Header Auth, Procore API OAuth2 (from client's Procore admin)
2
Import each workflow JSON from the Custom AI Components section
3
For each workflow, update: Credential references, Client-specific variables (company name, domain, requirement thresholds), Timezone settings, Email template content
4
Activate workflows one at a time, testing each before enabling the next
Note

Deploy workflows in this order: Expiration Monitor first (least risk, most visibility), then Incoming Email Monitor, then OCR/Verification, then Escalation. Allow each to run for 2-3 days before enabling the next. The Expiration Monitor should be set to run daily at 7:00 AM local time. The Email Monitor runs every 5 minutes. Keep all workflows in 'Inactive' status during initial testing — use the Manual Execute button to test with real data.

Step 9: Import Subcontractor Data and Existing COIs

Perform the initial data load: import the cleaned subcontractor master list into TrustLayer/bcs and upload all existing COI documents. The AI will parse existing certificates to establish baseline expiration dates and compliance status.

Note

Prepare a CSV with these columns: company_name, contact_first_name, contact_last_name, contact_email, contact_phone, trade_specialty, tax_id, requirement_template (High-Risk/Standard/Low-Risk), active_projects (comma-separated). Upload existing COIs organized by subcontractor. Expect the initial OCR/parsing run to take 2-4 hours for 100+ subcontractors. Review the AI's extraction results for the first 20 certificates manually to verify accuracy before trusting automated parsing for the remainder. Common issues: handwritten certificates, poor scan quality, non-standard certificate formats from small brokers.

Step 10: Configure Procore Integration

Connect the COI agent to the client's Procore instance to automatically sync subcontractor directories, project assignments, and compliance status. This ensures new subcontractors added in Procore are automatically enrolled in COI monitoring.

1
In Procore: Company Settings > App Management > Install Custom App — Or use Procore's Developer Portal to register an OAuth2 application
2
Required Procore permissions: Company Directory: Read, Project Directory: Read, Company > Vendors: Read, Compliance: Read/Write (if using Procore's compliance module)
3
In n8n, configure HTTP Request nodes with: Base URL: https://api.procore.com/rest/v1.0, Auth: OAuth2 Bearer Token, Company ID: <client_procore_company_id>
Note

If the client uses Buildertrend instead of Procore, use Buildertrend's API (https://api.buildertrend.com) with similar endpoints. If the client has no project management software, skip this step — the subcontractor list will be maintained directly in TrustLayer/bcs and synced to n8n via the platform's API or manual CSV updates. For Procore integration, the most useful endpoints are: GET /companies/{id}/vendors (subcontractor list) and GET /projects/{id}/vendors (project-specific subs).

Step 11: Configure Escalation Rules and Payment Hold Integration

Set up the escalation chain that defines what happens at each stage of non-compliance. Configure integration with the client's accounting system for optional payment hold triggers on expired subcontractors.

Note

Standard escalation timeline: 60 days = friendly reminder email, 30 days = firm request with deadline, 14 days = urgent notice CC'd to project manager, 7 days = final warning CC'd to CFO with payment hold warning, 0 days (expired) = compliance hold notice sent to sub + PM + CFO + accounting, with optional automatic payment hold flag. For QuickBooks integration, use n8n's QuickBooks node to flag vendor records. For Sage, use the Sage API or create a shared spreadsheet that accounting reviews daily. The payment hold is advisory only — the system flags vendors but does not unilaterally block payments without human approval. Always require CFO approval for actual payment holds.

Step 12: Go-Live and Monitoring Setup

Activate all workflows, verify the full cycle works end-to-end, and set up monitoring alerts for the MSP. Configure Azure VM monitoring and n8n health checks.

1
Set up Azure VM monitoring
2
Set up n8n health check (add to crontab on VM)
3
Set up daily backup
4
Configure Azure Backup for VM
Create Azure VM CPU alert
bash
az monitor metrics alert create --name 'n8n-vm-cpu-alert' --resource-group rg-coi-agent-<clientname> --scopes /subscriptions/<sub_id>/resourceGroups/rg-coi-agent-<clientname>/providers/Microsoft.Compute/virtualMachines/vm-n8n-<clientname> --condition 'avg Percentage CPU > 85' --window-size 15m --evaluation-frequency 5m --action-group <msp_action_group_id>
Add n8n health check to crontab
bash
crontab -e
# Add: */5 * * * * curl -sf https://n8n.<clientdomain>.com/healthz || curl -X POST https://hooks.slack.com/<msp_webhook> -d '{"text":"ALERT: n8n down for <clientname>"}'
Schedule daily PostgreSQL backup
bash
echo '0 2 * * * docker exec n8n-docker-n8n-db-1 pg_dump -U n8n n8n | gzip > /home/n8nadmin/backups/n8n-$(date +\%Y\%m\%d).sql.gz' | crontab -
Enable Azure Backup for VM
bash
az backup protection enable-for-vm --resource-group rg-coi-agent-<clientname> --vault-name <msp_backup_vault> --vm vm-n8n-<clientname> --policy-name DefaultPolicy
Note

Go-live should happen on a Tuesday or Wednesday morning to allow a full work week of monitoring before the weekend. The MSP should monitor the n8n execution logs daily for the first two weeks. Set up a weekly automated compliance summary report that emails to both the client stakeholder and the MSP account manager. Key metrics to track: certificates expiring in 30 days, outstanding renewal requests, auto-approved vs. escalated certificates, email delivery rates.

Custom AI Components

Expiration Monitor & Notification Scheduler

Type: workflow

This is the primary autonomous agent workflow that runs daily at 7:00 AM. It queries the subcontractor database for upcoming insurance expirations, determines which notification tier each subcontractor falls into based on days-until-expiration, and dispatches the appropriate email template via SendGrid. It maintains a notification log to prevent duplicate sends and tracks response status.

Implementation

Trigger

Schedule Trigger — CRON expression: daily at 7 AM

CRON expression for daily 7 AM trigger
text
0 7 * * *

Node 1: Fetch Subcontractor Data

  • Type: HTTP Request
  • Method: GET
  • URL: https://api.trustlayer.io/v1/vendors (or bcs API equivalent)
  • Headers: Authorization: Bearer {{$credentials.trustlayerApi.apiKey}}
  • Query: status=active&fields=id,name,email,insurance_policies
Note

Alternative (if no API): Google Sheets node reading from a master spreadsheet with columns: sub_id, company_name, contact_email, gl_expiry, wc_expiry, auto_expiry, umbrella_expiry, last_notified_date, notification_tier, requirement_template

Node 2: Calculate Days Until Expiration

  • Type: Code (JavaScript)
Node 2: Calculate days until expiration and assign notification tier
javascript
const today = new Date();
const results = [];

for (const item of $input.all()) {
  const sub = item.json;
  const policies = [
    { type: 'General Liability', expiry: sub.gl_expiry },
    { type: 'Workers Compensation', expiry: sub.wc_expiry },
    { type: 'Auto Liability', expiry: sub.auto_expiry },
    { type: 'Umbrella', expiry: sub.umbrella_expiry }
  ];
  
  for (const policy of policies) {
    if (!policy.expiry) continue;
    const expiryDate = new Date(policy.expiry);
    const daysUntil = Math.ceil((expiryDate - today) / (1000 * 60 * 60 * 24));
    
    let tier = null;
    if (daysUntil <= 0) tier = 'expired';
    else if (daysUntil <= 7) tier = 'final_warning';
    else if (daysUntil <= 14) tier = 'urgent';
    else if (daysUntil <= 30) tier = 'second_request';
    else if (daysUntil <= 60) tier = 'initial_request';
    
    if (tier) {
      results.push({
        json: {
          sub_id: sub.sub_id || sub.id,
          company_name: sub.company_name || sub.name,
          contact_email: sub.contact_email || sub.email,
          policy_type: policy.type,
          expiry_date: policy.expiry,
          days_until_expiry: daysUntil,
          notification_tier: tier,
          last_notified: sub.last_notified_date,
          requirement_template: sub.requirement_template
        }
      });
    }
  }
}

return results;

Node 3: Filter Already Notified

  • Type: Code (JavaScript)
Node 3: Filter subcontractors already notified within cooldown period
javascript
// Prevent sending same-tier notification within cooldown period
const cooldowns = {
  'initial_request': 7, // days between re-sends
  'second_request': 5,
  'urgent': 3,
  'final_warning': 2,
  'expired': 1
};

const today = new Date();
return $input.all().filter(item => {
  const d = item.json;
  if (!d.last_notified) return true;
  const lastNotified = new Date(d.last_notified);
  const daysSince = Math.ceil((today - lastNotified) / (1000*60*60*24));
  return daysSince >= (cooldowns[d.notification_tier] || 7);
});

Node 4: Switch by Notification Tier

  • Type: Switch
1
Route 1: notification_tier == 'initial_request' → Initial Email
2
Route 2: notification_tier == 'second_request' → Second Email
3
Route 3: notification_tier == 'urgent' → Urgent Email
4
Route 4: notification_tier == 'final_warning' → Final Warning Email
5
Route 5: notification_tier == 'expired' → Expired Notice + Escalation

Node 5a–5e: SendGrid Email Nodes (one per tier)

  • Type: SendGrid
  • Operation: Send Email
  • From: insurance@<clientdomain>.com
  • To: {{$json.contact_email}}
  • Template ID: (respective SendGrid dynamic template ID)
  • CC (for urgent/final/expired tiers): project_manager@clientdomain.com, cfo@clientdomain.com
SendGrid dynamic template data payload
json
{
  "company_name": "{{$json.company_name}}",
  "policy_type": "{{$json.policy_type}}",
  "expiry_date": "{{$json.expiry_date}}",
  "days_remaining": "{{$json.days_until_expiry}}",
  "upload_url": "https://app.trustlayer.io/vendor-portal/<client_org_id>",
  "gc_company_name": "<Client Company Name>",
  "gc_contact_name": "<Client Contact Name>",
  "gc_phone": "<Client Phone>"
}

Node 6: Update Notification Log

  • Type: Google Sheets (or HTTP Request to TrustLayer API)
  • Operation: Update Row
  • Update: last_notified_date = today, notification_tier = current tier

Node 7: Daily Summary Report

  • Type: SendGrid
  • Send daily digest to client stakeholder and MSP
  • Total subs monitored
  • Certificates expiring in 60/30/14/7 days
  • Expired certificates count
  • Notifications sent today
  • Compliance percentage

Incoming COI Email Monitor

Type: workflow

Monitors the shared insurance@clientdomain.com mailbox every 5 minutes for new incoming emails with PDF attachments. When a new COI is detected, it extracts the PDF attachment and routes it to the OCR & Verification pipeline. Handles multiple attachments per email and non-PDF formats.

Implementation:

n8n Workflow JSON Specification:

Trigger: Schedule Trigger — CRON: */5 * * * * (every 5 minutes)

Node 1: Check for New Emails

  • Type: Microsoft Graph (or IMAP)
  • Operation: Get Messages
  • Mailbox: insurance@clientdomain.com
  • Filter: isRead eq false AND hasAttachments eq true
  • Top: 10
  • Select: id,subject,from,receivedDateTime,hasAttachments

Node 2: Get Attachments

  • Type: Microsoft Graph
  • Operation: Get Attachments
  • Message ID: {{$json.id}}
  • Filter: contentType eq 'application/pdf' OR name ends with '.pdf' OR name ends with '.jpg' OR name ends with '.png'

Node 3: Identify Subcontractor from Email

Type: Code (JavaScript)

Node 3: Identify Subcontractor from Email
javascript
// Match sender email to subcontractor database
const senderEmail = $('Check for New Emails').item.json.from.emailAddress.address.toLowerCase();
const senderName = $('Check for New Emails').item.json.from.emailAddress.name;
const subject = $('Check for New Emails').item.json.subject;

// Attempt to match sender to known subcontractor
// This will be enriched by a lookup against the sub database
return [{
  json: {
    sender_email: senderEmail,
    sender_name: senderName,
    subject: subject,
    received_date: $('Check for New Emails').item.json.receivedDateTime,
    attachment_name: $json.name,
    attachment_content: $json.contentBytes, // base64 encoded
    attachment_type: $json.contentType,
    message_id: $('Check for New Emails').item.json.id
  }
}];

Node 4: Lookup Subcontractor

  • Type: HTTP Request (or Google Sheets Lookup)
  • Lookup sender_email in subcontractor database to get sub_id, company_name, requirement_template
  • If no match found: flag for manual review and continue processing

Node 5: Route to OCR Pipeline

  • Type: Execute Workflow
  • Call: 'Document OCR & AI Verification Pipeline' workflow
  • Pass: attachment_content (base64), attachment_type, sub_id, company_name, requirement_template

Node 6: Mark Email as Read

  • Type: Microsoft Graph
  • Operation: Update Message
  • Set: isRead = true
  • Add category: 'Processed by COI Agent'

Node 7: Error Handler

  • Type: Error Trigger
  • On error: Send Slack/Teams notification to MSP channel with error details
  • Save failed item to error queue for manual retry

Document OCR & AI Verification Pipeline

Type: agent The core AI agent that processes COI documents. It sends the document to Google Document AI for OCR extraction, then uses GPT-5.4 mini to parse the structured data, verify it against the subcontractor's requirement template, check for additional insured endorsements and waiver of subrogation, and make an approve/escalate decision. This is the autonomous decision-making component.

Implementation

Node 1: Convert to Binary

Type: Code Input: Receives attachment_content (base64 PDF/image), sub_id, company_name, requirement_template from the Email Monitor workflow

Node 1: Convert to Binary
javascript
const binaryData = Buffer.from($json.attachment_content, 'base64');
return [{ json: $json, binary: { data: { data: $json.attachment_content, mimeType: $json.attachment_type || 'application/pdf', fileName: 'coi.pdf' } } }];

Node 2: Google Document AI OCR

  • Type: HTTP Request
  • Method: POST
  • URL: https://us-documentai.googleapis.com/v1/projects/<project_id>/locations/us/processors/<processor_id>:process
  • Authentication: Google Service Account
Node 2: Google Document AI OCR — Request Body
json
{
  "rawDocument": {
    "content": "{{$json.attachment_content}}",
    "mimeType": "{{$json.attachment_type}}"
  }
}

Node 3: Extract OCR Text

Type: Code (JavaScript)

Node 3: Extract OCR Text
javascript
const response = $json;
const fullText = response.document?.text || '';
const pages = response.document?.pages || [];

// Extract form fields if available
const formFields = [];
for (const page of pages) {
  if (page.formFields) {
    for (const field of page.formFields) {
      formFields.push({
        name: field.fieldName?.textAnchor?.content?.trim() || '',
        value: field.fieldValue?.textAnchor?.content?.trim() || ''
      });
    }
  }
}

return [{
  json: {
    ...$input.first().json,
    ocr_full_text: fullText,
    ocr_form_fields: formFields,
    ocr_confidence: pages[0]?.confidence || 0
  }
}];

Node 4: GPT-5.4 mini Certificate Analysis

  • Type: OpenAI (Chat Completion)
  • Model: gpt-5.4-mini
  • Temperature: 0.1
  • Max Tokens: 2000
Node 4: GPT-5.4 mini — System Prompt
text
You are an expert insurance certificate analyst for a construction general contractor. You analyze Certificates of Insurance (COI) — typically ACORD 25 or ACORD 28 forms — to extract structured data and verify compliance.

You must extract the following fields and return them as valid JSON:
{
  "certificate_holder": "Name of the entity the certificate is issued to",
  "insured_name": "Name of the insured company (the subcontractor)",
  "insured_address": "Address of the insured",
  "insurance_broker": "Name and contact of the issuing broker/agent",
  "policies": [
    {
      "type": "General Liability | Workers Compensation | Auto Liability | Umbrella/Excess | Professional Liability",
      "carrier": "Insurance company name",
      "policy_number": "Policy number",
      "effective_date": "YYYY-MM-DD",
      "expiration_date": "YYYY-MM-DD",
      "limits": {
        "each_occurrence": "dollar amount or null",
        "general_aggregate": "dollar amount or null",
        "products_completed_ops": "dollar amount or null",
        "personal_advertising": "dollar amount or null",
        "damage_to_rented_premises": "dollar amount or null",
        "medical_expense": "dollar amount or null",
        "each_accident_wc": "dollar amount or null",
        "disease_each_employee": "dollar amount or null",
        "disease_policy_limit": "dollar amount or null",
        "combined_single_limit": "dollar amount or null",
        "bodily_injury_per_person": "dollar amount or null",
        "bodily_injury_per_accident": "dollar amount or null",
        "property_damage": "dollar amount or null",
        "umbrella_each_occurrence": "dollar amount or null",
        "umbrella_aggregate": "dollar amount or null"
      }
    }
  ],
  "additional_insured": true/false,
  "additional_insured_name": "Name if specified, or null",
  "waiver_of_subrogation": true/false,
  "primary_noncontributory": true/false,
  "certificate_date": "YYYY-MM-DD",
  "description_of_operations": "Text from that field",
  "parsing_confidence": "high | medium | low",
  "parsing_notes": "Any issues or uncertainties in parsing"
}

If any field cannot be determined, set it to null and note it in parsing_notes.
Always return valid JSON only, no additional text.
Node 4: GPT-5.4 mini — User Prompt
text
Analyze this Certificate of Insurance document.

OCR Extracted Text:
{{$json.ocr_full_text}}

Extracted Form Fields:
{{JSON.stringify($json.ocr_form_fields)}}

The subcontractor is: {{$json.company_name}}
The certificate holder should be: <Client Company Legal Name>

Extract all data and return as JSON.

Node 5: Parse GPT Response

Type: Code (JavaScript)

Node 5: Parse GPT Response
javascript
let parsed;
try {
  const content = $json.message?.content || $json.choices?.[0]?.message?.content || '';
  // Remove potential markdown code fences
  const cleaned = content.replace(/```[a-z]*\n?/gi, '').trim();
  parsed = JSON.parse(cleaned);
} catch (e) {
  parsed = { error: 'Failed to parse GPT response', raw: $json.message?.content };
}

return [{ json: { ...($input.first().json), certificate_data: parsed } }];

Node 6: Compliance Verification

Type: Code (JavaScript)

Node 6: Compliance Verification
javascript
const cert = $json.certificate_data;
const template = $json.requirement_template; // 'High-Risk', 'Standard', 'Low-Risk'

// Define requirement thresholds
const requirements = {
  'High-Risk': {
    gl_each_occurrence: 2000000,
    gl_aggregate: 4000000,
    wc_each_accident: 1000000,
    auto_combined_single: 1000000,
    umbrella_each: 5000000,
    require_additional_insured: true,
    require_waiver_of_subrogation: true
  },
  'Standard': {
    gl_each_occurrence: 1000000,
    gl_aggregate: 2000000,
    wc_each_accident: 500000,
    auto_combined_single: 1000000,
    umbrella_each: 2000000,
    require_additional_insured: true,
    require_waiver_of_subrogation: true
  },
  'Low-Risk': {
    gl_each_occurrence: 1000000,
    gl_aggregate: 2000000,
    wc_each_accident: 500000,
    auto_combined_single: 1000000,
    umbrella_each: 0,
    require_additional_insured: true,
    require_waiver_of_subrogation: false
  }
};

const req = requirements[template] || requirements['Standard'];
const issues = [];
let compliant = true;

if (cert.error) {
  return [{ json: { ...$json, compliant: false, issues: ['Certificate could not be parsed'], decision: 'escalate', confidence: 'low' } }];
}

// Check each policy
const parseDollar = (v) => {
  if (!v) return 0;
  return parseInt(String(v).replace(/[^0-9]/g, '')) || 0;
};

// GL Check
const glPolicy = (cert.policies || []).find(p => p.type?.toLowerCase().includes('general liability'));
if (!glPolicy) {
  issues.push('Missing General Liability policy');
  compliant = false;
} else {
  if (parseDollar(glPolicy.limits?.each_occurrence) < req.gl_each_occurrence) {
    issues.push(`GL Each Occurrence ($${parseDollar(glPolicy.limits?.each_occurrence).toLocaleString()}) below required $${req.gl_each_occurrence.toLocaleString()}`);
    compliant = false;
  }
  const expiry = new Date(glPolicy.expiration_date);
  if (expiry <= new Date()) {
    issues.push(`GL policy expired on ${glPolicy.expiration_date}`);
    compliant = false;
  }
}

// WC Check
const wcPolicy = (cert.policies || []).find(p => p.type?.toLowerCase().includes('workers'));
if (!wcPolicy) {
  issues.push('Missing Workers Compensation policy');
  compliant = false;
} else {
  const wcExpiry = new Date(wcPolicy.expiration_date);
  if (wcExpiry <= new Date()) {
    issues.push(`WC policy expired on ${wcPolicy.expiration_date}`);
    compliant = false;
  }
}

// Auto Check
const autoPolicy = (cert.policies || []).find(p => p.type?.toLowerCase().includes('auto'));
if (!autoPolicy) {
  issues.push('Missing Auto Liability policy');
  compliant = false;
} else {
  if (parseDollar(autoPolicy.limits?.combined_single_limit) < req.auto_combined_single) {
    issues.push(`Auto Combined Single Limit below required $${req.auto_combined_single.toLocaleString()}`);
    compliant = false;
  }
}

// Umbrella Check (if required)
if (req.umbrella_each > 0) {
  const umbrellaPolicy = (cert.policies || []).find(p => p.type?.toLowerCase().includes('umbrella') || p.type?.toLowerCase().includes('excess'));
  if (!umbrellaPolicy) {
    issues.push('Missing Umbrella/Excess policy');
    compliant = false;
  } else {
    if (parseDollar(umbrellaPolicy.limits?.umbrella_each_occurrence) < req.umbrella_each) {
      issues.push(`Umbrella Each Occurrence below required $${req.umbrella_each.toLocaleString()}`);
      compliant = false;
    }
  }
}

// Additional Insured Check
if (req.require_additional_insured && !cert.additional_insured) {
  issues.push('Additional Insured endorsement not found');
  compliant = false;
}

// Waiver of Subrogation Check
if (req.require_waiver_of_subrogation && !cert.waiver_of_subrogation) {
  issues.push('Waiver of Subrogation not found');
  compliant = false;
}

// Certificate holder check
const clientName = '<Client Company Legal Name>'.toLowerCase();
if (cert.certificate_holder && !cert.certificate_holder.toLowerCase().includes(clientName)) {
  issues.push(`Certificate holder (${cert.certificate_holder}) does not match client company name`);
  compliant = false;
}

const confidence = cert.parsing_confidence || 'medium';
const decision = compliant ? (confidence === 'high' ? 'auto_approve' : 'review_recommended') : 'escalate';

return [{
  json: {
    ...$json,
    compliant: compliant,
    issues: issues,
    decision: decision,
    confidence: confidence,
    verification_timestamp: new Date().toISOString()
  }
}];

Node 7: Decision Router

  • Type: Switch
  • decision == 'auto_approve' → Auto-Approve Branch
  • decision == 'review_recommended' → Soft Escalation Branch
  • decision == 'escalate' → Hard Escalation Branch

Node 8a: Auto-Approve Branch

  • Update TrustLayer/bcs: Mark certificate as approved, update expiration dates
  • Store PDF in SharePoint/Document archive
  • Send confirmation email to subcontractor
  • Log approval in compliance dashboard
  • Send email to client risk manager with: parsed certificate data, confidence note, request for human verification
  • Flag in dashboard as 'Pending Review'

Node 8c: Hard Escalation (Non-Compliant)

  • Send detailed non-compliance email to subcontractor listing specific deficiencies
  • Notify client risk manager and project manager
  • Flag in dashboard as 'Non-Compliant'
  • If expired: trigger payment hold advisory notification

Escalation & Compliance Reporting Engine

Type: workflow Generates weekly compliance summary reports and handles escalation events that require human intervention. Produces a formatted HTML email report sent every Monday morning showing overall compliance status, at-risk subcontractors, and agent activity metrics.

Implementation

n8n Workflow JSON Specification:

Trigger: Schedule Trigger — CRON: 0 8 * * 1 (Mondays at 8 AM)

1
Node 1: Fetch All Subcontractor Compliance Status — Type: HTTP Request or Google Sheets. Get: All subcontractors with current compliance status, expiration dates, notification history.
2
Node 2: Calculate Compliance Metrics — Type: Code (JavaScript)
3
Node 3: Generate HTML Report — Type: Code (JavaScript)
4
Node 4: Send Report Email — Type: SendGrid. To: cfo@clientdomain.com, riskmanager@clientdomain.com. CC: msp-account-manager@mspdomain.com. Subject: Weekly Insurance Compliance Report — {{$json.compliance_rate}}% Compliant. Content Type: text/html. Body: {{$json.html_report}}
Node 2: Calculate Compliance Metrics
javascript
const subs = $input.all().map(i => i.json);
const today = new Date();

const metrics = {
  total_subs: subs.length,
  fully_compliant: 0,
  expiring_60: 0,
  expiring_30: 0,
  expiring_14: 0,
  expiring_7: 0,
  expired: 0,
  missing_coi: 0,
  compliance_rate: 0,
  at_risk_subs: [],
  expired_subs: []
};

for (const sub of subs) {
  const policies = ['gl_expiry', 'wc_expiry', 'auto_expiry', 'umbrella_expiry'];
  let subCompliant = true;
  let nearestExpiry = Infinity;
  
  for (const p of policies) {
    if (!sub[p]) { subCompliant = false; continue; }
    const exp = new Date(sub[p]);
    const days = Math.ceil((exp - today) / (1000*60*60*24));
    if (days < nearestExpiry) nearestExpiry = days;
    if (days <= 0) { metrics.expired++; subCompliant = false; }
    else if (days <= 7) metrics.expiring_7++;
    else if (days <= 14) metrics.expiring_14++;
    else if (days <= 30) metrics.expiring_30++;
    else if (days <= 60) metrics.expiring_60++;
  }
  
  if (subCompliant) metrics.fully_compliant++;
  if (nearestExpiry <= 14) {
    metrics.at_risk_subs.push({ name: sub.company_name, days: nearestExpiry, email: sub.contact_email });
  }
  if (nearestExpiry <= 0) {
    metrics.expired_subs.push({ name: sub.company_name, days: nearestExpiry, email: sub.contact_email });
  }
}

metrics.compliance_rate = ((metrics.fully_compliant / metrics.total_subs) * 100).toFixed(1);
metrics.at_risk_subs.sort((a,b) => a.days - b.days);

return [{ json: metrics }];
Node 3: Generate HTML Report
javascript
const m = $json;
const riskColor = m.compliance_rate >= 90 ? '#28a745' : m.compliance_rate >= 75 ? '#ffc107' : '#dc3545';

const atRiskRows = m.at_risk_subs.map(s => 
  `<tr><td>${s.name}</td><td style="color: ${s.days <= 0 ? 'red' : s.days <= 7 ? 'orange' : '#333'}; font-weight: bold;">${s.days <= 0 ? 'EXPIRED' : s.days + ' days'}</td><td>${s.email}</td></tr>`
).join('');

const html = `
<div style="font-family: Arial, sans-serif; max-width: 700px; margin: 0 auto;">
  <h1 style="color: #333;">Weekly Insurance Compliance Report</h1>
  <p style="color: #666;">Generated: ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</p>
  
  <div style="background: ${riskColor}; color: white; padding: 20px; border-radius: 8px; text-align: center; margin: 20px 0;">
    <h2 style="margin: 0; font-size: 48px;">${m.compliance_rate}%</h2>
    <p style="margin: 5px 0 0 0;">Overall Compliance Rate (${m.fully_compliant} of ${m.total_subs} subcontractors)</p>
  </div>
  
  <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
    <tr><td style="padding: 8px; border-bottom: 1px solid #eee;">🔴 Expired Certificates</td><td style="font-weight: bold; color: red;">${m.expired}</td></tr>
    <tr><td style="padding: 8px; border-bottom: 1px solid #eee;">🟠 Expiring within 7 days</td><td style="font-weight: bold; color: orange;">${m.expiring_7}</td></tr>
    <tr><td style="padding: 8px; border-bottom: 1px solid #eee;">🟡 Expiring within 14 days</td><td style="font-weight: bold;">${m.expiring_14}</td></tr>
    <tr><td style="padding: 8px; border-bottom: 1px solid #eee;">🔵 Expiring within 30 days</td><td style="font-weight: bold;">${m.expiring_30}</td></tr>
    <tr><td style="padding: 8px; border-bottom: 1px solid #eee;">⚪ Expiring within 60 days</td><td>${m.expiring_60}</td></tr>
  </table>
  
  ${m.at_risk_subs.length > 0 ? `
  <h3>⚠️ At-Risk Subcontractors (14 days or less)</h3>
  <table style="width: 100%; border-collapse: collapse;">
    <thead><tr style="background: #f8f9fa;"><th style="padding: 8px; text-align: left;">Company</th><th style="padding: 8px; text-align: left;">Status</th><th style="padding: 8px; text-align: left;">Contact</th></tr></thead>
    <tbody>${atRiskRows}</tbody>
  </table>` : '<p style="color: green;">✅ No subcontractors at immediate risk.</p>'}
  
  <p style="color: #999; font-size: 12px; margin-top: 30px;">This report was generated automatically by your COI Compliance Agent. For questions, contact your MSP.</p>
</div>`;

return [{ json: { ...m, html_report: html } }];

Certificate Verification System Prompt

Type: prompt The core system prompt used by GPT-5.4 mini to analyze OCR-extracted certificate text and return structured data. This prompt is carefully engineered to handle ACORD 25 (Liability), ACORD 28 (Evidence of Property), and non-standard certificate formats commonly seen from small insurance brokers in construction.

Implementation:

System Prompt (store in n8n as a static text variable)
text
You are an expert insurance certificate analyst employed by a construction general contractor's risk management department. Your job is to analyze Certificates of Insurance (COI) and extract structured data with high accuracy.

CRITICAL CONTEXT:
- You are analyzing certificates submitted by subcontractors to a general contractor
- The standard form is ACORD 25 (Certificate of Liability Insurance) or ACORD 28 (Evidence of Commercial Property Insurance)
- Construction industry certificates MUST include: General Liability, Workers' Compensation, Commercial Auto, and often Umbrella/Excess Liability
- Key endorsements to check: Additional Insured status, Waiver of Subrogation, Primary & Non-Contributory
- Dates are in MM/DD/YYYY format on ACORD forms
- Dollar amounts may appear as "1,000,000" or "1000000" or "$1,000,000" or "1,000" (meaning $1,000 not $1,000,000)
- Workers' Comp limits are typically: "Each Accident", "Disease - Each Employee", "Disease - Policy Limit" — each is usually $500,000 or $1,000,000
- "Statutory" in Workers' Comp means the state-mandated minimum — record as "statutory"

INSTRUCTIONS:
1. Extract ALL policy information visible on the certificate
2. For each policy, extract: type, carrier name, policy number, effective date, expiration date, and all listed limits
3. Check the "Description of Operations" field for mentions of:
   - Additional Insured endorsement
   - Waiver of Subrogation endorsement  
   - Primary and Non-Contributory language
   - Specific project references
4. Identify the Certificate Holder (who the cert is issued TO)
5. Identify the Named Insured (the subcontractor company)
6. Rate your parsing confidence:
   - "high": Clear ACORD form, all fields legible, standard format
   - "medium": Most fields readable but some ambiguity (poor scan, handwriting)
   - "low": Significant portions unreadable or non-standard format
7. Note any parsing uncertainties in parsing_notes

COMMON PITFALLS TO AVOID:
- Do not confuse "Each Occurrence" limit with "General Aggregate" limit
- Do not confuse the broker/producer with the insurance carrier
- "Products-Completed Operations Aggregate" is separate from "General Aggregate"
- Auto liability may show separate Bodily Injury and Property Damage limits OR a Combined Single Limit
- If the certificate says "10 Day Notice" or "30 Day Notice" for cancellation, note this
- Certificates with future effective dates are valid but should be flagged

RETURN FORMAT:
Return ONLY valid JSON matching this exact schema (no markdown, no explanation):
{
  "certificate_holder": "string or null",
  "insured_name": "string or null",
  "insured_address": "string or null",
  "insurance_broker": "string or null",
  "certificate_number": "string or null",
  "certificate_date": "YYYY-MM-DD or null",
  "policies": [
    {
      "type": "General Liability | Workers Compensation | Auto Liability | Umbrella/Excess | Professional Liability | Other",
      "carrier": "string",
      "naic_number": "string or null",
      "policy_number": "string",
      "effective_date": "YYYY-MM-DD",
      "expiration_date": "YYYY-MM-DD",
      "limits": {
        "each_occurrence": "dollar amount string or null",
        "general_aggregate": "dollar amount string or null",
        "products_completed_ops": "dollar amount string or null",
        "personal_advertising": "dollar amount string or null",
        "damage_to_rented_premises": "dollar amount string or null",
        "medical_expense": "dollar amount string or null",
        "each_accident_wc": "dollar amount string or null",
        "disease_each_employee": "dollar amount string or null",
        "disease_policy_limit": "dollar amount string or null",
        "combined_single_limit": "dollar amount string or null",
        "bodily_injury_per_person": "dollar amount string or null",
        "bodily_injury_per_accident": "dollar amount string or null",
        "property_damage": "dollar amount string or null",
        "umbrella_each_occurrence": "dollar amount string or null",
        "umbrella_aggregate": "dollar amount string or null",
        "retention_deductible": "dollar amount string or null"
      },
      "subrogation_waived": true/false/null,
      "additional_notes": "string or null"
    }
  ],
  "additional_insured": true/false,
  "additional_insured_details": "string describing who is listed or null",
  "waiver_of_subrogation": true/false,
  "primary_noncontributory": true/false,
  "description_of_operations": "string or null",
  "cancellation_notice_days": "number or null",
  "parsing_confidence": "high | medium | low",
  "parsing_notes": "string describing any issues or null"
}

Procore Subcontractor Sync Integration

Type: integration Bidirectional sync between Procore's company directory and the COI tracking system. Runs nightly to detect new subcontractors added to Procore, update project assignments, and push compliance status back to Procore for visibility by project managers in the field.

Implementation

Trigger: Schedule Trigger — CRON: 0 1 * * * (daily at 1 AM)

Node 1: Fetch Procore Vendors

  • Type: HTTP Request
  • Method: GET
Fetch active vendors from Procore company directory
http
GET https://api.procore.com/rest/v1.0/companies/{{$vars.procore_company_id}}/vendors
Authorization: Bearer {{$credentials.procoreOAuth2.accessToken}}
Query Parameters: per_page=500&filters[is_active]=true

Node 2: Fetch Current Sub Database

Type: HTTP Request (TrustLayer API) or Google Sheets — Get all current subcontractors in the COI system.

Node 3: Identify New/Updated Subs

  • Type: Code (JavaScript)
Compare Procore vendor list against existing COI system subs to identify new entries
javascript
const procoreSubs = $('Fetch Procore Vendors').all().map(i => i.json);
const existingSubs = $('Fetch Current Sub Database').all().map(i => i.json);
const existingEmails = new Set(existingSubs.map(s => (s.contact_email || '').toLowerCase()));

const newSubs = [];
const updatedSubs = [];

for (const ps of procoreSubs) {
  const email = (ps.email_address || '').toLowerCase();
  if (!email) continue;
  
  if (!existingEmails.has(email)) {
    newSubs.push({
      company_name: ps.name,
      contact_email: email,
      contact_phone: ps.phone || '',
      trade: ps.trade || 'General',
      procore_id: ps.id,
      address: [ps.address, ps.city, ps.state_code, ps.zip].filter(Boolean).join(', '),
      requirement_template: 'Standard' // Default; admin can adjust
    });
  }
}

return [
  { json: { new_subs: newSubs, new_count: newSubs.length, total_procore: procoreSubs.length, total_existing: existingSubs.length } }
];

Node 4: Add New Subs to COI System

  • Type: SplitInBatches (process new subs one at a time)
  • HTTP Request POST to TrustLayer/bcs API to create vendor, OR: Google Sheets append row
  • Send welcome email to sub explaining COI requirements and upload portal link

Node 5: Push Compliance Status to Procore

  • Type: HTTP Request (for each sub with a procore_id)
  • Method: PATCH
Push COI compliance status back to Procore vendor record
http
PATCH https://api.procore.com/rest/v1.0/companies/{{$vars.procore_company_id}}/vendors/{{procore_id}}
Body: Update custom fields with compliance status (requires Procore custom fields to be configured)
Note

Procore custom field setup must be done manually in Procore Admin > Company Settings > Custom Fields > Vendor. Create fields: 'COI Status' (dropdown: Compliant/Non-Compliant/Expiring Soon/Expired), 'COI Expiry Date' (date), 'Last COI Update' (date).

Node 6: Notification for New Subs

Type: SendGrid — If new subs were found, email the client's admin with a list of newly detected subcontractors that need requirement template assignments.

Testing & Validation

  • SMOKE TEST — Platform Access: Verify the n8n instance is accessible at https://n8n.<clientdomain>.com with valid SSL certificate. Log in with admin credentials and confirm all four workflows appear in the workflow list.
  • SMOKE TEST — COI Platform: Log into TrustLayer/bcs and verify the subcontractor list imported correctly. Spot-check 10 random subcontractors to confirm names, emails, and trade classifications match the source data.
  • CREDENTIAL TEST: In n8n, go to Settings > Credentials and test each credential: OpenAI API (send a test completion), Google Document AI (process a sample document), SendGrid (send a test email to MSP inbox), Microsoft Graph (list mailbox messages), Procore API (list vendors).
  • UNIT TEST — OCR Pipeline: Download 5 sample ACORD 25 certificates with varying quality (clean PDF, scanned image, handwritten entries, multi-page, non-standard format). Process each through the Document OCR & AI Verification Pipeline manually. Verify extracted data matches the actual certificate content with 90%+ field accuracy.
  • UNIT TEST — Compliance Check Logic: Create 4 test certificates with known characteristics: (1) fully compliant High-Risk sub, (2) compliant Standard sub missing umbrella (should pass Standard template), (3) sub with GL limits below minimum (should fail), (4) sub with expired WC policy (should fail). Verify the Compliance Verification code produces correct pass/fail for each.
  • INTEGRATION TEST — Email Delivery: Trigger a test notification for each of the 6 email tiers (initial, second, urgent, final, expired, compliance hold). Verify emails arrive in the test recipient's inbox (not spam), display correctly, contain accurate dynamic data, and include the correct upload portal link.
  • INTEGRATION TEST — Full Cycle: Have an MSP team member send a test COI PDF to insurance@clientdomain.com from an external email address registered as a test subcontractor. Verify the complete pipeline: email detected within 5 minutes → OCR processed → GPT analysis completed → compliance decision rendered → status updated in COI platform → confirmation/escalation email sent.
  • INTEGRATION TEST — Procore Sync: Add a new test vendor in Procore. Run the Procore sync workflow manually. Verify the new vendor appears in the COI tracking system within one execution cycle with correct name, email, and default requirement template.
  • ESCALATION TEST: Create a test subcontractor with a certificate expiring tomorrow. Run the Expiration Monitor workflow. Verify: escalation email sent to sub (final_warning tier), CC'd to project manager and CFO, compliance dashboard shows the sub as at-risk.
  • LOAD TEST: If the client has 200+ subcontractors, run the Expiration Monitor during off-hours and verify it completes within 15 minutes without errors. Check n8n execution logs for any timeout or rate-limit issues with SendGrid or OpenAI APIs.
  • REPORT TEST: Manually trigger the Weekly Compliance Report workflow. Verify the HTML email renders correctly in Outlook and Gmail, contains accurate metrics matching the dashboard, and the compliance percentage calculation is correct.
  • SECURITY TEST: Attempt to access n8n from an IP address not in the allowed NSG list (should be blocked). Verify all API keys are stored in n8n credential store (not hardcoded in workflows). Confirm the shared mailbox requires proper OAuth2 authentication.
  • BACKUP/RECOVERY TEST: Stop the n8n Docker container, restore from the most recent database backup, restart, and verify all workflows and credentials are intact and functional.

Client Handoff

Conduct a 90-minute on-site or video handoff session with the client's designated compliance administrator, CFO/owner, and project managers. Cover the following topics:

1. Dashboard Overview (20 min)

  • Walk through the TrustLayer/bcs dashboard showing overall compliance status
  • How to view individual subcontractor details
  • How to see which subs have been notified and when
  • How to access stored COI documents
  • Show the weekly compliance report email and explain each metric

2. Day-to-Day Operations (20 min)

Explain the system runs autonomously — the admin's job is to review escalations, not chase renewals. Demonstrate how to:

  • Review a flagged non-compliant certificate and take action
  • Override an auto-decision (approve a certificate the AI flagged, or reject one it approved)
  • Add a new subcontractor manually
  • Change a sub's requirement template between High-Risk/Standard/Low-Risk
  • Temporarily pause notifications for a specific sub

3. What Requires Human Action (15 min)

Explain the three scenarios that need human intervention:

1
A certificate the AI couldn't parse with high confidence (rare)
2
A subcontractor who hasn't responded after all automated escalation tiers (the system has done its job; now the PM/CFO must decide whether to issue a stop-work order)
3
Adding or modifying insurance requirement templates for new project types

4. Procore Integration (10 min)

  • Show how new subs added in Procore automatically appear in the COI system
  • Demonstrate the compliance status fields in Procore that project managers can see in the field

5. Emergency Procedures (10 min)

Provide the MSP support contact for system issues. Explain what to do if:

  • The system stops sending emails (check n8n status, contact MSP)
  • A subcontractor disputes the AI's compliance determination (admin can manually review and override)
  • A certificate needs urgent processing outside the 5-minute polling cycle (forward to insurance@ mailbox and it will be picked up within 5 minutes)

6. Documentation Handoff (15 min)

Provide the following printed/digital documents:

  • Quick Reference Card (1-page laminated card with dashboard URL, login, key contacts, escalation timeline)
  • System Architecture Diagram (showing data flow from email receipt to compliance decision)
  • Insurance Requirement Templates (printable summary of GL/WC/Auto/Umbrella minimums for each risk tier)
  • Admin Guide (step-by-step instructions for common tasks with screenshots)
  • MSP Support SLA (response times, contact methods, escalation path)

Success Criteria to Review Together

Confirm the client can independently perform each of the following, then document sign-off from the client stakeholder confirming acceptance.

Maintenance

Weekly MSP Tasks (15-30 minutes)

  • Review n8n execution logs for failed workflow runs or API errors
  • Check SendGrid delivery reports for bounced emails (update sub contact info as needed)
  • Review OpenAI API usage and costs against budget ($5-25/month target)
  • Verify Azure VM health metrics (CPU, memory, disk) via Azure Monitor
  • Spot-check 2-3 recent AI verification decisions for accuracy

Monthly MSP Tasks (1-2 hours)

  • Review and reconcile the subcontractor list against Procore/client records
  • Update n8n Docker image: docker compose pull && docker compose up -d
  • Review compliance metrics with client stakeholder (15-min call)
  • Check for TrustLayer/bcs platform updates and release notes
  • Verify backup integrity: restore test to a staging environment quarterly
  • Review and rotate API keys per security policy (quarterly minimum)
Update n8n Docker image (monthly)
bash
docker compose pull && docker compose up -d

Quarterly MSP Tasks (2-4 hours)

  • Full compliance audit: compare AI decisions against manual spot-check of 10% of certificates
  • Review and update insurance requirement templates with client (regulations and contract terms change)
  • Performance review: agent processing times, accuracy rates, false positive/negative rates
  • Update email templates if client branding or contact information has changed
  • Review Azure VM sizing and costs — downsize or upsize as needed
  • Update system prompt if new certificate formats or edge cases have been identified

Trigger-Based Maintenance

  • OpenAI model updates: When OpenAI deprecates gpt-5.4-mini, test the replacement model against 20 sample certificates before switching. Update the model parameter in n8n.
  • n8n major version upgrades: Test in a staging Docker environment before updating production. Back up the database before any upgrade.
  • State regulatory changes: When state insurance requirements change (e.g., new WC minimums), update the compliance verification thresholds in the n8n Code node.
  • Client expansion: When client exceeds 50 subs on TrustLayer free tier, migrate to paid plan. When exceeding 200 subs, evaluate switching to bcs for per-vendor pricing efficiency.

SLA Considerations

  • System availability target: 99.5% uptime (allows ~44 hours/year downtime)
  • Email processing SLA: Incoming COIs processed within 15 minutes of receipt during business hours
  • Escalation response: MSP responds to system alerts within 4 business hours; critical issues (system down) within 1 hour
  • Monthly reporting: Compliance summary delivered to client by 2nd business day of each month

Escalation Path

1
Automated n8n error → Slack/Teams alert to MSP NOC channel
2
MSP L1 technician reviews within 4 hours → resolves or escalates
3
MSP L2 (implementation engineer) resolves within 1 business day
4
MSP escalates to platform vendor support (TrustLayer/bcs/n8n) if platform issue
5
Client notified of any system outage exceeding 4 hours with ETA for resolution

Alternatives

Pure SaaS Platform (No Custom Agent)

Deploy only TrustLayer, bcs, or myCOI/illumend as a turnkey SaaS solution without the custom n8n agent layer. The platform handles all monitoring, notifications, OCR, and compliance tracking natively. The MSP's role is limited to initial setup, configuration, data import, and ongoing account management.

Note

WHEN TO RECOMMEND: Clients with fewer than 100 subcontractors, budget-conscious clients, or when the MSP lacks n8n/automation expertise. Also appropriate as a Phase 1 deployment with the custom agent layer added later as Phase 2.

Microsoft Power Automate + Azure AI Document Intelligence

Build the automation layer using Microsoft Power Automate (included in many M365 plans) instead of n8n, paired with Azure AI Document Intelligence (formerly Form Recognizer) instead of Google Document AI. Uses the client's existing Microsoft ecosystem entirely.

Constrafor All-in-One Platform

Deploy Constrafor as a single platform that combines COI tracking with subcontractor prequalification, contract management, payment processing, and diversity tracking. Constrafor includes a built-in ChatGPT-powered AI risk assistant. This replaces both the COI platform and much of the custom agent layer.

CertFocus (Vertikal RMS) Value Approach

Deploy CertFocus as the COI tracking platform, leveraging their Hawk-I AI technology for automated certificate processing. CertFocus offers the lowest per-vendor pricing in the market at $6-$8/vendor/year for self-service or $13-$29/vendor/year for full-service (CertFocus handles the sub outreach).

Fully Custom Build with Open Source Stack

Build the entire system from scratch using open-source components: n8n for orchestration, Tesseract OCR (open source) instead of Google Document AI, a local LLM (Llama 3 or Mistral via Ollama) instead of OpenAI, PostgreSQL for the subcontractor database, and a custom web dashboard using Grafana or Metabase. No SaaS COI platform at all.

Want early access to the full toolkit?