Construction SaaS companies operate at the intersection of the most consequential safety regulations in U.S. law and some of the most complex payment compliance requirements across any industry. The OSHA §1904.39 fatality clock runs 8 hours — not 8 business hours, 8 actual hours — from the moment a worker dies on a project tracked in your platform. Davis-Bacon certified payroll reports are due weekly, and a single missed week triggers debarment proceedings against your customer's federal contract. The EPA SWPPP stormwater permit's Notice of Termination must be filed within 30 days of final stabilization — a project closeout event your SaaS almost certainly logs.
This post covers 5 n8n workflow templates built for ConstructionTech SaaS vendors — the companies building the platforms, not just the GCs using them. All workflows include import-ready JSON you can drop into any n8n instance.
Why Self-Hosted n8n for ConTech SaaS
When your platform routes safety incident reports, certified payroll data, worker PII, subcontractor financials, and lien waiver records through a cloud iPaaS like Zapier or Make, you inherit a third-party data processor liability chain under CCPA CPRA (worker geolocation and biometrics from wearable safety sensors = sensitive PI), state retainage audit requirements, and Davis-Bacon payroll record retention (29 CFR §5.5(a)(3)(ii)(A): 3 years after project completion). A cloud automation vendor in your compliance workflow is a SOC 2 CC9.2 vendor risk finding. Self-hosted n8n keeps incident reports, payroll records, and lien documentation in your own VPC — git-versioned workflows are your audit trail for OSHA inspections and Davis-Bacon wage investigations.
Workflow 1: New ConTech Platform Customer Onboarding Drip
Trigger: Webhook — new customer created event
What it does:
Segments incoming customers by tier and injects compliance flags into every email — Day 0 welcome, Day 3 OSHA/safety walkthrough, Day 7 integration check-in.
Customer Tiers:
-
LARGE_GENERAL_CONTRACTOR_SAAS— OSHA 29 CFR §1904.39 fatality/hospitalization reporting, Davis-Bacon Act 40 USC §3141 certified payroll, Miller Act 40 USC §3131 federal bonding >$150K, EPA SWPPP 40 CFR §122.26, OSHA 29 CFR §1926.1153 silica PEL 50 μg/m³ -
PROJECT_MANAGEMENT_SAAS— OSHA §1904 recordkeeping, RFI/submittal compliance timelines, LEED/BREEAM documentation windows, AIA G702/G703 payment application cycle -
BIM_COLLABORATION_SAAS— OSHA §1926.502 fall protection digital twin integration, BIM mandate compliance (UK BIM Level 2, US NIBS standards), COBie data handover, ISO 19650 -
CONSTRUCTION_FINANCE_SAAS— AIA G702/G703 payment application 21-day cycle, state retainage caps (typically 5-10%, release at 50% completion or substantial completion), Miller Act payment bond claims, state prompt payment acts -
FIELD_OPERATIONS_SAAS— OSHA §1904.39 incident reporting pipeline, daily report audit trail, punch list closeout, OSHA Form 300/300A annual posting Feb 1–Apr 30 -
SUBCONTRACTOR_MANAGEMENT_SAAS— Davis-Bacon certified payroll (WH-347 weekly), prevailing wage rate compliance, insurance certificate tracking, lien waiver management -
CONTECH_STARTUP_SAAS— Full compliance stack, SOC 2 Type II roadmap, state contractor licensing cascade
{
"name": "ConTech - Customer Onboarding Drip",
"nodes": [
{
"id": "1",
"type": "n8n-nodes-base.webhook",
"name": "New Customer Webhook",
"parameters": {
"path": "contech-onboarding",
"httpMethod": "POST"
}
},
{
"id": "2",
"type": "n8n-nodes-base.code",
"name": "Segment & Compliance Flags",
"parameters": {
"jsCode": "const d = $input.first().json; const tier = d.customer_tier || 'CONTECH_STARTUP_SAAS'; const flags = {LARGE_GENERAL_CONTRACTOR_SAAS: 'OSHA \u00a71904.39 fatality 8h/hospitalization 24h/Davis-Bacon WH-347 weekly/Miller Act >$150K/EPA SWPPP NOI/silica PEL 50\u03bcg/m\u00b3', PROJECT_MANAGEMENT_SAAS: 'OSHA \u00a71904 recordkeeping/LEED documentation/AIA G702 payment cycle/RFI compliance timelines', BIM_COLLABORATION_SAAS: 'OSHA \u00a71926.502 fall protection/BIM Level 2/ISO 19650/COBie handover', CONSTRUCTION_FINANCE_SAAS: 'AIA G702/G703 21-day cycle/state retainage caps/Miller Act payment bond/prompt payment acts', FIELD_OPERATIONS_SAAS: 'OSHA \u00a71904.39 incident pipeline/Form 300/300A Feb1-Apr30 posting/daily report audit', SUBCONTRACTOR_MANAGEMENT_SAAS: 'Davis-Bacon WH-347 weekly/prevailing wage/insurance certs/lien waiver management', CONTECH_STARTUP_SAAS: 'OSHA full stack/SOC2 roadmap/state contractor licensing'}; return [{json: {...d, tier, compliance_note: flags[tier] || flags.CONTECH_STARTUP_SAAS, ts: new Date().toISOString()}}];"
}
},
{
"id": "3",
"type": "n8n-nodes-base.gmail",
"name": "Day 0 Welcome Email",
"parameters": {
"to": "={{$json.contact_email}}",
"subject": "Welcome to FlowKit \u2014 OSHA Incident Reporting & Davis-Bacon Compliance Automation",
"message": "Welcome {{$json.company_name}}. Tier: {{$json.tier}}. Key compliance note: {{$json.compliance_note}}. Your self-hosted n8n instance keeps all incident reports, certified payroll records, and worker PII in your own VPC \u2014 git-versioned audit trail for OSHA inspections."
}
},
{
"id": "4",
"type": "n8n-nodes-base.googleSheets",
"name": "Log Onboarding",
"parameters": {
"operation": "append",
"sheetId": "SHEET_ID",
"range": "Onboarding!A:F",
"values": [
[
"={{$json.company_name}}",
"={{$json.tier}}",
"={{$json.contact_email}}",
"day0_sent",
"={{$json.ts}}",
"={{$json.compliance_note}}"
]
]
}
},
{
"id": "5",
"type": "n8n-nodes-base.wait",
"name": "Wait 3 Days",
"parameters": {
"amount": 3,
"unit": "days"
}
},
{
"id": "6",
"type": "n8n-nodes-base.gmail",
"name": "Day 3 OSHA Walkthrough",
"parameters": {
"to": "={{$json.contact_email}}",
"subject": "OSHA \u00a71904.39 Incident Reporting Automation \u2014 The 8-Hour Fatality Clock",
"message": "Day 3 check-in for {{$json.company_name}}. The OSHA \u00a71904.39 clock starts the moment a fatality is logged in your platform \u2014 8 hours to call 1-800-321-OSHA. Our workflow fires the Slack alert and prepares the OSHA notification draft in under 60 seconds. Is your incident webhook endpoint live? Reply if you have integration questions."
}
},
{
"id": "7",
"type": "n8n-nodes-base.wait",
"name": "Wait 4 Days",
"parameters": {
"amount": 4,
"unit": "days"
}
},
{
"id": "8",
"type": "n8n-nodes-base.gmail",
"name": "Day 7 Integration Check-In",
"parameters": {
"to": "={{$json.contact_email}}",
"subject": "Week 1 Check-In \u2014 Davis-Bacon Certified Payroll Monitor & SWPPP Deadline Tracker Ready?",
"message": "One week in, {{$json.company_name}}. Key milestone: is your Davis-Bacon certified payroll monitor running? WH-347 reports are due every week on federal projects \u2014 a missed week triggers debarment proceedings. Book a 30-min setup call if you haven't configured the payroll pipeline yet."
}
}
]
}
Workflow 2: OSHA §1904.39 Safety Incident Reporting Pipeline
Trigger: Webhook — safety incident event
What it does:
The most time-critical workflow in construction SaaS infrastructure. OSHA 29 CFR §1904.39: employers must report fatalities within 8 hours, and in-patient hospitalizations, amputations, and eye losses within 24 hours — by calling 1-800-321-OSHA or filing online. The clock starts the moment the incident occurs, not when someone reads the safety log. This pipeline processes the incident event, classifies severity, fires Slack alerts, prepares the OSHA notification, and creates the audit trail.
8 Incident Types:
-
WORK_RELATED_FATALITY— OSHA §1904.39(a)(1): 8-hour reporting window. IMMEDIATE Slack to #safety-critical + Gmail safety director + prepare OSHA 1-800-321-OSHA notification draft. Penalty for late/no report: up to $15,625/violation. -
IN_PATIENT_HOSPITALIZATION— §1904.39(a)(2): 24-hour window. Same pipeline, lower urgency tier. -
AMPUTATION— §1904.39(a)(2): 24-hour window. Must report even if worker returns to work same day. -
EYE_LOSS— §1904.39(a)(2): 24-hour window. -
RECORDABLE_INJURY_ILLNESS— §1904.7: must record on OSHA Form 300 within 7 calendar days. Not a phone report — a recordkeeping event. -
FIRST_AID_ONLY— §1904.7(a): not recordable. Log for internal tracking only. -
NEAR_MISS— Not OSHA-reportable. Log for safety program / EMR (Experience Modification Rate) tracking. -
OSHA_300A_ANNUAL_POSTING— §1904.32(b): must post Form 300A summary Feb 1 through Apr 30. This event fires when the annual posting window opens.
{
"name": "ConTech - OSHA Incident Reporting Pipeline",
"nodes": [
{
"id": "1",
"type": "n8n-nodes-base.webhook",
"name": "Safety Incident Webhook",
"parameters": {
"path": "contech-safety-incident",
"httpMethod": "POST"
}
},
{
"id": "2",
"type": "n8n-nodes-base.code",
"name": "Classify Incident Severity",
"parameters": {
"jsCode": "const d = $input.first().json; const incidents = {WORK_RELATED_FATALITY: {severity: 'CRITICAL', window: '8 HOURS', regulation: 'OSHA \u00a71904.39(a)(1)', action: 'CALL 1-800-321-OSHA IMMEDIATELY \u2014 8-hour clock running', penalty: 'Up to $15,625/violation for late/no report'}, IN_PATIENT_HOSPITALIZATION: {severity: 'URGENT', window: '24 HOURS', regulation: 'OSHA \u00a71904.39(a)(2)', action: 'Report hospitalization to OSHA within 24 hours'}, AMPUTATION: {severity: 'URGENT', window: '24 HOURS', regulation: 'OSHA \u00a71904.39(a)(2)', action: 'Report amputation to OSHA within 24 hours \u2014 even if worker returns same day'}, EYE_LOSS: {severity: 'URGENT', window: '24 HOURS', regulation: 'OSHA \u00a71904.39(a)(2)', action: 'Report eye loss to OSHA within 24 hours'}, RECORDABLE_INJURY_ILLNESS: {severity: 'WARNING', window: '7 CALENDAR DAYS', regulation: 'OSHA \u00a71904.7', action: 'Record on OSHA Form 300 within 7 days \u2014 not a phone report'}, FIRST_AID_ONLY: {severity: 'INFO', window: 'N/A', regulation: 'OSHA \u00a71904.7(a)', action: 'NOT recordable \u2014 log for internal EMR tracking only'}, NEAR_MISS: {severity: 'INFO', window: 'N/A', regulation: 'Internal safety program', action: 'Not OSHA-reportable \u2014 log for safety program and EMR tracking'}, OSHA_300A_ANNUAL_POSTING: {severity: 'WARNING', window: 'Feb 1 - Apr 30', regulation: 'OSHA \u00a71904.32(b)', action: 'POST Form 300A summary in workplace \u2014 window is Feb 1 through Apr 30'}}; const info = incidents[d.incident_type] || {severity: 'REVIEW', window: 'UNKNOWN', regulation: 'OSHA \u00a71904', action: 'MANUAL REVIEW REQUIRED'}; const deadlineMs = info.window === '8 HOURS' ? 8*60*60*1000 : info.window === '24 HOURS' ? 24*60*60*1000 : null; const deadlineTs = deadlineMs ? new Date(Date.now() + deadlineMs).toISOString() : null; return [{json: {...d, ...info, deadlineTs, incident_id: d.incident_id || 'INC-' + Date.now(), ts: new Date().toISOString()}}];"
}
},
{
"id": "3",
"type": "n8n-nodes-base.switch",
"name": "Route by Severity",
"parameters": {
"dataType": "string",
"value1": "={{$json.severity}}",
"rules": {
"rules": [
{
"value2": "CRITICAL",
"output": 0
},
{
"value2": "URGENT",
"output": 1
},
{
"value2": "WARNING",
"output": 2
}
]
},
"fallbackOutput": 3
}
},
{
"id": "4",
"type": "n8n-nodes-base.slack",
"name": "Slack CRITICAL - Fatality",
"parameters": {
"channel": "#safety-critical",
"text": "FATALITY REPORTED: {{$json.project_name}} \u2014 Worker: {{$json.worker_name}}. OSHA \u00a71904.39(a)(1): 8-HOUR CLOCK RUNNING. Deadline: {{$json.deadlineTs}}. ACTION: Call 1-800-321-OSHA NOW. Incident ID: {{$json.incident_id}}. {{$json.action}}. Penalty for late report: {{$json.penalty}}."
}
},
{
"id": "5",
"type": "n8n-nodes-base.gmail",
"name": "Email Safety Director - Fatality",
"parameters": {
"to": "safety@company.com",
"subject": "OSHA FATALITY REPORT REQUIRED \u2014 8 HOURS \u2014 {{$json.project_name}}",
"message": "IMMEDIATE ACTION REQUIRED. OSHA \u00a71904.39(a)(1) 8-hour fatality reporting clock is running. Incident: {{$json.incident_type}} at {{$json.project_name}}. Worker: {{$json.worker_name}}. Time of incident: {{$json.ts}}. OSHA reporting deadline: {{$json.deadlineTs}}. Call 1-800-321-OSHA or report at osha.gov. Incident ID: {{$json.incident_id}} \u2014 logged in system audit trail."
}
},
{
"id": "6",
"type": "n8n-nodes-base.slack",
"name": "Slack URGENT - Hospitalization/Amputation",
"parameters": {
"channel": "#safety-urgent",
"text": "OSHA REPORT REQUIRED: {{$json.incident_type}} at {{$json.project_name}}. {{$json.regulation}} \u2014 24-hour reporting window. Deadline: {{$json.deadlineTs}}. Action: {{$json.action}}. Incident ID: {{$json.incident_id}}."
}
},
{
"id": "7",
"type": "n8n-nodes-base.slack",
"name": "Slack WARNING - Recordable",
"parameters": {
"channel": "#safety-compliance",
"text": "RECORDABLE INCIDENT: {{$json.incident_type}} at {{$json.project_name}}. {{$json.regulation}}. Action: {{$json.action}} within {{$json.window}}. Incident ID: {{$json.incident_id}}."
}
},
{
"id": "8",
"type": "n8n-nodes-base.slack",
"name": "Slack INFO - Log",
"parameters": {
"channel": "#safety-log",
"text": "Safety event logged: {{$json.incident_type}} at {{$json.project_name}}. {{$json.action}}. Incident ID: {{$json.incident_id}}."
}
},
{
"id": "9",
"type": "n8n-nodes-base.postgres",
"name": "Audit Trail Log",
"parameters": {
"query": "INSERT INTO safety_incident_log (incident_id, project_id, incident_type, severity, regulation, action_required, window, deadline_ts, ts) VALUES ('{{$json.incident_id}}', '{{$json.project_id}}', '{{$json.incident_type}}', '{{$json.severity}}', '{{$json.regulation}}', '{{$json.action}}', '{{$json.window}}', '{{$json.deadlineTs}}', '{{$json.ts}}')"
}
}
]
}
Workflow 3: OSHA/Davis-Bacon/EPA/Building-Code Compliance Deadline Tracker
Trigger: Weekdays at 7:00 AM
What it does:
Reads a compliance calendar from Google Sheets or Postgres and fires tiered alerts for 14 regulatory deadlines unique to construction SaaS.
14 Deadline Types:
| Type | Regulation | Window |
|---|---|---|
OSHA_FATALITY_REPORT |
§1904.39(a)(1) — 8 hours from incident | 8 hours |
OSHA_HOSPITALIZATION_REPORT |
§1904.39(a)(2) — 24 hours | 24 hours |
OSHA_300A_ANNUAL_POSTING |
§1904.32(b) — Feb 1 through Apr 30 | Annual |
DAVIS_BACON_CERTIFIED_PAYROLL |
40 USC §3141 / 29 CFR §5.5 — WH-347 weekly | Weekly |
DAVIS_BACON_DEBARMENT_RISK |
29 CFR §5.12 — missed payroll triggers investigation | Per violation |
EPA_SWPPP_NOI |
40 CFR §122.26 — Notice of Intent before ground disturbance | Before start |
EPA_SWPPP_NOT |
40 CFR §122.26 — Notice of Termination within 30 days of final stabilization | 30 days |
LEED_CLOSEOUT_DOCS |
LEED v4/v4.1 — documentation within 60 days of substantial completion | 60 days |
MILLER_ACT_BOND_CLAIM |
40 USC §3133 — payment bond claim within 90 days of last furnishing | 90 days |
STATE_RETAINAGE_RELEASE |
State-specific — typically 5% cap, release at 50% or substantial completion | Per contract |
OSHA_SILICA_MEDICAL_EXAM |
29 CFR §1926.1153(h) — annual medical surveillance if >25 μg/m³ action level | Annual |
CONTRACTOR_LICENSE_RENEWAL |
State licensing boards — annual | Annual |
INSURANCE_CERT_EXPIRY |
COI/ACORD 25 — track subcontractor certificate expiry | Per cert |
AIA_PAYMENT_APPLICATION |
AIA G702/G703 — monthly billing cycle, 21-day payment window | Monthly |
Workflow 4: Payment Application & Retainage Release Pipeline
Trigger: Webhook — payment application or project milestone event
What it does:
Construction payment is the most litigated area of construction law. This pipeline handles the full AIA G702/G703 payment application cycle, retainage tracking, lien waiver exchange, and state prompt payment act compliance. Missing a single payment application cycle can trigger a mechanic's lien and stop a project.
7 Event Types:
-
PAYMENT_APPLICATION_SUBMITTED— AIA G702: log submission timestamp, compute 21-day payment window under most state prompt payment acts, set retainage hold amount (5-10% per contract), route to owner's review queue. -
PAYMENT_APPLICATION_APPROVED— Update pay app status, compute net payment after retainage, prepare lien waiver (conditional waiver on payment), Slack #finance-ops. -
PAYMENT_OVERDUE— State prompt payment act: most states impose interest (1.5-2% per month) on overdue payments. Flag overdue amount + interest accrual, escalate to legal. -
RETAINAGE_RELEASE_DUE— At substantial completion or 50% completion per contract: compute retainage balance, prepare release request, route for owner approval. Many states cap retainage at 5% after 50% completion (e.g., CA Public Contract Code §7201). -
LIEN_WAIVER_REQUIRED— Conditional or unconditional: route to signing workflow, track per-payment exchange, prevent payment without executed waiver. -
MECHANIC_LIEN_RISK— If payment overdue beyond state lien deadline (typically 90-120 days after last work): IMMEDIATE escalation to legal + project owner. Mechanic's liens attach to the property title. -
MILLER_ACT_BOND_CLAIM_DEADLINE— Federal projects: payment bond claimant must send written notice within 90 days of last furnishing date (40 USC §3133(b)(2)). Flag approaching deadline.
{
"name": "ConTech - Payment Application & Retainage Pipeline",
"nodes": [
{
"id": "1",
"type": "n8n-nodes-base.webhook",
"name": "Payment Event Webhook",
"parameters": {
"path": "contech-payment-event",
"httpMethod": "POST"
}
},
{
"id": "2",
"type": "n8n-nodes-base.code",
"name": "Parse & Classify Payment Event",
"parameters": {
"jsCode": "const d = $input.first().json; const events = {PAYMENT_APPLICATION_SUBMITTED: {status: 'PENDING_REVIEW', window: '21 days (most state prompt payment acts)', regulation: 'AIA G702/G703 + state prompt payment act', action: 'Route to owner review queue, set retainage hold, compute payment deadline'}, PAYMENT_APPLICATION_APPROVED: {status: 'APPROVED', window: 'Per contract payment terms', regulation: 'AIA G702', action: 'Compute net payment after retainage, prepare conditional lien waiver'}, PAYMENT_OVERDUE: {status: 'OVERDUE_ESCALATE', window: 'IMMEDIATE', regulation: 'State prompt payment act', action: 'Flag overdue + interest accrual (1.5-2%/mo), escalate to legal, check lien deadline'}, RETAINAGE_RELEASE_DUE: {status: 'RETAINAGE_ACTION', window: 'At substantial completion or 50% per contract', regulation: 'AIA + state retainage caps (CA PCC \u00a77201: 5% cap at 50%)', action: 'Compute retainage balance, prepare release request, route for owner approval'}, LIEN_WAIVER_REQUIRED: {status: 'WAIVER_PENDING', window: 'Before payment release', regulation: 'State lien law', action: 'Route to signing workflow, block payment until executed waiver received'}, MECHANIC_LIEN_RISK: {status: 'CRITICAL_LEGAL', window: 'IMMEDIATE', regulation: 'State mechanic lien law (typically 90-120d after last work)', action: 'IMMEDIATE escalation to legal + project owner \u2014 lien attaches to title'}, MILLER_ACT_BOND_CLAIM_DEADLINE: {status: 'BOND_CLAIM_URGENT', window: '90 days from last furnishing', regulation: '40 USC \u00a73133(b)(2)', action: 'Send written notice to prime contractor before 90-day deadline expires'}}; const info = events[d.event_type] || {status: 'REVIEW', window: 'UNKNOWN', regulation: 'Construction contract', action: 'MANUAL REVIEW'}; return [{json: {...d, ...info, payment_event_id: d.payment_event_id || 'PAY-' + Date.now(), ts: new Date().toISOString()}}];"
}
},
{
"id": "3",
"type": "n8n-nodes-base.switch",
"name": "Route by Status",
"parameters": {
"dataType": "string",
"value1": "={{$json.status}}",
"rules": {
"rules": [
{
"value2": "CRITICAL_LEGAL",
"output": 0
},
{
"value2": "OVERDUE_ESCALATE",
"output": 1
},
{
"value2": "RETAINAGE_ACTION",
"output": 2
}
]
},
"fallbackOutput": 3
}
},
{
"id": "4",
"type": "n8n-nodes-base.slack",
"name": "Slack CRITICAL - Lien Risk",
"parameters": {
"channel": "#legal-urgent",
"text": "MECHANIC LIEN RISK: Project {{$json.project_name}} / {{$json.company_name}}. {{$json.regulation}}. {{$json.action}}. Payment event ID: {{$json.payment_event_id}}. Amount: ${{$json.amount}}. ESCALATE TO LEGAL IMMEDIATELY."
}
},
{
"id": "5",
"type": "n8n-nodes-base.slack",
"name": "Slack - Payment Overdue",
"parameters": {
"channel": "#finance-ops",
"text": "PAYMENT OVERDUE: {{$json.project_name}} \u2014 {{$json.company_name}}. Amount: ${{$json.amount}}. {{$json.regulation}}. Window: {{$json.window}}. Action: {{$json.action}}. Event ID: {{$json.payment_event_id}}."
}
},
{
"id": "6",
"type": "n8n-nodes-base.slack",
"name": "Slack - Retainage Release",
"parameters": {
"channel": "#finance-ops",
"text": "RETAINAGE RELEASE DUE: {{$json.project_name}}. {{$json.regulation}}. Action: {{$json.action}}. Amount: ${{$json.retainage_amount}}. Event ID: {{$json.payment_event_id}}."
}
},
{
"id": "7",
"type": "n8n-nodes-base.slack",
"name": "Slack - Standard Payment",
"parameters": {
"channel": "#payment-ops-log",
"text": "Payment event: {{$json.event_type}} \u2014 {{$json.project_name}}. Status: {{$json.status}}. Window: {{$json.window}}. Action: {{$json.action}}. Event ID: {{$json.payment_event_id}}."
}
},
{
"id": "8",
"type": "n8n-nodes-base.postgres",
"name": "Payment Audit Log",
"parameters": {
"query": "INSERT INTO payment_audit_log (payment_event_id, project_id, event_type, status, amount, regulation, action_required, window, ts) VALUES ('{{$json.payment_event_id}}', '{{$json.project_id}}', '{{$json.event_type}}', '{{$json.status}}', {{$json.amount}}, '{{$json.regulation}}', '{{$json.action}}', '{{$json.window}}', '{{$json.ts}}')"
}
}
]
}
Workflow 5: Weekly ConTech SaaS Platform KPI Dashboard
Trigger: Every Monday at 7:30 AM
What it does:
Pulls platform metrics from Postgres, computes WoW% changes, and emails a compliance-weighted HTML dashboard to the CEO and VP Safety. The safety officer BCC is intentional — OSHA recordkeeping requires annual summary posting (Form 300A), and the safety officer needs weekly visibility into incident rates and open OSHA obligations before the Feb 1 posting deadline arrives.
Metrics tracked:
- MRR and WoW% change (flagged if drop >5%)
- Active platform customers by tier
- Projects active this week across all customers
- Open OSHA fatality/hospitalization reports — §1904.39 any >0 = immediate escalation
- OSHA Form 300 recordable incidents this week — §1904.7 (running YTD total)
- Davis-Bacon certified payroll compliance rate — % of federal project subs who submitted WH-347 this week
- Open payment applications overdue — state prompt payment act exposure
- Retainage release requests pending — finance ops backlog
- Mechanic lien risk flags — any = legal escalation required
- Active EPA SWPPP projects without filed NOI — §122.26 pre-ground-disturbance requirement
- Subcontractor insurance certs expiring ≤30 days
{
"name": "ConTech - Weekly KPI Dashboard",
"nodes": [
{
"id": "1",
"type": "n8n-nodes-base.scheduleTrigger",
"name": "Monday 7:30 AM",
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "30 7 * * 1"
}
]
}
}
},
{
"id": "2",
"type": "n8n-nodes-base.postgres",
"name": "Query Platform Metrics",
"parameters": {
"query": "SELECT COUNT(DISTINCT customer_id) as active_customers, SUM(mrr_usd) as total_mrr, SUM(active_projects) as projects_active, SUM(osha_open_reports) as osha_open, SUM(recordable_incidents_ytd) as recordable_ytd, AVG(davis_bacon_compliance_rate) as db_compliance_rate, SUM(payment_apps_overdue) as pay_apps_overdue, SUM(retainage_release_pending) as retainage_pending, SUM(mechanic_lien_risk_flags) as lien_risks, SUM(swppp_missing_noi) as swppp_no_noi, SUM(insurance_certs_expiring_30d) as certs_expiring FROM platform_metrics WHERE metric_date = CURRENT_DATE"
}
},
{
"id": "3",
"type": "n8n-nodes-base.postgres",
"name": "Query Prior Week MRR",
"parameters": {
"query": "SELECT SUM(mrr_usd) as prior_mrr FROM platform_metrics WHERE metric_date = CURRENT_DATE - INTERVAL '7 days'"
}
},
{
"id": "4",
"type": "n8n-nodes-base.merge",
"name": "Merge Metrics",
"parameters": {
"mode": "mergeByIndex"
}
},
{
"id": "5",
"type": "n8n-nodes-base.code",
"name": "Build HTML Dashboard",
"parameters": {
"jsCode": "const c = $input.first().json; const mrrWoW = c.prior_mrr > 0 ? (((c.total_mrr - c.prior_mrr) / c.prior_mrr) * 100).toFixed(1) : 'N/A'; const mrrFlag = parseFloat(mrrWoW) < -5 ? ' ALERT: DROP >5%' : ''; const riskFlags = []; if (parseInt(c.osha_open) > 0) riskFlags.push('OPEN OSHA REPORTS: ' + c.osha_open + ' (\u00a71904.39 \u2014 immediate action required)'); if (parseInt(c.lien_risks) > 0) riskFlags.push('MECHANIC LIEN RISKS: ' + c.lien_risks + ' (escalate to legal NOW)'); if (parseFloat(c.db_compliance_rate) < 0.9) riskFlags.push('DAVIS-BACON COMPLIANCE RATE: ' + (parseFloat(c.db_compliance_rate)*100).toFixed(0) + '% (target >90% \u2014 debarment risk for federal contracts)'); if (parseInt(c.swppp_no_noi) > 0) riskFlags.push('EPA SWPPP MISSING NOI: ' + c.swppp_no_noi + ' projects (\u00a7122.26 \u2014 required before ground disturbance)'); const html = '<h2>ConTech SaaS \u2014 Weekly Platform Dashboard</h2><table border=1 cellpadding=5><tr><th>Metric</th><th>Value</th></tr><tr><td>Total MRR</td><td>$' + (c.total_mrr||0) + ' (WoW: ' + mrrWoW + '%' + mrrFlag + ')</td></tr><tr><td>Active Customers</td><td>' + (c.active_customers||0) + '</td></tr><tr><td>Active Projects</td><td>' + (c.projects_active||0) + '</td></tr><tr><td>Open OSHA Reports (\u00a71904.39)</td><td>' + (c.osha_open||0) + '</td></tr><tr><td>Recordable Incidents YTD</td><td>' + (c.recordable_ytd||0) + '</td></tr><tr><td>Davis-Bacon Compliance Rate</td><td>' + (parseFloat(c.db_compliance_rate||0)*100).toFixed(0) + '%</td></tr><tr><td>Pay Apps Overdue</td><td>' + (c.pay_apps_overdue||0) + '</td></tr><tr><td>Retainage Release Pending</td><td>' + (c.retainage_pending||0) + '</td></tr><tr><td>Mechanic Lien Risks</td><td>' + (c.lien_risks||0) + '</td></tr><tr><td>SWPPP Missing NOI</td><td>' + (c.swppp_no_noi||0) + '</td></tr><tr><td>Insurance Certs Expiring \u226430d</td><td>' + (c.certs_expiring||0) + '</td></tr></table>' + (riskFlags.length ? '<h3>Risk Flags</h3><ul>' + riskFlags.map(f => '<li>' + f + '</li>').join('') + '</ul>' : '<p>No active risk flags.</p>'); return [{json: {html, summary: 'MRR $' + (c.total_mrr||0) + ' | WoW ' + mrrWoW + '% | OSHA open: ' + (c.osha_open||0) + ' | Lien risks: ' + (c.lien_risks||0) + ' | DB compliance: ' + (parseFloat(c.db_compliance_rate||0)*100).toFixed(0) + '%', ts: new Date().toISOString()}}];"
}
},
{
"id": "6",
"type": "n8n-nodes-base.gmail",
"name": "Email CEO + VP Safety",
"parameters": {
"to": "ceo@company.com",
"bcc": "vpsafety@company.com",
"subject": "ConTech Platform KPI \u2014 Week of {{$json.ts.substring(0,10)}}",
"message": "={{$json.html}}",
"options": {
"bodyContentType": "html"
}
}
},
{
"id": "7",
"type": "n8n-nodes-base.slack",
"name": "Slack One-Liner",
"parameters": {
"channel": "#management",
"text": "Weekly KPI: {{$json.summary}}"
}
}
]
}
Quick Comparison: n8n vs Zapier/Make for ConstructionTech SaaS
| Factor | Self-Hosted n8n | Zapier / Make |
|---|---|---|
| OSHA incident audit trail | Git-versioned JSON, on-prem, timestamp-exact | Third-party vendor logs, no OSHA chain of custody |
| Davis-Bacon payroll records | Stays in your VPC — 29 CFR §5.5 3yr retention | Routes through cloud vendor |
| 8-hour fatality clock | Webhook fires in seconds | Cloud queue introduces unpredictable latency |
| Worker PII / biometrics | Self-hosted — CCPA CPRA sensitive PI in your enclave | Cloud vendor = data processor liability |
| Mechanic lien deadline logic | Custom state-by-state date math in Code node | No custom SQL or date arithmetic |
| SWPPP permit tracking | Custom fields, any project structure | Fixed-field integration templates |
| Retainage state logic | Per-state retainage cap rules in Code node | One-size-fits-all |
| Per-month cost at scale | ~$20/month VPS | $599-$999+/month |
Get the Complete ConTech Workflow Pack
All 5 workflows above — plus 9 more for construction SaaS ops — are available as a single import-ready pack at stripeai.gumroad.com.
Individual templates: $12–$29. Full automation bundle (14 workflows): $97.
Questions about OSHA incident reporting or Davis-Bacon n8n implementation? Drop a comment below.
Top comments (0)