n8n for HRTech/WorkforceTech SaaS Vendors: 5 Automations for FCRA, I-9, EEOC Disparate Impact, and FLSA Worker Classification
Self-hosting angle: FCRA adverse action report data, I-9 immigration records, and EEOC hiring analytics routed through cloud iPaaS nodes are accessible to your cloud automation vendor under their standard ToS. A CRA audit or EEOC investigation subpoena can reach vendor records outside your litigation privilege boundary. Self-hosted n8n keeps all sensitive employment data inside your compliance perimeter.
This article covers 5 production-ready n8n workflows for HRTech SaaS vendors, background screening platforms, workforce management software, employer of record (EOR) companies, and immigration compliance tools — focused on the employment compliance clocks most likely to generate class action exposure: FCRA adverse action timing, I-9 re-verification windows, EEOC 80% rule monitoring, FLSA worker classification, and ADA/NLRA protective logging.
The Compliance Clocks That Define Employment Litigation Risk
| Clock | Regulation | Window | Miss = |
|---|---|---|---|
| FCRA Pre-Adverse Action | 15 U.S.C. §615 | 5 business days | Class action exposure — Spokeo, TransUnion LLC v. Ramirez |
| I-9 Re-Verification | 8 CFR §274a.2(b)(1)(vii) | On or before work auth expiry | ICE civil fine $272–$2,701/employee, criminal if pattern |
| EEOC 80% Rule | 29 CFR §1607.4(D) | Ongoing — hire-cycle analysis | Disparate impact litigation, OFCCP audit trigger |
| FLSA §13 Classification | 29 CFR §541 | Before next pay period | 2-3yr back pay + liquidated damages + DOL WHD |
| ADA Interactive Process | 42 U.S.C. §12111 | Immediate — 10-day initial response | ADA Title I failure-to-accommodate + litigation |
| NLRA §7 Protected Activity | 29 U.S.C. §157 | 6-month SOL to NLRB charge | ULP charge, reinstatement + back pay |
Customer Tier Matrix
| Tier | Primary Compliance Exposure |
|---|---|
| BACKGROUND_SCREENING_SAAS_VENDOR | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
| APPLICANT_TRACKING_SAAS_VENDOR | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
| WORKFORCE_MANAGEMENT_SAAS | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
| EMPLOYER_OF_RECORD_SAAS | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
| IMMIGRATION_COMPLIANCE_SAAS | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
| COMPENSATION_ANALYTICS_SAAS | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
| HRTECH_STARTUP | FCRA/EEOC/I-9/FLSA/ADA/NLRA |
Workflow 1: FCRA Adverse Action Notification Pipeline
Compliance target: FCRA §604 (permissible purpose) + §615 (adverse action notice) + §607 (reasonable procedures)
The clock: 5 business days between pre-adverse action notice and final adverse action decision. Miss it and every affected applicant has individual statutory damages of $100–$1,000 plus punitive damages — class actions settle at $2M–$15M+.
{
"name": "FCRA Adverse Action Notification Pipeline",
"nodes": [
{
"id": "n1",
"name": "Background Check Result Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {
"path": "bgcheck-result",
"responseMode": "onReceived"
}
},
{
"id": "n2",
"name": "Validate Permissible Purpose \u00a7604",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
450,
300
],
"parameters": {
"jsCode": "const d = $input.first().json.body;\nconst permissiblePurposes = ['employment','credit','insurance','housing','licensing'];\nconst purpose = d.permissible_purpose || 'employment';\nconst isValid = permissiblePurposes.includes(purpose);\nconst isAdverseAction = d.status === 'adverse_action_pending';\nreturn [{ json: { ...d, permissiblePurposeValid: isValid, isAdverseAction, adverseActionDeadlineTs: new Date(Date.now() + 5*24*60*60*1000).toISOString(), purpose } }];"
}
},
{
"id": "n3",
"name": "IF Adverse Action Pending",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
650,
300
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true
},
"conditions": [
{
"leftValue": "={{ $json.isAdverseAction }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
}
}
},
{
"id": "n4",
"name": "Send Pre-Adverse Action Notice + CFPB Summary of Rights",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [
850,
200
],
"parameters": {
"sendTo": "={{ $json.applicant_email }}",
"subject": "Important Notice Regarding Your Background Check \u2014 Action Required",
"message": "={{ 'Dear ' + $json.applicant_name + ',\\n\\nPursuant to the Fair Credit Reporting Act (FCRA) 15 U.S.C. \u00a7615, we are providing you with a pre-adverse action notice.\\n\\nAttached: (1) A copy of your consumer report, (2) CFPB Summary of Rights Under the FCRA.\\n\\nYou have 5 business days to review this report and dispute any inaccuracies with the Consumer Reporting Agency before a final decision is made.\\n\\nReport provided by: ' + ($json.cra_name || 'the Consumer Reporting Agency') + '\\nJob Position: ' + $json.job_title + '\\nApplication ID: ' + $json.application_id }}",
"options": {}
}
},
{
"id": "n5",
"name": "Log to Adverse Action Tracker",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1050,
200
],
"parameters": {
"operation": "append",
"documentId": "={{ $vars.ADVERSE_ACTION_SHEET_ID }}",
"sheetName": "adverse_action_log",
"columns": {
"mappingMode": "defineBelow",
"value": {
"applicant_id": "={{ $json.applicant_id }}",
"applicant_name": "={{ $json.applicant_name }}",
"job_title": "={{ $json.job_title }}",
"pre_notice_sent_at": "={{ $now.toISO() }}",
"five_day_deadline": "={{ $json.adverseActionDeadlineTs }}",
"cra_name": "={{ $json.cra_name }}",
"status": "PRE_ADVERSE_NOTICE_SENT"
}
}
}
},
{
"id": "n6",
"name": "Wait 5 Business Days",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
1250,
200
],
"parameters": {
"amount": 5,
"unit": "days"
}
},
{
"id": "n7",
"name": "Check Dispute Filed",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1450,
200
],
"parameters": {
"operation": "read",
"documentId": "={{ $vars.ADVERSE_ACTION_SHEET_ID }}",
"sheetName": "adverse_action_log",
"filtersUI": {
"values": [
{
"lookupColumn": "applicant_id",
"lookupValue": "={{ $json.applicant_id }}"
}
]
}
}
},
{
"id": "n8",
"name": "IF Dispute Filed",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1650,
200
],
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.dispute_filed }}",
"rightValue": "true",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
}
},
{
"id": "n9",
"name": "Route to Legal Review Queue \u2014 Dispute Active",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [
1850,
100
],
"parameters": {
"channel": "#fcra-disputes",
"text": "={{ '\u26a0\ufe0f FCRA DISPUTE FILED \u2014 ' + $json.applicant_name + ' | Application: ' + $json.application_id + ' | Dispute must be resolved before final adverse action. \u00a7611 reinvestigation within 30 days.' }}"
}
},
{
"id": "n10",
"name": "Send Final Adverse Action Notice",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [
1850,
300
],
"parameters": {
"sendTo": "={{ $json.applicant_email }}",
"subject": "Final Employment Decision Notice \u2014 FCRA \u00a7615",
"message": "={{ 'Dear ' + $json.applicant_name + ',\\n\\nAfter the 5-business-day review period, we have made a final employment decision regarding your application for ' + $json.job_title + '.\\n\\nThis decision was based in part on information obtained from a consumer reporting agency under the FCRA (15 U.S.C. \u00a7615).\\n\\nConsumer Reporting Agency: ' + ($json.cra_name || 'N/A') + '\\n\\nYou have the right to obtain a free copy of your report and dispute inaccurate information with the CRA within 60 days.' }}",
"options": {}
}
},
{
"id": "n11",
"name": "Log Final Decision",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
2050,
300
],
"parameters": {
"operation": "update",
"documentId": "={{ $vars.ADVERSE_ACTION_SHEET_ID }}",
"sheetName": "adverse_action_log",
"columns": {
"mappingMode": "defineBelow",
"value": {
"status": "FINAL_ADVERSE_ACTION_SENT",
"final_decision_at": "={{ $now.toISO() }}",
"applicant_id": "={{ $json.applicant_id }}"
}
}
}
}
],
"connections": {
"Background Check Result Webhook": {
"main": [
[
{
"node": "Validate Permissible Purpose \u00a7604",
"type": "main",
"index": 0
}
]
]
},
"Validate Permissible Purpose \u00a7604": {
"main": [
[
{
"node": "IF Adverse Action Pending",
"type": "main",
"index": 0
}
]
]
},
"IF Adverse Action Pending": {
"main": [
[
{
"node": "Send Pre-Adverse Action Notice + CFPB Summary of Rights",
"type": "main",
"index": 0
}
],
[]
]
},
"Send Pre-Adverse Action Notice + CFPB Summary of Rights": {
"main": [
[
{
"node": "Log to Adverse Action Tracker",
"type": "main",
"index": 0
}
]
]
},
"Log to Adverse Action Tracker": {
"main": [
[
{
"node": "Wait 5 Business Days",
"type": "main",
"index": 0
}
]
]
},
"Wait 5 Business Days": {
"main": [
[
{
"node": "Check Dispute Filed",
"type": "main",
"index": 0
}
]
]
},
"Check Dispute Filed": {
"main": [
[
{
"node": "IF Dispute Filed",
"type": "main",
"index": 0
}
]
]
},
"IF Dispute Filed": {
"main": [
[
{
"node": "Route to Legal Review Queue \u2014 Dispute Active",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Final Adverse Action Notice",
"type": "main",
"index": 0
}
]
]
},
"Send Final Adverse Action Notice": {
"main": [
[
{
"node": "Log Final Decision",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
What it does: Webhook receives background check result → validates FCRA §604 permissible purpose → sends Pre-Adverse Action Notice + CFPB Summary of Rights to applicant → logs to Sheets with 5-business-day countdown → waits → checks for dispute → routes to legal review queue if dispute filed, else sends Final Adverse Action Notice → logs outcome.
Why this matters for HRTech SaaS: Every background screening SaaS vendor whose customers miss FCRA adverse action timing is one class-action plaintiff away from reputational damage that follows them into every enterprise sales cycle. Building this into your platform as an automated safeguard converts a liability into a selling point.
Workflow 2: I-9 E-Verify Tracking & Re-Verification Pipeline
Compliance target: Immigration Reform and Control Act 8 U.S.C. §1324a + 8 CFR §274a.2
The clock: Section 3 re-verification must occur on or before the work authorization document expiry date — not after. ICE audits can reach 3 years of I-9 records. Civil penalties: $272–$2,701 per unauthorized employee (first offense); $542–$5,404 (second).
{
"name": "I-9 E-Verify Tracking & Re-Verification Pipeline",
"nodes": [
{
"id": "n1",
"name": "Daily Weekday 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1-5"
}
]
}
}
},
{
"id": "n2",
"name": "Read Employee I-9 Records",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
450,
300
],
"parameters": {
"operation": "readAllRows",
"documentId": "={{ $vars.I9_SHEET_ID }}",
"sheetName": "i9_records"
}
},
{
"id": "n3",
"name": "Classify I-9 Status & Retention",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
650,
300
],
"parameters": {
"jsCode": "const items = $input.all();\nconst today = new Date();\nconst results = [];\nfor (const item of items) {\n const d = item.json;\n const hireDate = new Date(d.hire_date);\n const termDate = d.termination_date ? new Date(d.termination_date) : null;\n // I-9 retention: 3yr from hire OR 1yr post-termination, whichever is later\n const retentionDate = termDate\n ? new Date(Math.max(new Date(hireDate.getTime() + 3*365*24*3600*1000), new Date(termDate.getTime() + 1*365*24*3600*1000)))\n : new Date(hireDate.getTime() + 3*365*24*3600*1000);\n const daysToRetention = Math.floor((retentionDate - today) / (24*3600*1000));\n // Work authorization re-verification\n const workAuthExpiry = d.work_auth_expiry ? new Date(d.work_auth_expiry) : null;\n const daysToWorkAuthExpiry = workAuthExpiry ? Math.floor((workAuthExpiry - today) / (24*3600*1000)) : null;\n let status = 'OK';\n let urgency = 'NONE';\n if (daysToRetention < 0) { status = 'RETENTION_EXPIRED'; urgency = 'CRITICAL'; }\n else if (daysToRetention <= 30) { status = 'RETENTION_EXPIRING'; urgency = 'WARNING'; }\n if (workAuthExpiry) {\n if (daysToWorkAuthExpiry <= 0) { status = 'WORK_AUTH_EXPIRED'; urgency = 'CRITICAL'; }\n else if (daysToWorkAuthExpiry <= 14) { urgency = 'CRITICAL'; status = 'REVERIFY_REQUIRED_14D'; }\n else if (daysToWorkAuthExpiry <= 30) { urgency = 'WARNING'; status = 'REVERIFY_DUE_30D'; }\n else if (daysToWorkAuthExpiry <= 90) { urgency = 'NOTICE'; status = 'REVERIFY_DUE_90D'; }\n }\n if (urgency !== 'NONE') results.push({ json: { ...d, status, urgency, daysToRetention, daysToWorkAuthExpiry, retentionDate: retentionDate.toISOString() } });\n}\nreturn results;"
}
},
{
"id": "n4",
"name": "IF Critical I-9 Action Required",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
850,
300
],
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.urgency }}",
"rightValue": "CRITICAL",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
}
},
{
"id": "n5",
"name": "Slack #i9-compliance \u2014 CRITICAL",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [
1050,
200
],
"parameters": {
"channel": "#i9-compliance",
"text": "={{ '\ud83d\udea8 I-9 CRITICAL \u2014 ' + $json.employee_name + ' | Status: ' + $json.status + ' | Work Auth Expiry: ' + ($json.work_auth_expiry || 'N/A') + ' | Days remaining: ' + ($json.daysToWorkAuthExpiry || 'N/A') + ' | ICE audit exposure if not resolved. Re-verify using List A or C on or before expiry date per 8 CFR \u00a7274a.2(b)(1)(vii).' }}"
}
},
{
"id": "n6",
"name": "Gmail HR Compliance Owner",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [
1050,
400
],
"parameters": {
"sendTo": "={{ $vars.HR_COMPLIANCE_EMAIL }}",
"subject": "={{ '[I-9 ' + $json.urgency + '] ' + $json.employee_name + ' \u2014 ' + $json.status }}",
"message": "={{ 'Employee: ' + $json.employee_name + '\\nEmployee ID: ' + $json.employee_id + '\\nStatus: ' + $json.status + '\\nWork Authorization Expiry: ' + ($json.work_auth_expiry || 'N/A') + '\\nDays Remaining: ' + ($json.daysToWorkAuthExpiry || 'N/A') + '\\nI-9 Retention Deadline: ' + $json.retentionDate + '\\n\\nAction Required: Complete Section 3 re-verification using an unexpired List A or List C document. Do NOT re-verify List B documents. Per 8 CFR \u00a7274a.2(b)(1)(vii), re-verification must occur on or before the document expiry date.' }}",
"options": {}
}
},
{
"id": "n7",
"name": "Log I-9 Action to Compliance Sheet",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1250,
300
],
"parameters": {
"operation": "append",
"documentId": "={{ $vars.I9_SHEET_ID }}",
"sheetName": "i9_action_log",
"columns": {
"mappingMode": "defineBelow",
"value": {
"employee_id": "={{ $json.employee_id }}",
"employee_name": "={{ $json.employee_name }}",
"status": "={{ $json.status }}",
"urgency": "={{ $json.urgency }}",
"action_date": "={{ $now.toISO() }}",
"work_auth_expiry": "={{ $json.work_auth_expiry }}",
"days_to_expiry": "={{ $json.daysToWorkAuthExpiry }}"
}
}
}
}
],
"connections": {
"Daily Weekday 8AM": {
"main": [
[
{
"node": "Read Employee I-9 Records",
"type": "main",
"index": 0
}
]
]
},
"Read Employee I-9 Records": {
"main": [
[
{
"node": "Classify I-9 Status & Retention",
"type": "main",
"index": 0
}
]
]
},
"Classify I-9 Status & Retention": {
"main": [
[
{
"node": "IF Critical I-9 Action Required",
"type": "main",
"index": 0
}
]
]
},
"IF Critical I-9 Action Required": {
"main": [
[
{
"node": "Slack #i9-compliance \u2014 CRITICAL",
"type": "main",
"index": 0
}
],
[
{
"node": "Gmail HR Compliance Owner",
"type": "main",
"index": 0
}
]
]
},
"Slack #i9-compliance \u2014 CRITICAL": {
"main": [
[
{
"node": "Log I-9 Action to Compliance Sheet",
"type": "main",
"index": 0
}
]
]
},
"Gmail HR Compliance Owner": {
"main": [
[
{
"node": "Log I-9 Action to Compliance Sheet",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
What it does: Runs daily on weekdays → reads all employee I-9 records → calculates IRCA retention periods (3yr from hire OR 1yr post-termination, whichever is later) → flags work authorization expiries at 90/30/14-day thresholds → alerts Slack #i9-compliance for CRITICAL cases → emails HR compliance owner with specific CFR citation and action instructions → logs all actions.
Key nuance: The workflow correctly handles the List B re-verification prohibition — IRCA prohibits re-verifying List B documents (state ID, driver's license); only List A (passport, EAD) or List C (Social Security card, birth certificate) can be used for Section 3. This is the most common I-9 audit finding.
Workflow 3: EEOC Disparate Impact Monitor — 80% Rule
Compliance target: EEOC Title VII §2000e + Uniform Guidelines on Employee Selection Procedures 29 CFR §1607.4(D)
The test: Adverse impact ratio = (selection rate of protected class) / (selection rate of highest-selected group). Below 0.80 = potential disparate impact. EEOC and OFCCP both use this trigger. Federal contractors face AAP obligations under Executive Order 11246.
{
"name": "EEOC Disparate Impact Monitor \u2014 80% Rule",
"nodes": [
{
"id": "n1",
"name": "Weekly Monday 7AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 7 * * 1"
}
]
}
}
},
{
"id": "n2",
"name": "Query Hiring Decisions by Job Category",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
450,
300
],
"parameters": {
"operation": "executeQuery",
"query": "SELECT job_category, protected_class, COUNT(*) as applicants, SUM(CASE WHEN selected THEN 1 ELSE 0 END) as selected FROM hiring_decisions WHERE decision_date >= NOW() - INTERVAL '90 days' GROUP BY job_category, protected_class ORDER BY job_category, protected_class"
}
},
{
"id": "n3",
"name": "Compute 80% Adverse Impact Ratio",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
650,
300
],
"parameters": {
"jsCode": "const items = $input.all();\n// Group by job_category\nconst byCategory = {};\nfor (const item of items) {\n const { job_category, protected_class, applicants, selected } = item.json;\n if (!byCategory[job_category]) byCategory[job_category] = {};\n byCategory[job_category][protected_class] = { applicants: parseInt(applicants), selected: parseInt(selected) };\n}\nconst alerts = [];\nfor (const [job_category, classes] of Object.entries(byCategory)) {\n // Find highest selection rate (comparison group)\n let maxRate = 0;\n let maxClass = '';\n for (const [cls, data] of Object.entries(classes)) {\n const rate = data.applicants > 0 ? data.selected / data.applicants : 0;\n if (rate > maxRate) { maxRate = rate; maxClass = cls; }\n }\n // Check 80% rule for all other groups\n for (const [cls, data] of Object.entries(classes)) {\n if (cls === maxClass) continue;\n const rate = data.applicants > 0 ? data.selected / data.applicants : 0;\n const adverseImpactRatio = maxRate > 0 ? rate / maxRate : 1;\n const adverseImpact = adverseImpactRatio < 0.8 && data.applicants >= 30; // min sample\n if (adverseImpact) {\n alerts.push({ json: { job_category, protected_class: cls, applicants: data.applicants, selected: data.selected, selectionRate: rate.toFixed(4), maxClass, maxRate: maxRate.toFixed(4), adverseImpactRatio: adverseImpactRatio.toFixed(4), severity: adverseImpactRatio < 0.5 ? 'CRITICAL' : 'WARNING' } });\n }\n }\n}\nreturn alerts.length > 0 ? alerts : [{ json: { noAlerts: true, message: 'No disparate impact detected this week.' } }];"
}
},
{
"id": "n4",
"name": "IF Disparate Impact Detected",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
850,
300
],
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.noAlerts }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals"
}
}
]
}
}
},
{
"id": "n5",
"name": "Slack #eeoc-compliance \u2014 Disparate Impact Alert",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [
1050,
200
],
"parameters": {
"channel": "#eeoc-compliance",
"text": "={{ '\u26a0\ufe0f EEOC TITLE VII DISPARATE IMPACT \u2014 ' + $json.job_category + ' | Protected Class: ' + $json.protected_class + ' | Selection Rate: ' + ($json.selectionRate*100).toFixed(1) + '% vs ' + $json.maxClass + ': ' + ($json.maxRate*100).toFixed(1) + '% | Adverse Impact Ratio: ' + $json.adverseImpactRatio + ' (< 0.80 = EEOC 80% rule violation) | Applicants analyzed: ' + $json.applicants + ' | Severity: ' + $json.severity + '. Review selection criteria under EEOC Uniform Guidelines 29 CFR \u00a71607 before next hiring cycle.' }}"
}
},
{
"id": "n6",
"name": "Gmail EEO Officer + Legal Counsel",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [
1050,
400
],
"parameters": {
"sendTo": "={{ $vars.EEO_OFFICER_EMAIL }}",
"subject": "={{ '[EEOC ' + $json.severity + '] Disparate Impact Detected \u2014 ' + $json.job_category }}",
"message": "={{ 'EEOC Title VII \u00a72000e Disparate Impact Monitor Report\\n\\nJob Category: ' + $json.job_category + '\\nProtected Class with Lower Selection Rate: ' + $json.protected_class + '\\nSelection Rate: ' + ($json.selectionRate*100).toFixed(1) + '%\\nHighest Selection Rate (comparison group ' + $json.maxClass + '): ' + ($json.maxRate*100).toFixed(1) + '%\\nAdverse Impact Ratio: ' + $json.adverseImpactRatio + '\\n\\nEEOC 80% Rule: An adverse impact ratio below 0.80 indicates potential disparate impact under EEOC Uniform Guidelines 29 CFR \u00a71607.4(D).\\n\\nRecommended Action:\\n1. Review selection criteria for job-relatedness and business necessity\\n2. Document legitimate, non-discriminatory reasons for selection decisions\\n3. Consult legal counsel before next hiring cycle\\n4. Consider validation study if criteria continue to produce disparate impact\\n\\nNote: This is a statistical indicator, not a legal determination. Patterns with fewer than 30 applicants per class are excluded from this report.' }}",
"options": {}
}
},
{
"id": "n7",
"name": "Log to EEOC Audit Trail",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1250,
300
],
"parameters": {
"operation": "append",
"documentId": "={{ $vars.EEOC_SHEET_ID }}",
"sheetName": "disparate_impact_log",
"columns": {
"mappingMode": "defineBelow",
"value": {
"detected_at": "={{ $now.toISO() }}",
"job_category": "={{ $json.job_category }}",
"protected_class": "={{ $json.protected_class }}",
"adverse_impact_ratio": "={{ $json.adverseImpactRatio }}",
"applicants": "={{ $json.applicants }}",
"severity": "={{ $json.severity }}"
}
}
}
}
],
"connections": {
"Weekly Monday 7AM": {
"main": [
[
{
"node": "Query Hiring Decisions by Job Category",
"type": "main",
"index": 0
}
]
]
},
"Query Hiring Decisions by Job Category": {
"main": [
[
{
"node": "Compute 80% Adverse Impact Ratio",
"type": "main",
"index": 0
}
]
]
},
"Compute 80% Adverse Impact Ratio": {
"main": [
[
{
"node": "IF Disparate Impact Detected",
"type": "main",
"index": 0
}
]
]
},
"IF Disparate Impact Detected": {
"main": [
[
{
"node": "Slack #eeoc-compliance \u2014 Disparate Impact Alert",
"type": "main",
"index": 0
}
],
[]
]
},
"Slack #eeoc-compliance \u2014 Disparate Impact Alert": {
"main": [
[
{
"node": "Gmail EEO Officer + Legal Counsel",
"type": "main",
"index": 0
}
]
]
},
"Gmail EEO Officer + Legal Counsel": {
"main": [
[
{
"node": "Log to EEOC Audit Trail",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
What it does: Runs weekly Monday 7AM → queries hiring decisions by job category and protected class → computes 80% adverse impact ratio using EEOC Uniform Guidelines formula → filters for statistical significance (minimum 30 applicants) → alerts Slack #eeoc-compliance + emails EEO Officer with specific §1607 citation → logs to EEOC audit trail for annual EEO-1 reporting.
Enterprise selling point: Federal contractors ($50K+ government contracts) face OFCCP Scheduling Letter audits that request 2 years of applicant flow data. Having a real-time disparate impact monitor that produces audit-ready logs is a procurement requirement for HR platforms serving federal contractors — not a nice-to-have.
Workflow 4: FLSA Worker Classification Alert — §13 Exemption Checker
Compliance target: Fair Labor Standards Act §13 white-collar exemptions + 29 CFR §541 + state-specific salary thresholds
The exposure: FLSA misclassification: 2-year back pay + equal liquidated damages. Willful misclassification: 3-year SOL. DOL WHD collected $274M in back wages in FY2023 — misclassification is the #1 finding.
{
"name": "FLSA Worker Classification Alert \u2014 \u00a713 Exemption Checker",
"nodes": [
{
"id": "n1",
"name": "New Hire or Role Change Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {
"path": "worker-classification",
"responseMode": "onReceived"
}
},
{
"id": "n2",
"name": "FLSA \u00a713 Exemption Analysis",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
450,
300
],
"parameters": {
"jsCode": "const d = $input.first().json.body;\n// FLSA \u00a713 white-collar exemptions require ALL criteria\nconst weeklyPay = parseFloat(d.weekly_salary || 0);\nconst annualPay = weeklyPay * 52;\n// Federal salary threshold: $684/wk ($35,568/yr) DOL 29 CFR \u00a7541\nconst fedThreshold = 684;\n// State-specific overrides (higher wins)\nconst stateThresholds = { 'CA': 1040, 'NY': 1125, 'WA': 975, 'CO': 961, 'AK': 867 };\nconst stateThreshold = stateThresholds[d.state] || fedThreshold;\nconst effectiveThreshold = Math.max(fedThreshold, stateThreshold);\nconst meetsSalaryBasis = weeklyPay >= effectiveThreshold;\n// Duty tests (simplified \u2014 must be documented)\nconst duties = d.job_duties || [];\nconst isExecutive = meetsSalaryBasis && duties.includes('management') && duties.includes('hires_fires') && (d.direct_reports || 0) >= 2;\nconst isAdministrative = meetsSalaryBasis && duties.includes('office_nonmanual') && duties.includes('discretion_judgment');\nconst isProfessional = meetsSalaryBasis && (duties.includes('advanced_knowledge') || duties.includes('creative_professional'));\nconst isHCE = annualPay >= 107432 && duties.includes('office_nonmanual');\nconst isExempt = isExecutive || isAdministrative || isProfessional || isHCE;\nconst exemptionType = isExempt ? (isExecutive ? 'EXECUTIVE' : isAdministrative ? 'ADMINISTRATIVE' : isProfessional ? 'PROFESSIONAL' : 'HIGHLY_COMPENSATED') : 'NONEXEMPT';\nconst riskLevel = isExempt ? 'LOW' : (weeklyPay < effectiveThreshold * 0.9 ? 'HIGH' : 'MEDIUM');\nreturn [{ json: { ...d, weeklyPay, annualPay, effectiveThreshold, meetsSalaryBasis, isExempt, exemptionType, riskLevel, stateApplied: d.state, stateThreshold, misclassificationRisk: !isExempt && d.current_classification === 'exempt' } }];"
}
},
{
"id": "n3",
"name": "IF Misclassification Risk",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
650,
300
],
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.misclassificationRisk }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
}
}
},
{
"id": "n4",
"name": "Slack #hr-compliance \u2014 FLSA Risk",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [
850,
200
],
"parameters": {
"channel": "#hr-compliance",
"text": "={{ '\ud83d\udea8 FLSA MISCLASSIFICATION RISK \u2014 ' + $json.employee_name + ' | Current Classification: ' + $json.current_classification + ' | FLSA Analysis: NONEXEMPT | Weekly Pay: $' + $json.weeklyPay + ' (threshold: $' + $json.effectiveThreshold + '/wk ' + ($json.stateApplied || 'Federal') + ') | Risk: ' + $json.riskLevel + '. FLSA \u00a7207 overtime liability applies. Review before next pay period.' }}"
}
},
{
"id": "n5",
"name": "Gmail Payroll Manager",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [
850,
400
],
"parameters": {
"sendTo": "={{ $vars.PAYROLL_MANAGER_EMAIL }}",
"subject": "={{ '[FLSA RISK] Potential Misclassification \u2014 ' + $json.employee_name }}",
"message": "={{ 'FLSA Worker Classification Alert\\n\\nEmployee: ' + $json.employee_name + '\\nPosition: ' + $json.job_title + '\\nCurrent Classification: ' + $json.current_classification + '\\nFLSA Analysis Result: ' + $json.exemptionType + '\\n\\nSalary Basis Test:\\n- Weekly Pay: $' + $json.weeklyPay + '\\n- Required Threshold: $' + $json.effectiveThreshold + '/week (' + ($json.stateApplied || 'Federal') + ' FLSA)\\n- Meets Salary Basis: ' + $json.meetsSalaryBasis + '\\n\\nDuties Test: See HR file\\n\\nRisk Level: ' + $json.riskLevel + '\\n\\nAction Required: Review classification against 29 CFR \u00a7541 before next pay period. Misclassification exposes the company to FLSA \u00a7207 overtime back-pay, liquidated damages (equal to back pay), and DOL WHD enforcement. Statute of limitations: 2 years (willful: 3 years).' }}",
"options": {}
}
},
{
"id": "n6",
"name": "Log to Classification Audit",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1050,
300
],
"parameters": {
"operation": "append",
"documentId": "={{ $vars.FLSA_AUDIT_SHEET_ID }}",
"sheetName": "classification_audit",
"columns": {
"mappingMode": "defineBelow",
"value": {
"employee_id": "={{ $json.employee_id }}",
"employee_name": "={{ $json.employee_name }}",
"current_classification": "={{ $json.current_classification }}",
"flsa_analysis": "={{ $json.exemptionType }}",
"risk_level": "={{ $json.riskLevel }}",
"weekly_pay": "={{ $json.weeklyPay }}",
"effective_threshold": "={{ $json.effectiveThreshold }}",
"flagged_at": "={{ $now.toISO() }}"
}
}
}
}
],
"connections": {
"New Hire or Role Change Webhook": {
"main": [
[
{
"node": "FLSA \u00a713 Exemption Analysis",
"type": "main",
"index": 0
}
]
]
},
"FLSA \u00a713 Exemption Analysis": {
"main": [
[
{
"node": "IF Misclassification Risk",
"type": "main",
"index": 0
}
]
]
},
"IF Misclassification Risk": {
"main": [
[
{
"node": "Slack #hr-compliance \u2014 FLSA Risk",
"type": "main",
"index": 0
}
],
[]
]
},
"Slack #hr-compliance \u2014 FLSA Risk": {
"main": [
[
{
"node": "Gmail Payroll Manager",
"type": "main",
"index": 0
}
]
]
},
"Gmail Payroll Manager": {
"main": [
[
{
"node": "Log to Classification Audit",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
What it does: Webhook fires on new hire or role change → FLSA §13 analysis checks salary basis test against federal ($684/wk) AND state-specific thresholds (CA $1,040/wk, NY $1,125/wk, WA $975/wk, CO $961/wk) → applies duties test for executive/administrative/professional/HCE exemptions → flags misclassification risk → alerts #hr-compliance + emails payroll manager with specific CFR citations and 29 CFR §541 duties criteria → logs to classification audit trail.
Differentiation for workforce management SaaS: Scheduling platforms that manage exempt/non-exempt shift assignments carry indirect FLSA risk if their scheduling logic treats misclassified workers as exempt from overtime caps. This workflow adds a classification gate before schedule generation.
Workflow 5: ADA Accommodation & NLRA Protected Activity Pipeline
Compliance target: Americans with Disabilities Act §12111 + NLRA §7 protected concerted activity
Two distinct legal obligations: ADA requires good-faith interactive process (IMMEDIATE trigger, 10-day best practice for initial response). NLRA §7 protects wage discussions, working condition complaints, and union organizing — any adverse action within 6 months of protected activity creates NLRB charge exposure.
{
"name": "ADA Accommodation & NLRA Protected Activity Pipeline",
"nodes": [
{
"id": "n1",
"name": "Accommodation or Activity Report Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {
"path": "hr-activity-report",
"responseMode": "onReceived"
}
},
{
"id": "n2",
"name": "Classify ADA vs NLRA Event",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
450,
300
],
"parameters": {
"jsCode": "const d = $input.first().json.body;\nconst isADA = d.event_type === 'accommodation_request' || d.event_type === 'disability_disclosure' || d.event_type === 'medical_leave_ada';\nconst isNLRA = d.event_type === 'protected_concerted_activity' || d.event_type === 'union_organizing' || d.event_type === 'wage_discussion' || d.event_type === 'working_condition_complaint';\n// ADA interactive process must begin immediately \u00a712111(9)\nconst adaDeadline = new Date(Date.now() + 10*24*3600*1000).toISOString(); // 10-day initial response best practice\n// NLRA \u00a77: employees have right to discuss wages/conditions; adverse action = ULP \u00a78(a)(1)\nconst nlraWarning = isNLRA ? 'CAUTION: NLRA \u00a77 protects this activity. Any adverse action against this employee (termination, discipline, reduced hours) could constitute an Unfair Labor Practice under \u00a78(a)(1). SOL: 6 months from ULP to NLRB charge.' : null;\nreturn [{ json: { ...d, isADA, isNLRA, adaDeadline, nlraWarning, interactiveProcessRequired: isADA } }];"
}
},
{
"id": "n3",
"name": "Switch: ADA or NLRA",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
650,
300
],
"parameters": {
"mode": "rules",
"rules": {
"values": [
{
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.isADA }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "ADA"
},
{
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.isNLRA }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "NLRA"
}
]
}
}
},
{
"id": "n4",
"name": "Gmail HR \u2014 ADA Interactive Process Required",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [
850,
150
],
"parameters": {
"sendTo": "={{ $vars.HR_MANAGER_EMAIL }}",
"subject": "=[ADA \u00a712111] Accommodation Request \u2014 Interactive Process Required",
"message": "={{ 'ADA Accommodation Request Received\\n\\nEmployee: ' + $json.employee_name + '\\nEvent Type: ' + $json.event_type + '\\nDate Received: ' + $now.toISO() + '\\nInitial Response Deadline: ' + $json.adaDeadline + '\\n\\nRequired Actions (ADA \u00a712111 Interactive Process):\\n1. Acknowledge receipt within 5 business days\\n2. Schedule interactive process meeting with employee\\n3. Request medical documentation if needed (\u00a71630.2(o)(3))\\n4. Identify potential reasonable accommodations\\n5. Implement accommodation or document undue hardship analysis\\n\\nCaution: Failure to engage in good-faith interactive process exposes the company to ADA Title I liability even if no reasonable accommodation exists. Document ALL steps.' }}",
"options": {}
}
},
{
"id": "n5",
"name": "Schedule ADA Follow-Up",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
1050,
150
],
"parameters": {
"amount": 10,
"unit": "days"
}
},
{
"id": "n6",
"name": "ADA Follow-Up Check",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [
1250,
150
],
"parameters": {
"channel": "#hr-ada",
"text": "={{ '\u23f0 ADA FOLLOW-UP \u2014 ' + $json.employee_name + ' | Accommodation request received 10 days ago. Confirm: (1) Interactive process meeting completed, (2) Accommodation decision made and documented, (3) Response sent to employee. ADA \u00a712111 requires good-faith engagement.' }}"
}
},
{
"id": "n7",
"name": "Slack #hr-legal \u2014 NLRA \u00a77 Protected Activity",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [
850,
450
],
"parameters": {
"channel": "#hr-legal",
"text": "={{ '\u26a0\ufe0f NLRA \u00a77 PROTECTED ACTIVITY LOGGED \u2014 ' + $json.employee_name + ' | Event: ' + $json.event_type + ' | ' + $json.nlraWarning }}"
}
},
{
"id": "n8",
"name": "Log All Events to HR Compliance Audit",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1050,
400
],
"parameters": {
"operation": "append",
"documentId": "={{ $vars.HR_COMPLIANCE_SHEET_ID }}",
"sheetName": "accommodation_activity_log",
"columns": {
"mappingMode": "defineBelow",
"value": {
"employee_id": "={{ $json.employee_id }}",
"employee_name": "={{ $json.employee_name }}",
"event_type": "={{ $json.event_type }}",
"event_category": "={{ $json.isADA ? 'ADA' : $json.isNLRA ? 'NLRA' : 'OTHER' }}",
"logged_at": "={{ $now.toISO() }}",
"ada_deadline": "={{ $json.adaDeadline }}",
"nlra_warning": "={{ $json.nlraWarning }}"
}
}
}
}
],
"connections": {
"Accommodation or Activity Report Webhook": {
"main": [
[
{
"node": "Classify ADA vs NLRA Event",
"type": "main",
"index": 0
}
]
]
},
"Classify ADA vs NLRA Event": {
"main": [
[
{
"node": "Switch: ADA or NLRA",
"type": "main",
"index": 0
}
]
]
},
"Switch: ADA or NLRA": {
"main": [
[
{
"node": "Gmail HR \u2014 ADA Interactive Process Required",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack #hr-legal \u2014 NLRA \u00a77 Protected Activity",
"type": "main",
"index": 0
}
]
]
},
"Gmail HR \u2014 ADA Interactive Process Required": {
"main": [
[
{
"node": "Schedule ADA Follow-Up",
"type": "main",
"index": 0
}
]
]
},
"Schedule ADA Follow-Up": {
"main": [
[
{
"node": "ADA Follow-Up Check",
"type": "main",
"index": 0
}
]
]
},
"Slack #hr-legal \u2014 NLRA \u00a77 Protected Activity": {
"main": [
[
{
"node": "Log All Events to HR Compliance Audit",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
What it does: Webhook receives event (accommodation request, disability disclosure, protected concerted activity report) → classifies as ADA or NLRA event → ADA path: emails HR manager with specific §12111 interactive process checklist + schedules 10-day follow-up Slack reminder → NLRA path: logs to #hr-legal with §7 protection warning and 6-month SOL clock → all events logged to Google Sheets compliance audit trail.
Why log NLRA activity: The most common NLRB charge pattern is discipline/termination within weeks of protected activity (wage discussion, group complaint). Having a timestamped log that pre-dates any adverse action decision creates evidence of good-faith process — and forces management to see the legal context before taking action.
Self-Hosting vs Cloud iPaaS: The HRTech Compliance Argument
| Data Type | Cloud iPaaS Risk | Self-Hosted n8n |
|---|---|---|
| FCRA consumer reports | Vendor = third-party data recipient under §604 permissible purpose | Data never leaves your AWS/Azure/GCP environment |
| I-9 immigration records | DHS/ICE subpoena can reach cloud vendor records | I-9 data stays inside your compliance perimeter |
| EEOC hiring analytics | OFCCP AAP data in vendor logs = undocumented data sharing | Audit-ready logs stay inside privilege boundary |
| FLSA payroll records | DOL WHD subpoena reaches cloud vendor | Pay records in self-hosted DB outside vendor reach |
| ADA medical documentation | ADA §503/HIPAA crossover — third-party access prohibited | PHI-adjacent data never transits external system |
| NLRA organizing communications | NLRB discovery can subpoena vendor records | Organizing activity logs stay inside legal boundary |
Five Buyer Questions to Address in Your Sales Cycle
Q: Does your platform maintain FCRA §607 'reasonable procedures' for adverse action timing?
A: Yes — the FCRA pipeline enforces the 5-business-day pre-adverse-action window and logs every step for audit.
Q: Can your I-9 module handle List B re-verification prohibition?
A: Yes — the I-9 workflow explicitly alerts HR that only List A or C can be used for Section 3; re-verifying List B is a common ICE audit finding.
Q: Do you produce OFCCP-ready adverse impact analysis?
A: The EEOC monitor generates 80-rule analysis logs in the format OFCCP requests in Scheduling Letters — 2 years of job-category applicant flow with adverse impact ratios.
Q: Does your platform cover state FLSA threshold variations?
A: Yes — the FLSA classifier covers California ($1,040/wk), New York ($1,125/wk), Washington, Colorado, and Alaska salary basis thresholds automatically.
Q: How do you handle the NLRA protected activity logging requirement?
A: All §7 activity is logged with timestamps before any adverse action is taken — creating the good-faith evidence trail that negates most NLRB §8(a)(1) charges.
Get the Complete FlowKit n8n Template Pack
These 5 workflows are import-ready JSON. The full FlowKit template library includes 15 production-ready n8n workflows across compliance, ops, and revenue automation:
stripeai.gumroad.com — individual templates $12–$29 | complete bundle $97
Tags: n8n, hrtech, automation, compliance
Top comments (0)