This workflow corresponds to n8n.io template #17894 — 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": "GuwtmolHpSZYA8lW",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Tax-ID Verification - Groq + PDF + Lovable",
"tags": [
{
"id": "7O26FSCVRdaD3J6i",
"name": "lovable",
"createdAt": "2026-08-06T11:54:17.397Z",
"updatedAt": "2026-08-06T11:54:17.397Z"
},
{
"id": "Fs23y8VBhrRrZSjQ",
"name": "groq",
"createdAt": "2026-08-06T11:54:17.338Z",
"updatedAt": "2026-08-06T11:54:17.338Z"
},
{
"id": "V6qrZh5OMy0YYKis",
"name": "tax-verification",
"createdAt": "2026-08-06T11:54:17.369Z",
"updatedAt": "2026-08-06T11:54:17.369Z"
},
{
"id": "fAc4NquWRUBXKvh2",
"name": "google-sheets",
"createdAt": "2026-08-06T11:54:17.421Z",
"updatedAt": "2026-08-06T11:54:17.421Z"
},
{
"id": "sU9NqRnBGLmULKHh",
"name": "pdf-intake",
"createdAt": "2026-08-06T11:54:17.450Z",
"updatedAt": "2026-08-06T11:54:17.450Z"
}
],
"nodes": [
{
"id": "f33029c2-37a2-4fba-a601-c37c1e93291d",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4512,
-992
],
"parameters": {
"width": 480,
"height": 704,
"content": "## AI Tax ID Verification - Groq + PDF + Lovable\n\n### How it works\n\n1. The workflow is triggered by a webhook from Lovable.\n2. It checks for duplicate filenames in Google Sheets.\n3. If a duplicate is found, it returns a cached result.\n4. Otherwise, it extracts the text layer from a PDF and sends it to Groq for parsing.\n5. The parsed tax document is then validated and checked against a database.\n6. Based on the validation result, it updates the database record or logs an unknown tax ID.\n\n### Setup steps\n\n- - [ ] Set up a Google Sheets node with the verification log.\n- - [ ] Configure a webhook trigger from Lovable.\n- - [ ] Set up a Groq API endpoint for parsing tax documents.\n\n### Customization\n\nThe workflow can be customized by modifying the Groq API endpoint, Google Sheets node, or the webhook trigger."
},
"typeVersion": 1
},
{
"id": "17931b2d-36af-484b-b552-e778a091164d",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3888,
-432
],
"parameters": {
"color": 7,
"width": 352,
"height": 304,
"content": "## Initial Trigger & Normalisation\n\nWebhook trigger from Lovable"
},
"typeVersion": 1
},
{
"id": "6d16aff9-74a4-4ea3-a669-28f453bf9e74",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3504,
-592
],
"parameters": {
"color": 7,
"width": 1200,
"height": 448,
"content": "## Duplicate Check\n\nChecks for duplicates and returns cached result if found"
},
"typeVersion": 1
},
{
"id": "47b72932-1f91-4b90-9f04-f4d3b90315b6",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2720,
-112
],
"parameters": {
"color": 7,
"width": 560,
"height": 464,
"content": "## PDF Extraction and Groq Parsing\n\nExtracts text layer from PDF and sends to Groq for parsing"
},
"typeVersion": 1
},
{
"id": "c419cf6c-c707-4cf6-aa01-b4c45215cc94",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2160,
-432
],
"parameters": {
"color": 7,
"width": 912,
"height": 288,
"content": "## Tax ID Extraction and Validation\n\nExtracts tax ID and validates it"
},
"typeVersion": 1
},
{
"id": "38c70368-5fd7-4614-9884-55a34b98fe61",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1200,
-432
],
"parameters": {
"color": 7,
"width": 384,
"height": 272,
"content": "## Final Compliance Verdict\n\nSends the final compliance verdict to Groq"
},
"typeVersion": 1
},
{
"id": "29801a58-275b-4989-8c49-b5162fe12e90",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-768,
-592
],
"parameters": {
"color": 7,
"width": 656,
"height": 560,
"content": "## Verification Log and DB Update\n\nWrites the verification log and updates the database record"
},
"typeVersion": 1
},
{
"id": "9ed96a8c-53f1-44c1-ba9d-3358c9b7b9cd",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-64,
-448
],
"parameters": {
"color": 7,
"height": 336,
"content": "## Route by Final Status\n\nRoutes the workflow based on the final status"
},
"typeVersion": 1
},
{
"id": "ebe92df6-5928-4a88-ae81-d67478a6d5b7",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
-672
],
"parameters": {
"color": 7,
"height": 800,
"content": "## Send Messages\n\nSends messages based on the final status"
},
"typeVersion": 1
},
{
"id": "32d21bf8-8727-4ab1-a8c3-c9d0f88d811f",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
624,
-448
],
"parameters": {
"color": 7,
"height": 304,
"content": "## Return JSON to Lovable\n\nReturns the JSON response to Lovable"
},
"typeVersion": 1
},
{
"id": "01e0537b-11b8-4f72-8e78-73f8ea8e7368",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1888,
-112
],
"parameters": {
"color": 7,
"height": 304,
"content": "## No Tax ID Found\n\nResponds with no tax ID found"
},
"typeVersion": 1
},
{
"id": "d41c20b7-0925-432e-bf61-da88d62a69b7",
"name": "Filename Duplicate Check",
"type": "n8n-nodes-base.code",
"position": [
-3456,
-304
],
"parameters": {
"jsCode": "const item = $input.first().json;\n\nconst fileName = (\n item[\"File Name\"] || item.file_name || ''\n).trim().toLowerCase();\n\nconst submitter = (\n item[\"Submitted By\"] || item.submitted_by || ''\n).trim().toLowerCase();\n\nconst org = (\n item[\"Organization\"] || item.organization || ''\n).trim().toLowerCase();\n\nconst fingerprint = fileName || org || submitter || '';\n\nreturn [{ json: {\n ...item,\n quick_tax_id: '',\n duplicate_check_id: fingerprint,\n fingerprint: fingerprint,\n fingerprint_type: fileName ? 'filename'\n : org ? 'organization'\n : 'submitter'\n}}];"
},
"typeVersion": 2
},
{
"id": "7ab1c8e2-9dcb-4b23-b098-c07d1ea9dd5b",
"name": "Sheets \u2014 Lookup Verification Log",
"type": "n8n-nodes-base.googleSheets",
"position": [
-3264,
-304
],
"parameters": {
"options": {
"dataLocationOnSheet": {
"values": {
"headerRow": 3,
"firstDataRow": 4,
"rangeDefinition": "specifyRange"
}
}
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1692878098,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1tHpCHYxhAFaZrK3IYbT-a1TTIJQUKwFZKjrlB36CDuM/edit#gid=1692878098",
"cachedResultName": "Verification_Log"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1tHpCHYxhAFaZrK3IYbT-a1TTIJQUKwFZKjrlB36CDuM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1tHpCHYxhAFaZrK3IYbT-a1TTIJQUKwFZKjrlB36CDuM/edit?usp=drivesdk",
"cachedResultName": "TaxID verify&Logs"
},
"combineFilters": "OR"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "2e3609d2-e163-4601-8ccd-eb00613b73c1",
"name": "Duplicate Decision",
"type": "n8n-nodes-base.code",
"position": [
-3088,
-304
],
"parameters": {
"jsCode": "const incoming = $('Filename Duplicate Check').first().json;\nconst rows = $input.all();\nconst fp = (incoming.fingerprint || '').toLowerCase();\n\nconst existingRows = rows.filter(r => {\n const f = (\n r.json['File Name'] || r.json['file_name'] || ''\n ).trim().toLowerCase();\n return f.length > 0 && f === fp;\n});\n\nconst isDuplicate = existingRows.length > 0;\nconst existingRecord = isDuplicate\n ? existingRows[existingRows.length - 1].json\n : null;\n\n// \u2500\u2500 Calculate cache freshness \u2500\u2500\nlet cacheHit = false;\nlet hoursSince = 999;\n\nif (existingRecord) {\n const completedRaw = \n existingRecord['Completed At'] ||\n existingRecord['completed_at'] || '';\n\n if (completedRaw) {\n const completedAt = new Date(completedRaw);\n // Check it parsed correctly\n if (!isNaN(completedAt.getTime())) {\n hoursSince = (Date.now() - completedAt.getTime()) / 3600000;\n cacheHit = hoursSince < 24;\n }\n }\n}\n\n// \u2500\u2500 IMPORTANT: do NOT spread ...incoming \u2500\u2500\n// Build output fresh so no stale cache_hit leaks through\nreturn [{ json: {\n // Carry forward only what we need\n \"Request ID\": incoming[\"Request ID\"] || incoming.request_id,\n \"Received At\": incoming[\"Received At\"] || incoming.received_at,\n \"Completed At\": incoming[\"Completed At\"] || incoming.received_at,\n \"Submitted By\": incoming[\"Submitted By\"] || incoming.submitted_by,\n \"Email\": incoming[\"Email\"] || incoming.submission_email,\n \"Organization\": incoming[\"Organization\"] || incoming.organization,\n \"File Name\": incoming[\"File Name\"] || incoming.file_name,\n pdf_base64: incoming.pdf_base64,\n request_id: incoming[\"Request ID\"] || incoming.request_id,\n submitted_by: incoming[\"Submitted By\"] || incoming.submitted_by,\n submission_email: incoming[\"Email\"] || incoming.submission_email,\n organization: incoming[\"Organization\"] || incoming.organization,\n file_name: incoming[\"File Name\"] || incoming.file_name,\n received_at: incoming[\"Received At\"] || incoming.received_at,\n fingerprint: incoming.fingerprint,\n fingerprint_type: incoming.fingerprint_type,\n\n // \u2500\u2500 Duplicate result \u2014 set explicitly, never from spread \u2500\u2500\n is_duplicate: isDuplicate,\n cache_hit: cacheHit, // \u2190 explicitly calculated\n hours_since_last_run: parseFloat(hoursSince.toFixed(1)),\n existing_final_status: existingRecord ? (existingRecord['Final Status'] || '') : '',\n existing_risk_score: existingRecord ? (existingRecord['Risk Score'] || 0) : 0,\n existing_risk_level: existingRecord ? (existingRecord['Risk Level'] || '') : '',\n existing_ai_summary: existingRecord ? (existingRecord['AI Summary'] || '') : '',\n existing_completed_at: existingRecord ? (existingRecord['Completed At'] || '') : '',\n existing_request_id: existingRecord ? (existingRecord['Request ID'] || '') : '',\n existing_entity_name: existingRecord ? (existingRecord['Entity Name (Doc)'] || '') : '',\n existing_tax_id: existingRecord ? (existingRecord['Tax ID Normalized'] || '') : '',\n\n // \u2500\u2500 Debug (remove after confirmed) \u2500\u2500\n _debug_fp: fp,\n _debug_rows_found: existingRows.length,\n _debug_completed_raw: existingRecord ? (existingRecord['Completed At'] || 'EMPTY') : 'NO_RECORD',\n _debug_hours: parseFloat(hoursSince.toFixed(1)),\n _debug_cache_hit: cacheHit\n}}];"
},
"typeVersion": 2
},
{
"id": "e1266690-bc28-4ce3-aaf6-2dda6549f43a",
"name": "Switch \u2014 Duplicate Route",
"type": "n8n-nodes-base.switch",
"position": [
-2912,
-304
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Output 1 \u2014 Cache hit",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "3c804646-aa9f-4560-b3fd-c3842dde1047",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.is_duplicate }}",
"rightValue": "true"
}
]
},
"renameOutput": true
},
{
"outputKey": "Output 2 \u2014 New ",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "906a1da3-cc87-4d56-84e7-8273b74a37c1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.is_duplicate }}",
"rightValue": "false"
}
]
},
"renameOutput": true
}
]
},
"options": {},
"looseTypeValidation": true
},
"typeVersion": 3.4
},
{
"id": "a7fc659d-8225-46f0-b03e-dcbb8a3c821b",
"name": "Return Cached Result",
"type": "n8n-nodes-base.code",
"position": [
-2672,
-480
],
"parameters": {
"jsCode": "const item = $input.first().json;\n\nreturn [{ json: {\n success: true,\n final_status: 'DUPLICATE',\n is_duplicate: true,\n cache_hit: true,\n request_id: item.existing_request_id,\n new_request_id: item['Request ID'] || item.request_id,\n tax_id_normalized: item.existing_tax_id,\n entity_name: item.existing_entity_name,\n risk_score: item.existing_risk_score,\n risk_level: item.existing_risk_level,\n ai_confidence: 100,\n compliance_flags: [],\n summary: item.existing_ai_summary,\n recommended_action: 'No action required \u2014 cached result',\n hours_since: item.hours_since_last_run,\n completed_at: item.existing_completed_at,\n message: 'This document was verified ' + item.hours_since_last_run + ' hours ago. Returning cached result.',\n}}];"
},
"typeVersion": 2
},
{
"id": "b08c42ee-53ca-4d1c-ba2a-2c6c3a5721c9",
"name": "Webhook \u2014 Lovable Form POST1",
"type": "n8n-nodes-base.webhook",
"position": [
-3840,
-304
],
"parameters": {
"path": "tax-ID-verify",
"options": {
"allowedOrigins": "*"
},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "4d60e321-c15b-4e79-ac97-a11c9b04c9dc",
"name": "Normalize Lovable Payload1",
"type": "n8n-nodes-base.code",
"position": [
-3648,
-304
],
"parameters": {
"jsCode": "// \u2500\u2500 INTAKE: normalize Lovable form payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst body = $input.first().json.body || $input.first().json;\n\nconst requestId = 'TXID-'\n + new Date().toISOString().slice(0,10).replace(/-/g,'')\n + '-'\n + Math.random().toString(36).substring(2,8).toUpperCase();\n\nconst pdfBase64 = (body.pdf_base64 || '').replace(/^data:application\\/pdf;base64,/, '');\nconst fileName = body.file_name || body.fileName || 'tax_document.pdf';\nconst submitter = body.submitted_by || body.name || 'Unknown';\nconst email = body.email || '';\nconst org = body.organization || '';\n\nif (!pdfBase64) {\n throw new Error('NO_PDF: pdf_base64 field is empty or missing');\n}\n\nconst now = new Date().toISOString();\n\nreturn [{ json: {\n // \u2500\u2500 Matches sheet Row 3 headers EXACTLY (capitals + spaces) \u2500\u2500\n \"Request ID\": requestId,\n \"Received At\": now,\n \"Completed At\": now,\n \"Submitted By\": submitter,\n \"Email\": email,\n \"Organization\": org,\n \"File Name\": fileName,\n \"Entity Name (Doc)\": '',\n \"Tax ID Raw\": '',\n \"Tax ID Normalized\": '',\n \"Tax ID Type\": '',\n \"Doc Type\": '',\n \"Tax Year\": '',\n \"Signature\": false,\n \"Format Valid\": false,\n \"Format Error\": '',\n \"DB Match Score\": 0,\n \"DB Entity Name\": '',\n \"DB Status\": '',\n \"Name Mismatch\": false,\n \"Final Status\": 'PROCESSING',\n \"Risk Level\": '',\n \"Risk Score\": 0,\n \"AI Confidence\": 0,\n \"Compliance Flags\": '',\n \"AI Summary\": '',\n \"Recommended Action\": '',\n \"Parse Confidence\": 0,\n\n // \u2500\u2500 Internal fields (not written to sheet, used by pipeline nodes) \u2500\u2500\n pdf_base64: pdfBase64,\n request_id: requestId,\n submitted_by: submitter,\n submission_email: email,\n organization: org,\n file_name: fileName,\n received_at: now,\n status: 'PROCESSING'\n}}];"
},
"typeVersion": 2
},
{
"id": "a8cdfcb1-1bb2-4947-9d5f-5a35f388ca5b",
"name": "Extract PDF Text Layer1",
"type": "n8n-nodes-base.code",
"position": [
-2672,
208
],
"parameters": {
"jsCode": "const item = $input.first().json;\nconst b64 = item.pdf_base64 || '';\n\nlet extractedText = '';\n\ntry {\n const buf = Buffer.from(b64, 'base64');\n const raw = buf.toString('binary');\n\n const found = [];\n\n // \u2500\u2500 Pull all literal strings from PDF parenthesis notation (Tj / TJ ops) \u2500\u2500\n const parenStrings = raw.match(/\\(([^\\)\\\\]{1,200})\\)/g) || [];\n for (const s of parenStrings) {\n const inner = s.slice(1, -1).trim();\n // Keep only strings with real alphanumeric content\n if (/[a-zA-Z0-9@\\.\\-]/.test(inner) && inner.length >= 2) {\n found.push(inner);\n }\n }\n\n // \u2500\u2500 Directly scan for EIN / SSN / 9-digit patterns \u2500\u2500\n const eins = raw.match(/\\b\\d{2}[-\\s]?\\d{7}\\b/g) || [];\n const ssns = raw.match(/\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}\\b/g) || [];\n const nine = raw.match(/\\b\\d{9}\\b/g) || [];\n\n // \u2500\u2500 Also try UTF-8 decode for modern PDFs \u2500\u2500\n let utf8text = '';\n try {\n utf8text = Buffer.from(b64, 'base64').toString('utf8');\n // pull any lines that look like real text\n const lines = utf8text.split(/[\\r\\n]+/);\n for (const line of lines) {\n const clean = line.replace(/[^\\x20-\\x7E]/g, ' ').trim();\n if (clean.length > 4 && /[a-zA-Z]{2,}/.test(clean)) {\n found.push(clean);\n }\n }\n } catch(e) {}\n\n // \u2500\u2500 Combine everything \u2500\u2500\n const allText = [\n ...found,\n ...eins,\n ...ssns,\n ...nine\n ].join(' ').replace(/\\s+/g, ' ').trim();\n\n // \u2500\u2500 Remove pure PDF operator noise \u2500\u2500\n extractedText = allText\n .replace(/\\b(obj|endobj|stream|endstream|xref|trailer|startxref|BT|ET)\\b/g, ' ')\n .replace(/\\/(BaseFont|Subtype|Encoding|Type|FontDescriptor|WinAnsiEncoding|Name)\\b/g, ' ')\n .replace(/\\s+/g, ' ')\n .substring(0, 5000)\n .trim();\n\n} catch(e) {\n extractedText = '[extraction error: ' + e.message + ']';\n}\n\nreturn [{\n json: {\n ...item,\n pdf_text: extractedText || '[no readable text found]',\n pdf_text_len: extractedText.length\n }\n}];"
},
"typeVersion": 2
},
{
"id": "bde51d20-9cdd-4224-8bee-381a4d59a9c3",
"name": "Groq \u2014 Parse PDF Tax Document1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2496,
16
],
"parameters": {
"url": "https://api.groq.com/openai/v1/chat/completions",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({\n \"model\": \"llama-3.3-70b-versatile\",\n \"temperature\": 0.05,\n \"max_tokens\": 600,\n \"response_format\": { \"type\": \"json_object\" },\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are a tax document parser. The text may be messy or extracted from a PDF binary. Search aggressively for any 9-digit number, XX-XXXXXXX pattern, or XXX-XX-XXXX pattern. Return ONLY a JSON object.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Find the Tax ID in this text. Look for patterns like 12-3456789 (EIN) or 123-45-6789 (SSN) or any 9 consecutive digits. The document is a W-9 form.\\n\\nReturn JSON: {tax_id, tax_id_raw, tax_id_type, entity_name, doc_type, tax_year, signature_present, parse_confidence, parse_notes}\\n\\nText:\\n\" + $json.pdf_text\n }\n ]\n}) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "7f876872-4051-4e20-a2c9-6a25a365cabf",
"name": "Parse Groq Extract Response1",
"type": "n8n-nodes-base.code",
"position": [
-2304,
16
],
"parameters": {
"jsCode": "const item = $input.first().json;\nconst prev = $('Normalize Lovable Payload1').first().json;\n\nlet raw = '';\ntry {\n raw = item.choices[0].message.content;\n} catch(e) {\n raw = '';\n}\n\nraw = raw.replace(/\\\\n/g, ' ').replace(/\\n/g, ' ').trim();\n\nlet parsed = {};\ntry {\n parsed = JSON.parse(raw);\n} catch(e) {\n const match = raw.match(/\\{[\\s\\S]+\\}/);\n if (match) {\n try { parsed = JSON.parse(match[0]); } catch(e2) {\n parsed = {\n tax_id: '', tax_id_raw: '', tax_id_type: 'UNKNOWN',\n entity_name: '', doc_type: 'UNKNOWN',\n tax_year: null, signature_present: false,\n parse_confidence: 0,\n parse_notes: 'Parse failed: ' + e.message\n };\n }\n }\n}\n\nreturn [{\n json: {\n // \u2500\u2500 Sheet headers (capitals + spaces) \u2500\u2500\n \"Request ID\": prev[\"Request ID\"] || prev.request_id,\n \"Received At\": prev[\"Received At\"] || prev.received_at,\n \"Completed At\": prev[\"Completed At\"] || prev.received_at,\n \"Submitted By\": prev[\"Submitted By\"] || prev.submitted_by,\n \"Email\": prev[\"Email\"] || prev.submission_email,\n \"Organization\": prev[\"Organization\"] || prev.organization,\n \"File Name\": prev[\"File Name\"] || prev.file_name,\n \"Entity Name (Doc)\": parsed.entity_name || '',\n \"Tax ID Raw\": parsed.tax_id_raw || parsed.tax_id || '',\n \"Tax ID Normalized\": parsed.tax_id || '',\n \"Tax ID Type\": parsed.tax_id_type || 'UNKNOWN',\n \"Doc Type\": parsed.doc_type || 'UNKNOWN',\n \"Tax Year\": parsed.tax_year || '',\n \"Signature\": parsed.signature_present || false,\n \"Format Valid\": false,\n \"Format Error\": '',\n \"DB Match Score\": 0,\n \"DB Entity Name\": '',\n \"DB Status\": '',\n \"Name Mismatch\": false,\n \"Final Status\": 'PROCESSING',\n \"Risk Level\": '',\n \"Risk Score\": 0,\n \"AI Confidence\": 0,\n \"Compliance Flags\": '',\n \"AI Summary\": '',\n \"Recommended Action\":'',\n \"Parse Confidence\": parsed.parse_confidence || 0,\n\n // \u2500\u2500 Internal pipeline fields (used by downstream nodes) \u2500\u2500\n request_id: prev[\"Request ID\"] || prev.request_id,\n submitted_by: prev[\"Submitted By\"] || prev.submitted_by,\n submission_email: prev[\"Email\"] || prev.submission_email,\n organization: prev[\"Organization\"] || prev.organization,\n file_name: prev[\"File Name\"] || prev.file_name,\n received_at: prev[\"Received At\"] || prev.received_at,\n tax_id: parsed.tax_id || '',\n tax_id_raw: parsed.tax_id_raw || parsed.tax_id || '',\n tax_id_type: parsed.tax_id_type || 'UNKNOWN',\n entity_name: parsed.entity_name || '',\n doc_type: parsed.doc_type || 'UNKNOWN',\n tax_year: parsed.tax_year || null,\n signature_present: parsed.signature_present || false,\n parse_confidence: parsed.parse_confidence || 0,\n parse_notes: parsed.parse_notes || ''\n }\n}];"
},
"typeVersion": 2
},
{
"id": "10490a9b-050d-4ffc-a7a3-053766510ddb",
"name": "Tax ID Extracted?1",
"type": "n8n-nodes-base.if",
"position": [
-2112,
-304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-taxid",
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json.tax_id }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "01818249-868a-4a0b-aa60-c91fd82391ab",
"name": "Format Validation1",
"type": "n8n-nodes-base.code",
"position": [
-1888,
-320
],
"parameters": {
"jsCode": "const item = $input.first().json;\nconst rawId = item[\"Tax ID Normalized\"] || item.tax_id_normalized ||\n item[\"Tax ID Raw\"] || item.tax_id_raw ||\n item.tax_id || '';\nconst taxType = item[\"Tax ID Type\"] || item.tax_id_type || 'UNKNOWN';\n\nlet ok = false, normalized = rawId.replace(/-/g,''), err = null;\nconst raw = normalized;\n\nif (taxType === 'EIN' && /^\\d{9}$/.test(raw)) {\n const prefix = parseInt(raw.substring(0,2));\n const bad = [0,7,8,9,17,18,19,28,29,49,69,70,78,79,89,96,97];\n normalized = raw.substring(0,2) + '-' + raw.substring(2);\n ok = !bad.includes(prefix);\n if (!ok) err = 'EIN prefix ' + prefix + ' is not a valid IRS-assigned prefix';\n\n} else if (taxType === 'SSN' && /^\\d{9}$/.test(raw)) {\n normalized = raw.substring(0,3)+'-'+raw.substring(3,5)+'-'+raw.substring(5);\n if (raw==='+1234567890'||raw.substring(0,3)==='000'||raw.substring(3,5)==='00'||raw.substring(5)==='0000') {\n err = 'SSN contains all-zero segment'; ok = false;\n } else if (raw.substring(0,3)==='666'||parseInt(raw.substring(0,3))>=900) {\n err = 'SSN area 666 or 900-999 is invalid'; ok = false;\n } else { ok = true; }\n\n} else if (taxType === 'ITIN' && /^\\d{9}$/.test(raw)) {\n normalized = raw.substring(0,3)+'-'+raw.substring(3,5)+'-'+raw.substring(5);\n ok = raw[0]==='9' && ['70','71','72','73','74','75','76','77','78','79',\n '80','81','82','83','84','85','86','87','88'].includes(raw.substring(3,5));\n if (!ok) err = 'ITIN must start with 9 and have specific 4th-5th digit values';\n\n} else if (taxType === 'VAT' && raw.length >= 5) {\n normalized = raw.toUpperCase(); ok = true;\n\n} else if (raw.length >= 5) {\n ok = true;\n err = 'Tax ID type unrecognized \u2014 proceeding with caution';\n\n} else {\n err = 'Tax ID too short or unrecognized for type ' + taxType;\n}\n\nif (ok && /^(.)\\1+$/.test(raw)) {\n ok = false; err = 'Tax ID is all repeating digits \u2014 invalid';\n}\n\nreturn [{ json: {\n ...item,\n // \u2500\u2500 Sheet-header keys \u2500\u2500\n \"Tax ID Normalized\": normalized,\n \"Format Valid\": ok,\n \"Format Error\": err || '',\n // \u2500\u2500 Internal keys \u2500\u2500\n normalized_tax_id: normalized,\n format_valid: ok,\n format_error: err || ''\n}}];"
},
"typeVersion": 2
},
{
"id": "d1e99328-1c0b-41ec-b3a5-54af77aa523b",
"name": "Google Sheets \u2014 Read TaxID Database1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-1632,
-320
],
"parameters": {
"options": {
"dataLocationOnSheet": {
"values": {
"headerRow": 3,
"firstDataRow": 4,
"rangeDefinition": "specifyRange"
}
}
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1971988441,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1tHpCHYxhAFaZrK3IYbT-a1TTIJQUKwFZKjrlB36CDuM/edit#gid=1971988441",
"cachedResultName": "TaxID_Database"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1tHpCHYxhAFaZrK3IYbT-a1TTIJQUKwFZKjrlB36CDuM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1tHpCHYxhAFaZrK3IYbT-a1TTIJQUKwFZKjrlB36CDuM/edit?usp=drivesdk",
"cachedResultName": "TaxID verify&Logs"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "3cd6b706-931e-42d6-8031-a8da8dc6a307",
"name": "Database Lookup + Match Logic1",
"type": "n8n-nodes-base.code",
"position": [
-1392,
-320
],
"parameters": {
"jsCode": "const incoming = $('Format Validation1').first().json;\n\n// \u2500\u2500 THIS is the only line that was wrong \u2014 reads BOTH formats \u2500\u2500\nconst submittedRaw =\n incoming[\"Tax ID Normalized\"] ||\n incoming[\"Tax ID Raw\"] ||\n incoming.normalized_tax_id ||\n incoming.tax_id_raw ||\n incoming.tax_id || '';\n\nconst submitted = submittedRaw.replace(/-/g, '').toUpperCase();\n\nconst dbRows = $input.all();\nlet matchedRow = null, matchScore = 0;\n\nfor (const row of dbRows) {\n const d = row.json;\n const rawId = d['Tax ID'] || d['tax_id'] || d['TAX ID'] || '';\n const dbId = rawId.replace(/[^0-9A-Za-z]/g, '').toUpperCase();\n if (!dbId) continue;\n if (dbId === submitted) { matchedRow = d; matchScore = 100; break; }\n if (dbId.substring(0,6) === submitted.substring(0,6) && submitted.length >= 6) {\n matchedRow = d; matchScore = 60;\n }\n}\n\nconst exact = matchScore === 100;\nconst partial = matchScore === 60;\n\nconst dbName = matchedRow ? (matchedRow['Entity Name'] || matchedRow['entity_name'] || '') : '';\nconst dbStat = matchedRow ? (matchedRow['Status'] || matchedRow['status'] || 'NOT_FOUND') : 'NOT_FOUND';\nconst dbActive= matchedRow ? (matchedRow['Active Since'] || matchedRow['active_since'] || '') : '';\nconst dbType = matchedRow ? (matchedRow['Tax ID Type'] || matchedRow['tax_id_type'] || '') : '';\nconst dbState = matchedRow ? (matchedRow['Registered State'] || matchedRow['registered_state'] || '') : '';\nconst dbSource= matchedRow ? (matchedRow['Source'] || matchedRow['source'] || '') : '';\nconst dbLastV = matchedRow ? (matchedRow['Last Verified'] || matchedRow['last_verified'] || '') : '';\nconst dbLastR = matchedRow ? (matchedRow['Last Verification Result'] || matchedRow['last_verification_result'] || '') : '';\nconst dbRisk = matchedRow ? (matchedRow['Last Risk Score'] || matchedRow['last_risk_score'] || '') : '';\nconst dbNotes = matchedRow ? (matchedRow['Notes'] || matchedRow['notes'] || '') : '';\n\nconst entityFromDoc = incoming[\"Entity Name (Doc)\"] || incoming.entity_name || '';\nconst formatValid = incoming[\"Format Valid\"] ?? incoming.format_valid ?? false;\n\nlet nameMismatch = false;\nif (exact && dbName && entityFromDoc) {\n const clean = s => s.toLowerCase().replace(/[^a-z0-9 ]/g,'').split(' ').filter(w => w.length > 2);\n const overlap = clean(dbName).filter(w => clean(entityFromDoc).includes(w)).length;\n nameMismatch = overlap === 0;\n}\n\nlet vr = 'UNVERIFIED';\nif (!formatValid) vr = 'FORMAT_INVALID';\nelse if (!exact && !partial) vr = 'NOT_IN_DATABASE';\nelse if (exact && dbStat.toUpperCase().includes('REVOK')) vr = 'REVOKED';\nelse if (exact && dbStat.toUpperCase().includes('SUSPEND'))vr = 'SUSPENDED';\nelse if (exact && nameMismatch) vr = 'ID_NAME_MISMATCH';\nelse if (partial && !exact) vr = 'POSSIBLE_MATCH_REVIEW';\nelse if (exact) vr = 'VERIFIED';\n\nreturn [{ json: {\n ...incoming,\n // \u2500\u2500 Sheet-header keys \u2500\u2500\n \"DB Match Score\": matchScore,\n \"DB Entity Name\": dbName,\n \"DB Status\": dbStat,\n \"Name Mismatch\": nameMismatch,\n \"Final Status\": vr,\n // \u2500\u2500 Internal keys \u2500\u2500\n db_match_score: matchScore,\n db_exact_match: exact,\n db_partial_match: partial,\n db_entity_name: dbName,\n db_status: dbStat,\n db_active: dbActive,\n db_type: dbType,\n db_state: dbState,\n db_source: dbSource,\n db_last_verified: dbLastV,\n db_last_result: dbLastR,\n db_risk_score: dbRisk,\n db_notes: dbNotes,\n db_row: matchedRow,\n name_mismatch: nameMismatch,\n verification_result: vr,\n is_verified: vr === 'VERIFIED',\n format_valid: formatValid,\n looked_up_at: new Date().toISOString(),\n // \u2500\u2500 Debug (remove after confirmed working) \u2500\u2500\n _debug_submitted: submitted,\n _debug_raw_field: submittedRaw,\n _debug_rows: dbRows.length\n}}];"
},
"typeVersion": 2
},
{
"id": "5d5cf0a3-6ca5-4fe1-ac3f-587223aab2af",
"name": "Groq \u2014 Final Compliance Verdict1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1152,
-320
],
"parameters": {
"url": "https://api.groq.com/openai/v1/chat/completions",
"body": "={\"model\":\"llama-3.3-70b-versatile\",\"temperature\":0.05,\"max_tokens\":500,\"response_format\":{\"type\":\"json_object\"},\"messages\":[{\"role\":\"system\",\"content\":\"You are a tax compliance analyst. Return ONLY a JSON object.\"},{\"role\":\"user\",\"content\":\"Given these verification facts, issue a final compliance verdict. Return ONLY a JSON object with keys: final_status (VERIFIED or REJECTED or MANUAL_REVIEW), risk_level (LOW or MEDIUM or HIGH or CRITICAL), risk_score (integer 0-100), confidence (integer 0-100), flags (array of strings), summary (2 sentences plain English), recommended_action (string).\\n\\nFacts: tax_id={{ $json.normalized_tax_id }}, type={{ $json.tax_id_type }}, doc_entity={{ $json.entity_name }}, db_entity={{ $json.db_entity_name }}, format_valid={{ $json.format_valid }}, format_error={{ $json.format_error }}, db_match_score={{ $json.db_match_score }}, db_status={{ $json.db_status }}, name_mismatch={{ $json.name_mismatch }}, signature={{ $json.signature_present }}, parse_confidence={{ $json.parse_confidence }}, system_result={{ $json.verification_result }}\"}]}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "genericCredentialType",
"rawContentType": "application/json",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "b06e6f66-3390-4916-91c3-8e50838bf9b3",
"name": "Parse Groq Verdict1",
"type": "n8n-nodes-base.code",
"position": [
-960,
-320
],
"parameters": {
"jsCode": "// \u2500\u2500 Groq raw response comes via $input \u2500\u2500\nconst inputJson = $input.first().json;\n\n// \u2500\u2500 DB + document fields come from the named upstream node \u2500\u2500\nconst incoming = $('Database Lookup + Match Logic1').first().json;\n\n// \u2500\u2500 Parse Groq verdict from choices[0].message.content \u2500\u2500\nlet groqVerdict = {};\ntry {\n const raw =\n inputJson?.choices?.[0]?.message?.content ||\n inputJson?.content ||\n inputJson?.message?.content ||\n '{}';\n const cleaned = raw.replace(/```json|```/gi, '').trim();\n groqVerdict = JSON.parse(cleaned);\n} catch (e) {\n groqVerdict = {};\n}\n\nconst aiSummary = groqVerdict.summary || groqVerdict.ai_summary || '';\nconst riskLevel = groqVerdict.risk_level || '';\nconst riskScore = groqVerdict.risk_score ?? 0;\nconst aiConfidence = groqVerdict.confidence ?? groqVerdict.ai_confidence ?? 0;\nconst complianceFlags = groqVerdict.flags || groqVerdict.compliance_flags || [];\nconst recommendedAction = groqVerdict.recommended_action || '';\nconst parseConfidence = groqVerdict.parse_confidence ?? groqVerdict.confidence ?? 0;\n\n// \u2500\u2500 Read Tax ID \u2500\u2500\nconst submittedRaw =\n incoming[\"Tax ID Normalized\"] ||\n incoming.tax_id_normalized ||\n incoming[\"Tax ID Raw\"] ||\n incoming.tax_id_raw ||\n incoming.tax_id || '';\nconst submitted = submittedRaw.replace(/-/g, '').toUpperCase();\n\n// \u2500\u2500 Read DB match results (already computed upstream) \u2500\u2500\nconst matchScore = incoming[\"DB Match Score\"] ?? incoming.db_match_score ?? 0;\nconst exact = matchScore === 100;\nconst partial = matchScore === 60;\n\nconst dbName = incoming[\"DB Entity Name\"] || incoming.db_entity_name || '';\nconst dbStat = incoming[\"DB Status\"] || incoming.db_status || 'NOT_FOUND';\nconst dbActive = incoming[\"Active Since\"] || incoming.db_active || '';\nconst dbType = incoming[\"Tax ID Type\"] || incoming.db_type || '';\nconst dbState = incoming[\"Registered State\"] || incoming.db_state || '';\nconst dbSource = incoming[\"Source\"] || incoming.db_source || '';\nconst dbLastV = incoming[\"Last Verified\"] || incoming.db_last_verified || '';\nconst dbLastR = incoming[\"Last Verification Result\"] || incoming.db_last_result || '';\nconst dbRisk = incoming[\"Last Risk Score\"] || incoming.db_risk_score || '';\nconst dbNotes = incoming[\"Notes\"] || incoming.db_notes || '';\nconst dbRow = incoming.db_row || null;\n\n// \u2500\u2500 Name similarity check \u2500\u2500\nlet nameMismatch = false;\nconst entityFromDoc = incoming[\"Entity Name (Doc)\"] || incoming.entity_name || '';\nif (exact && dbName && entityFromDoc) {\n const clean = s => s.toLowerCase().replace(/[^a-z0-9 ]/g, '').split(' ').filter(w => w.length > 2);\n const overlap = clean(dbName).filter(w => clean(entityFromDoc).includes(w)).length;\n nameMismatch = overlap === 0;\n}\n\n// \u2500\u2500 Verification result \u2500\u2500\nconst formatValid = incoming[\"Format Valid\"] ?? incoming.format_valid ?? false;\nlet vr = 'UNVERIFIED';\nif (!formatValid) vr = 'FORMAT_INVALID';\nelse if (!exact && !partial) vr = 'NOT_IN_DATABASE';\nelse if (exact && dbStat.toUpperCase().includes('REVOK')) vr = 'REVOKED';\nelse if (exact && dbStat.toUpperCase().includes('SUSPEND')) vr = 'SUSPENDED';\nelse if (exact && nameMismatch) vr = 'ID_NAME_MISMATCH';\nelse if (partial && !exact) vr = 'POSSIBLE_MATCH_REVIEW';\nelse if (exact) vr = 'VERIFIED';\n\nreturn [{ json: {\n // \u2500\u2500 Document fields \u2500\u2500\n \"Request ID\": incoming[\"Request ID\"] || incoming.request_id || '',\n \"Submitted By\": incoming[\"Submitted By\"] || incoming.submitted_by || '',\n \"Email\": incoming[\"Email\"] || incoming.email || '',\n \"Organization\": incoming[\"DB Entity Name\"] || incoming.organization || '',\n \"File Name\": incoming[\"File Name\"] || incoming.file_name || '',\n \"Entity Name (Doc)\": entityFromDoc,\n \"Tax ID Raw\": incoming[\"Tax ID Raw\"] || incoming.tax_id_raw || '',\n \"Tax ID Normalized\": submittedRaw,\n \"Tax ID Type\": dbType || incoming[\"Tax ID Type\"] || '',\n \"Format Valid\": formatValid,\n \"Format Error\": incoming[\"Format Error\"] || incoming.format_error || '',\n \"Signature\": incoming[\"Signature\"] ?? incoming.signature ?? false,\n\n // \u2500\u2500 DB match results \u2500\u2500\n \"DB Match Score\": matchScore,\n \"DB Entity Name\": dbName,\n \"DB Status\": dbStat,\n \"DB Active Since\": dbActive,\n \"DB Tax ID Type\": dbType,\n \"DB State\": dbState,\n \"DB Source\": dbSource,\n \"DB Last Verified\": dbLastV,\n \"DB Last Result\": dbLastR,\n \"DB Risk Score\": dbRisk,\n \"DB Notes\": dbNotes,\n \"Name Mismatch\": nameMismatch,\n\n // \u2500\u2500 Verdict \u2500\u2500\n \"Final Status\": vr,\n\n // \u2500\u2500 AI / Groq fields \u2500\u2500\n \"Risk Level\": riskLevel,\n \"Risk Score\": riskScore,\n \"AI Confidence\": aiConfidence,\n \"Compliance Flags\": Array.isArray(complianceFlags) ? complianceFlags.join(', ') : complianceFlags,\n \"AI Summary\": aiSummary,\n \"Recommended Action\": recommendedAction,\n \"Parse Confidence\": parseConfidence,\n\n // \u2500\u2500 Internal pipeline keys \u2500\u2500\n request_id: incoming[\"Request ID\"] || incoming.request_id || '',\n submitted_by: incoming[\"Submitted By\"] || incoming.submitted_by || '',\n tax_id_normalized: submittedRaw,\n format_valid: formatValid,\n db_match_score: matchScore,\n db_exact_match: exact,\n db_partial_match: partial,\n db_entity_name: dbName,\n db_status: dbStat,\n db_active: dbActive,\n db_type: dbType,\n db_state: dbState,\n db_source: dbSource,\n db_last_verified: dbLastV,\n db_last_result: dbLastR,\n db_risk_score: dbRisk,\n db_notes: dbNotes,\n db_row: dbRow,\n name_mismatch: nameMismatch,\n verification_result: vr,\n is_verified: vr === 'VERIFIED',\n ai_summary: aiSummary,\n risk_level: riskLevel,\n risk_score: riskScore,\n ai_confidence: aiConfidence,\n compliance_flags: complianceFlags,\n recommended_action: recommendedAction,\n looked_up_at: new Date().toISOString(),\n\n // \u2500\u2500 Debug: show raw groq parse \u2500\u2500\n _debug_groq_raw: inputJson?.choices?.[0]?.message?.content || '',\n _debug_groq_parsed: groqVerdict,\n}}];"
},
"typeVersion": 2
},
{
"id": "e86bd649-3084-4487-a546-0a91949d1436",
"name": "Sheets \u2014 Write Verification Log1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-720,
-320
],
"parameters": {
"columns": {
"value": {
"Email": "={{ $json.Email }}",
"Doc Type": "=",
"File Name": "={{ $json['File Name'] }}",
"AI Summary": "={{ $json.ai_summary }}",
"Request ID": "={{ $json.request_id }}",
"Tax ID Raw": "={{ $json['Tax ID Raw'] }}",
"Received At": "={{ $json['Request ID'] }}",
"Completed At": "=",
"Organization": "={{ $json['DB Entity Name'] }}",
"Submitted By": "={{ $json.submitted_by }}",
"verification_result": "={{ $json.verification_result }}"
},
"schema": [
{
"id": "Request ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Request ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Received At",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Received At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Completed At",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Completed At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Submitted By",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Submitted By",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Organization",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Organization",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "File Name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "File Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Entity Name (Doc)",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Entity Name (Doc)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tax ID Raw",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Tax ID Raw",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tax ID Normalized",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Tax ID Normalized",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tax ID Type",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Tax ID Type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Doc Type",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Doc Type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tax Year",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Tax Year",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Signature",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Signature",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Format Valid",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Format Valid",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Format Error",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Format Error",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "DB Match Score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "DB Match Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "DB Entity Name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "DB Entity Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "DB Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "DB Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Name Mismatch",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Name Mismatch",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Final Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Final Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Risk Level",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Risk Level",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Risk Score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Risk Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Confidence",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "AI Confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Compliance Flags",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Compliance Flags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Summary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "AI Summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Recommended Action",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Recommended Action",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Parse Confidence",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Parse Confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "request_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "request_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "submitted_by",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "submitted_by",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "submission_email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "submission_email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "organization",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "organization",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "file_name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "file_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "received_at",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "received_at",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "tax_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "tax_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "tax_id_raw",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "tax_id_raw",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "tax_id_type",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "tax_id_type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "entity_name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "entity_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "doc_type",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "doc_type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "tax_year",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "tax_year",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "signature_present",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "signature_present",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "parse_confidence",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "parse_confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "parse_notes",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "parse_notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "normalized_tax_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "normalized_tax_id",
"defaultMatch": false,
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.
gmailOAuth2googleSheetsOAuth2ApihttpHeaderAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow receives tax document PDFs from a Lovable form webhook, extracts and parses the Tax ID with Groq, validates and matches it against a Google Sheets “TaxID_Database,” logs results to Google Sheets, emails the submitter via Gmail, and returns a JSON verdict back to…
Source: https://n8n.io/workflows/17894/ — 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 receives new signup events, enriches each contact with Twain (and optionally a LinkedIn lookup), drafts a personalized welcome email in Gmail, and posts review-ready previews to Slack, w
Receive orders via webhook with automatic payment verification, stock checking, inventory reservation, shipping label generation, confirmation emails, and Slack notifications.
OTP Verification System. Uses googleSheets, gmail. Webhook trigger; 23 nodes.
Transform your visitor management process with this fully automated, enterprise-grade workflow. The Verified Visitor Pass Generator eliminates manual data entry, prevents fake registrations through em
This workflow automates the process of retrieving Stripe invoices, validating API responses, generating payment receipts, sending them via email, storing PDFs in Google Drive, and appending details to