{ "name": "Delivery Note OCR", "nodes": [ { "parameters": { "rule": { "interval": [ { "field": "seconds", "secondsInterval": 30 } ] } }, "id": "uuid-schedule", "name": "Poll every 30 s", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.3, "position": [ 240, 300 ], "webhookId": "" }, { "parameters": { "method": "GET", "url": "={{ $env.MEGA_PB_URL }}/api/collections/delivery_notes/records", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "filter", "value": "=processing_status='pending'" }, { "name": "fields", "value": "id,delivery_note_number,site,project_code,slip_image,purchase_order,date_received" }, { "name": "perPage", "value": "20" }, { "name": "sort", "value": "created" } ] }, "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $credentials.pbToken }}" } ] }, "options": {} }, "id": "uuid-list", "name": "List pending DNs", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 460, 300 ], "credentials": { "httpHeaderAuth": { "id": "REPLACE_PB_TOKEN_ID", "name": "PocketBase Admin Token" } } }, { "parameters": { "batchSize": 1, "options": {} }, "id": "uuid-split", "name": "For each pending DN", "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [ 680, 300 ] }, { "parameters": { "method": "GET", "url": "={{ $env.MEGA_PB_URL }}/api/files/delivery_notes/{{ $json.id }}/{{ $json.slip_image }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $credentials.pbToken }}" } ] }, "options": {} }, "id": "uuid-download", "name": "Download slip image", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 900, 300 ], "credentials": { "httpHeaderAuth": { "id": "REPLACE_PB_TOKEN_ID", "name": "PocketBase Admin Token" } } }, { "parameters": { "method": "POST", "url": "https://api.openai.com/v1/chat/completions", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "Bearer {{ $credentials.openaiKey }}" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"model\": \"gpt-4o-mini\",\n \"response_format\": { \"type\": \"json_object\" },\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are extracting structured data from a delivery note slip image. Return ONLY valid JSON matching this schema: { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"po_number\\\": { \\\"type\\\": [\\\"string\\\", \\\"null\\\"] }, \\\"delivery_note_number\\\": { \\\"type\\\": [\\\"string\\\", \\\"null\\\"] }, \\\"date_received\\\": { \\\"type\\\": [\\\"string\\\", \\\"null\\\"] }, \\\"received_by_name\\\": { \\\"type\\\": [\\\"string\\\", \\\"null\\\"] }, \\\"confidence\\\": { \\\"type\\\": \\\"number\\\", \\\"minimum\\\": 0, \\\"maximum\\\": 1 }, \\\"items\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"description\\\": { \\\"type\\\": \\\"string\\\" }, \\\"qty\\\": { \\\"type\\\": \\\"number\\\" }, \\\"unit\\\": { \\\"type\\\": [\\\"string\\\", \\\"null\\\"] }, \\\"unit_price\\\": { \\\"type\\\": [\\\"number\\\", \\\"null\\\"] } } } } } }. Rules: confidence is your honest assessment (0.9+ only if everything is clearly legible); quantities must be numbers; if the slip is unreadable return empty items and confidence below 0.3.\"\n },\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Extract the delivery note data from this slip image.\"\n },\n {\n \"type\": \"image_url\",\n \"image_url\": { \"url\": \"data:image/jpeg;base64,{{ $binary.data }}\" }\n }\n ]\n }\n ]\n}", "options": {} }, "id": "uuid-ocr", "name": "OCR via OpenAI Vision", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 1120, 300 ], "credentials": { "httpHeaderAuth": { "id": "REPLACE_OPENAI_KEY_ID", "name": "OpenAI API Key" } } }, { "parameters": { "jsCode": "// OpenAI returns { choices: [{ message: { content: \"\" }] }\nconst raw = $json.choices?.[0]?.message?.content;\nif (!raw) throw new Error('No OCR content in response');\nconst parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\nreturn [{ json: parsed }];" }, "id": "uuid-parse", "name": "Parse OCR JSON", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1340, 300 ] }, { "parameters": { "method": "POST", "url": "={{ $env.MEGA_NEXT_URL }}/api/delivery-notes/{{ $('List pending DNs').item.json.id }}/ocr-result", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "x-ingest-secret", "value": "={{ $env.N8N_INGEST_SECRET }}" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"po_number\": {{ $json.po_number ? '\"' + $json.po_number + '\"' : 'null' }},\n \"delivery_note_number\": {{ $json.delivery_note_number ? '\"' + $json.delivery_note_number + '\"' : 'null' }},\n \"date_received\": {{ $json.date_received ? '\"' + $json.date_received + '\"' : 'null' }},\n \"received_by_name\": {{ $json.received_by_name ? '\"' + $json.received_by_name + '\"' : 'null' }},\n \"confidence\": {{ $json.confidence || 0 }},\n \"items\": {{ JSON.stringify($json.items || []) }},\n \"raw_ocr\": null\n}", "options": { "continueOnFail": true } }, "id": "uuid-callback", "name": "POST result to app", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 1560, 300 ] }, { "parameters": { "method": "PATCH", "url": "={{ $env.MEGA_PB_URL }}/api/collections/delivery_notes/records/{{ $('List pending DNs').item.json.id }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $credentials.pbToken }}" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={ \"processing_status\": \"ocr-complete\" }", "options": { "continueOnFail": true } }, "id": "uuid-mark", "name": "Mark ocr-complete in PB", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 1780, 300 ], "credentials": { "httpHeaderAuth": { "id": "REPLACE_PB_TOKEN_ID", "name": "PocketBase Admin Token" } } } ], "connections": { "Poll every 30 s": { "main": [ [ { "node": "List pending DNs", "type": "main", "index": 0 } ] ] }, "List pending DNs": { "main": [ [ { "node": "For each pending DN", "type": "main", "index": 0 } ] ] }, "For each pending DN": { "main": [ [ { "node": "Download slip image", "type": "main", "index": 0 } ] ] }, "Download slip image": { "main": [ [ { "node": "OCR via OpenAI Vision", "type": "main", "index": 0 } ] ] }, "OCR via OpenAI Vision": { "main": [ [ { "node": "Parse OCR JSON", "type": "main", "index": 0 } ] ] }, "Parse OCR JSON": { "main": [ [ { "node": "POST result to app", "type": "main", "index": 0 } ] ] }, "POST result to app": { "main": [ [ { "node": "Mark ocr-complete in PB", "type": "main", "index": 0 } ] ] }, "Mark ocr-complete in PB": { "main": [ [ { "node": "For each pending DN", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "saveExecutionProgress": true, "saveDataSuccessExecution": "all", "saveDataErrorExecution": "all", "saveManualExecutions": true }, "versionId": "1", "id": "delivery-note-ocr", "meta": { "templateCredsSetupCompleted": false, "description": "Polls PocketBase every 30 s for delivery_notes with processing_status='pending', OCRs the slip via OpenAI Vision, and POSTs the result back to /api/delivery-notes/{id}/ocr-result." }, "tags": [ { "name": "ocr" }, { "name": "delivery-notes" }, { "name": "pocketbase" } ] }