This workflow corresponds to n8n.io template #14416 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "rW0wVKb45UbwYE9K",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Automated Data Quality Monitoring & Reporting (SQL + Email + Google Sheets)",
"tags": [],
"nodes": [
{
"id": "52fa31e9-1811-4999-8254-4516e56610e7",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
112,
400
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24
}
]
}
},
"typeVersion": 1.2
},
{
"id": "8db194eb-88ae-4ff5-bee9-46da8c912288",
"name": "Config",
"type": "n8n-nodes-base.set",
"position": [
336,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-01",
"name": "tableName",
"type": "string",
"value": "restaurant_orders"
},
{
"id": "cfg-02",
"name": "nullCheckCol",
"type": "string",
"value": "restaurant_orders"
},
{
"id": "cfg-03",
"name": "duplicateCheckCol",
"type": "string",
"value": "order_id"
},
{
"id": "cfg-04",
"name": "outlierCol",
"type": "string",
"value": "order_id"
},
{
"id": "cfg-05",
"name": "outlierMin",
"type": "number",
"value": 0
},
{
"id": "cfg-06",
"name": "outlierMax",
"type": "number",
"value": 150
},
{
"id": "cfg-07",
"name": "rowCountMin",
"type": "number",
"value": 500
},
{
"id": "cfg-08",
"name": "rowCountMax",
"type": "number",
"value": 100000
},
{
"id": "cfg-09",
"name": "nullThresholdPct",
"type": "number",
"value": 5
},
{
"id": "cfg-10",
"name": "dupThresholdPct",
"type": "number",
"value": 1
},
{
"id": "25a47144-53ab-4923-ae70-78d297745dd9",
"name": "Distribution list",
"type": "string",
"value": "user@example.com"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "d809a9c0-0dd8-45b8-be46-91132ecb234c",
"name": "Null Check",
"type": "n8n-nodes-base.postgres",
"position": [
560,
112
],
"parameters": {
"query": "SELECT 'null_check' AS check_type, COUNT(*) AS total_rows, SUM(CASE WHEN {{ $('Config').item.json.nullCheckCol }} IS NULL OR CAST({{ $('Config').item.json.nullCheckCol }} AS TEXT) = '' THEN 1 ELSE 0 END) AS null_count, ROUND(SUM(CASE WHEN {{ $('Config').item.json.nullCheckCol }} IS NULL OR CAST({{ $('Config').item.json.nullCheckCol }} AS TEXT) = '' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS null_pct FROM {{ $('Config').item.json.tableName }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.5
},
{
"id": "bbf08a47-ce4b-49fa-8557-1227f6b30dd6",
"name": "Duplicate Check",
"type": "n8n-nodes-base.postgres",
"position": [
560,
304
],
"parameters": {
"query": "SELECT 'dup_check' AS check_type, COUNT(*) AS total_rows, COUNT(*) - COUNT(DISTINCT {{ $('Config').item.json.duplicateCheckCol }}) AS dup_count, ROUND((COUNT(*) - COUNT(DISTINCT {{ $('Config').item.json.duplicateCheckCol }})) * 100.0 / NULLIF(COUNT(*), 0), 2) AS dup_pct FROM {{ $('Config').item.json.tableName }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.5
},
{
"id": "4ec88710-8d0b-480a-8239-ed25e0d118c6",
"name": "Row Count",
"type": "n8n-nodes-base.postgres",
"position": [
560,
496
],
"parameters": {
"query": "SELECT 'row_count' AS check_type, COUNT(*) AS row_count FROM {{ $('Config').item.json.tableName }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.5
},
{
"id": "f3d04639-05a9-4b69-ba7c-87ac6d9fafc2",
"name": "Outlier Check",
"type": "n8n-nodes-base.postgres",
"position": [
560,
688
],
"parameters": {
"query": "SELECT 'outlier_check' AS check_type, COUNT(*) AS total_rows, SUM(CASE WHEN {{ $('Config').item.json.outlierCol }} < {{ $('Config').item.json.outlierMin }} OR {{ $('Config').item.json.outlierCol }} > {{ $('Config').item.json.outlierMax }} THEN 1 ELSE 0 END) AS outlier_count, ROUND(SUM(CASE WHEN {{ $('Config').item.json.outlierCol }} < {{ $('Config').item.json.outlierMin }} OR {{ $('Config').item.json.outlierCol }} > {{ $('Config').item.json.outlierMax }} THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS outlier_pct FROM {{ $('Config').item.json.tableName }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.5
},
{
"id": "cea373b4-c913-4d9f-b566-90b9ddb35879",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
832,
352
],
"parameters": {
"numberInputs": 5
},
"typeVersion": 3
},
{
"id": "0b323db9-8b30-453b-a396-ca74ce1b0308",
"name": "Evaluate & Format",
"type": "n8n-nodes-base.code",
"position": [
1008,
400
],
"parameters": {
"jsCode": "const cfg = $('Config').item.json;\nconst items = $input.all();\n\nfunction findCheck(type) {\n return (items.find(i => i.json.check_type === type) || { json: {} }).json;\n}\n\nconst nullR = findCheck('null_check');\nconst dupR = findCheck('dup_check');\nconst countR = findCheck('row_count');\nconst outlierR = findCheck('outlier_check');\n\nfunction evalStatus(value, threshold, mode) {\n if (mode === 'above') {\n if (value === 0) return { label: 'PASS', color: '#16a34a' };\n if (value <= threshold) return { label: 'WARN', color: '#d97706' };\n return { label: 'FAIL', color: '#dc2626' };\n }\n if (mode === 'range') {\n if (value >= cfg.rowCountMin && value <= cfg.rowCountMax) return { label: 'PASS', color: '#16a34a' };\n if (value < cfg.rowCountMin * 0.8 || value > cfg.rowCountMax * 1.2) return { label: 'FAIL', color: '#dc2626' };\n return { label: 'WARN', color: '#d97706' };\n }\n}\n\nconst nullPct = parseFloat(nullR.null_pct ?? 0);\nconst dupPct = parseFloat(dupR.dup_pct ?? 0);\nconst rowCount = parseInt(countR.row_count ?? 0, 10);\nconst outlierPct = parseFloat(outlierR.outlier_pct ?? 0);\n\nconst checks = [\n { name: 'Null / missing values', col: cfg.nullCheckCol, value: `${nullPct}%`, threshold: `< ${cfg.nullThresholdPct}%`, st: evalStatus(nullPct, cfg.nullThresholdPct, 'above') },\n { name: 'Duplicate rows', col: cfg.duplicateCheckCol, value: `${dupPct}%`, threshold: `< ${cfg.dupThresholdPct}%`, st: evalStatus(dupPct, cfg.dupThresholdPct, 'above') },\n { name: 'Row count anomaly', col: cfg.tableName, value: rowCount.toLocaleString(), threshold: `${cfg.rowCountMin} \u2013 ${cfg.rowCountMax}`, st: evalStatus(rowCount, null, 'range') },\n { name: 'Outlier values', col: cfg.outlierCol, value: `${outlierPct}%`, threshold: `${cfg.outlierMin} \u2013 ${cfg.outlierMax}`, st: evalStatus(outlierPct, 5, 'above') },\n];\n\nconst hasFail = checks.some(c => c.st.label === 'FAIL');\nconst hasWarn = checks.some(c => c.st.label === 'WARN');\nconst overall = hasFail ? 'FAIL' : hasWarn ? 'WARN' : 'PASS';\nconst oc = { PASS: '#16a34a', WARN: '#d97706', FAIL: '#dc2626' }[overall];\nconst now = new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';\n\nconst badge = (label, color) =>\n `<span style='background:${color}18;color:${color};border:1px solid ${color}40;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600'>${label}</span>`;\n\nconst tableRow = c =>\n `<tr style='border-bottom:1px solid #e5e7eb'>` +\n `<td style='padding:10px 14px'>${c.name}</td>` +\n `<td style='padding:10px 14px;color:#6b7280'>${c.col}</td>` +\n `<td style='padding:10px 14px;font-weight:600'>${c.value}</td>` +\n `<td style='padding:10px 14px;color:#6b7280'>${c.threshold}</td>` +\n `<td style='padding:10px 14px'>${badge(c.st.label, c.st.color)}</td>` +\n `</tr>`;\n\nconst htmlReport =\n `<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;background:#f9fafb;padding:32px;color:#111827'>` +\n `<div style='max-width:640px;margin:auto;background:white;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.08)'>` +\n `<div style='padding:24px 28px;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between;align-items:center'>` +\n `<div>` +\n `<div style='font-size:18px;font-weight:600'>Data Quality Report</div>` +\n `<div style='font-size:13px;color:#9ca3af;margin-top:2px'>Table: <strong style='color:#374151'>${cfg.tableName}</strong> · ${now}</div>` +\n `</div>${badge(overall, oc)}</div>` +\n `<table style='width:100%;border-collapse:collapse;font-size:14px'>` +\n `<thead><tr style='background:#f9fafb;text-align:left'>` +\n `<th style='padding:10px 14px;color:#6b7280;font-weight:500'>Check</th>` +\n `<th style='padding:10px 14px;color:#6b7280;font-weight:500'>Column</th>` +\n `<th style='padding:10px 14px;color:#6b7280;font-weight:500'>Value</th>` +\n `<th style='padding:10px 14px;color:#6b7280;font-weight:500'>Threshold</th>` +\n `<th style='padding:10px 14px;color:#6b7280;font-weight:500'>Status</th>` +\n `</tr></thead>` +\n `<tbody>${checks.map(tableRow).join('')}</tbody>` +\n `</table>` +\n `<div style='padding:16px 28px;background:#f9fafb;font-size:12px;color:#9ca3af'>` +\n `Generated by n8n Data Quality Bot · Thresholds configured in the Config (Set) node` +\n `</div></div></body></html>`;\n\nreturn [{\n json: {\n overall,\n htmlReport,\n emailSubject: `[${overall}] Data Quality \u2014 ${cfg.tableName} \u2014 ${now}`,\n reportEmail: cfg.reportEmail,\n sheet_timestamp: now,\n sheet_table: cfg.tableName,\n sheet_null_pct: nullPct,\n sheet_dup_pct: dupPct,\n sheet_row_count: rowCount,\n sheet_outlier_pct: outlierPct,\n sheet_status: overall\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f678cd51-4d24-4318-993d-2795fad9bce4",
"name": "Log to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
1232,
496
],
"parameters": {
"columns": {
"value": {
"Dup %": "={{ $json.sheet_dup_pct }}",
"Table": "={{ $json.sheet_table }}",
"Null %": "={{ $json.sheet_null_pct }}",
"Status": "={{ $json.sheet_status }}",
"overall": "={{ $json.overall }}",
"Outlier %": "={{ $json.sheet_outlier_pct }}",
"Row Count": "={{ $json.sheet_row_count }}",
"Timestamp": "={{ $json.sheet_timestamp }}"
},
"schema": [
{
"id": "Timestamp",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Table",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Table",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Null %",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Null %",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Dup %",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Dup %",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Row Count",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Row Count",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Outlier %",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Outlier %",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "overall",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "overall",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "htmlReport",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "htmlReport",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "emailSubject",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "emailSubject",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_timestamp",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_table",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_table",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_null_pct",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_null_pct",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_dup_pct",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_dup_pct",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_row_count",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_row_count",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_outlier_pct",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_outlier_pct",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sheet_status",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "sheet_status",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zIzM4gtIvtEj6t9YmBCQtieYLZLRmXj_37BoBtVTWw8/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1zIzM4gtIvtEj6t9YmBCQtieYLZLRmXj_37BoBtVTWw8",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zIzM4gtIvtEj6t9YmBCQtieYLZLRmXj_37BoBtVTWw8/edit?usp=drivesdk",
"cachedResultName": "Log for order"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "51ba3e03-248d-4e7a-8df2-7c11a3a4eca5",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
1232,
304
],
"parameters": {
"sendTo": "={{ $('Config').item.json['Distribution list'] }}",
"message": "={{ $json.htmlReport }}",
"options": {},
"subject": "={{ $json.emailSubject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "80b53380-1912-4945-add4-4e4d976bad67",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-672,
16
],
"parameters": {
"width": 528,
"height": 976,
"content": "\ud83d\udcca Automated Data Quality Report Bot\n\nThis workflow automatically monitors data quality for a selected database table and generates a structured report with actionable insights. It performs four key checks\u2014null values, duplicates, row count anomalies, and outliers\u2014then evaluates results against configurable thresholds to determine overall data health (PASS / WARN / FAIL). The output is sent via email and logged into Google Sheets for historical tracking and auditing.\n\nHow it works\n\nThe workflow runs on a schedule trigger (daily by default). It reads configuration values (table name, columns, thresholds) from the Config node and executes four SQL checks in parallel. Results are merged and processed in a code node, which evaluates each metric and generates a formatted HTML report. Finally, results are sent via email and appended to a Google Sheet.\n\nSetup\n## \ud83d\udee0\ufe0f Setup Guide\n\n**Step 1 \u2014 Config node**\nEdit the `Config` node to set your table name, column names, thresholds and Email Distribution list\n\n**Step 2 \u2014 DB credentials**\nOpen each of the 4 teal query nodes and attach your Postgres credential. Swap the node type if using MySQL or another DB.\n\n**Step 3 \u2014 Email**\nConnect your SMTP or Gmail credential to the `Send Email` node.\n\n**Step 4 \u2014 Google Sheets**\nConnect your Google account to `Log to Google Sheets`. Create a sheet with columns: Timestamp, Table, Null_Pct, Dup_Pct, Row_Count, Outlier_Pct, Status.\n\n**Step 5 \u2014 Activate**\nToggle the workflow on. It runs daily at 8am by default.\nCustomization\nAdd more checks by duplicating query nodes.\nAdjust thresholds for stricter/looser validation.\nExtend reporting (Slack, Teams, dashboards).\n\n**Customization**\nAdd more checks by duplicating query nodes.\nAdjust thresholds for stricter/looser validation.\nExtend reporting (Slack, Teams, dashboards)."
},
"typeVersion": 1
},
{
"id": "ebb0c98d-0995-40e4-b753-87b683c4230c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
48,
272
],
"parameters": {
"color": 7,
"width": 400,
"height": 288,
"content": "\u23f0 Trigger & Configuration\n\nControls workflow execution and defines all dynamic inputs like table name, columns, thresholds, and email recipients."
},
"typeVersion": 1
},
{
"id": "a0e34fa9-e962-4c7d-ad42-45e9be249169",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
-80
],
"parameters": {
"color": 7,
"height": 928,
"content": "\ud83e\uddea Data Quality Checks (SQL)\n\nRuns four parallel checks:\n\nNull values\nDuplicate records\nRow count validation\nOutlier detection"
},
"typeVersion": 1
},
{
"id": "c0b454bc-1fb1-4af2-8ba1-96c351d98402",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
768,
192
],
"parameters": {
"color": 7,
"width": 160,
"height": 416,
"content": "\ud83d\udd17 Merge Results\n\nCombines outputs from all SQL checks into a single dataset for evaluation."
},
"typeVersion": 1
},
{
"id": "933b2425-3eb3-4821-91e3-f53719cd0ba1",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
976,
176
],
"parameters": {
"color": 7,
"width": 160,
"height": 352,
"content": "\ud83e\udde0 Evaluate & Format Report\n\nApplies threshold logic (PASS/WARN/FAIL) and generates a clean HTML report with status indicators."
},
"typeVersion": 1
},
{
"id": "26018a1a-1fb3-4c75-9665-71e11095d902",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1168,
144
],
"parameters": {
"color": 7,
"height": 528,
"content": "\ud83d\udce4 Output & Notifications\n\nSends report via email and logs results into Google Sheets for tracking and auditing."
},
"typeVersion": 1
},
{
"id": "e33474a1-3dfb-45a3-ae4f-8cc71850f245",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
288,
384
],
"parameters": {
"color": "#D82222",
"width": 192,
"height": 368,
"content": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u26a0\ufe0f Critical Setup\n\nEnsure all column names in the Config node match your database schema exactly. Incorrect names will break SQL queries."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "88272d2c-f833-4962-9606-9c39c27a2208",
"connections": {
"Merge": {
"main": [
[
{
"node": "Evaluate & Format",
"type": "main",
"index": 0
}
]
]
},
"Config": {
"main": [
[
{
"node": "Null Check",
"type": "main",
"index": 0
},
{
"node": "Duplicate Check",
"type": "main",
"index": 0
},
{
"node": "Row Count",
"type": "main",
"index": 0
},
{
"node": "Outlier Check",
"type": "main",
"index": 0
}
]
]
},
"Row Count": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 3
}
]
]
},
"Null Check": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Outlier Check": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 4
}
]
]
},
"Duplicate Check": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Evaluate & Format": {
"main": [
[
{
"node": "Log to Google Sheets",
"type": "main",
"index": 0
},
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleSheetsOAuth2Apipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automatically monitors and reports data quality for any SQL table using configurable checks and thresholds. It evaluates key metrics—including null values, duplicate records, row count anomalies, and outliers—and assigns a clear PASS, WARN, or FAIL status.
Source: https://n8n.io/workflows/14416/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow monitors customer health by combining payment behavior, complaint signals, and AI-driven feedback analysis. It runs on daily and weekly schedules to evaluate risk levels, escalate high-r
Continuous monitoring: Real-time surveillance of supplier performance, financial health, and operational status Risk scoring: AI-powered assessment of supplier risks across multiple dimensions (financ
Regulatory monitoring: Continuously tracks changes in laws, regulations, and compliance requirements across multiple jurisdictions Contract analysis: AI-powered review of existing contracts to identif
pedidosProductos. Uses postgres, googleSheets, gmail. Scheduled trigger; 15 nodes.
This workflow is designed for Customer Success Managers, Growth Teams, and SaaS Business Owners who want to proactively reduce churn using AI. It automates the analysis of customer health and the deli