This workflow corresponds to n8n.io template #14320 — we link there as the canonical source.
This workflow follows the Agent → HTTP Request 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 →
{
"nodes": [
{
"id": "509fea67-c604-42e9-abc9-ce599ad31da8",
"name": "Document Upload Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-2480,
448
],
"parameters": {
"path": "gdpr-document-upload",
"options": {
"rawBody": true
},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2.1
},
{
"id": "2d5bcfdf-1489-43ad-8504-87f6a3db157b",
"name": "Workflow Configuration",
"type": "n8n-nodes-base.set",
"position": [
-2240,
448
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "documentId",
"type": "string",
"value": "={{ $now.toISO() }}"
},
{
"id": "id-2",
"name": "confidenceThreshold",
"type": "number",
"value": 0.8
},
{
"id": "id-3",
"name": "vaultTable",
"type": "string",
"value": "pii_vault"
},
{
"id": "id-4",
"name": "auditTable",
"type": "string",
"value": "pii_audit_log"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "b3a269ce-b37e-4541-a87f-e5a0b4dae12d",
"name": "Email Detector",
"type": "n8n-nodes-base.code",
"position": [
-1632,
160
],
"parameters": {
"jsCode": "// Email Detector - Extract all email addresses from OCR text\nconst items = $input.all();\nconst results = [];\n\n// Email regex pattern with domain validation\nconst emailRegex = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b/g;\n\nfor (const item of items) {\n const text = item.json.text || '';\n const detectedEmails = [];\n \n let match;\n while ((match = emailRegex.exec(text)) !== null) {\n detectedEmails.push({\n value: match[0],\n type: 'email',\n start_pos: match.index,\n end_pos: match.index + match[0].length,\n confidence: 1.0\n });\n }\n \n results.push({\n json: {\n detections: detectedEmails,\n detector: 'email',\n original_text: text\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "b6ebdf9f-f3de-4a80-beb4-f726821403f5",
"name": "Phone Detector",
"type": "n8n-nodes-base.code",
"position": [
-1632,
352
],
"parameters": {
"jsCode": "// Phone number detection with country-aware patterns\nconst items = $input.all();\nconst results = [];\n\n// Get the extracted text from OCR node\nfor (const item of items) {\n const text = item.json.text || '';\n \n // Comprehensive phone number patterns for various countries\n // Matches formats like: +1234567890, +1234567890, (555) 123-4567, etc.\n const phonePatterns = [\n // International format with country code\n /\\+\\d{1,3}[\\s.-]?\\(?\\d{1,4}\\)?[\\s.-]?\\d{1,4}[\\s.-]?\\d{1,4}[\\s.-]?\\d{1,9}/g,\n // US/Canada format with parentheses\n /\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}/g,\n // General format with separators\n /\\d{3,4}[\\s.-]\\d{3,4}[\\s.-]\\d{3,4}/g\n ];\n \n const detectedPhones = [];\n const seenPhones = new Set();\n \n for (const pattern of phonePatterns) {\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const phoneValue = match[0];\n \n // Avoid duplicates\n if (!seenPhones.has(phoneValue)) {\n seenPhones.add(phoneValue);\n \n detectedPhones.push({\n value: phoneValue,\n type: 'phone',\n start_pos: match.index,\n end_pos: match.index + phoneValue.length,\n confidence: 0.95\n });\n }\n }\n }\n \n // Return results for this item\n if (detectedPhones.length > 0) {\n results.push({\n json: {\n detected_pii: detectedPhones,\n original_text: text,\n detection_type: 'phone',\n count: detectedPhones.length\n }\n });\n }\n}\n\n// If no phones detected, return empty result\nif (results.length === 0) {\n return [{\n json: {\n detected_pii: [],\n detection_type: 'phone',\n count: 0\n }\n }];\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "024b8041-4754-4318-86e5-957b877ba894",
"name": "ID Number Detector",
"type": "n8n-nodes-base.code",
"position": [
-1632,
544
],
"parameters": {
"jsCode": "// ID Number Detector - Detects SSN, PAN, License Numbers, Bank Account Numbers\n// Returns array of objects with value, type, start_pos, end_pos, confidence\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const text = item.json.extractedText || item.json.text || '';\n const detections = [];\n \n // SSN Pattern (XXX-XX-XXXX or XXXXXXXXX)\n const ssnPattern = /\\b(?:\\d{3}-\\d{2}-\\d{4}|\\d{9})\\b/g;\n let match;\n \n while ((match = ssnPattern.exec(text)) !== null) {\n const value = match[0];\n // Basic validation: not all zeros, not sequential\n const digits = value.replace(/-/g, '');\n if (digits !== '+1234567890' && digits !== '123456789') {\n detections.push({\n value: value,\n type: 'id_number',\n subtype: 'ssn',\n start_pos: match.index,\n end_pos: match.index + value.length,\n confidence: 0.9\n });\n }\n }\n \n // PAN (Permanent Account Number - India) Pattern (AAAAA9999A)\n const panPattern = /\\b[A-Z]{5}[0-9]{4}[A-Z]\\b/g;\n \n while ((match = panPattern.exec(text)) !== null) {\n const value = match[0];\n detections.push({\n value: value,\n type: 'id_number',\n subtype: 'pan',\n start_pos: match.index,\n end_pos: match.index + value.length,\n confidence: 0.9\n });\n }\n \n // Driver's License Pattern (varies by region, common formats)\n // US format: 1-2 letters followed by 6-8 digits\n const licensePattern = /\\b[A-Z]{1,2}[0-9]{6,8}\\b/g;\n \n while ((match = licensePattern.exec(text)) !== null) {\n const value = match[0];\n detections.push({\n value: value,\n type: 'id_number',\n subtype: 'drivers_license',\n start_pos: match.index,\n end_pos: match.index + value.length,\n confidence: 0.9\n });\n }\n \n // Bank Account Number Pattern (8-17 digits)\n const bankAccountPattern = /\\b[0-9]{8,17}\\b/g;\n \n while ((match = bankAccountPattern.exec(text)) !== null) {\n const value = match[0];\n // Avoid false positives by checking it's not already detected as SSN\n const isSSN = detections.some(d => d.subtype === 'ssn' && d.value.replace(/-/g, '') === value);\n if (!isSSN) {\n detections.push({\n value: value,\n type: 'id_number',\n subtype: 'bank_account',\n start_pos: match.index,\n end_pos: match.index + value.length,\n confidence: 0.9\n });\n }\n }\n \n // IBAN Pattern (International Bank Account Number)\n const ibanPattern = /\\b[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}\\b/g;\n \n while ((match = ibanPattern.exec(text)) !== null) {\n const value = match[0];\n detections.push({\n value: value,\n type: 'id_number',\n subtype: 'iban',\n start_pos: match.index,\n end_pos: match.index + value.length,\n confidence: 0.9\n });\n }\n \n results.push({\n json: {\n detections: detections,\n originalText: text,\n detectionCount: detections.length\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "66ab913d-3b63-4f75-8a1b-8044f9d87de5",
"name": "Address Detector AI",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-1696,
832
],
"parameters": {
"text": "={{ $json.text }}",
"options": {
"systemMessage": "You are a PII detection specialist. Analyze the provided text and identify all physical addresses (street, city, state, postal code, country). Return each address found with its position in the text and a confidence score between 0 and 1."
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 3
},
{
"id": "e98b7074-76d6-47ee-8ef2-675f3bd0072f",
"name": "Anthropic Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
-1680,
1056
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-5-20250929",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "a62f38ce-f36f-44b6-8779-300ed5696de7",
"name": "Address Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-1552,
1056
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"addresses\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"type\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"address\"]\n\t\t\t\t\t},\n\t\t\t\t\t\"start_pos\": {\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t},\n\t\t\t\t\t\"end_pos\": {\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t},\n\t\t\t\t\t\"confidence\": {\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"required\": [\"value\", \"type\", \"start_pos\", \"end_pos\", \"confidence\"]\n\t\t\t}\n\t\t}\n\t},\n\t\"required\": [\"addresses\"]\n}"
},
"typeVersion": 1.3
},
{
"id": "20e85e22-f4bd-4f36-9c8d-a035bd4ac003",
"name": "Merge PII Detections",
"type": "n8n-nodes-base.merge",
"position": [
-1344,
256
],
"parameters": {
"numberInputs": 4
},
"typeVersion": 3.2
},
{
"id": "38f16629-57c7-45df-ba10-d9ea4f82369a",
"name": "PII Consolidation & Conflict Resolver",
"type": "n8n-nodes-base.code",
"position": [
-1104,
288
],
"parameters": {
"jsCode": "// PII Consolidation & Conflict Resolver\n// Merges all PII detections, resolves overlaps, deduplicates, and returns consolidated PII map\n\nconst allDetections = [];\n\n// Collect all PII detections from all input items\nfor (const item of $input.all()) {\n if (item.json.detections && Array.isArray(item.json.detections)) {\n allDetections.push(...item.json.detections);\n }\n}\n\nconsole.log(`Total detections collected: ${allDetections.length}`);\n\n// Sort detections by start position for easier overlap detection\nallDetections.sort((a, b) => a.start - b.start);\n\n// Resolve overlaps: higher confidence wins, longer span preferred if confidence is equal\nconst resolvedDetections = [];\n\nfor (const detection of allDetections) {\n let shouldAdd = true;\n let indexToReplace = -1;\n \n for (let i = 0; i < resolvedDetections.length; i++) {\n const existing = resolvedDetections[i];\n \n // Check for overlap\n const hasOverlap = (\n (detection.start >= existing.start && detection.start < existing.end) ||\n (detection.end > existing.start && detection.end <= existing.end) ||\n (detection.start <= existing.start && detection.end >= existing.end)\n );\n \n if (hasOverlap) {\n const detectionLength = detection.end - detection.start;\n const existingLength = existing.end - existing.start;\n \n // Higher confidence wins\n if (detection.confidence > existing.confidence) {\n indexToReplace = i;\n break;\n } else if (detection.confidence === existing.confidence) {\n // If confidence is equal, prefer longer span\n if (detectionLength > existingLength) {\n indexToReplace = i;\n break;\n } else {\n shouldAdd = false;\n break;\n }\n } else {\n shouldAdd = false;\n break;\n }\n }\n }\n \n if (indexToReplace >= 0) {\n resolvedDetections[indexToReplace] = detection;\n } else if (shouldAdd) {\n resolvedDetections.push(detection);\n }\n}\n\nconsole.log(`Detections after overlap resolution: ${resolvedDetections.length}`);\n\n// Deduplicate exact matches (same type, value, and position)\nconst deduplicatedDetections = [];\nconst seen = new Set();\n\nfor (const detection of resolvedDetections) {\n const key = `${detection.type}|${detection.value}|${detection.start}|${detection.end}`;\n \n if (!seen.has(key)) {\n seen.add(key);\n deduplicatedDetections.push(detection);\n }\n}\n\nconsole.log(`Detections after deduplication: ${deduplicatedDetections.length}`);\n\n// Create consolidated PII map\nconst piiMap = deduplicatedDetections.map((detection, index) => ({\n id: `pii_${index + 1}`,\n type: detection.type,\n value: detection.value,\n start: detection.start,\n end: detection.end,\n confidence: detection.confidence,\n source: detection.source || 'unknown'\n}));\n\n// Sort by start position for final output\npiiMap.sort((a, b) => a.start - b.start);\n\nreturn [\n {\n json: {\n piiMap: piiMap,\n totalDetections: piiMap.length,\n originalText: $input.first().json.extractedText || $input.first().json.text || '',\n timestamp: new Date().toISOString()\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "772fe712-8991-4aee-a79d-62477300c34a",
"name": "Tokenization & Vault Storage",
"type": "n8n-nodes-base.code",
"position": [
-864,
288
],
"parameters": {
"jsCode": "// Tokenization & Vault Storage\n// Generates secure tokens for PII and prepares vault records\n\nconst items = $input.all();\nconst vaultRecords = [];\nconst tokenMap = {};\n\n// Helper function to generate random hash\nfunction generateHash(length = 4) {\n const chars = '0123456789ABCDEF';\n let hash = '';\n for (let i = 0; i < length; i++) {\n hash += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return hash;\n}\n\n// Process all PII items from consolidated data\nfor (const item of items) {\n const piiData = item.json.consolidatedPII || [];\n const originalText = item.json.originalText || '';\n const documentId = item.json.documentId || item.json.document_id || 'unknown';\n \n for (const pii of piiData) {\n const type = pii.type.toUpperCase();\n const originalValue = pii.value;\n const hash = generateHash(4);\n const token = `<<${type}_${hash}>>`;\n \n // Create vault record\n vaultRecords.push({\n token: token,\n original_value: originalValue,\n type: type,\n document_id: documentId,\n created_at: new Date().toISOString()\n });\n \n // Store token mapping for text replacement\n tokenMap[originalValue] = token;\n }\n}\n\n// Generate masked text by replacing all PII with tokens\nlet maskedText = items[0]?.json?.originalText || '';\nfor (const [originalValue, token] of Object.entries(tokenMap)) {\n // Use global replace to handle multiple occurrences\n const regex = new RegExp(originalValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g');\n maskedText = maskedText.replace(regex, token);\n}\n\nreturn [{\n json: {\n vaultRecords: vaultRecords,\n tokenMap: tokenMap,\n maskedText: maskedText,\n documentId: items[0]?.json?.documentId || items[0]?.json?.document_id || 'unknown',\n originalText: items[0]?.json?.originalText || '',\n totalTokensGenerated: vaultRecords.length\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b47316b1-0af7-4008-879d-3e6cb8e9bbd5",
"name": "Store Tokens in Vault",
"type": "n8n-nodes-base.postgres",
"position": [
-688,
288
],
"parameters": {
"table": {
"__rl": true,
"mode": "name",
"value": "={{ $('Workflow Configuration').first().json.vaultTable }}"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"type": "={{ $json.type }}",
"token": "YOUR_CREDENTIAL_HERE",
"created_at": "={{ $json.created_at }}",
"document_id": "={{ $json.document_id }}",
"original_value": "={{ $json.original_value }}"
},
"mappingMode": "defineBelow"
},
"options": {}
},
"typeVersion": 2.6
},
{
"id": "af0a7c94-8163-438d-a6c5-aa8f6f7b9087",
"name": "Generate Masked Text",
"type": "n8n-nodes-base.code",
"position": [
-448,
288
],
"parameters": {
"jsCode": "// Generate Masked Text - Replace PII with tokens\nconst items = $input.all();\n\nif (items.length === 0) {\n return [{\n json: {\n masked_text: '',\n token_count: 0,\n masking_success: false,\n error: 'No input items'\n }\n }];\n}\n\n// Get the original text from OCR Extract Text node\nconst ocrData = $('Extract Text').first().json;\nlet originalText = ocrData.text || '';\n\n// Get the tokenized PII data from Store Tokens in Vault node\nconst vaultData = $('Store Tokens in Vault').all();\n\nif (!vaultData || vaultData.length === 0) {\n return [{\n json: {\n masked_text: originalText,\n token_count: 0,\n masking_success: false,\n error: 'No tokenized data available'\n }\n }];\n}\n\nlet maskedText = originalText;\nlet tokenCount = 0;\nlet allReplacementsSucceeded = true;\nconst replacements = [];\n\n// Process each tokenized PII item\nfor (const item of vaultData) {\n const piiData = item.json;\n \n if (piiData.original_value && piiData.token) {\n const originalValue = piiData.original_value;\n const token = piiData.token;\n \n // Check if the original value exists in the text\n if (maskedText.includes(originalValue)) {\n // Replace all occurrences of the original value with the token\n const regex = new RegExp(originalValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g');\n const beforeLength = maskedText.length;\n maskedText = maskedText.replace(regex, token);\n const afterLength = maskedText.length;\n \n // Count successful replacements\n if (beforeLength !== afterLength || maskedText.includes(token)) {\n tokenCount++;\n replacements.push({\n original: originalValue,\n token: token,\n type: piiData.pii_type || 'unknown',\n replaced: true\n });\n } else {\n allReplacementsSucceeded = false;\n replacements.push({\n original: originalValue,\n token: token,\n type: piiData.pii_type || 'unknown',\n replaced: false\n });\n }\n } else {\n // Original value not found in text\n allReplacementsSucceeded = false;\n replacements.push({\n original: originalValue,\n token: token,\n type: piiData.pii_type || 'unknown',\n replaced: false,\n reason: 'Value not found in text'\n });\n }\n }\n}\n\n// Return the masked text with metadata\nreturn [{\n json: {\n masked_text: maskedText,\n original_text: originalText,\n token_count: tokenCount,\n masking_success: allReplacementsSucceeded && tokenCount > 0,\n replacements: replacements,\n timestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "bc397644-596a-4577-bbfc-1b07e642c8bd",
"name": "AI Processing (Masked Data)",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
0,
-80
],
"parameters": {
"text": "={{ $json.masked_text }}",
"options": {
"systemMessage": "You are a document processing AI. Extract structured information from the provided document. IMPORTANT: You are working with masked data where PII has been replaced with tokens like <<EMAIL_7F3A>>. Process the document normally and preserve these tokens in your output exactly as they appear."
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 3
},
{
"id": "6635ccf1-cf62-4d25-9380-82f069f03ac4",
"name": "AI Processing Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
-48,
160
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-5-20250929",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "38140b45-2819-4624-9205-3d6a34e57990",
"name": "AI Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
176,
176
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"documentType\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Type of document processed (e.g., invoice, contract, form)\"\n\t\t},\n\t\t\"summary\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Brief summary of the document content\"\n\t\t},\n\t\t\"keyEntities\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"entityType\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"Key entities extracted from the document\"\n\t\t},\n\t\t\"dates\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"string\"\n\t\t\t},\n\t\t\t\"description\": \"Important dates found in the document\"\n\t\t},\n\t\t\"amounts\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"currency\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"amount\": {\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t},\n\t\t\t\t\t\"description\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"Monetary amounts found in the document\"\n\t\t},\n\t\t\"processedData\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"description\": \"Additional processed information specific to document type\"\n\t\t}\n\t},\n\t\"required\": [\"documentType\", \"summary\"]\n}"
},
"typeVersion": 1.3
},
{
"id": "4ebcce26-37f1-4648-83a6-1b02a85a6b14",
"name": "Re-Injection Controller",
"type": "n8n-nodes-base.code",
"position": [
400,
96
],
"parameters": {
"jsCode": "// Re-Injection Controller\n// Analyzes AI output, identifies fields requiring PII restoration,\n// extracts tokens, and prepares re-injection request\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const aiOutput = item.json.aiOutput || item.json;\n const fieldPermissions = item.json.fieldPermissions || {};\n const tokensToRetrieve = [];\n const reinjectionMap = {};\n \n // Define which fields should have PII restored based on permissions\n // Fields marked as 'restore' or 'unmask' will have original PII restored\n const fieldsToRestore = Object.keys(fieldPermissions).filter(\n field => fieldPermissions[field] === 'restore' || fieldPermissions[field] === 'unmask'\n );\n \n // Traverse AI output and identify token placeholders\n function extractTokens(obj, path = '') {\n for (const key in obj) {\n const currentPath = path ? `${path}.${key}` : key;\n const value = obj[key];\n \n if (typeof value === 'string') {\n // Check if value contains token pattern (e.g., TOKEN_EMAIL_123, TOKEN_PHONE_456)\n const tokenPattern = /TOKEN_([A-Z_]+)_([a-f0-9-]+)/g;\n const matches = value.matchAll(tokenPattern);\n \n for (const match of matches) {\n const fullToken = match[0];\n const tokenType = match[1];\n const tokenId = match[2];\n \n // Check if this field should be restored\n const shouldRestore = fieldsToRestore.some(field => \n currentPath.includes(field) || key === field\n );\n \n if (shouldRestore) {\n tokensToRetrieve.push({\n token: fullToken,\n tokenId: tokenId,\n tokenType: tokenType,\n field: currentPath,\n originalValue: value\n });\n \n if (!reinjectionMap[currentPath]) {\n reinjectionMap[currentPath] = [];\n }\n reinjectionMap[currentPath].push(fullToken);\n }\n }\n } else if (typeof value === 'object' && value !== null) {\n extractTokens(value, currentPath);\n }\n }\n }\n \n extractTokens(aiOutput);\n \n // Prepare output with re-injection instructions\n results.push({\n json: {\n aiOutput: aiOutput,\n tokensToRetrieve: tokensToRetrieve,\n reinjectionMap: reinjectionMap,\n fieldPermissions: fieldPermissions,\n totalTokensFound: tokensToRetrieve.length,\n fieldsToRestore: fieldsToRestore,\n timestamp: new Date().toISOString(),\n workflowExecutionId: $execution.id\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "0600a96e-6d42-4f3f-8873-053e4bc165f8",
"name": "Retrieve Original Values",
"type": "n8n-nodes-base.postgres",
"position": [
592,
96
],
"parameters": {
"table": {
"__rl": true,
"mode": "name",
"value": "={{ $('Workflow Configuration').first().json.vaultTable }}"
},
"where": {
"values": [
{
"value": "={{ $('Re-Injection Controller').item.json.token }}",
"column": "token"
}
]
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"options": {},
"operation": "select",
"returnAll": true
},
"typeVersion": 2.6
},
{
"id": "421b28cc-27c5-4657-ad05-18580a6fe572",
"name": "Restore Original PII",
"type": "n8n-nodes-base.code",
"position": [
800,
96
],
"parameters": {
"jsCode": "// Restore Original PII - Replace tokens with original values from vault\n// Only restore PII for explicitly allowed fields based on re-injection policy\n\nconst aiOutput = $input.first().json;\nconst vaultData = $('Retrieve Original Values').all();\n\n// Create a map of tokens to original values\nconst tokenMap = {};\nvaultData.forEach(item => {\n const data = item.json;\n tokenMap[data.token] = {\n originalValue: data.original_value,\n piiType: data.pii_type,\n allowedForReinjection: data.allowed_for_reinjection\n };\n});\n\n// Function to recursively restore PII in objects\nfunction restorePII(obj) {\n if (typeof obj === 'string') {\n // Check if string contains tokens (format: [TOKEN_xxxxx])\n const tokenRegex = /\\[TOKEN_[A-Z0-9]+\\]/g;\n return obj.replace(tokenRegex, (token) => {\n const tokenData = tokenMap[token];\n if (tokenData && tokenData.allowedForReinjection) {\n return tokenData.originalValue;\n }\n // Keep token if not allowed for re-injection\n return token;\n });\n } else if (Array.isArray(obj)) {\n return obj.map(item => restorePII(item));\n } else if (obj !== null && typeof obj === 'object') {\n const restored = {};\n for (const key in obj) {\n restored[key] = restorePII(obj[key]);\n }\n return restored;\n }\n return obj;\n}\n\n// Restore PII in the AI output\nconst restoredOutput = restorePII(aiOutput);\n\n// Create audit trail\nconst restoredFields = [];\nfor (const token in tokenMap) {\n if (tokenMap[token].allowedForReinjection) {\n restoredFields.push({\n token: token,\n piiType: tokenMap[token].piiType\n });\n }\n}\n\nreturn [\n {\n json: {\n finalOutput: restoredOutput,\n restoredFields: restoredFields,\n restorationTimestamp: new Date().toISOString(),\n totalTokensRestored: restoredFields.length\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "2c6f4f1e-4a06-4e1d-8b42-cc7900705149",
"name": "Store Audit Log",
"type": "n8n-nodes-base.postgres",
"position": [
1024,
96
],
"parameters": {
"table": {
"__rl": true,
"mode": "name",
"value": "={{ $('Workflow Configuration').first().json.auditTable }}"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"actor": "system",
"timestamp": "={{ $json.timestamp }}",
"document_id": "={{ $json.document_id }}",
"token_count": "={{ $json.token_count }}",
"pii_types_detected": "={{ $json.pii_types_detected }}",
"ai_access_confirmed": true,
"re_injection_events": "={{ $json.re_injection_events }}"
},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {}
},
"typeVersion": 2.6
},
{
"id": "8da4b467-5509-4ebb-9c02-569ab4d641e6",
"name": "Masking Success Check",
"type": "n8n-nodes-base.if",
"position": [
-224,
288
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "id-1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $('Generate Masked Text').item.json.masking_success }}",
"rightValue": "true"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "0f13d421-974b-4be1-b63d-14f960285477",
"name": "Block AI Processing",
"type": "n8n-nodes-base.set",
"position": [
32,
656
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "error",
"type": "string",
"value": "Masking failed - AI processing blocked"
},
{
"id": "id-2",
"name": "status",
"type": "string",
"value": "BLOCKED"
},
{
"id": "id-3",
"name": "requires_manual_review",
"type": "boolean",
"value": true
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a7569d7e-db56-4b12-9791-b706598c336a",
"name": "Send Alert Notification",
"type": "n8n-nodes-base.httpRequest",
"position": [
272,
656
],
"parameters": {
"url": "<__PLACEHOLDER_VALUE__Alert webhook or notification endpoint URL__>",
"method": "POST",
"options": {},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "error_details",
"value": "={{ $json.error_details }}"
},
{
"name": "document_id",
"value": "={{ $json.document_id }}"
},
{
"name": "timestamp",
"value": "={{ $now.toISO() }}"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "77b63ba2-8ba4-405a-998c-bf223e7e9a1c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2544,
304
],
"parameters": {
"color": 7,
"width": 422,
"height": 336,
"content": "## Document Intake & Configuration\n\nReceives documents via webhook and initializes workflow settings such as document ID, confidence thresholds, and database table configuration."
},
"typeVersion": 1
},
{
"id": "8c88f2f3-b3e1-4c2d-a3ae-3f114abf5687",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2096,
304
],
"parameters": {
"color": 7,
"width": 326,
"height": 336,
"content": "## Text Extraction\n\nExtracts raw text from uploaded PDF or document files while preserving original content for downstream processing."
},
"typeVersion": 1
},
{
"id": "4e63dfbc-bfd9-4ca3-bd59-3e8203bd306d",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1744,
-48
],
"parameters": {
"color": 7,
"width": 326,
"height": 720,
"content": "## Multi-Detector PII Detection\n\nIdentifies sensitive data including emails, phone numbers, IDs, and addresses using regex-based logic and AI-powered detection."
},
"typeVersion": 1
},
{
"id": "0693dde2-322f-460a-81ee-818fc47d9069",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1792,
688
],
"parameters": {
"color": 7,
"width": 486,
"height": 528,
"content": "## Address Detection (AI-Powered)\n\nUses an AI model to identify physical addresses in text and returns structured results with location, position, and confidence for each detected address."
},
"typeVersion": 1
},
{
"id": "422a1021-f3ca-4548-9e36-edd8bcca935a",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1392,
48
],
"parameters": {
"color": 7,
"width": 214,
"height": 416,
"content": "## PII Aggregation\n\nCombines outputs from multiple detectors into a unified dataset for further processing."
},
"typeVersion": 1
},
{
"id": "af88bf3a-0499-4e2c-a1c7-2701604aac83",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1152,
48
],
"parameters": {
"color": 7,
"width": 230,
"height": 416,
"content": "## Conflict Resolution Engine\n\nResolves overlapping detections, prioritizes higher confidence matches, and removes duplicate PII entries."
},
"typeVersion": 1
},
{
"id": "dab87540-2c97-41af-895c-758a92ec7499",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
368,
-48
],
"parameters": {
"color": 7,
"width": 566,
"height": 352,
"content": "## Re-Injection Controller\n\nDetermines which fields are allowed to restore original PII based on permissions and prepares retrieval requests and Fetches original sensitive values and Replaces tokens with original values only"
},
"typeVersion": 1
},
{
"id": "75e4b5f1-545c-40cf-8737-780eb2b79ead",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
-144,
-208
],
"parameters": {
"color": 7,
"width": 470,
"height": 336,
"content": "## AI Processing (Safe Data)\n\nProcesses the masked document using AI while preserving tokens to prevent exposure of sensitive information."
},
"typeVersion": 1
},
{
"id": "15588210-d142-4328-83a2-cba8155a23ef",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
960,
-64
],
"parameters": {
"color": 7,
"width": 246,
"height": 368,
"content": "## Audit Logging\n\nStores processing metadata, detected PII types, and re-injection events for compliance and traceability."
},
"typeVersion": 1
},
{
"id": "5050b4ea-ac25-43d9-9d3b-88b392e70636",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-16,
528
],
"parameters": {
"color": 7,
"width": 454,
"height": 272,
"content": "## Error Handling & Alerts\n\nBlocks AI processing and triggers alerts when masking fails or compliance rules are violated."
},
"typeVersion": 1
},
{
"id": "e6490d3e-4ed4-45f6-b851-3c57f6974863",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
-896,
48
],
"parameters": {
"color": 7,
"width": 358,
"height": 416,
"content": "## Tokenization & Vault Storage\n\nReplaces detected PII with secure tokens and stores original values in a protected database vault."
},
"typeVersion": 1
},
{
"id": "0391e0e3-e5b1-4f7c-9e8e-8638bf9c3c35",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
-512,
48
],
"parameters": {
"color": 7,
"width": 278,
"height": 416,
"content": "## Masking Validation\n\nEnsures all PII has been successfully masked and blocks further processing if any sensitive data remains exposed."
},
"typeVersion": 1
},
{
"id": "f3b40a67-5f75-4495-93cb-1f1939af6088",
"name": "Sticky Note13",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3232,
192
],
"parameters": {
"width": 416,
"height": 512,
"content": "## GDPR-Compliant AI Document Processing Pipeline\n\nThis workflow securely processes documents by detecting and tokenizing PII, masking sensitive data before AI analysis, and selectively restoring original values with full audit logging to ensure privacy, security, and regulatory compliance.\n\n## Setup steps\n\n1. Activate the webhook and upload a document (PDF or supported file) \n2. Configure AI credentials (Anthropic / OpenAI for detection and processing) \n3. Set database credentials for PII vault and audit log storage \n4. Adjust detection thresholds and compliance settings if needed \n5. Execute workflow and review masked output, AI results, and audit logs "
},
"typeVersion": 1
},
{
"id": "d1a3aad2-b91c-4551-851c-0c52035b89ef",
"name": "Extract Text",
"type": "n8n-nodes-base.extractFromFile",
"position": [
-2032,
448
],
"parameters": {
"options": {
"keepSource": "both"
},
"operation": "pdf"
},
"typeVersion": 1.1
}
],
"connections": {
"Extract Text": {
"main": [
[
{
"node": "Email Detector",
"type": "main",
"index": 0
},
{
"node": "Phone Detector",
"type": "main",
"index": 0
},
{
"node": "ID Number Detector",
"type": "main",
"index": 0
},
{
"node": "Address Detector AI",
"type": "main",
"index": 0
}
]
]
},
"Email Detector": {
"main": [
[
{
"node": "Merge PII Detections",
"type": "main",
"index": 0
}
]
]
},
"Phone Detector": {
"main": [
[
{
"node": "Merge PII Detections",
"type": "main",
"index": 1
}
]
]
},
"AI Output Parser": {
"ai_outputParser": [
[
{
"node": "AI Processing (Masked Data)",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"ID Number Detector": {
"main": [
[
{
"node": "Merge PII Detections",
"type": "main",
"index": 2
}
]
]
},
"AI Processing Model": {
"ai_languageModel": [
[
{
"node": "AI Processing (Masked Data)",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Address Detector AI": {
"main": [
[
{
"node": "Merge PII Detections",
"type": "main",
"index": 3
}
]
]
},
"Block AI Processing": {
"main": [
[
{
"node": "Send Alert Notification",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model": {
"ai_languageModel": [
[
{
"node": "Address Detector AI",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Generate Masked Text": {
"main": [
[
{
"node": "Masking Success Check",
"type": "main",
"index": 0
}
]
]
},
"Merge PII Detections": {
"main": [
[
{
"node": "PII Consolidation & Conflict Resolver",
"type": "main",
"index": 0
}
]
]
},
"Restore Original PII": {
"main": [
[
{
"node": "Store Audit Log",
"type": "main",
"index": 0
}
]
]
},
"Address Output Parser": {
"ai_outputParser": [
[
{
"node": "Address Detector AI",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Masking Success Check": {
"main": [
[
{
"node": "AI Processing (Masked Data)",
"type": "main",
"index": 0
}
],
[
{
"node": "Block AI Processing",
"type": "main",
"index": 0
}
]
]
},
"Store Tokens in Vault": {
"main": [
[
{
"node": "Generate Masked Text",
"type": "main",
"index": 0
}
]
]
},
"Workflow Configuration": {
"main": [
[
{
"node": "Extract Text",
"type": "main",
"index": 0
}
]
]
},
"Document Upload Webhook": {
"main": [
[
{
"node": "Workflow Configuration",
"type": "main",
"index": 0
}
]
]
},
"Re-Injection Controller": {
"main": [
[
{
"node": "Retrieve Original Values",
"type": "main",
"index": 0
}
]
]
},
"Retrieve Original Values": {
"main": [
[
{
"node": "Restore Original PII",
"type": "main",
"index": 0
}
]
]
},
"AI Processing (Masked Data)": {
"main": [
[
{
"node": "Re-Injection Controller",
"type": "main",
"index": 0
}
]
]
},
"Tokenization & Vault Storage": {
"main": [
[
{
"node": "Store Tokens in Vault",
"type": "main",
"index": 0
}
]
]
},
"PII Consolidation & Conflict Resolver": {
"main": [
[
{
"node": "Tokenization & Vault Storage",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow enables GDPR-compliant document processing by detecting, masking, and securely handling personally identifiable information (PII) before AI analysis.
Source: https://n8n.io/workflows/14320/ — 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 implements a privacy-preserving AI document processing pipeline that detects, masks, and securely manages Personally Identifiable Information (PII) before any AI processing occurs.
⏺ 🚀 How it works
Fully automates your service order pipeline from incoming booking to supplier confirmation — with built-in SLA enforcement and automatic escalation if a supplier goes silent. 📥 Receives orders via web
Tired of grinding out YouTube content? This n8n workflow turns AI into your personal video factory—creating engaging, faceless shorts on autopilot. Perfect for creators, marketers, or side-hustlers lo
Faceless YouTube Generator. Uses httpRequest, limit, googleDrive, googleSheets. Webhook trigger; 49 nodes.