DEV Community

Alex Kane
Alex Kane

Posted on

n8n for ConstructionTech SaaS: 5 Automations for OSHA, Davis-Bacon, AIA Contracts, and LEED Compliance (Free Workflow JSON)

If you sell construction project management software, BIM coordination tools, or field management apps to general contractors, subcontractors, or project owners — your customers operate under OSHA 29 CFR Part 1926, Davis-Bacon certified payroll requirements, AIA contract notice deadlines, and LEED documentation obligations.

Every one of those compliance cycles creates a window for your platform to deliver proactive value, reduce customer churn, and win procurement reviews.

Here are 5 production-ready n8n automations built specifically for ConstructionTech SaaS vendors, with complete import-ready workflow JSON.

Why self-host n8n? Federal construction contracts under FAR 52.204-21 impose Controlled Unclassified Information (CUI) basic handling requirements — project specifications, floor plans, and subcontractor data cannot flow through cloud iPaaS. Davis-Bacon WH-347 certified payroll data is worker wage PII that becomes an FLSA audit discovery target in cloud environments. OSHA 29 CFR §1904.39 requires 8-hour telephonic notification for fatal accidents — if your automation layer goes down, you missed a mandatory deadline. OSHA §1926.16 makes the prime contractor responsible for subcontractor coordination model staleness. Self-hosted n8n keeps sensitive construction data in your customer's enclave and gives you Git-versioned workflow JSON as an audit-ready evidence trail.


Workflow 1: New Construction Customer Onboarding & Compliance Drip

Trigger: Webhook (CRM new customer event)

Segments customers by role and flags applicable compliance obligations at Day 0, Day 3, and Day 7.

{
  "name": "ConstructionTech Customer Compliance Onboarding Drip",
  "nodes": [
    {
      "name": "New Customer Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "construction-customer-onboarding",
        "responseMode": "lastNode"
      }
    },
    {
      "name": "Classify Tier & Flags",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst customer = $json;\nconst tier = customer.company_type || 'GENERAL_CONTRACTOR';\n// GENERAL_CONTRACTOR | SUBCONTRACTOR | OWNER_DEVELOPER | CM_GC | SPECIALTY_TRADE\nconst flags = [];\nif (customer.federal_contract) flags.push('FEDERAL_CONTRACT');\nif (customer.davis_bacon_required) flags.push('DAVIS_BACON_ACT');\nif (customer.leed_certified || customer.leed_target) flags.push('LEED_CERTIFIED');\nif (customer.prevailing_wage_state) flags.push('PREVAILING_WAGE_REQUIRED');\nif (customer.public_contract) flags.push('PUBLIC_CONTRACT');\nif (customer.dbe_certified) flags.push('DBE_CERTIFIED');\nif (customer.aia_contracts) flags.push('AIA_CONTRACT');\nflags.push('OSHA_CONSTRUCTION_1926'); // universal\nreturn [{json: {...customer, tier, compliance_flags: flags, onboarding_start: new Date().toISOString()}}];\n"
      }
    },
    {
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO customer_compliance_profiles (customer_id, tier, compliance_flags, created_at) VALUES ('{{ $json.customer_id }}', '{{ $json.tier }}', '{{ $json.compliance_flags.join(\",\") }}', NOW()) ON CONFLICT (customer_id) DO UPDATE SET compliance_flags = EXCLUDED.compliance_flags, updated_at = NOW()"
      }
    },
    {
      "name": "Day 0 Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.contact_email }}",
        "subject": "Welcome to [Your Platform] \u2014 Your Compliance Checklist",
        "message": "Hi {{ $json.contact_name }},\n\nWelcome aboard. Based on your profile ({{ $json.tier }}), here's your initial compliance checklist:\n\n{{ $json.compliance_flags.includes('FEDERAL_CONTRACT') ? '\u2022 Federal contract: FAR 52.204-21 CUI handling requirements apply\\n\u2022 Davis-Bacon WH-347 certified payroll weekly submission\\n' : '' }}{{ $json.compliance_flags.includes('LEED_CERTIFIED') ? '\u2022 LEED documentation: credit audit trail required\\n' : '' }}\u2022 OSHA 29 CFR Part 1926: safety incident reporting obligations apply to all construction projects\n\nYour dedicated CSM will reach out within 24 hours.\n\nBest,\n[Your Platform] Team"
      }
    },
    {
      "name": "Day 3 Integration Setup",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 3,
        "unit": "days"
      }
    },
    {
      "name": "Day 3 Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.contact_email }}",
        "subject": "Set up your OSHA incident webhook \u2014 3-minute guide",
        "message": "Hi {{ $json.contact_name }},\n\nDay 3 checklist:\n\n1. Connect your OSHA incident reporting webhook (8-hour telephonic notification for fatal accidents \u2014 \u00a71904.39)\n2. Import your Davis-Bacon prevailing wage determinations\n3. Set up your BIM model sync endpoint\n\nNeed help? Reply to this email.\n\nBest,\n[Your Platform] Team"
      }
    },
    {
      "name": "Day 7 Compliance Review",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 4,
        "unit": "days"
      }
    },
    {
      "name": "Day 7 Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.contact_email }}",
        "subject": "Week 1 complete \u2014 compliance audit trail ready",
        "message": "Hi {{ $json.contact_name }},\n\nYour compliance profile is live. All workflow executions are logged with timestamps \u2014 your OSHA recordable injury reports, Davis-Bacon payroll submissions, and AIA contract notice deadlines are now tracked automatically.\n\nFor federal contracts: your n8n workflow JSON is Git-versioned, satisfying FAR 52.204-21 audit trail requirements.\n\nQuestions? Reply here.\n\n[Your Platform] Team"
      }
    },
    {
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "responseCode": 200,
        "responseBody": "{\"status\":\"ok\"}"
      }
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: OSHA 29 CFR Part 1926 Safety Incident Pipeline

Trigger: Webhook (field app incident report)

Classifies construction safety incidents by OSHA severity and routes alerts with citation-specific deadlines.

{
  "name": "OSHA Construction Safety Incident Pipeline",
  "nodes": [
    {
      "name": "Safety Incident Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "osha-safety-incident",
        "responseMode": "lastNode"
      }
    },
    {
      "name": "Dedup 30min",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst state = $getWorkflowStaticData('global');\nconst key = $json.incident_id;\nconst now = Date.now();\nif (state[key] && now - state[key] < 30 * 60 * 1000) {\n  return [];\n}\nstate[key] = now;\nreturn [$input.item];\n"
      }
    },
    {
      "name": "Classify Incident",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst type = $json.incident_type;\nconst typeMap = {\n  'FATAL_ACCIDENT': {severity: 'CRITICAL', citation: 'OSHA 29 CFR \u00a71904.39', deadline: '8h telephonic notification to OSHA', color: '#FF0000'},\n  'OSHA_RECORDABLE_INJURY': {severity: 'CRITICAL', citation: 'OSHA 29 CFR \u00a71904.7', deadline: '7-day OSHA 300 log entry required', color: '#FF4444'},\n  'EXCAVATION_COLLAPSE': {severity: 'CRITICAL', citation: 'OSHA 29 CFR \u00a71926.651', deadline: 'Immediate stop-work + OSHA notification', color: '#FF4444'},\n  'ELECTRICAL_HAZARD': {severity: 'CRITICAL', citation: 'OSHA 29 CFR \u00a71926.416', deadline: 'Immediate stop-work + lockout/tagout', color: '#FF4444'},\n  'LOST_TIME_INJURY': {severity: 'HIGH', citation: 'OSHA 29 CFR \u00a71904.7', deadline: 'OSHA 300 log entry within 7 days', color: '#FF8800'},\n  'FALL_FROM_HEIGHT': {severity: 'HIGH', citation: 'OSHA 29 CFR \u00a71926.502', deadline: 'Incident report + fall protection review', color: '#FF8800'},\n  'CONFINED_SPACE_ENTRY': {severity: 'HIGH', citation: 'OSHA 29 CFR \u00a71926.1203', deadline: 'Atmospheric test + permit required', color: '#FF8800'},\n  'EQUIPMENT_FAILURE': {severity: 'HIGH', citation: 'OSHA 29 CFR \u00a71926.302', deadline: 'Equipment removed from service', color: '#FF8800'},\n  'NEAR_MISS': {severity: 'MEDIUM', citation: 'OSHA 29 CFR \u00a71926.20(a)', deadline: 'Safety investigation within 24h', color: '#FFAA00'}\n};\nconst info = typeMap[type] || {severity: 'MEDIUM', citation: 'OSHA 29 CFR Part 1926', deadline: 'Review required', color: '#FFAA00'};\nreturn [{json: {...$json, ...info, ts: new Date().toISOString()}}];\n"
      }
    },
    {
      "name": "Route by Severity",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "rules": [
          {
            "value1": "={{ $json.severity }}",
            "operation": "equal",
            "value2": "CRITICAL"
          },
          {
            "value1": "={{ $json.severity }}",
            "operation": "equal",
            "value2": "HIGH"
          }
        ]
      }
    },
    {
      "name": "Slack Critical",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#safety-incidents-critical",
        "text": "\ud83d\udea8 CRITICAL OSHA INCIDENT \u2014 {{ $json.incident_type }}\nProject: {{ $json.project_name }} | Worker: {{ $json.worker_name }}\nCitation: {{ $json.citation }}\nDeadline: {{ $json.deadline }}\nCustomer: {{ $json.customer_name }}\nIncident ID: {{ $json.incident_id }}"
      }
    },
    {
      "name": "Email Safety Director",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "safety-director@yourcompany.com",
        "subject": "CRITICAL OSHA {{ $json.incident_type }} \u2014 {{ $json.project_name }}",
        "message": "OSHA critical incident detected at {{ $json.project_name }}.\n\nType: {{ $json.incident_type }}\nCitation: {{ $json.citation }}\nRequired action: {{ $json.deadline }}\nCustomer: {{ $json.customer_name }}\nWorker: {{ $json.worker_name }}\nTimestamp: {{ $json.ts }}\nIncident ID: {{ $json.incident_id }}\n\nThis record is logged in Postgres for OSHA \u00a71904 retention (5 years)."
      }
    },
    {
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO osha_incidents (incident_id, customer_id, project_name, incident_type, severity, citation, deadline, worker_name, ts, notified_at) VALUES ('{{ $json.incident_id }}', '{{ $json.customer_id }}', '{{ $json.project_name }}', '{{ $json.incident_type }}', '{{ $json.severity }}', '{{ $json.citation }}', '{{ $json.deadline }}', '{{ $json.worker_name }}', '{{ $json.ts }}', NOW()) ON CONFLICT (incident_id) DO NOTHING"
      }
    },
    {
      "name": "Respond ACK",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "responseCode": 200,
        "responseBody": "{\"status\":\"logged\"}"
      }
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

Key: OSHA §1904.39 requires 8-hour telephonic notification to OSHA for fatalities and 24-hour for hospitalizations. If your automation platform goes down during a fatal incident, your customer missed a mandatory federal deadline. Self-hosted n8n eliminates that single point of failure.


Workflow 3: Davis-Bacon & Prevailing Wage Compliance Deadline Tracker

Trigger: Schedule — weekdays 8 AM

Tracks 12 critical construction compliance deadline types with OSHA and Davis-Bacon citations.

{
  "name": "Davis-Bacon & Construction Compliance Deadline Tracker",
  "nodes": [
    {
      "name": "Weekday 8AM Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "name": "Read Deadlines Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "read",
        "sheetId": "YOUR_SHEET_ID",
        "range": "Deadlines!A2:L500"
      }
    },
    {
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst today = new Date();\nconst items = [];\nfor (const row of $input.all()) {\n  const r = row.json;\n  const due = new Date(r.due_date);\n  const daysLeft = Math.floor((due - today) / 86400000);\n  let urgency = 'OK';\n  if (daysLeft < 0) urgency = 'OVERDUE';\n  else if (daysLeft <= 7) urgency = 'CRITICAL';\n  else if (daysLeft <= 21) urgency = 'URGENT';\n  else if (daysLeft <= 45) urgency = 'WARNING';\n  else if (daysLeft <= 90) urgency = 'NOTICE';\n  if (urgency === 'OK') continue;\n  const today_str = today.toISOString().slice(0, 10);\n  if (r.alert_sent_date === today_str) continue;\n  items.push({json: {...r, urgency, daysLeft, today_str}});\n}\nreturn items;\n"
      }
    },
    {
      "name": "Route Deadline Type",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "rules": [
          {
            "value1": "={{ $json.deadline_type }}",
            "operation": "equal",
            "value2": "CERTIFIED_PAYROLL_WH347_WEEKLY"
          },
          {
            "value1": "={{ $json.deadline_type }}",
            "operation": "equal",
            "value2": "OSHA_TRAINING_RENEWAL_10HR"
          },
          {
            "value1": "={{ $json.deadline_type }}",
            "operation": "equal",
            "value2": "OSHA_TRAINING_RENEWAL_30HR"
          },
          {
            "value1": "={{ $json.deadline_type }}",
            "operation": "equal",
            "value2": "AIA_A201_CHANGE_ORDER_RESPONSE"
          }
        ]
      }
    },
    {
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#compliance-deadlines",
        "text": "{{ $json.urgency }}: {{ $json.deadline_type }} \u2014 {{ $json.customer_name }}\nDue: {{ $json.due_date }} ({{ $json.daysLeft }}d)\nCitation: {{ $json.citation }}\nOwner: {{ $json.owner_email }}"
      }
    },
    {
      "name": "Email Owner",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.owner_email }}",
        "subject": "[{{ $json.urgency }}] {{ $json.deadline_type }} due {{ $json.due_date }} \u2014 {{ $json.customer_name }}",
        "message": "Compliance deadline: {{ $json.deadline_type }}\nCustomer: {{ $json.customer_name }}\nDue: {{ $json.due_date }} ({{ $json.daysLeft }} days)\nCitation: {{ $json.citation }}\nAction required: {{ $json.action_required }}"
      }
    },
    {
      "name": "Mark Alert Sent",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "update",
        "sheetId": "YOUR_SHEET_ID",
        "range": "Deadlines",
        "data": {
          "values": [
            [
              "={{ $json.today_str }}"
            ]
          ]
        },
        "options": {
          "locationDefine": "specifyRange",
          "rangeDefinition": "={{ $json.sheet_row }}"
        }
      }
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

12 deadline types tracked:

Deadline Type Citation Penalty
Certified Payroll WH-347 (weekly) 40 USC §276a-7 Contract debarment
Prevailing Wage Determination Annual Review 29 CFR Part 5 Back pay liability
Davis-Bacon Apprentice Ratio Review 29 CFR §5.5(a)(4) Contract default
AIA A201 Change Order Response AIA §7.3.1 Waiver of claim
Conditional Lien Waiver (progress payment) State lien law Lien rights lost
Performance Bond Renewal Miller Act 40 USC §3131 Contract breach
Payment Bond Renewal Miller Act 40 USC §3133 Subcontractor claims
Contractor License Renewal State licensing board Stop-work order
Insurance COI Expiry AIA A201 §11.1 Contract default
OSHA 10-Hour Training Renewal OSHA §1926.503 $15,625/violation
OSHA 30-Hour Training Renewal OSHA §1926.503 $15,625/violation
LEED Documentation Submission USGBC certification Credit loss

Workflow 4: BIM Model & Field Data Sync Health Monitor

Trigger: Schedule — every 10 minutes

Monitors BIM coordination model freshness and field data sync endpoints. Stale BIM models create OSHA §1926.16 prime contractor liability.

{
  "name": "BIM Model & Field Data Sync Health Monitor",
  "nodes": [
    {
      "name": "10min Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/10 * * * *"
            }
          ]
        }
      }
    },
    {
      "name": "Read Endpoints",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "read",
        "sheetId": "YOUR_SHEET_ID",
        "range": "BIM_Endpoints!A2:G100"
      }
    },
    {
      "name": "Ping Endpoints",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "={{ $json.endpoint_url }}",
        "method": "GET",
        "continueOnFail": true,
        "timeout": 10000
      }
    },
    {
      "name": "Evaluate State",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst state = $getWorkflowStaticData('global');\nconst results = [];\nfor (const item of $input.all()) {\n  const r = item.json;\n  const endpoint_id = r.endpoint_id;\n  const statusCode = r.statusCode || 0;\n  const lastSync = r.last_sync_ts ? new Date(r.last_sync_ts) : null;\n  const staleMs = lastSync ? Date.now() - lastSync.getTime() : Infinity;\n  let status = 'OK';\n  if (statusCode !== 200) status = 'DOWN';\n  else if (staleMs > 60 * 60 * 1000) status = 'STALE'; // >1h\n  else if (statusCode >= 400) status = 'AUTH_ERROR';\n  const prevStatus = state[endpoint_id] || 'OK';\n  state[endpoint_id] = status;\n  if (status !== 'OK' && status !== prevStatus) {\n    const note = status === 'STALE' ?\n      'OSHA \u00a71926.16: Prime contractor responsible for subcontractor coordination model freshness. Stale BIM data = clash detection gap.' :\n      '';\n    results.push({json: {...r, status, staleMs, note, ts: new Date().toISOString()}});\n  }\n}\nreturn results.length ? results : [{\"json\": {\"skip\": true}}];\n"
      }
    },
    {
      "name": "Filter Alerts",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.skip }}",
              "value2": true,
              "operation": "notEqual"
            }
          ]
        }
      }
    },
    {
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#platform-bim-ops",
        "text": "BIM SYNC {{ $json.status }} \u2014 {{ $json.endpoint_name }}\nCustomer: {{ $json.customer_name }}\nType: {{ $json.endpoint_type }}\nLast sync: {{ $json.last_sync_ts }}\n{{ $json.note ? '\u26a0\ufe0f ' + $json.note : '' }}\nTimestamp: {{ $json.ts }}"
      }
    },
    {
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO bim_sync_incidents (endpoint_id, customer_id, status, stale_ms, note, ts) VALUES ('{{ $json.endpoint_id }}', '{{ $json.customer_id }}', '{{ $json.status }}', {{ $json.staleMs }}, '{{ $json.note }}', '{{ $json.ts }}') ON CONFLICT DO NOTHING"
      }
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly ConstructionTech Platform KPI Dashboard

Trigger: Schedule — Monday 8 AM

Rolls up safety incidents, Davis-Bacon deadlines, and platform health for executive review.

{
  "name": "Weekly ConstructionTech Platform KPI Dashboard",
  "nodes": [
    {
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      }
    },
    {
      "name": "Query Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT customer_id) AS active_customers, COUNT(DISTINCT CASE WHEN tier='GENERAL_CONTRACTOR' THEN customer_id END) AS gc_count, COUNT(DISTINCT CASE WHEN tier='SUBCONTRACTOR' THEN customer_id END) AS sub_count, SUM(mrr_usd) AS total_mrr FROM customer_compliance_profiles WHERE status='ACTIVE'"
      }
    },
    {
      "name": "Query Safety Incidents",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) AS osha_incidents_7d, COUNT(CASE WHEN severity='CRITICAL' THEN 1 END) AS critical_incidents, COUNT(CASE WHEN severity='CRITICAL' AND resolved_at IS NULL THEN 1 END) AS critical_open, COUNT(CASE WHEN incident_type='FATAL_ACCIDENT' THEN 1 END) AS fatalities_7d FROM osha_incidents WHERE ts >= NOW() - INTERVAL '7 days'"
      }
    },
    {
      "name": "Query Deadlines",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) AS deadlines_overdue, COUNT(CASE WHEN deadline_type LIKE 'DAVIS_BACON%' THEN 1 END) AS davis_bacon_overdue, COUNT(CASE WHEN deadline_type LIKE 'OSHA%' THEN 1 END) AS osha_training_overdue FROM compliance_deadlines WHERE urgency='OVERDUE'"
      }
    },
    {
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst prev = $getWorkflowStaticData('global');\nconst m = $('Query Platform Metrics').first().json;\nconst s = $('Query Safety Incidents').first().json;\nconst d = $('Query Deadlines').first().json;\nconst mrrWoW = prev.last_mrr ? ((m.total_mrr - prev.last_mrr) / prev.last_mrr * 100).toFixed(1) : '\u2014';\nprev.last_mrr = m.total_mrr;\nconst flags = [];\nif (parseInt(s.critical_open) > 0) flags.push(`[OSHA CRITICAL: ${s.critical_open} OPEN]`);\nif (parseInt(s.fatalities_7d) > 0) flags.push(`[FATAL ACCIDENT: ${s.fatalities_7d}]`);\nif (parseInt(d.davis_bacon_overdue) > 0) flags.push(`[DAVIS-BACON OVERDUE: ${d.davis_bacon_overdue}]`);\nconst subject = `${flags.length ? flags.join(' ') + ' ' : ''}ConstructionTech Weekly KPI \u2014 ${new Date().toISOString().slice(0,10)}`;\nconst html = `\n<h2>ConstructionTech Platform KPI \u2014 Week of ${new Date().toISOString().slice(0,10)}</h2>\n<table border=\"1\" cellpadding=\"6\" style=\"border-collapse:collapse\">\n<tr><th>Metric</th><th>Value</th><th>WoW</th></tr>\n<tr><td>Active Customers</td><td>${m.active_customers}</td><td>\u2014</td></tr>\n<tr><td>GC / Subcontractor</td><td>${m.gc_count} / ${m.sub_count}</td><td>\u2014</td></tr>\n<tr><td>MRR</td><td>$${m.total_mrr?.toLocaleString()}</td><td>${mrrWoW}%</td></tr>\n<tr style=\"background:${parseInt(s.critical_incidents)>0?'#ffe0e0':''}\"><td>OSHA Incidents (7d)</td><td>${s.osha_incidents_7d}</td><td>\u2014</td></tr>\n<tr style=\"background:${parseInt(s.critical_open)>0?'#ff9999':''}\"><td>Critical Incidents Open</td><td>${s.critical_open}</td><td>\u2014</td></tr>\n<tr style=\"background:${parseInt(d.davis_bacon_overdue)>0?'#fff3cd':''}\"><td>Davis-Bacon Overdue</td><td>${d.davis_bacon_overdue}</td><td>\u2014</td></tr>\n<tr><td>OSHA Training Overdue</td><td>${d.osha_training_overdue}</td><td>\u2014</td></tr>\n</table>\n${flags.length ? '<p style=\"color:red\"><strong>Action required: ' + flags.join(', ') + '</strong></p>' : ''}\n`;\nreturn [{json: {subject, html}}];\n"
      }
    },
    {
      "name": "Send Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "ceo@yourcompany.com",
        "cc": "safety-director@yourcompany.com",
        "bcc": "ciso@yourcompany.com",
        "subject": "={{ $json.subject }}",
        "message": "={{ $json.html }}",
        "options": {
          "bodyContentType": "html"
        }
      }
    },
    {
      "name": "Slack Summary",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#exec-construction-kpis",
        "text": "ConstructionTech Weekly KPI sent. {{ $json.subject }}"
      }
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

Why ConstructionTech SaaS vendors should self-host n8n

Risk Cloud iPaaS Exposure Self-Hosted n8n
OSHA §1904.39 8h fatal accident deadline Platform outage = missed federal deadline In-enclave, no external dependency
Davis-Bacon WH-347 certified payroll Worker wage PII in cloud = FLSA discovery target Stays in customer's enclave
AIA A201 §7.3.1 change order notices Cloud delay = waiver of claim rights Execution log is audit evidence
FAR 52.204-21 CUI handling Project specs/floor plans cannot exit enclave Git-versioned JSON = CUI audit trail
OSHA §1926.16 BIM coordination Stale cloud sync = prime contractor liability 10-min monitor with citation-specific alerts
LEED credit documentation No chain-of-custody for credit data Workflow execution log = USGBC evidence

Get the complete template bundle

These 5 workflows are part of the FlowKit n8n automation template library — production-ready, import-ready JSON covering 15+ industries and 50+ compliance frameworks.

Get all templates at stripeai.gumroad.com

Next in this series: CivicTech/Smart City SaaS, EdTech SaaS (FERPA/COPPA), MarTech SaaS (CCPA/GDPR consent).

Questions about adapting these for your stack? Drop them in the comments.

Top comments (0)