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 →
{
"settings": {
"executionOrder": "v1",
"timezone": "Asia/Seoul"
},
"staticData": null,
"tags": [],
"name": "OdidujI MVP - Image Upload",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "image-upload",
"responseMode": "responseNode",
"options": {
"binaryData": true
}
},
"id": "b6938e51-14ec-45ad-b532-950ad153d901",
"name": "Webhook - image-upload",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-980,
-220
]
},
{
"parameters": {
"jsCode": "const crypto = require('crypto');\nconst maxBytes = 10 * 1024 * 1024;\nconst allowedMime = new Set(['image/jpeg', 'image/png']);\nconst binaryKeys = Object.keys($binary || {});\nconst imageKey = binaryKeys[0];\nconst binary = imageKey ? $binary[imageKey] : null;\nconst fileName = binary?.fileName || $json?.file_name || 'upload.jpg';\nconst mimeType = binary?.mimeType || $json?.mime_type || '';\nconst ext = fileName.split('.').pop().toLowerCase();\nconst normalizedExt = ext === 'jpeg' ? 'jpg' : ext;\nconst validType = Boolean(binary) && allowedMime.has(mimeType) && ['jpg', 'png'].includes(normalizedExt);\nif (!validType) {\n return [{ json: { status: 'rejected', validation_status: 'rejected', reject_reason: 'unsupported_file_type', allowed_extensions: ['jpg', 'png'], processing_log: ['received', 'rejected'] } }];\n}\nconst data = await this.helpers.getBinaryDataBuffer(0, imageKey);\nconst fileSize = data.length;\nif (fileSize > maxBytes) {\n return [{ json: { status: 'rejected', validation_status: 'rejected', reject_reason: 'file_too_large', max_file_size_bytes: maxBytes, processing_log: ['received', 'rejected'] } }];\n}\nconst fileHash = crypto.createHash('sha256').update(data).digest('hex');\nconst liveId = String(Date.now()).slice(-9);\nconst captureId = `CAP-LIVE-${liveId}`;\nconst sourceImagePath = `/static/captures/${captureId}.${normalizedExt}`;\nreturn [{\n json: {\n upload_job_id: `JOB-${Date.now()}`,\n capture_id: captureId,\n status: 'validated',\n validation_status: 'accepted',\n file_hash: fileHash,\n file_name: fileName,\n file_ext: normalizedExt,\n mime_type: mimeType,\n file_size_bytes: fileSize,\n source_image_path: sourceImagePath,\n sqlite_db_path: $env.ODIDUJI_DB_PATH || '/database/odiduji.sqlite',\n capture_dir: $env.ODIDUJI_CAPTURE_DIR || '/static/captures',\n processing_log: ['received', 'validated']\n },\n binary: { data: binary }\n}];"
},
"id": "e308a01b-6ca6-4492-9de5-37621f3b4802",
"name": "Validate JPG PNG and hash",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-740,
-220
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.status }}",
"operation": "equal",
"value2": "rejected"
}
]
}
},
"id": "afd5b33e-1f9a-4b2c-a5e9-d90199168047",
"name": "Rejected?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-500,
-220
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "bd4b2ba9-036c-42b2-a802-cd60176a635d",
"name": "Respond rejected upload",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
-260,
-360
]
},
{
"parameters": {
"jsCode": "const fs = require('fs');\nconst path = require('path');\nconst captureDir = $json.capture_dir || '/static/captures';\nfs.mkdirSync(captureDir, { recursive: true });\nconst filePath = path.join(captureDir, `${$json.capture_id}.${$json.file_ext}`);\nconst data = await this.helpers.getBinaryDataBuffer(0, 'data');\nfs.writeFileSync(filePath, data);\nreturn [{ json: { ...$json, status: 'received', disk_path: filePath, processing_log: [...$json.processing_log, 'stored_original'] }, binary: $binary }];"
},
"id": "e5fd5095-3bcf-46a4-8643-59f3bd7627a1",
"name": "Store original capture",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-260,
-120
]
},
{
"parameters": {
"jsCode": "const referenceText = [\n 'OdidujI\ub294 \ub300\ud559\uc0dd \ucea1\ucc98 \uc774\ubbf8\uc9c0\uc5d0\uc11c \ub9c8\uac10, \uae08\uc561, \uc81c\ucd9c\ubb3c, \uc2e0\uccad \uae30\uac04, \uc7a5\uc18c/\uc8fc\uc18c\ub97c \ucc3e\ub294 \uc11c\ube44\uc2a4\ub2e4.',\n '\ubb38\uc11c \uc720\ud615\uc740 notice, assignment, scholarship, receipt, place_link, noise \uc911 \ud558\ub098\ub85c \ubcf8\ub2e4.',\n 'assignment\ub294 \uacfc\uc81c, \uc81c\ucd9c, \ub9c8\uac10, \ubcf4\uace0\uc11c, \ubc1c\ud45c\uc790\ub8cc, LMS/WISE \uacfc\uc81c \uc548\ub0b4\ub97c \uc6b0\uc120 \uadfc\uac70\ub85c \ubcf8\ub2e4.',\n 'scholarship\uc740 \uc7a5\ud559\uae08, \uadfc\ub85c\uc7a5\ud559, \uc9c0\uc6d0\uae08, \uc2e0\uccad \uae30\uac04, \uc81c\ucd9c \uc11c\ub958, \ud559\uc0dd\uc9c0\uc6d0\uc13c\ud130 \ubb38\uad6c\ub97c \uc6b0\uc120 \uadfc\uac70\ub85c \ubcf8\ub2e4.',\n 'receipt\ub294 \uc601\uc218\uc99d, \uacb0\uc81c, \uae08\uc561, \uc6d0, KRW, \uce74\ub4dc \uc2b9\uc778, \ud2f0\ucf13 \uac00\uaca9, \ub9e4\ucd9c\uc804\ud45c \ubb38\uad6c\ub97c \uc6b0\uc120 \uadfc\uac70\ub85c \ubcf8\ub2e4.',\n 'place_link\ub294 \uc7a5\uc18c, \uc8fc\uc18c, \uc704\uce58, \uac15\uc758\uc2e4, \ud68c\uc758\uc2e4, \uc13c\ud130, \uc9c0\ub3c4/\ub9c1\ud06c \ubb38\uad6c\ub97c \uc6b0\uc120 \uadfc\uac70\ub85c \ubcf8\ub2e4.',\n '\ub0a0\uc9dc, \uae08\uc561, \uc81c\ucd9c\ubb3c\uc740 \uc6d0\ubb38 \ud45c\ud604\uc744 \ud6fc\uc190\ud558\uc9c0 \ub9d0\uace0 markdown\uacfc element \uc88c\ud45c\uc5d0 \ub0a8\uaca8\uc57c \ud55c\ub2e4.',\n '\uadfc\uac70 \uc5c6\ub294 \uac1c\uc778\uc815\ubcf4, \ube44\ubc00\ubc88\ud638, \uce74\ub4dc\ubc88\ud638, \ud559\ubc88 \ucd94\ub860\uc740 noise \ub610\ub294 no-answer \ud6c4\ubcf4\ub85c \ub0a8\uaca8\uc57c \ud55c\ub2e4.'\n].join('\\n');\nconst referenceBlock = referenceText || '\ucc38\uace0\uc790\ub8cc \uc5c6\uc74c';\nconst parsePrompt = '\ub108\ub294 \ub300\ud559\uc0dd \ucea1\ucc98 \uc774\ubbf8\uc9c0\ub97c \uad6c\uc870\ud654\ud558\ub294 \ubb38\uc11c \ubd84\uc11d\uac00\uc57c. OCR \uacb0\uacfc\ub97c \uc784\uc758\ub85c \uc694\uc57d\ud558\uc9c0 \ub9d0\uace0, \uac80\uc0c9\uacfc \uadfc\uac70 \ud45c\uc2dc\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d \uc6d0\ubb38\uc5d0 \uac00\uae4c\uc6b4 markdown\uacfc element \uad6c\uc870\ub97c \ub0a8\uaca8\uc918.\\n'\n + '1. \ucc38\uace0\uc790\ub8cc\uac00 \uc788\uc73c\uba74 \ucc38\uace0\uc790\ub8cc\ub97c \uc6b0\uc120 \uae30\uc900\uc73c\ub85c \uc0bc\uc544 \ubb38\uc11c \uc720\ud615 \ud310\ub2e8\uc5d0 \ud544\uc694\ud55c \ub2e8\uc5b4\ub97c \ubcf4\uc874\ud574\uc918.\\n'\n + '2. \ub9c8\uac10/\uae30\uac04/\uae08\uc561/\uc81c\ucd9c\ubb3c/\uc7a5\uc18c/\uc8fc\uc18c/\ucd9c\ucc98/\ubb38\uc758\ucc98\ub294 \uc904\ubc14\uafc8\uacfc \ubb38\ub9e5\uc744 \uc720\uc9c0\ud574\uc918.\\n'\n + '3. \ub0a0\uc9dc\uc640 \uae08\uc561\uc740 \uc6d0\ubb38 \ud45c\uae30\ub97c \ubc18\ub4dc\uc2dc \ub0a8\uaca8\uc918. \uc608: 2026\ub144 5\uc6d4 7\uc77c, 5/24, 80,000\uc6d0.\\n'\n + '4. \ud45c, \ubaa9\ub85d, \uc548\ub0b4\ubb38, \uba54\uc2e0\uc800 \ucea1\ucc98\uc758 \uac01 \ud56d\ubaa9\uc740 markdown list \ub610\ub294 element \ub2e8\uc704\ub85c \ubd84\ub9ac\ud574\uc918.\\n'\n + '5. \ubd88\ud655\uc2e4\ud558\uac70\ub098 \uae68\uc9c4 \uae00\uc790\ub294 \ucd94\uc815\ud558\uc9c0 \ub9d0\uace0 \uc6d0\ubb38\uc5d0 \uac00\uae4c\uc6b4 \ud615\ud0dc\ub85c \ub0a8\uaca8\uc918.\\n\\n'\n + '<\uc815\ubcf4>\\n' + referenceBlock + '\\n</\uc815\ubcf4>';\nconst classifyPrompt = '\uc544\ub798 parsed_markdown\uc744 \ubcf4\uace0 doc_type\uc744 notice, assignment, scholarship, receipt, place_link, noise \uc911 \ud558\ub098\ub85c \uace0\ub978\ub2e4. \ucc38\uace0\uc790\ub8cc \uae30\uc900\uc744 \uc6b0\uc120 \uc801\uc6a9\ud558\uace0, \uadfc\uac70\uac00 \uc57d\ud558\uba74 Classify_Ambiguous \ud6c4\ubcf4\ub97c \ub0a8\uae34\ub2e4.\\n\\n<\uc815\ubcf4>\\n' + referenceBlock + '\\n</\uc815\ubcf4>';\nconst extractPrompt = '\uc544\ub798 parsed_markdown\uc5d0\uc11c title, deadline, amount, required_submission, location_name, address, source\uc640 evidence_text\ub97c \ucd94\ucd9c\ud55c\ub2e4. \uac12\uc740 \ubc18\ub4dc\uc2dc \uc6d0\ubb38 \uadfc\uac70\uac00 \uc788\ub294 \uacbd\uc6b0\uc5d0\ub9cc \ucc44\uc6b4\ub2e4.\\n\\n<\uc815\ubcf4>\\n' + referenceBlock + '\\n</\uc815\ubcf4>';\nreturn [{ json: { ...$json, upstage_parse_url: $env.UPSTAGE_DOCUMENT_PARSE_URL || 'https://api.upstage.ai/v1/document-ai/document-parse', reference_text: referenceText, reference_used: true, parsePrompt, classifyPrompt, extractPrompt, parse_request: { ocr: 'force', output_format: 'markdown' }, status: 'parsed_pending' }, binary: $binary }];"
},
"id": "58d3eda4-ee5b-46dd-a492-4058f86150b0",
"name": "Prepare Document Parse",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-20,
-120
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $json.upstage_parse_url }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.UPSTAGE_API_KEY }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "document",
"inputDataFieldName": "data"
},
{
"name": "ocr",
"value": "={{ $json.parse_request.ocr }}"
},
{
"name": "output_format",
"value": "={{ $json.parse_request.output_format }}"
}
]
},
"options": {}
},
"id": "871f9953-b540-4809-85e8-ee1b421fe64a",
"name": "Upstage Document Parse",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
220,
-120
],
"continueOnFail": true
},
{
"parameters": {
"jsCode": "const response = $json || {};\nfunction toText(value) {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return value.map(toText).filter(Boolean).join(String.fromCharCode(10));\n if (typeof value === 'object') {\n for (const key of ['parsed_markdown', 'markdown', 'text', 'content', 'html']) {\n if (value[key] !== undefined && value[key] !== value) {\n const nested = toText(value[key]);\n if (nested) return nested;\n }\n }\n if (Array.isArray(value.elements)) return toText(value.elements);\n if (Array.isArray(value.pages)) return toText(value.pages);\n if (value.content && value.content !== value) return toText(value.content);\n return '';\n }\n return String(value);\n}\nconst content = toText(response.parsed_markdown || response.markdown || response.text || response.content || response.elements || response.pages || '');\nconst confidence = Number(response.confidence || response.parse_confidence || (content ? 0.8 : 0.5));\nreturn [{ json: { ...$('Store original capture').item.json, parsed_markdown: content, layout_blocks: response.elements || response.layout_blocks || response.pages || [], evidence_candidates: response.evidence_candidates || [], parse_confidence: confidence, status: 'parsed', processing_log: [...$('Store original capture').item.json.processing_log, 'parsed'] }, binary: $('Store original capture').item.binary }];"
},
"id": "3a07b5c4-92a6-4ee2-9268-df5b99533c0d",
"name": "Normalize Parse Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
-120
]
},
{
"parameters": {
"jsCode": "function normalizeText(value) {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return value.map(normalizeText).filter(Boolean).join(String.fromCharCode(10));\n if (typeof value === 'object') {\n for (const key of ['parsed_markdown', 'markdown', 'text', 'content', 'html']) {\n if (value[key] !== undefined && value[key] !== value) {\n const nested = normalizeText(value[key]);\n if (nested) return nested;\n }\n }\n if (Array.isArray(value.elements)) return normalizeText(value.elements);\n if (Array.isArray(value.pages)) return normalizeText(value.pages);\n return JSON.stringify(value);\n }\n return String(value);\n}\nfunction hasAny(text, keywords) {\n return keywords.some((keyword) => text.includes(keyword));\n}\nconst text = normalizeText($json.parsed_markdown);\nconst lowerText = text.toLowerCase();\nconst amountPattern = /(?:\\u20a9\\s*)?[0-9][0-9,]*\\s*(?:\\uC6D0|krw)/i;\nconst receiptKeywords = [\n '\\uC601\\uC218\\uC99D',\n '\\uACB0\\uC81C',\n '\\uCE74\\uB4DC \\uC2B9\\uC778',\n '\\uB9E4\\uCD9C\\uC804\\uD45C',\n '\\uC608\\uB9E4',\n '\\uD2F0\\uCF13',\n '\\uC8FC\\uCC28\\uC815\\uC0B0',\n '\\uC2E0\\uC6A9\\uCE74\\uB4DC',\n '\\uD604\\uAE08\\uC601\\uC218\\uC99D'\n];\nconst paymentKeywords = [\n '\\uACB0\\uC81C',\n '\\uCE74\\uB4DC',\n '\\uC2B9\\uC778',\n '\\uC608\\uB9E4',\n '\\uD2F0\\uCF13',\n '\\uB9E4\\uCD9C',\n '\\uC8FC\\uBB38'\n];\nconst scholarshipKeywords = [\n '\\uC7A5\\uD559',\n '\\uADFC\\uB85C\\uC7A5\\uD559',\n '\\uC9C0\\uC6D0\\uAE08',\n '\\uB4F1\\uB85D\\uAE08',\n '\\uD559\\uC0DD\\uC9C0\\uC6D0\\uC13C\\uD130'\n];\nconst assignmentKeywords = [\n '\\uACFC\\uC81C',\n '\\uC81C\\uCD9C\\uBB3C',\n '\\uBCF4\\uACE0\\uC11C',\n '\\uB808\\uD3EC\\uD2B8',\n '\\uBC1C\\uD45C\\uC790\\uB8CC',\n '\\uACFC\\uC81C \\uC548\\uB0B4',\n '\\uB9C8\\uAC10\\uC77C'\n];\nconst submissionContextKeywords = [\n '\\uC218\\uC5C5',\n '\\uAC15\\uC758',\n 'LMS',\n 'WISE',\n '\\uACFC\\uBAA9',\n '\\uAE30\\uD55C'\n];\nconst placeKeywords = [\n '\\uC8FC\\uC18C',\n '\\uC9C0\\uB3C4',\n '\\uC624\\uC2DC\\uB294 \\uAE38',\n '\\uC704\\uCE58 \\uACF5\\uC720',\n '\\uAC15\\uC758\\uC2E4',\n '\\uD68C\\uC758\\uC2E4'\n];\nlet docType = 'noise';\nif (!lowerText.trim()) {\n docType = 'noise';\n} else if (hasAny(text, scholarshipKeywords)) {\n docType = 'scholarship';\n} else if (hasAny(text, receiptKeywords) || (amountPattern.test(text) && hasAny(text, paymentKeywords))) {\n docType = 'receipt';\n} else if (hasAny(text, assignmentKeywords) || (text.includes('\\uC81C\\uCD9C') && hasAny(text, submissionContextKeywords))) {\n docType = 'assignment';\n} else if (hasAny(text, placeKeywords)) {\n docType = 'place_link';\n} else {\n docType = 'notice';\n}\nconst categoryConfidence = $json.parsed_markdown ? 0.72 : 0.5;\nconst secondary = docType === 'notice' ? 'assignment' : 'notice';\nconst candidates = [\n { doc_type: docType, confidence: categoryConfidence },\n { doc_type: secondary, confidence: 0.58 },\n { doc_type: 'noise', confidence: lowerText.trim() ? 0.22 : 0.5 }\n];\nconst classificationState = categoryConfidence < 0.7 ? 'Classify_Ambiguous' : 'Classified';\nreturn [{ json: { ...$json, doc_type: docType, category_confidence: categoryConfidence, category_candidates: candidates, classification_state: classificationState, status: 'classified', processing_log: [...$json.processing_log, 'classified'] }, binary: $binary }];"
},
"id": "a93de515-68e8-4594-a2cf-9ac40236f852",
"name": "Document Classify MVP",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
-120
]
},
{
"parameters": {
"jsCode": "const registry = {\n notice: { schema_id: 'notice_v1', fields: ['title', 'source', 'created_date', 'event_date', 'deadline', 'has_submission', 'location_name'] },\n assignment: { schema_id: 'assignment_v1', fields: ['title', 'deadline', 'required_submission', 'has_submission'] },\n scholarship: { schema_id: 'scholarship_v1', fields: ['title', 'source', 'deadline', 'amount', 'required_submission', 'location_name'] },\n receipt: { schema_id: 'receipt_v1', fields: ['title', 'created_date', 'amount', 'currency', 'location_name', 'address'] },\n place_link: { schema_id: 'place_link_v1', fields: ['title', 'source', 'event_date', 'location_name', 'address'] },\n noise: { schema_id: 'noise_v1', fields: ['title', 'source'] }\n};\nconst schema = registry[$json.doc_type];\nif (!schema) return [{ json: { ...$json, status: 'failed', error_code: 'schema_not_found' }, binary: $binary }];\nreturn [{ json: { ...$json, extract_schema_id: schema.schema_id, schema_json: schema, processing_log: [...$json.processing_log, 'schema_routed'] }, binary: $binary }];"
},
"id": "60fc7f04-a3d8-4bdc-b548-0a60ca91ee33",
"name": "Schema Router",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
940,
-120
]
},
{
"parameters": {
"jsCode": "function normalizeText(value) {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return value.map(normalizeText).filter(Boolean).join(String.fromCharCode(10));\n if (typeof value === 'object') {\n for (const key of ['parsed_markdown', 'markdown', 'text', 'content', 'html']) {\n if (value[key] !== undefined && value[key] !== value) {\n const nested = normalizeText(value[key]);\n if (nested) return nested;\n }\n }\n if (Array.isArray(value.elements)) return normalizeText(value.elements);\n if (Array.isArray(value.pages)) return normalizeText(value.pages);\n return JSON.stringify(value);\n }\n return String(value);\n}\nconst text = normalizeText($json.parsed_markdown);\nconst lines = text.split(/\\n+/).map((line) => line.trim()).filter(Boolean);\nconst title = lines[0]?.replace(/^#+\\s*/, '') || $json.file_name;\nconst amountMatch = text.match(/([0-9][0-9,]*)\\s*(\uc6d0|krw|\u20a9)/i);\nconst dateMatch = text.match(/(20\\d{2}[.\\-\ub144\\s]+\\d{1,2}[.\\-\uc6d4\\s]+\\d{1,2})/);\nconst amount = amountMatch ? Number(amountMatch[1].replace(/,/g, '')) : null;\nconst deadline = dateMatch ? dateMatch[1].replace(/[\ub144\uc6d4.]/g, '-').replace(/\\s+/g, '').replace(/-$/g, '') : null;\nconst extractConfidence = text ? 0.72 : 0.5;\nconst evidence = {\n title,\n amount: amountMatch?.[0] || null,\n deadline: dateMatch?.[0] || null,\n doc_type: $json.doc_type\n};\nconst needsReview = [$json.parse_confidence, $json.category_confidence, extractConfidence].some((value) => Number(value) < 0.7);\nreturn [{ json: { ...$json, title, source: 'live_upload', deadline, amount, currency: amount ? 'KRW' : null, evidence_text: evidence, extract_confidence: extractConfidence, confidence: Math.min(Number($json.parse_confidence || 0), Number($json.category_confidence || 0), extractConfidence), needs_review: needsReview, status: 'extracted', processing_log: [...$json.processing_log, 'extracted', 'normalized'] }, binary: $binary }];"
},
"id": "e84dc4d0-97d8-4d45-bcef-5530a7f1328d",
"name": "Information Extract MVP",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1180,
-120
]
},
{
"parameters": {
"jsCode": "function loadSqlite3() {\n try { return require('sqlite3'); } catch (firstError) {\n try { return require('/usr/local/lib/node_modules/n8n/node_modules/.pnpm/sqlite3@5.1.7/node_modules/sqlite3'); }\n catch (secondError) { throw new Error(`${firstError.message}; ${secondError.message}`); }\n }\n}\nfunction openDb(dbPath) {\n const sqlite3 = loadSqlite3();\n const db = new sqlite3.Database(dbPath);\n return {\n exec(sql) { return new Promise((resolve, reject) => db.exec(sql, (error) => error ? reject(error) : resolve())); },\n run(sql, params = []) { return new Promise((resolve, reject) => db.run(sql, params, function(error) { error ? reject(error) : resolve(this); })); },\n get(sql, params = []) { return new Promise((resolve, reject) => db.get(sql, params, (error, row) => error ? reject(error) : resolve(row))); },\n all(sql, params = []) { return new Promise((resolve, reject) => db.all(sql, params, (error, rows) => error ? reject(error) : resolve(rows || []))); },\n close() { return new Promise((resolve, reject) => db.close((error) => error ? reject(error) : resolve())); },\n };\n}\nconst fs = require('fs');\nlet db;\ntry {\n db = openDb($json.sqlite_db_path);\n await db.exec('PRAGMA foreign_keys = ON');\n const duplicate = await db.get('SELECT capture_id, source_image_path FROM capture_records WHERE file_hash = ?', [$json.file_hash]);\n if (duplicate) {\n if ($json.disk_path && fs.existsSync($json.disk_path)) fs.unlinkSync($json.disk_path);\n await db.close();\n return [{ json: { ...$json, capture_id: duplicate.capture_id, source_image_path: duplicate.source_image_path, status: 'ready', cache_hit: true, processing_log: [...$json.processing_log, 'cache_hit', 'ready'] } }];\n }\n await db.exec('BEGIN IMMEDIATE');\n await db.run(\n `INSERT INTO capture_records (capture_id, source_image_path, file_hash, file_name, file_ext, mime_type, file_size_bytes, file_meta_json, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'ready')`,\n [$json.capture_id, $json.source_image_path, $json.file_hash, $json.file_name, $json.file_ext, $json.mime_type, Number($json.file_size_bytes || 0), JSON.stringify({ upload_source: 'n8n', disk_path: $json.disk_path })]\n );\n await db.run(\n `INSERT INTO metadata_index (capture_id, doc_type, title, source, deadline, amount, currency, evidence_text_json, parsed_markdown, layout_blocks_json, category_candidates_json, parse_confidence, category_confidence, extract_confidence, confidence, classification_state, needs_review) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [$json.capture_id, $json.doc_type, $json.title || null, $json.source || null, $json.deadline || null, $json.amount || null, $json.currency || null, JSON.stringify($json.evidence_text || {}), $json.parsed_markdown || null, JSON.stringify($json.layout_blocks || []), JSON.stringify($json.category_candidates || []), Number($json.parse_confidence || 0), Number($json.category_confidence || 0), Number($json.extract_confidence || 0), Number($json.confidence || 0), $json.classification_state || 'Classified', $json.needs_review ? 1 : 0]\n );\n await db.run(\n `INSERT OR REPLACE INTO cache_entries (file_hash, capture_id, cached_parse_json, cached_classify_json, cached_extract_json, api_version) VALUES (?, ?, ?, ?, ?, ?)`,\n [$json.file_hash, $json.capture_id, JSON.stringify({ parsed_markdown: $json.parsed_markdown, layout_blocks: $json.layout_blocks || [], parse_confidence: $json.parse_confidence }), JSON.stringify({ doc_type: $json.doc_type, category_confidence: $json.category_confidence, category_candidates: $json.category_candidates || [], classification_state: $json.classification_state }), JSON.stringify($json), 'phase3-n8n']\n );\n await db.exec('COMMIT');\n await db.close();\n return [{ json: { ...$json, status: 'ready', cache_hit: false, processing_log: [...$json.processing_log, 'indexed', 'ready'] } }];\n} catch (error) {\n try { if (db) await db.exec('ROLLBACK'); } catch (_) {}\n try { if (db) await db.close(); } catch (_) {}\n return [{ json: { ...$json, status: 'failed', error_code: 'sqlite_write_failed', error_message: error.message, processing_log: [...($json.processing_log || []), 'sqlite_failed'] } }];\n}"
},
"id": "c85fcff0-ff02-48ef-b8c0-362f282498f3",
"name": "SQLite Index Write",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1420,
-120
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "f90601c4-bb8a-48af-96f0-69e6a071661f",
"name": "Respond image-upload",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1660,
-120
]
}
],
"connections": {
"Webhook - image-upload": {
"main": [
[
{
"node": "Validate JPG PNG and hash",
"type": "main",
"index": 0
}
]
]
},
"Validate JPG PNG and hash": {
"main": [
[
{
"node": "Rejected?",
"type": "main",
"index": 0
}
]
]
},
"Rejected?": {
"main": [
[
{
"node": "Respond rejected upload",
"type": "main",
"index": 0
}
],
[
{
"node": "Store original capture",
"type": "main",
"index": 0
}
]
]
},
"Store original capture": {
"main": [
[
{
"node": "Prepare Document Parse",
"type": "main",
"index": 0
}
]
]
},
"Prepare Document Parse": {
"main": [
[
{
"node": "Upstage Document Parse",
"type": "main",
"index": 0
}
]
]
},
"Upstage Document Parse": {
"main": [
[
{
"node": "Normalize Parse Result",
"type": "main",
"index": 0
}
]
]
},
"Normalize Parse Result": {
"main": [
[
{
"node": "Document Classify MVP",
"type": "main",
"index": 0
}
]
]
},
"Document Classify MVP": {
"main": [
[
{
"node": "Schema Router",
"type": "main",
"index": 0
}
]
]
},
"Schema Router": {
"main": [
[
{
"node": "Information Extract MVP",
"type": "main",
"index": 0
}
]
]
},
"Information Extract MVP": {
"main": [
[
{
"node": "SQLite Index Write",
"type": "main",
"index": 0
}
]
]
},
"SQLite Index Write": {
"main": [
[
{
"node": "Respond image-upload",
"type": "main",
"index": 0
}
]
]
}
},
"triggerCount": 1,
"versionId": "aaa994c1-e809-4ea6-9290-53648ee31c89"
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
OdidujI MVP - Image Upload. Uses httpRequest. Webhook trigger; 13 nodes.
Source: https://github.com/locomoco-studio/OdiDuji/blob/8da23021500e317898b147666dcc605efed307c5/workflows/image-upload.json — 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 n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c