{ "updatedAt": "2026-05-07T20:24:47.896Z", "createdAt": "2026-05-07T20:24:40.153Z", "id": "dQ1d5deV4mF0s5eP", "name": "Paperless AI Processing v1.0", "description": null, "active": true, "isArchived": false, "nodes": [ { "parameters": { "httpMethod": "POST", "path": "paperless/new-document", "options": {} }, "id": "3d8bc021-e27f-4f7b-b716-a202d66551be", "name": "Paperless Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ 0, 0 ], "webhookId": "fd1eaa3a-fbf1-4a17-94fd-bb483df602b7" }, { "parameters": { "jsCode": "// Extract document ID from whatever Paperless sends\nconst data = $input.first().json;\nconst body = data.body || data;\n\n// Try multiple possible field names and formats\nlet docId = body.document_id || body.id || body.doc_id || body.pk;\n\n// If it's a string like '{document_id}' (template not resolved), fall back to latest doc\nif (!docId || typeof docId === 'string' && docId.includes('{')) {\n docId = null;\n}\n\nreturn [{ json: { docId } }];" }, "id": "48277e9e-a835-4e12-8608-63e905abe74d", "name": "Extract Document ID", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 224, 0 ] }, { "parameters": { "url": "=https://paperless.paccoco.com/api/documents/{{ $json.docId ? $json.docId + '/' : '?ordering=-added&page_size=1' }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "Token 4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d" } ] }, "options": {} }, "id": "17ed7e6f-ccaa-43d3-9031-9f3d2bb12dec", "name": "Fetch Document", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 448, 0 ] }, { "parameters": { "jsCode": "const response = $input.first().json;\n\n// Handle both single doc response and list response\nconst doc = response.results ? response.results[0] : response;\n\nif (!doc || !doc.id) {\n throw new Error('No document found in response');\n}\n\n// Content is already in the metadata from Paperless OCR\nconst content = (doc.content || '').substring(0, 4000);\n\nreturn [{\n json: {\n documentId: doc.id,\n title: doc.title || 'Untitled',\n originalFilename: doc.original_file_name || '',\n correspondent: doc.correspondent_name || null,\n content: content,\n created: doc.created || new Date().toISOString()\n }\n}];" }, "id": "3a026b3b-141b-48b4-a9ae-03998914f45c", "name": "Prepare Content", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 672, 0 ] }, { "parameters": { "method": "POST", "url": "http://10.5.1.6:4000/v1/chat/completions", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "Authorization", "value": "Bearer sk-a95bd142d43d175b6476ebc862e81aa1f6ef34b1153f839698d07541d2f55308" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"model\": \"medium\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are a document classification assistant. Analyze the document and return ONLY valid JSON. Do not include markdown. Do not include code fences.\\n\\nReturn this exact JSON structure:\\n{\\n \\\"summary\\\": \\\"short 1-2 sentence summary with no sensitive identifiers\\\",\\n \\\"doc_type\\\": \\\"invoice | receipt | letter | contract | manual | report | medical | tax | military | other\\\",\\n \\\"suggested_tags\\\": [\\\"tag1\\\", \\\"tag2\\\", \\\"tag3\\\"]\\n}\\n\\nIf the document is sensitive, summarize only generally and say sensitive personal identifiers were present and redacted. If the document is not sensitive, provide a normal 1-2 sentence summary. Never mention or summarize sensitive field categories such as SSNs, dates of birth, addresses, phone numbers, financial amounts, ID numbers, service numbers, or exact dates.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ JSON.stringify('Document title: ' + ($json.title || 'Untitled') + '\\nFilename: ' + ($json.originalFilename || '') + '\\nCorrespondent: ' + ($json.correspondent || 'Unknown') + '\\n\\nContent:\\n' + ($json.content || '')).slice(1, -1) }}\"\n }\n ],\n \"max_tokens\": 350,\n \"temperature\": 0.1,\n \"response_format\": {\n \"type\": \"json_object\"\n }\n}", "options": {} }, "id": "51374897-d90e-49a2-889e-0476817dd9d8", "name": "AI Classify & Extract", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 880, 0 ] }, { "parameters": { "jsCode": "const response = $input.first().json;\nconst docData = $('Prepare Content').first().json;\n\nconst raw = response.choices?.[0]?.message?.content || '{}';\nlet analysis;\n\ntry {\n analysis = JSON.parse(raw);\n} catch (e) {\n analysis = {\n summary: 'Document processed, but AI response could not be parsed cleanly. Sensitive details were not included.',\n doc_type: 'other',\n suggested_tags: []\n };\n}\n\nfunction cleanText(value) {\n return String(value || '')\n .replace(/\\b\\d{3}[- ]?\\d{2}[- ]?\\d{4}\\b/g, '[REDACTED_SSN]')\n .replace(/\\b\\d{8}\\b/g, '[REDACTED_DATE]')\n .replace(/\\b\\d{1,5}\\s+[A-Za-z0-9 .'-]+\\s+(Road|RD|Street|St|Avenue|Ave|Drive|Dr|Lane|Ln|Court|Ct|Branch|Blvd|Boulevard)\\b[^,\\n]*/gi, '[REDACTED_ADDRESS]')\n .replace(/\\b\\d{5}(?:-\\d{4})?\\b/g, '[REDACTED_ZIP]');\n}\n\nconst tags = Array.isArray(analysis.suggested_tags)\n ? analysis.suggested_tags\n .map(t => cleanText(t).trim().toLowerCase())\n .filter(Boolean)\n .map(t => t.replace(/[^a-z0-9 _-]/gi, '').trim())\n .filter(Boolean)\n .slice(0, 5)\n : [];\n\nreturn [\n {\n json: {\n documentId: docData.documentId,\n title: cleanText(docData.title || 'Untitled'),\n summary: cleanText(analysis.summary || 'Document processed.'),\n doc_type: cleanText(analysis.doc_type || 'other').toLowerCase(),\n suggested_tags: tags\n }\n }\n];" }, "id": "1f37f096-132b-43b4-bcb4-f5ed094bc9e5", "name": "Parse Analysis", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1104, 0 ] }, { "parameters": { "method": "POST", "url": "=https://paperless.paccoco.com/api/documents/{{ $json.documentId }}/notes/", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "Token 4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"note\": \"{{ JSON.stringify('**AI Analysis**\\nType: ' + ($json.doc_type || 'other') + '\\nSummary: ' + ($json.summary || '') + '\\nSuggested tags: ' + (($json.suggested_tags || []).join(', '))).slice(1, -1) }}\"\n}", "options": {} }, "id": "5597c8c1-436e-4373-bb54-0180be4e7bca", "name": "Add Note to Document", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1328, 0 ] }, { "parameters": { "jsCode": "const data = $input.first().json;\n\nconst tags = Array.isArray(data.suggested_tags)\n ? data.suggested_tags.join(', ')\n : 'None';\n\nconst message = [\n data.title || 'Untitled',\n '',\n `Type: ${data.doc_type || 'other'}`,\n '',\n `Summary: ${data.summary || 'No summary returned'}`,\n '',\n `Suggested tags: ${tags || 'None'}`\n].join('\\n');\n\nreturn [{\n json: {\n title: '\ud83d\udcc4 New Document Processed',\n message,\n priority: 4\n }\n}];" }, "id": "c3319471-e3f1-4cd3-aae1-c794f081b1e3", "name": "Format Gotify Document Message", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1328, 208 ] }, { "parameters": { "message": "={{ $json.message }}", "additionalFields": { "priority": "={{ $json.priority }}", "title": "={{ $json.title }}" }, "options": { "contentType": "text/plain" } }, "id": "193f19dc-2ad6-41af-bd29-a0f5dc0ef1a0", "name": "Send Gotify Message", "type": "n8n-nodes-base.gotify", "typeVersion": 1, "position": [ 1552, 208 ], "credentials": { "gotifyApi": { "id": "ajvRjvj0QldLQYmo", "name": "Paperless" } } }, { "parameters": { "jsCode": "const data = $input.first().json;\n\nconst PAPERLESS_BASE_URL = 'https://paperless.paccoco.com';\nconst PAPERLESS_TOKEN = '4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d';\n\nconst headers = {\n Authorization: `Token ${PAPERLESS_TOKEN}`,\n 'Content-Type': 'application/json',\n};\n\nfunction normalizeTagName(tag) {\n return String(tag || '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9 _-]/gi, '')\n .replace(/\\s+/g, ' ')\n .slice(0, 64);\n}\n\nconst suggestedTags = Array.isArray(data.suggested_tags)\n ? [...new Set(data.suggested_tags.map(normalizeTagName).filter(Boolean))].slice(0, 5)\n : [];\n\nif (!data.documentId) {\n throw new Error('No documentId found; cannot apply tags.');\n}\n\nif (suggestedTags.length === 0) {\n return [{\n json: {\n ...data,\n tag_apply_status: 'skipped_no_suggested_tags',\n applied_tag_names: [],\n applied_tag_ids: []\n }\n }];\n}\n\nasync function request(method, url, body) {\n const options = {\n method,\n url,\n headers,\n json: true,\n };\n\n if (body !== undefined) {\n options.body = body;\n }\n\n return await this.helpers.httpRequest(options);\n}\n\n// Fetch all existing tags. Handles pagination.\nconst existingTags = [];\nlet tagsUrl = `${PAPERLESS_BASE_URL}/api/tags/?page_size=100`;\n\nwhile (tagsUrl) {\n const page = await request.call(this, 'GET', tagsUrl);\n const results = Array.isArray(page.results) ? page.results : (Array.isArray(page) ? page : []);\n existingTags.push(...results);\n\n if (page.next) {\n tagsUrl = page.next.startsWith('http') ? page.next : `${PAPERLESS_BASE_URL}${page.next}`;\n } else {\n tagsUrl = null;\n }\n}\n\nconst tagsByName = new Map(\n existingTags.map(tag => [normalizeTagName(tag.name), tag])\n);\n\nconst appliedTags = [];\n\nfor (const tagName of suggestedTags) {\n let tag = tagsByName.get(tagName);\n\n if (!tag) {\n tag = await request.call(this, 'POST', `${PAPERLESS_BASE_URL}/api/tags/`, {\n name: tagName,\n });\n tagsByName.set(tagName, tag);\n }\n\n if (tag?.id) {\n appliedTags.push(tag);\n }\n}\n\n// Fetch current document so we merge tags instead of overwriting existing ones.\nconst doc = await request.call(this, 'GET', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`);\nconst currentTagIds = Array.isArray(doc.tags) ? doc.tags : [];\nconst appliedTagIds = appliedTags.map(tag => tag.id);\nconst mergedTagIds = [...new Set([...currentTagIds, ...appliedTagIds])];\n\nawait request.call(this, 'PATCH', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`, {\n tags: mergedTagIds,\n});\n\nreturn [{\n json: {\n ...data,\n tag_apply_status: 'updated',\n applied_tag_names: appliedTags.map(tag => tag.name),\n applied_tag_ids: appliedTagIds,\n final_tag_ids: mergedTagIds,\n }\n}];" }, "id": "b0d5423b-dc83-4adf-98c6-d95afa3e5c1b", "name": "Apply Suggested Tags to Paperless", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1328, 400 ] } ], "connections": { "Paperless Webhook": { "main": [ [ { "node": "Extract Document ID", "type": "main", "index": 0 } ] ] }, "Extract Document ID": { "main": [ [ { "node": "Fetch Document", "type": "main", "index": 0 } ] ] }, "Fetch Document": { "main": [ [ { "node": "Prepare Content", "type": "main", "index": 0 } ] ] }, "Prepare Content": { "main": [ [ { "node": "AI Classify & Extract", "type": "main", "index": 0 } ] ] }, "AI Classify & Extract": { "main": [ [ { "node": "Parse Analysis", "type": "main", "index": 0 } ] ] }, "Parse Analysis": { "main": [ [ { "node": "Add Note to Document", "type": "main", "index": 0 }, { "node": "Format Gotify Document Message", "type": "main", "index": 0 }, { "node": "Apply Suggested Tags to Paperless", "type": "main", "index": 0 } ] ] }, "Format Gotify Document Message": { "main": [ [ { "node": "Send Gotify Message", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "binaryMode": "separate" }, "staticData": null, "meta": { "templateCredsSetupCompleted": true }, "pinData": {}, "versionId": "199d2609-e3e7-46aa-b144-df038eb10a0d", "activeVersionId": "199d2609-e3e7-46aa-b144-df038eb10a0d", "versionCounter": 11, "triggerCount": 1, "tags": [ { "updatedAt": "2026-05-06T22:39:46.433Z", "createdAt": "2026-05-06T22:39:46.433Z", "id": "dZUptsMy60GbLHft", "name": "documents" }, { "updatedAt": "2026-05-06T22:38:51.678Z", "createdAt": "2026-05-06T22:38:51.678Z", "id": "S9FxzVfHLsHmyzyt", "name": "ai" }, { "updatedAt": "2026-05-06T22:39:46.476Z", "createdAt": "2026-05-06T22:39:46.476Z", "id": "NxHjuO4MMd5kdPR7", "name": "paperless" } ], "shared": [ { "updatedAt": "2026-05-07T20:24:40.153Z", "createdAt": "2026-05-07T20:24:40.153Z", "role": "workflow:owner", "workflowId": "dQ1d5deV4mF0s5eP", "projectId": "dxCRnBdX5uJizCGa", "project": { "updatedAt": "2026-05-06T01:10:37.484Z", "createdAt": "2026-05-06T01:08:07.721Z", "id": "dxCRnBdX5uJizCGa", "name": "Wilfred Fizzlepoof ", "type": "personal", "icon": null, "description": null, "creatorId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d" } } ], "versionMetadata": { "name": "Version 199d2609", "description": "" } }