{ "name": "78 FAQ import + gate (reference)", "nodes": [ { "name": "Import webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "parameters": { "httpMethod": "POST", "path": "my-faq-import", "responseMode": "responseNode", "options": {} }, "position": [ 0, 300 ], "id": "a56860cf-55b7-5565-b18d-ca3e5e9abe79", "webhookId": "f3d72461-ee99-519a-a141-33709a703108" }, { "name": "Run check", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "http://localhost:5678/webhook/my-faq-check", "options": { "response": { "response": { "neverError": true } }, "timeout": 30000 }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.body) }}" }, "position": [ 220, 300 ], "id": "e9b3fe87-c7c3-5eaf-897f-45406aefb704" }, { "name": "Checked?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ Array.isArray($json.rows) && $json.rows.length > 0 }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "position": [ 440, 300 ], "id": "78c41fec-cc62-5ce7-9c47-ae10eedd7245" }, { "name": "Reject body", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ JSON.stringify({ error: $json.error ?? 'check failed' }) }}", "options": { "responseCode": 400 } }, "position": [ 660, 760 ], "id": "57d70716-121e-589f-9617-31756fb52264" }, { "name": "Batch ok?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ $json.invalid / $json.total <= ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3) }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "position": [ 660, 300 ], "id": "a60e6dbb-88a3-59b1-9a97-6efe355e18f1" }, { "name": "Reject batch", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ JSON.stringify({ error: 'batch rejected', batch: $('Run check').first().json.batch, invalid_ratio: Math.round($('Run check').first().json.invalid / $('Run check').first().json.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3), codes: $('Run check').first().json.codes }) }}", "options": { "responseCode": 422 } }, "position": [ 880, 560 ], "id": "a0faba36-ffdc-5d7c-a3ad-27655254174b" }, { "name": "Split rows", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const c = $('Run check').first().json;\nconst items = $('Import webhook').first().json.body.items;\nreturn c.rows.map((r, i) => ({ json: { ...r, row: items[i], batch: c.batch } }));" }, "position": [ 880, 300 ], "id": "666c96f6-be31-54d9-a0dc-924fb089b786" }, { "name": "Decide", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "return $input.all();" }, "position": [ 1100, 300 ], "id": "8e4f7788-d9d4-513c-8702-1a4d0218faa5" }, { "name": "Store row", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "parameters": { "url": "={{ $json.ok ? 'http://data-api:8800/faq/add' : 'http://data-api:8800/quarantine' }}", "options": { "response": { "response": { "neverError": true } } }, "method": "POST", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.ok ? $json.row : { items: [{ batch: $json.batch, row: $json.row, errors: $json.errors }] }) }}" }, "position": [ 1320, 300 ], "id": "e35c6cb5-7cc0-5d07-8d38-68d6f7a116d4" }, { "name": "Summarize", "type": "n8n-nodes-base.code", "typeVersion": 2, "parameters": { "jsCode": "const c = $('Run check').first().json;\nconst rows = $('Decide').all().map((i) => i.json);\nconst imported = rows.filter((r) => r.ok);\nconst quarantined = rows.filter((r) => !r.ok);\nconst codes = { ...c.codes };\nfor (const r of quarantined) for (const e of r.errors) if (e === 'llm_mismatch') codes[e] = (codes[e] ?? 0) + 1;\nreturn [{ json: { batch: c.batch, total: c.total, imported: imported.length, quarantined: quarantined.length, rejected: false,\n score: Math.round(imported.length / c.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3),\n codes, imported_ids: imported.map((r) => r.id), quarantined_ids: quarantined.map((r) => r.id) } }];" }, "position": [ 1540, 300 ], "id": "80a8e374-c3d2-5f31-bce1-160bd265176c" }, { "name": "Send result", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": {} }, "position": [ 1760, 300 ], "id": "861cf41c-8519-549b-ba20-7cd0943caf23" } ], "connections": { "Import webhook": { "main": [ [ { "node": "Run check", "type": "main", "index": 0 } ] ] }, "Run check": { "main": [ [ { "node": "Checked?", "type": "main", "index": 0 } ] ] }, "Checked?": { "main": [ [ { "node": "Batch ok?", "type": "main", "index": 0 } ], [ { "node": "Reject body", "type": "main", "index": 0 } ] ] }, "Batch ok?": { "main": [ [ { "node": "Split rows", "type": "main", "index": 0 } ], [ { "node": "Reject batch", "type": "main", "index": 0 } ] ] }, "Split rows": { "main": [ [ { "node": "Decide", "type": "main", "index": 0 } ] ] }, "Decide": { "main": [ [ { "node": "Store row", "type": "main", "index": 0 } ] ] }, "Store row": { "main": [ [ { "node": "Summarize", "type": "main", "index": 0 } ] ] }, "Summarize": { "main": [ [ { "node": "Send result", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } }