DEV Community

Alex Kane
Alex Kane

Posted on

n8n for ConstructionTech & BuildTech SaaS Vendors: 5 Automations for OSHA 1926, Davis-Bacon, EPA SWPPP, and AIA Contract Compliance

n8n for ConstructionTech & BuildTech SaaS Vendors: 5 Automations for OSHA 1926, Davis-Bacon, EPA SWPPP, and AIA Contract Compliance

ConstructionTech and BuildTech SaaS platforms sit at the center of a compliance minefield. Your customers are managing OSHA 29 CFR §1926 fatality and injury reporting clocks measured in hours, weekly Davis-Bacon Act certified payroll obligations on every federally funded project, EPA Construction General Permit stormwater inspection logs that must run every 7 days, and AIA contract change order audit trails that become Exhibit A in construction lien disputes.

If your platform routes any of this data through a cloud automation layer — and most do — that automation vendor is now in the chain of custody for every OSHA investigation, DOL WHD payroll audit, EPA enforcement action, and construction litigation discovery request. Here are 5 n8n workflows your platform should be running today.


The ConstructionTech Compliance Tier Map

Tier Compliance Flags Primary n8n Use Cases
GENERAL_CONTRACTOR_SAAS_VENDOR OSHA_1926_REGULATED, DAVIS_BACON_COVERED, AIA_CONTRACT_USER Incident reporting, certified payroll, change orders, subcontractor compliance
SPECIALTY_TRADE_SAAS_VENDOR OSHA_1926_REGULATED, OSHA_1926_502_FALL_PROTECTION Pre-shift safety inspection, fall protection compliance, lockout/tagout logs
CONSTRUCTION_PAYROLL_SAAS_VENDOR DAVIS_BACON_COVERED_CONTRACTOR, FEDERAL_CONTRACTOR WH-347 certified payroll, prevailing wage monitoring, debarment check automation
ENVIRONMENTAL_COMPLIANCE_SAAS EPA_CGP_SWPPP_PERMIT_HOLDER, NEPA_EIS_REQUIRED SWPPP weekly inspection, BMP tracking, corrective action deadlines, permit expiry
BUILDING_INSPECTION_SAAS_VENDOR IBC_2021_COMPLIANCE, ADA_TITLE_III Inspection workflow, CO tracking, failed inspection escalation, permit management
REAL_ESTATE_DEVELOPMENT_SAAS NEPA_REVIEW, DAVIS_BACON_COVERED, AIA_CONTRACT_USER EIS/EA tracking, change order management, subcontractor prequalification
CONSTRUCTIONTECH_STARTUP SOC2_REQUIRED Core compliance automation, webhook ingestion, Slack/Gmail alerting

Workflow 1: Tier-Segmented Customer Onboarding

Every ConstructionTech customer lands in a different compliance posture. A general contractor on a federal project carries Davis-Bacon, OSHA multi-employer doctrine, and EPA CGP obligations simultaneously. A specialty electrical trade has lockout/tagout and fall protection as primary risks. Route each customer to the right onboarding message on Day 0.

{
  "name": "ConstructionTech Tier-Segmented Customer Onboarding",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "constructiontech-signup",
        "responseMode": "lastNode"
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Code - Classify Tier",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst tier = data.customer_tier || 'CONSTRUCTIONTECH_STARTUP';\nconst msgs = {\n  'GENERAL_CONTRACTOR_SAAS_VENDOR': 'Note: OSHA 29 CFR \u00a71926.1150 fatality reporting within 8 hours, hospitalization within 8h, amputation/eye loss within 24h \u2014 directly to OSHA (1-800-321-OSHA). Your platform holds the first-notice-of-injury data. Cloud iPaaS routing = incident records outside SOC2 boundary. Davis-Bacon Act 40 USC \u00a73142 certified payroll (WH-347) weekly \u2014 DOL WHD audit access to 3-year payroll records.',\n  'SPECIALTY_TRADE_SAAS_VENDOR': 'Note: OSHA 29 CFR \u00a71926.502 fall protection pre-shift inspection \u2014 GC holds downstream liability for subcontractor non-compliance. OSHA 1926.417 lockout/tagout for electrical. Your platform may hold the inspection audit trail subpoenaed in a fatality investigation.',\n  'CONSTRUCTION_PAYROLL_SAAS_VENDOR': 'Note: Davis-Bacon Act 40 USC \u00a73142 + DOL WHD Wage Determination \u2014 federally funded projects require WH-347 certified payroll weekly. Willful underpayment = debarment from federal contracts. Your platform is the record of account. DOL WHD subpoena goes here first.',\n  'ENVIRONMENTAL_COMPLIANCE_SAAS': 'Note: EPA Construction General Permit (CGP) \u2014 SWPPP weekly inspection log required, corrective action within 7 days. CWA \u00a7402 NPDES stormwater violation = $25,000/day. NEPA EA/EIS records in your platform are subpoena targets for environmental litigation.',\n  'BUILDING_INSPECTION_SAAS_VENDOR': 'Note: ADA Title III + IBC 2021 accessibility inspection records \u2014 building permit denial appeals, construction defect litigation, and DOJ investigations all subpoena your platform. Certificate of Occupancy records and failed inspection history are legally discoverable.',\n  'REAL_ESTATE_DEVELOPMENT_SAAS': 'Note: NEPA 40 CFR \u00a71501 environmental review records + CEQA (California) \u2014 EIS/EA documents stored in cloud automation are discoverable in NIMBY litigation. Davis-Bacon applies to any project with federal funding. AIA G701 change order audit trail critical for construction lien disputes.',\n  'CONSTRUCTIONTECH_STARTUP': 'Welcome to FlowKit. Your onboarding is set up. Reach out anytime.'\n};\nreturn [{json: {customer_id: data.customer_id, email: data.email, company: data.company, tier, onboarding_note: msgs[tier] || msgs['CONSTRUCTIONTECH_STARTUP']}}];"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Gmail - Day 0 Welcome",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{$json.email}}",
        "subject": "={{$json.company}} is live on FlowKit \u2014 OSHA/Davis-Bacon/SWPPP compliance automations ready",
        "message": "={{$json.onboarding_note}}\n\nYour FlowKit workflows are ready to activate."
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "Sheets - Log Onboarding",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "Onboarding!A:G",
        "dataMode": "autoMapInputData"
      },
      "position": [
        900,
        300
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Code - Classify Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Classify Tier": {
      "main": [
        [
          {
            "node": "Gmail - Day 0 Welcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - Day 0 Welcome": {
      "main": [
        [
          {
            "node": "Sheets - Log Onboarding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: OSHA 1926 Construction Site Incident Reporter

The compliance clock that ends careers. OSHA 29 CFR §1904.39 requires fatalities and in-patient hospitalizations to be reported to OSHA within 8 hours. Amputations and eye loss: 24 hours. Missing these deadlines is a willful violation — $15,625/day. Your platform probably receives the first-notice-of-injury data. This workflow converts that intake into an automated incident classification and escalation engine with the right clock on every severity level.

{
  "name": "OSHA 1926 Construction Site Incident Reporter",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "construction-incident",
        "responseMode": "lastNode"
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Code - Classify Incident",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst type = data.incident_type || 'NEAR_MISS';\nconst clocks = {\n  'FATALITY': {deadline: '8 HOURS', clock: 'OSHA 29 CFR \u00a71904.39(a)(1) \u2014 report fatality within 8 hours to OSHA (1-800-321-OSHA or online). Missing deadline = willful violation $15,625/day.', regulator: 'OSHA Area Director', action: 'IMMEDIATE: preserve scene, call 1-800-321-OSHA, notify GC/owner, retain counsel, begin root cause'},\n  'HOSPITALIZATION': {deadline: '8 HOURS', clock: 'OSHA 29 CFR \u00a71904.39(a)(2) \u2014 in-patient hospitalization within 8 hours. Same deadline as fatality.', regulator: 'OSHA Area Director', action: '8 HOURS: report to OSHA, document workers comp, notify insurance carrier, begin incident investigation'},\n  'AMPUTATION': {deadline: '24 HOURS', clock: 'OSHA 29 CFR \u00a71904.39(a)(3) \u2014 amputation or eye loss within 24 hours.', regulator: 'OSHA Area Director', action: '24 HOURS: report to OSHA, preserve evidence, begin \u00a71926.416 energy control review'},\n  'EYE_LOSS': {deadline: '24 HOURS', clock: 'OSHA 29 CFR \u00a71904.39(a)(3) \u2014 loss of eye within 24 hours.', regulator: 'OSHA Area Director', action: '24 HOURS: report to OSHA, review \u00a71926.102 PPE compliance, document eyewash station access'},\n  'RECORDABLE_INJURY': {deadline: '7 DAYS', clock: 'OSHA 29 CFR \u00a71904.29 \u2014 log on Form 300 within 7 calendar days. Annual 300A summary Feb 1 - Apr 30 posting.', regulator: 'OSHA Records', action: '7 DAYS: complete OSHA 300/301 forms, notify workers comp, update incident log'},\n  'FALL_HAZARD': {deadline: 'BEFORE NEXT SHIFT', clock: 'OSHA 29 CFR \u00a71926.502 fall protection \u2014 unguarded edge/opening must be corrected before next shift or competent person must provide constant monitoring.', regulator: 'OSHA Compliance', action: 'BEFORE NEXT SHIFT: erect guardrails or personal fall arrest, document correction, re-inspect'},\n  'NEAR_MISS': {deadline: '24 HOURS INTERNAL', clock: 'No OSHA reporting required but internal review within 24h recommended. Near-miss data drives preventive action.', regulator: 'Internal Safety Officer', action: '24 HOURS: complete near-miss report, identify root cause, implement corrective action'}\n};\nconst clock = clocks[type] || clocks['NEAR_MISS'];\nreturn [{json: {incident_id: data.incident_id || ('INC-' + Date.now()), incident_type: type, project: data.project, location: data.location, worker: data.worker, ...clock, reported_by: data.reported_by, timestamp: new Date().toISOString()}}];"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Slack - #safety-critical",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "operation": "post",
        "channel": "#safety-critical",
        "text": "CONSTRUCTION INCIDENT: {{$json.incident_type}} | Project: {{$json.project}} | Clock: {{$json.deadline}} | Action: {{$json.action}}"
      },
      "position": [
        680,
        200
      ]
    },
    {
      "id": "4",
      "name": "Gmail - Safety Officer + GC",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "safety@yourcompany.com",
        "subject": "OSHA INCIDENT: {{$json.incident_type}} \u2014 {{$json.deadline}} DEADLINE",
        "message": "Incident ID: {{$json.incident_id}}\nType: {{$json.incident_type}}\nProject: {{$json.project}}\nDeadline: {{$json.deadline}}\nClock: {{$json.clock}}\nRegulator: {{$json.regulator}}\nRequired Action: {{$json.action}}\nTimestamp: {{$json.timestamp}}"
      },
      "position": [
        680,
        340
      ]
    },
    {
      "id": "5",
      "name": "Sheets - Incident Log",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "IncidentLog!A:K",
        "dataMode": "autoMapInputData"
      },
      "position": [
        900,
        300
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Code - Classify Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Classify Incident": {
      "main": [
        [
          {
            "node": "Slack - #safety-critical",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail - Safety Officer + GC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - #safety-critical": {
      "main": [
        [
          {
            "node": "Sheets - Incident Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - Safety Officer + GC": {
      "main": [
        [
          {
            "node": "Sheets - Incident Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

OSHA reporting clocks:

  • Fatality → 8 hours → 1-800-321-OSHA or online
  • Hospitalization → 8 hours → same
  • Amputation / Eye Loss → 24 hours → same
  • Recordable Injury (OSHA 300 log) → 7 calendar days
  • Fall Hazard (§1926.502) → corrected before next shift

Workflow 3: Davis-Bacon Certified Payroll Compliance Automation

The audit that reaches three years back. The Davis-Bacon Act (40 USC §3142) requires contractors on federally funded construction projects to pay prevailing wages set by DOL Wage Determinations and submit DOL WH-347 certified payroll weekly. DOL WHD audits routinely reach back 3 years. Willful underpayment means debarment from all federal contracts. Your payroll platform is the record of account — every WHD subpoena goes there first.

{
  "name": "Davis-Bacon Certified Payroll Compliance Automation",
  "nodes": [
    {
      "id": "1",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 5"
            }
          ]
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Sheets - Pull Payroll Data",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "WeeklyPayroll!A:M"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Code - Davis-Bacon Compliance Check",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst today = new Date();\nconst issues = [];\n\nfor (const row of rows) {\n  // Davis-Bacon Act 40 USC \u00a73142 + DOL WHD Wage Determination\n  const wageRate = parseFloat(row.hourly_rate || 0);\n  const prevailingWage = parseFloat(row.prevailing_wage_rate || 0);\n  const fringeActual = parseFloat(row.fringe_benefits || 0);\n  const fringeRequired = parseFloat(row.fringe_required || 0);\n  const weeklyHours = parseFloat(row.hours_worked || 0);\n  const overtimeHours = Math.max(0, weeklyHours - 40);\n  \n  // Check prevailing wage compliance (straight time)\n  if (wageRate < prevailingWage) {\n    issues.push({\n      worker: row.worker_name,\n      trade: row.trade_classification,\n      issue: 'UNDERPAYMENT',\n      detail: `Paid $${wageRate}/hr vs prevailing $${prevailingWage}/hr \u2014 DOL WHD violation 40 USC \u00a73142. Underpayment amount: $${((prevailingWage - wageRate) * weeklyHours).toFixed(2)}/week.`,\n      severity: 'CRITICAL'\n    });\n  }\n  \n  // Check fringe benefit requirements\n  if (fringeActual < fringeRequired) {\n    issues.push({\n      worker: row.worker_name,\n      trade: row.trade_classification,\n      issue: 'FRINGE_SHORTFALL',\n      detail: `Fringe: $${fringeActual}/hr vs required $${fringeRequired}/hr. Can be paid as cash supplement.`,\n      severity: 'WARNING'\n    });\n  }\n  \n  // Check overtime (Davis-Bacon + FLSA \u00a7207 1.5x after 40h on federal work)\n  if (overtimeHours > 0) {\n    const expectedOTRate = prevailingWage * 1.5;\n    const actualOTRate = parseFloat(row.overtime_rate || 0);\n    if (actualOTRate < expectedOTRate) {\n      issues.push({\n        worker: row.worker_name,\n        trade: row.trade_classification,\n        issue: 'OT_UNDERPAYMENT',\n        detail: `OT rate $${actualOTRate}/hr vs required $${expectedOTRate}/hr (1.5x prevailing). CWHSSA 40 USC \u00a73702 violation.`,\n        severity: 'CRITICAL'\n      });\n    }\n  }\n  \n  // WH-347 certified payroll required weekly\n  if (!row.wh347_submitted || row.wh347_submitted !== 'YES') {\n    issues.push({\n      worker: row.worker_name,\n      trade: row.trade_classification,\n      issue: 'WH347_MISSING',\n      detail: 'DOL WH-347 certified payroll not submitted this week. Required weekly for federal/federally-assisted construction contracts.',\n      severity: 'CRITICAL'\n    });\n  }\n}\n\nif (issues.length === 0) {\n  return [{json: {status: 'COMPLIANT', message: 'Davis-Bacon payroll check passed \u2014 all workers at or above prevailing wage', week_ending: today.toISOString().split('T')[0]}}];\n}\nreturn issues.map(i => ({json: {...i, week_ending: today.toISOString().split('T')[0]}}));"
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "IF - Violations Found",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.issue}}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack - #payroll-compliance",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "operation": "post",
        "channel": "#payroll-compliance",
        "text": "DAVIS-BACON VIOLATION: {{$json.issue}} \u2014 Worker: {{$json.worker}} | {{$json.detail}}"
      },
      "position": [
        1120,
        200
      ]
    },
    {
      "id": "6",
      "name": "Gmail - Payroll Officer",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "payroll@yourcompany.com",
        "subject": "Davis-Bacon Violation: {{$json.issue}} \u2014 {{$json.worker}} week ending {{$json.week_ending}}",
        "message": "Issue: {{$json.issue}}\nWorker: {{$json.worker}}\nTrade: {{$json.trade}}\nDetail: {{$json.detail}}\nSeverity: {{$json.severity}}"
      },
      "position": [
        1120,
        380
      ]
    },
    {
      "id": "7",
      "name": "Sheets - Compliance Log",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "DavisBackonLog!A:H",
        "dataMode": "autoMapInputData"
      },
      "position": [
        1340,
        300
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Sheets - Pull Payroll Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets - Pull Payroll Data": {
      "main": [
        [
          {
            "node": "Code - Davis-Bacon Compliance Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Davis-Bacon Compliance Check": {
      "main": [
        [
          {
            "node": "IF - Violations Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Violations Found": {
      "main": [
        [
          {
            "node": "Slack - #payroll-compliance",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail - Payroll Officer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - #payroll-compliance": {
      "main": [
        [
          {
            "node": "Sheets - Compliance Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - Payroll Officer": {
      "main": [
        [
          {
            "node": "Sheets - Compliance Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Davis-Bacon enforcement points:

  • Prevailing wage: DOL Wage Determination by trade classification and county
  • Fringe benefits: required cash equivalent if no bona fide plan
  • CWHSSA overtime: 1.5x prevailing wage after 40h on federal work
  • WH-347: certified payroll due weekly, retained 3 years
  • SAM.gov debarment: active registration required for all federal contractors

Workflow 4: EPA SWPPP Stormwater Inspection and Corrective Action Tracker

$25,000/day for a missed inspection log. The EPA Construction General Permit (CGP) under CWA §402 NPDES requires weekly stormwater inspections during active construction and corrective action within 7 days of finding a deficiency. Missing an inspection or failing to document corrective action is a permit violation. CWA §309 civil penalties run to $25,000/day per violation. Environmental NGOs and state regulators regularly pull inspection records in enforcement actions.

{
  "name": "EPA SWPPP Stormwater Inspection and Corrective Action Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1"
            }
          ]
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Sheets - Pull Inspection Data",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "SWPPPInspections!A:J"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Code - SWPPP Compliance Check",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst today = new Date();\nconst sevenDaysAgo = new Date(today - 7 * 24 * 60 * 60 * 1000);\nconst fourteenDaysAgo = new Date(today - 14 * 24 * 60 * 60 * 1000);\nconst alerts = [];\n\nfor (const row of rows) {\n  const lastInspection = new Date(row.last_inspection_date);\n  const deficiencyDate = row.deficiency_found_date ? new Date(row.deficiency_found_date) : null;\n  const correctedDate = row.corrected_date ? new Date(row.corrected_date) : null;\n  \n  // EPA CGP Part 4.1: weekly inspection required during active construction\n  if (lastInspection < sevenDaysAgo) {\n    const daysMissed = Math.floor((today - lastInspection) / (1000 * 60 * 60 * 24));\n    alerts.push({\n      site: row.site_name,\n      permit: row.permit_number,\n      issue: 'INSPECTION_OVERDUE',\n      detail: `EPA CGP Part 4.1 requires weekly stormwater inspection. Last inspection: ${row.last_inspection_date} (${daysMissed} days ago). CWA \u00a7402 NPDES violation = $25,000/day civil penalty.`,\n      severity: 'CRITICAL'\n    });\n  }\n  \n  // EPA CGP Part 4.2: deficiencies must be corrected within 7 days\n  if (deficiencyDate && !correctedDate) {\n    const daysOpen = Math.floor((today - deficiencyDate) / (1000 * 60 * 60 * 24));\n    if (daysOpen > 7) {\n      alerts.push({\n        site: row.site_name,\n        permit: row.permit_number,\n        issue: 'CORRECTIVE_ACTION_OVERDUE',\n        detail: `EPA CGP Part 4.2 requires deficiency correction within 7 days. Deficiency found ${row.deficiency_found_date} (${daysOpen} days ago). Uncorrected = permit violation. CWA \u00a7309 enforcement.`,\n        severity: 'CRITICAL'\n      });\n    }\n  }\n  \n  // Check BMP (Best Management Practice) installation\n  if (row.bmp_installed !== 'YES') {\n    alerts.push({\n      site: row.site_name,\n      permit: row.permit_number,\n      issue: 'BMP_NOT_INSTALLED',\n      detail: 'EPA CGP Part 2.1 requires BMPs installed before land disturbance begins. Missing BMPs = discharge violation.',\n      severity: 'WARNING'\n    });\n  }\n  \n  // Annual permit review\n  if (row.permit_expiry_date) {\n    const expiry = new Date(row.permit_expiry_date);\n    const daysToExpiry = Math.floor((expiry - today) / (1000 * 60 * 60 * 24));\n    if (daysToExpiry <= 30 && daysToExpiry > 0) {\n      alerts.push({\n        site: row.site_name,\n        permit: row.permit_number,\n        issue: 'PERMIT_EXPIRING',\n        detail: `EPA CGP permit expires ${row.permit_expiry_date} (${daysToExpiry} days). File NOI renewal before expiry.`,\n        severity: 'WARNING'\n      });\n    }\n  }\n}\n\nif (alerts.length === 0) {\n  return [{json: {status: 'COMPLIANT', message: 'All SWPPP sites inspected and BMPs in place', check_date: today.toISOString().split('T')[0]}}];\n}\nreturn alerts.map(a => ({json: {...a, check_date: today.toISOString().split('T')[0]}}));"
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "IF - Violations",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.issue}}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack - #environmental-compliance",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "operation": "post",
        "channel": "#environmental-compliance",
        "text": "SWPPP VIOLATION: {{$json.issue}} | Site: {{$json.site}} | Permit: {{$json.permit}} | {{$json.detail}}"
      },
      "position": [
        1120,
        200
      ]
    },
    {
      "id": "6",
      "name": "Gmail - Environmental Manager",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "environmental@yourcompany.com",
        "subject": "EPA SWPPP Violation: {{$json.issue}} \u2014 {{$json.site}}",
        "message": "Site: {{$json.site}}\nPermit: {{$json.permit}}\nIssue: {{$json.issue}}\nDetail: {{$json.detail}}\nSeverity: {{$json.severity}}\nCheck Date: {{$json.check_date}}"
      },
      "position": [
        1120,
        380
      ]
    },
    {
      "id": "7",
      "name": "Sheets - SWPPP Log",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "SWPPPLog!A:I",
        "dataMode": "autoMapInputData"
      },
      "position": [
        1340,
        300
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Sheets - Pull Inspection Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets - Pull Inspection Data": {
      "main": [
        [
          {
            "node": "Code - SWPPP Compliance Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - SWPPP Compliance Check": {
      "main": [
        [
          {
            "node": "IF - Violations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Violations": {
      "main": [
        [
          {
            "node": "Slack - #environmental-compliance",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail - Environmental Manager",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - #environmental-compliance": {
      "main": [
        [
          {
            "node": "Sheets - SWPPP Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - Environmental Manager": {
      "main": [
        [
          {
            "node": "Sheets - SWPPP Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

EPA CGP stormwater compliance timeline:

  • Weekly inspection: every 7 days during active construction (Part 4.1)
  • Post-storm inspection: within 24h of 0.25" rainfall event
  • Deficiency correction: within 7 days of identification (Part 4.2)
  • SWPPP amendments: within 7 days of change in site conditions
  • NOI renewal: file before permit expiry (typically annual)

Workflow 5: Subcontractor Insurance & License Compliance Monitor

OSHA multi-employer doctrine means the GC is liable for the sub's expired COI. Under OSHA's multi-employer worksite doctrine, a controlling employer (GC) can be cited for a subcontractor's safety violations if the GC reasonably should have known. Expired general liability, workers' comp, or contractor license = GC exposure. This workflow monitors your entire subcontractor database weekly and fires alerts at 30-day, 14-day, and expired thresholds.

{
  "name": "Subcontractor Insurance & License Compliance Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Sheets - Subcontractor Database",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "Subcontractors!A:N"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Code - COI and License Check",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst today = new Date();\nconst alerts = [];\n\nfor (const row of rows) {\n  const sub = row.subcontractor_name;\n  const trade = row.trade;\n  \n  // COI expiry check (Certificate of Insurance)\n  const fields = [\n    {field: 'gl_expiry', label: 'General Liability', min: 30, critical: 7},\n    {field: 'wc_expiry', label: \"Workers' Comp\", min: 30, critical: 7},\n    {field: 'auto_expiry', label: 'Automobile Liability', min: 30, critical: 7},\n    {field: 'umbrella_expiry', label: 'Umbrella/Excess', min: 30, critical: 7},\n    {field: 'license_expiry', label: 'Contractor License', min: 60, critical: 14}\n  ];\n  \n  for (const f of fields) {\n    if (!row[f.field]) continue;\n    const expiry = new Date(row[f.field]);\n    const daysLeft = Math.floor((expiry - today) / (1000 * 60 * 60 * 24));\n    \n    if (daysLeft < 0) {\n      alerts.push({\n        subcontractor: sub, trade,\n        issue: `${f.label.toUpperCase().replace(/ /g,'_')}_EXPIRED`,\n        detail: `${f.label} EXPIRED ${row[f.field]}. Subcontractor must NOT perform work until renewed. GC liability exposure \u2014 OSHA multi-employer doctrine applies.`,\n        severity: 'CRITICAL', days_left: daysLeft\n      });\n    } else if (daysLeft <= f.critical) {\n      alerts.push({\n        subcontractor: sub, trade,\n        issue: `${f.label.toUpperCase().replace(/ /g,'_')}_CRITICAL`,\n        detail: `${f.label} expires ${row[f.field]} (${daysLeft} days). Obtain renewal COI immediately.`,\n        severity: 'CRITICAL', days_left: daysLeft\n      });\n    } else if (daysLeft <= f.min) {\n      alerts.push({\n        subcontractor: sub, trade,\n        issue: `${f.label.toUpperCase().replace(/ /g,'_')}_EXPIRING`,\n        detail: `${f.label} expires ${row[f.field]} (${daysLeft} days). Request updated COI.`,\n        severity: 'WARNING', days_left: daysLeft\n      });\n    }\n  }\n  \n  // Davis-Bacon debarment check\n  if (row.debarment_check_date) {\n    const lastCheck = new Date(row.debarment_check_date);\n    const daysSinceCheck = Math.floor((today - lastCheck) / (1000 * 60 * 60 * 24));\n    if (daysSinceCheck > 90) {\n      alerts.push({\n        subcontractor: sub, trade,\n        issue: 'DEBARMENT_CHECK_OVERDUE',\n        detail: `SAM.gov debarment check last performed ${row.debarment_check_date} (${daysSinceCheck} days ago). Federal contracts require active SAM.gov registration and debarment check. 2 CFR \u00a7180.300.`,\n        severity: 'WARNING', days_left: 0\n      });\n    }\n  }\n}\n\nreturn alerts.length > 0 ? alerts.map(a => ({json: a})) : [{json: {status: 'ALL_COMPLIANT', message: 'All subcontractor insurance and licenses current', check_date: today.toISOString().split('T')[0]}}];"
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "IF - Issues Found",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.subcontractor}}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack - #subcontractor-compliance",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "operation": "post",
        "channel": "#subcontractor-compliance",
        "text": "SUB COMPLIANCE: {{$json.issue}} | Sub: {{$json.subcontractor}} | Trade: {{$json.trade}} | {{$json.detail}}"
      },
      "position": [
        1120,
        200
      ]
    },
    {
      "id": "6",
      "name": "Gmail - Project Manager",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "pm@yourcompany.com",
        "subject": "Sub COI Alert: {{$json.issue}} \u2014 {{$json.subcontractor}}",
        "message": "Subcontractor: {{$json.subcontractor}}\nTrade: {{$json.trade}}\nIssue: {{$json.issue}}\nDetail: {{$json.detail}}\nSeverity: {{$json.severity}}\nDays Left: {{$json.days_left}}"
      },
      "position": [
        1120,
        380
      ]
    },
    {
      "id": "7",
      "name": "Sheets - COI Log",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "COILog!A:J",
        "dataMode": "autoMapInputData"
      },
      "position": [
        1340,
        300
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Sheets - Subcontractor Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets - Subcontractor Database": {
      "main": [
        [
          {
            "node": "Code - COI and License Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - COI and License Check": {
      "main": [
        [
          {
            "node": "IF - Issues Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Issues Found": {
      "main": [
        [
          {
            "node": "Slack - #subcontractor-compliance",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail - Project Manager",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - #subcontractor-compliance": {
      "main": [
        [
          {
            "node": "Sheets - COI Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - Project Manager": {
      "main": [
        [
          {
            "node": "Sheets - COI Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 6 (Bonus): AIA G701 Change Order and Construction Lien Compliance Pipeline

Change orders that aren't documented properly become construction liens. AIA A201-2017 §7.2 requires changes to the contract to be executed as written Change Orders signed by Owner, Contractor, and Architect. Undocumented verbal change orders are the #1 source of construction disputes. This workflow captures every change order event, calculates cumulative contract impact, and fires state-specific lien waiver guidance.

{
  "name": "AIA G701 Change Order and Construction Lien Compliance Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "change-order",
        "responseMode": "lastNode"
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Code - Change Order Classifier",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst amount = parseFloat(data.change_amount || 0);\nconst cumulativeTotal = parseFloat(data.cumulative_changes || 0) + amount;\nconst contractValue = parseFloat(data.original_contract_value || 0);\nconst cumulativePct = contractValue > 0 ? (cumulativeTotal / contractValue * 100).toFixed(1) : 0;\n\n// AIA A201-2017 \u00a77.1: changes require written change order signed by Owner, Contractor, Architect\nconst requiresOwnerApproval = amount > parseFloat(data.minor_change_threshold || 5000);\n\n// Lien waiver requirements vary by state \u2014 track critical states\nconst stateLienRules = {\n  'CA': 'California Civil Code \u00a78132 unconditional lien waiver required before final payment. Conditional waiver allowed for progress payments.',\n  'TX': 'Texas Property Code \u00a753.284 partial lien waiver with each draw. Trapping funds statute \u00a753.026.',\n  'FL': 'Florida Statute \u00a7713.20 lien waiver + notice to owner required. Release within 30 days of final payment.',\n  'NY': 'NY Lien Law \u00a734 trust fund doctrine \u2014 misapplication of funds = criminal liability.',\n  'IL': 'Illinois Mechanics Lien Act \u00a721 \u2014 90-day deadline to file lien after completion.',\n  'DEFAULT': 'Review state mechanics lien statute for lien waiver and notice requirements.'\n};\n\nconst state = data.project_state || 'DEFAULT';\nconst lienRule = stateLienRules[state] || stateLienRules['DEFAULT'];\n\n// Budget alert thresholds\nlet budgetAlert = null;\nif (cumulativePct > 20) budgetAlert = `CRITICAL: Cumulative changes = ${cumulativePct}% of contract value ($${cumulativeTotal.toLocaleString()}). Owner may have grounds to terminate per AIA A201 \u00a714.1.`;\nelse if (cumulativePct > 10) budgetAlert = `WARNING: Cumulative changes = ${cumulativePct}% of contract value. Notify Owner and document scope changes.`;\n\nreturn [{json: {\n  co_number: data.co_number || ('CO-' + Date.now()),\n  project: data.project,\n  change_amount: amount,\n  cumulative_total: cumulativeTotal,\n  cumulative_pct: cumulativePct,\n  requires_owner_approval: requiresOwnerApproval,\n  aia_requirement: requiresOwnerApproval ? 'AIA G701 Change Order: requires Owner + Contractor + Architect signatures per AIA A201-2017 \u00a77.2.' : 'AIA G714 Construction Change Directive or Minor Change by Architect \u2014 \u00a77.3 and \u00a77.4.',\n  lien_rule: lienRule,\n  budget_alert: budgetAlert,\n  timestamp: new Date().toISOString()\n}}];"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Slack - #change-orders",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "operation": "post",
        "channel": "#change-orders",
        "text": "CHANGE ORDER {{$json.co_number}}: ${{$json.change_amount}} | Cumulative: ${{$json.cumulative_total}} ({{$json.cumulative_pct}}%) | {{$json.aia_requirement}}"
      },
      "position": [
        680,
        200
      ]
    },
    {
      "id": "4",
      "name": "Gmail - PM + Owner",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "pm@yourcompany.com",
        "subject": "Change Order {{$json.co_number}}: ${{$json.change_amount}} \u2014 {{$json.project}}",
        "message": "Change Order: {{$json.co_number}}\nAmount: ${{$json.change_amount}}\nCumulative Total: ${{$json.cumulative_total}} ({{$json.cumulative_pct}}% of contract)\n\nAIA Requirement:\n{{$json.aia_requirement}}\n\nState Lien Rule:\n{{$json.lien_rule}}\n\n{{$json.budget_alert}}"
      },
      "position": [
        680,
        340
      ]
    },
    {
      "id": "5",
      "name": "Sheets - Change Order Log",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "spreadsheetId": "YOUR_SHEET_ID",
        "range": "ChangeOrders!A:K",
        "dataMode": "autoMapInputData"
      },
      "position": [
        900,
        300
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Code - Change Order Classifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Change Order Classifier": {
      "main": [
        [
          {
            "node": "Slack - #change-orders",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail - PM + Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - #change-orders": {
      "main": [
        [
          {
            "node": "Sheets - Change Order Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - PM + Owner": {
      "main": [
        [
          {
            "node": "Sheets - Change Order Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Construction lien law by state:

  • California: Civil Code §8132 unconditional waiver before final payment
  • Texas: Property Code §53.284 + trapping funds statute §53.026
  • Florida: FS §713.20 + Notice to Owner required
  • New York: Lien Law §34 trust fund doctrine — misapplication = criminal
  • Illinois: IMLA §21 — 90-day deadline to file lien after completion

The Subpoena Risk Your ConstructionTech Platform Carries

When OSHA investigates a fall fatality, the agency subpoenas the GC's project management platform. When DOL WHD audits a Davis-Bacon violation, they request 3 years of payroll records from your SaaS. When EPA pursues a NPDES stormwater enforcement action, inspection logs from your platform are Exhibit A.

If those records flow through a cloud automation layer (Zapier, Make, n8n Cloud), that automation vendor is also in the subpoena chain. The question every ConstructionTech SaaS vendor should be asking: does your automation audit trail pass a DOL, OSHA, or EPA review?

These 5 n8n workflows are designed to make sure the answer is yes.


All workflows available at stripeai.gumroad.com — grab the full bundle or individual templates.

FlowKit — n8n automation templates for regulated industries.

Top comments (0)