DEV Community

Alex Kane
Alex Kane

Posted on

n8n for HRTech SaaS: 5 Automations That Scale HR Platform Ops and Keep Employee Data Compliant (Free Workflow JSON)

Why HRTech SaaS Teams Self-Host Their Automation Layer

HRTech platforms — HRMS vendors, payroll SaaS, ATS companies, benefits technology providers — face a paradox: your product is HR automation, but your own platform ops still run on manual processes. Payroll run failures get caught by a Slack message from an angry CSM. Compliance deadlines live in someone's calendar. New customer onboarding drips are sent manually by the CS team.

n8n closes that gap — and for HRTech SaaS companies specifically, self-hosting matters beyond cost. Employee SSNs, salary data, health benefits information, EEO-1 demographic data, and biometric time-clock records are among the most regulated data categories in existence: CCPA, GDPR Article 9, HIPAA (for benefits), EEO-1 federal requirements, state payroll laws. Running those workflows through Zapier or Make routes them through a third-party cloud, adding sub-processors to every customer DPA and expanding your HIPAA Business Associate Agreement exposure.

Self-hosted n8n eliminates the sub-processor chain. Your automation runs inside your VPC. Here are 5 workflows HRTech platform ops teams use today, with import-ready JSON.


Workflow 1: New Enterprise Client Onboarding & HRIS Data Migration Drip

When a new enterprise signs up, three things need to happen immediately: API credentials delivered, CSM notified, and a multi-day check-in sequence started to drive activation (first employee import → first payroll run). Doing this manually for 50+ new clients per month doesn't scale.

Trigger: Google Sheets row added (CRM or Salesforce → Sheets export), or webhook from your CRM

Flow: Classify tier (ENTERPRISE/MID_MARKET/SMB by employee_count) → Gmail Day 0 (API keys + sandbox link + HRIS migration guide) → Slack DM to CSM → log to onboarding tracker → Wait 3d → Day 3 check-in email (did employee import complete?) → Wait 4d → Day 7 first payroll run milestone checklist → Sheets mark onboarding_complete = true

{
  "name": "HR Client Onboarding Drip",
  "nodes": [
    {
      "name": "Watch New Customers",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "parameters": {
        "sheetId": "YOUR_SHEET_ID",
        "range": "Customers!A:J",
        "event": "rowAdded"
      },
      "position": [240, 300]
    },
    {
      "name": "Classify Client Tier",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const emp = parseInt($json.employee_count || 0); const tier = emp >= 1000 ? 'ENTERPRISE' : emp >= 100 ? 'MID_MARKET' : 'SMB'; return [{ json: { ...$json, tier }}];"
      },
      "position": [440, 300]
    },
    {
      "name": "Day 0 — API Credentials",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.admin_email }}",
        "subject": "Your {{ $json.company_name }} HRIS account is ready — API credentials inside",
        "message": "Hi {{ $json.admin_name }}, your API key is: {{ $json.api_key }}. Migration guide: <link>. Sandbox: <link>."
      },
      "position": [640, 300]
    },
    {
      "name": "Slack CSM DM",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "={{ $json.csm_slack_id }}",
        "text": "New {{ $json.tier }} client: {{ $json.company_name }} ({{ $json.employee_count }} employees). Credentials sent to {{ $json.admin_email }}."
      },
      "position": [840, 300]
    },
    { "name": "Log to Onboarding Sheet", "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "append", "sheetId": "ONBOARDING_SHEET_ID" }, "position": [1040, 300] },
    { "name": "Wait 3 Days", "type": "n8n-nodes-base.wait",
      "parameters": { "amount": 3, "unit": "days" }, "position": [1240, 300] },
    {
      "name": "Day 3 — Employee Import Check-in",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.admin_email }}",
        "subject": "Day 3 check-in: how is the employee data import going?",
        "message": "Quick check — have you completed the initial employee CSV import? If you hit any issues with SSN/bank data mapping, reply here and we'll schedule a call."
      },
      "position": [1440, 300]
    },
    { "name": "Wait 4 Days", "type": "n8n-nodes-base.wait",
      "parameters": { "amount": 4, "unit": "days" }, "position": [1640, 300] },
    {
      "name": "Day 7 — First Payroll Run Checklist",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.admin_email }}",
        "subject": "Day 7 milestone: run your first payroll — step-by-step checklist inside"
      },
      "position": [1840, 300]
    },
    { "name": "Mark Complete", "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "update", "sheetId": "ONBOARDING_SHEET_ID" }, "position": [2040, 300] }
  ],
  "connections": {
    "Watch New Customers": { "main": [[{ "node": "Classify Client Tier" }]] },
    "Classify Client Tier": { "main": [[{ "node": "Day 0 — API Credentials" }]] },
    "Day 0 — API Credentials": { "main": [[{ "node": "Slack CSM DM" }]] },
    "Slack CSM DM": { "main": [[{ "node": "Log to Onboarding Sheet" }]] },
    "Log to Onboarding Sheet": { "main": [[{ "node": "Wait 3 Days" }]] },
    "Wait 3 Days": { "main": [[{ "node": "Day 3 — Employee Import Check-in" }]] },
    "Day 3 — Employee Import Check-in": { "main": [[{ "node": "Wait 4 Days" }]] },
    "Wait 4 Days": { "main": [[{ "node": "Day 7 — First Payroll Run Checklist" }]] },
    "Day 7 — First Payroll Run Checklist": { "main": [[{ "node": "Mark Complete" }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: Payroll Run Status Monitor & Failure Alert

A failed payroll run at 2 AM means employees don't get paid on Friday. That's a trust-destroying, churn-causing event. Most HRTech platforms have a payroll jobs API or database table — this workflow polls it every 5 minutes, classifies run status, and deduplicates alerts so ops gets one Slack ping per failure, not 288 per day.

Key pattern: $getWorkflowStaticData('global') stores alerted run IDs so you don't spam the channel on every 5-minute check.

{
  "name": "Payroll Run Monitor",
  "nodes": [
    {
      "name": "Schedule Every 5min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": { "rule": { "interval": [{ "field": "minutes", "minutesInterval": 5 }] } },
      "position": [240, 300]
    },
    {
      "name": "Fetch Active Payroll Jobs",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "method": "GET",
        "url": "https://api.yourplatform.com/v1/payroll/runs?status=active",
        "authentication": "headerAuth"
      },
      "position": [440, 300]
    },
    {
      "name": "Classify & Deduplicate",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const runs = $json.runs || [];\nconst state = $getWorkflowStaticData('global');\nif (!state.alerted) state.alerted = {};\nconst alerts = [];\nfor (const run of runs) {\n  const elapsed = (Date.now() - new Date(run.started_at).getTime()) / 60000;\n  let severity = null;\n  if (run.status === 'FAILED') severity = 'CRITICAL';\n  else if (run.status === 'PROCESSING' && elapsed > 90) severity = 'STALLED';\n  if (severity && !state.alerted[run.id]) {\n    state.alerted[run.id] = Date.now();\n    alerts.push({ json: { ...run, severity, elapsed_min: Math.round(elapsed) }});\n  }\n}\n$setWorkflowStaticData('global', state);\nreturn alerts.length ? alerts : [{ json: { skip: true }}];"
      },
      "position": [640, 300]
    },
    {
      "name": "Filter Alerts Only",
      "type": "n8n-nodes-base.filter",
      "parameters": { "conditions": { "options": [{ "leftValue": "={{ $json.skip }}", "operation": "notExists" }] } },
      "position": [840, 300]
    },
    {
      "name": "Slack #payroll-ops",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#payroll-ops",
        "text": "{{ $json.severity }}: Payroll run {{ $json.id }} for {{ $json.company_name }} ({{ $json.employee_count }} employees) has been {{ $json.status }} for {{ $json.elapsed_min }}min. Started: {{ $json.started_at }}."
      },
      "position": [1040, 300]
    },
    {
      "name": "Postgres: Log Run Event",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "insert",
        "table": "payroll_run_events",
        "columns": "run_id, company_id, status, severity, elapsed_min, logged_at"
      },
      "position": [1240, 300]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: HR Compliance & Regulatory Deadline Tracker

HRTech platforms operate under a dense compliance calendar:

  • EEO-1: Annual filing (September 30)
  • ACA 1095-C: Employer forms due January 31 / March 31
  • W-2 distribution: January 31
  • State payroll tax registration: State-specific quarterly renewals
  • GDPR DPA renewals: With each customer who is an EU data controller
  • SOC2 Type II: Annual evidence collection window
  • HIPAA BAA reviews: If handling health benefits (FSA/HSA/COBRA) data

Missing any of these is a regulatory breach or a contractual violation with your customers. This workflow runs every weekday at 8 AM and escalates with urgency-tiered Slack alerts and Gmail notifications.

{
  "name": "HR Compliance Deadline Tracker",
  "nodes": [
    {
      "name": "Weekdays 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * 1-5" }] } },
      "position": [240, 300]
    },
    {
      "name": "Read Compliance Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "readAllRows", "sheetId": "COMPLIANCE_SHEET_ID", "range": "Deadlines!A:G" },
      "position": [440, 300]
    },
    {
      "name": "Classify Urgency & Action",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const today = new Date();\nconst regulations = {\n  'EEO-1': 'Submit EEO-1 Component 1 at EEOC EEO-1 Data Collection portal.',\n  'ACA_1095C': 'Distribute 1095-C forms to employees. File with IRS by March 31.',\n  'W2_DISTRIBUTION': 'Distribute W-2 forms. File with SSA by January 31.',\n  'SOC2_EVIDENCE': 'Begin evidence collection for SOC2 Type II audit period. Engage auditor.',\n  'GDPR_DPA_RENEWAL': 'Renew Data Processing Agreement with EU customer. Update Annex I-III.',\n  'HIPAA_BAA_REVIEW': 'Annual review of HIPAA Business Associate Agreements. Update if subprocessors changed.',\n  'STATE_PAYROLL_TAX': 'Renew state employer account registration. File quarterly return.'\n};\nreturn $input.all().map(item => {\n  const due = new Date(item.json.due_date);\n  const days = Math.ceil((due - today) / 86400000);\n  let urgency = null;\n  if (days < 0) urgency = 'OVERDUE';\n  else if (days <= 7) urgency = 'CRITICAL';\n  else if (days <= 21) urgency = 'URGENT';\n  else if (days <= 60) urgency = 'WARNING';\n  else if (days <= 90) urgency = 'NOTICE';\n  if (!urgency) return null;\n  return { json: { ...item.json, urgency, days_left: days,\n    action: regulations[item.json.regulation_code] || 'Review compliance requirement.'\n  }};\n}).filter(Boolean);"
      },
      "position": [640, 300]
    },
    {
      "name": "Slack by Urgency",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "dataType": "string",
        "value1": "={{ $json.urgency }}",
        "rules": { "rules": [
          { "value2": "OVERDUE", "output": 0 },
          { "value2": "CRITICAL", "output": 1 },
          { "value2": "URGENT", "output": 2 }
        ]}
      },
      "position": [840, 300]
    },
    {
      "name": "Slack #compliance-critical",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#compliance-ops",
        "text": "{{ $json.urgency }}: {{ $json.regulation_name }} deadline in {{ $json.days_left }} days ({{ $json.due_date }}). Owner: {{ $json.owner }}. Action: {{ $json.action }}"
      },
      "position": [1040, 300]
    },
    {
      "name": "Gmail Compliance Owner",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.owner_email }}",
        "subject": "{{ $json.urgency }}: {{ $json.regulation_name }} due {{ $json.due_date }}"
      },
      "position": [1240, 300]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: Employee Data Subject Request & Privacy Pipeline

When employees (or their former employer's employees) submit privacy requests — GDPR Article 15 access, CCPA deletion, rectification — your platform is typically the data processor. The clock starts the moment the request arrives:

  • GDPR: 30 days (extendable to 90 with notice)
  • CCPA: 45 days (extendable to 90)
  • Virginia CDPA, Colorado CPA, Connecticut CTDPA: 45 days

Managing SLA timers manually across 500 enterprise clients is untenable. This webhook-triggered workflow auto-acknowledges the submitter, routes to your privacy team on Slack, logs to a Postgres audit table, and fires a follow-up reminder 5 days before SLA expiry.

{
  "name": "Employee Privacy Request Pipeline",
  "nodes": [
    {
      "name": "Privacy Request Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": { "httpMethod": "POST", "path": "privacy-request", "responseMode": "onReceived" },
      "position": [240, 300]
    },
    {
      "name": "Classify & Set SLA",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const sla = { GDPR_ACCESS: 30, GDPR_DELETE: 30, CCPA_DELETE: 45, CCPA_ACCESS: 45, RECTIFICATION: 30 };\nconst req = $json;\nconst days = sla[req.request_type] || 30;\nconst due = new Date(Date.now() + days * 86400000).toISOString().split('T')[0];\nconst ticketId = 'PRV-' + Date.now();\nreturn [{ json: { ...req, sla_days: days, sla_due: due, ticket_id: ticketId }}];"
      },
      "position": [440, 300]
    },
    {
      "name": "Gmail ACK to Requester",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.requester_email }}",
        "subject": "Privacy request received — Ticket {{ $json.ticket_id }}",
        "message": "We have received your {{ $json.request_type }} request. Reference: {{ $json.ticket_id }}. We will respond within {{ $json.sla_days }} days by {{ $json.sla_due }}."
      },
      "position": [640, 300]
    },
    {
      "name": "Slack #privacy-ops",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#privacy-ops",
        "text": "New {{ $json.request_type }}: {{ $json.ticket_id }}\nSubmitter: {{ $json.requester_email }}\nClient: {{ $json.company_name }}\nSLA due: {{ $json.sla_due }} ({{ $json.sla_days }} days)"
      },
      "position": [840, 300]
    },
    {
      "name": "Postgres: Audit Log",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "insert",
        "table": "privacy_request_audit_log",
        "columns": "ticket_id, request_type, requester_email, company_id, sla_due, status, received_at"
      },
      "position": [1040, 300]
    },
    {
      "name": "Wait Until SLA - 5 Days",
      "type": "n8n-nodes-base.wait",
      "parameters": { "resume": "specificTime", "dateTime": "={{ new Date(new Date($json.sla_due) - 5 * 86400000).toISOString() }}" },
      "position": [1240, 300]
    },
    {
      "name": "SLA Reminder to Privacy Team",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#privacy-ops",
        "text": "SLA WARNING: {{ $json.ticket_id }} ({{ $json.request_type }}) due in 5 days on {{ $json.sla_due }}. Status: {{ $json.status }}. Take action now to avoid regulatory breach."
      },
      "position": [1440, 300]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly HRTech Platform KPI Dashboard

Every Monday morning, leadership wants one email: how many organizations are active, how many employees are on the platform, how many payroll runs completed, what's MRR WoW. This workflow runs two Postgres queries in parallel (this week vs last week), calculates WoW% changes, and sends a color-coded HTML dashboard.

Key pattern: Two parallel Postgres nodes + Merge → Code (WoW% math + $getWorkflowStaticData for persistent last-week baseline) → Gmail HTML.

{
  "name": "Weekly HRTech Platform KPI Dashboard",
  "nodes": [
    {
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * 1" }] } },
      "position": [240, 300]
    },
    {
      "name": "Postgres: This Week KPIs",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT org_id) AS active_orgs, SUM(employee_count) AS employees_managed, COUNT(*) FILTER (WHERE event_type='payroll_run_complete' AND created_at > NOW()-INTERVAL'7d') AS payroll_runs, COUNT(*) FILTER (WHERE event_type='onboarding_complete' AND created_at > NOW()-INTERVAL'7d') AS onboardings, SUM(mrr_usd) AS mrr FROM organizations WHERE status='active'"
      },
      "position": [440, 200]
    },
    {
      "name": "Postgres: Last Week KPIs",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT org_id) AS active_orgs_lw, SUM(employee_count) AS employees_managed_lw, COUNT(*) FILTER (WHERE event_type='payroll_run_complete' AND created_at BETWEEN NOW()-INTERVAL'14d' AND NOW()-INTERVAL'7d') AS payroll_runs_lw FROM organizations WHERE status='active' AND created_at < NOW()-INTERVAL'7d'"
      },
      "position": [440, 400]
    },
    {
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "parameters": { "mode": "combineByIndex" },
      "position": [640, 300]
    },
    {
      "name": "Build Dashboard HTML",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const tw = $json; const pct = (a, b) => b > 0 ? (((a-b)/b)*100).toFixed(1) + '%' : 'N/A';\nconst color = (a, b) => a >= b ? '#16a766' : '#fb4c2f';\nconst html = `<h2>HRTech Platform — Weekly KPIs</h2><table border=1 cellpadding=6><tr><th>Metric</th><th>This Week</th><th>Last Week</th><th>WoW</th></tr><tr><td>Active Orgs</td><td>${tw.active_orgs}</td><td>${tw.active_orgs_lw}</td><td style=color:${color(tw.active_orgs,tw.active_orgs_lw)}>${pct(tw.active_orgs,tw.active_orgs_lw)}</td></tr><tr><td>Employees Managed</td><td>${Number(tw.employees_managed).toLocaleString()}</td><td>${Number(tw.employees_managed_lw).toLocaleString()}</td><td>-</td></tr><tr><td>Payroll Runs</td><td>${tw.payroll_runs}</td><td>${tw.payroll_runs_lw}</td><td style=color:${color(tw.payroll_runs,tw.payroll_runs_lw)}>${pct(tw.payroll_runs,tw.payroll_runs_lw)}</td></tr><tr><td>MRR</td><td>$${Number(tw.mrr).toLocaleString()}</td><td>-</td><td>-</td></tr></table>`;\nreturn [{ json: { html }}];"
      },
      "position": [840, 300]
    },
    {
      "name": "Gmail to Leadership",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "ceo@yourplatform.com",
        "bcc": "cto@yourplatform.com, vp-cs@yourplatform.com",
        "subject": "HRTech Platform KPIs — Week of {{ $now.format('MMM DD') }}",
        "message": "={{ $json.html }}",
        "additionalFields": { "isBodyHtml": true }
      },
      "position": [1040, 300]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Why HRTech SaaS Teams Choose Self-Hosted n8n Over Zapier or Make

Factor n8n (self-hosted) Zapier Make
Employee SSN / salary routing Stays in your VPC Routes through Zapier cloud Routes through Make cloud
HIPAA BAA exposure No additional sub-processor Adds Zapier as HIPAA sub-processor to every customer BAA Adds Make as sub-processor
GDPR Art. 28 sub-processor Zero added to customer DPAs Must update all customer DPAs Must update all customer DPAs
EEO-1 demographic data sovereignty On-premises Third-party cloud Third-party cloud
SOC2 CM-3 change control Git-versioned workflow JSON Unversioned UI changes Unversioned
Volume (500 orgs × 10K employees, daily sync) ~$20/mo VPS ~$85,000/mo at Zapier pricing Prohibitive
Payroll run monitor (every 5 min, 24/7) 288 checks/day included Depletes Zap runs rapidly Task-limited
State payroll tax data sovereignty Configurable by state Single cloud Single cloud

Get the Full Workflow Pack

These 5 workflows are part of the FlowKit n8n Automation Templates collection — 14+ production-ready workflows covering onboarding drips, monitoring, compliance tracking, and analytics dashboards. Individual templates from $12, complete bundle at $97.

HRTech SaaS ops teams: if you're running Zapier for anything that touches payroll data, employee PII, or health benefits — every task execution is routing that data through a third-party cloud. n8n moves that off your risk register entirely.

Drop any workflow questions below.

Top comments (0)