{
  "updatedAt": "2026-05-07T20:24:47.896Z",
  "createdAt": "2026-05-07T20:24:40.153Z",
  "id": "dQ1d5deV4mF0s5eP",
  "name": "Paperless AI Processing v1.0",
  "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
  "active": false,
  "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 ' + $env.PAPERLESS_API_TOKEN}}"
            }
          ]
        },
        "options": {}
      },
      "id": "17ed7e6f-ccaa-43d3-9031-9f3d2bb12dec",
      "name": "Fetch Document",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        448,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst response = $input.first().json;\nconst doc = response.results ? response.results[0] : response;\nif (!doc || !doc.id) {\n  throw new Error('No document found in response');\n}\nconst title = doc.title || 'Untitled';\nconst originalFilename = doc.original_file_name || '';\nconst isSchoolIntake = /^school-/i.test(originalFilename) || / \u2014 (draft|final|revision|proposal)$/i.test(title);\nconst content = (doc.content || '').substring(0, 4000);\nreturn [{\n  json: {\n    documentId: doc.id,\n    title,\n    originalFilename,\n    correspondent: doc.correspondent_name || null,\n    content,\n    created: doc.created || new Date().toISOString(),\n    isSchoolIntake\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.30.6:4000/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
            }
          ]
        },
        "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 ' + $env.PAPERLESS_API_TOKEN}}"
            },
            {
              "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 = ($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '');\nconst PAPERLESS_TOKEN = $env.PAPERLESS_API_TOKEN;\nif (!PAPERLESS_TOKEN) throw new Error('PAPERLESS_API_TOKEN is not set.');\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 = { method, url, headers, json: true };\n  if (body !== undefined) options.body = body;\n  return await this.helpers.httpRequest(options);\n}\n\nconst existingTags = [];\nlet tagsUrl = `${PAPERLESS_BASE_URL}/api/tags/?page_size=100`;\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  tagsUrl = page.next ? (page.next.startsWith('http') ? page.next : `${PAPERLESS_BASE_URL}${page.next}`) : null;\n}\n\nconst tagsByName = new Map(existingTags.map(tag => [normalizeTagName(tag.name), tag]));\nconst appliedTags = [];\nfor (const tagName of suggestedTags) {\n  let tag = tagsByName.get(tagName);\n  if (!tag) {\n    tag = await request.call(this, 'POST', `${PAPERLESS_BASE_URL}/api/tags/`, { name: tagName });\n    tagsByName.set(tagName, tag);\n  }\n  if (tag?.id) appliedTags.push(tag);\n}\n\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}/`, { tags: mergedTagIds });\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
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "skip-school-docs",
              "leftValue": "={{$json.isSchoolIntake}}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-school-intake-skip",
      "name": "Is School Intake?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        800,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "return [{ json: { skipped: true, reason: 'school_intake_owned_by_school_workflow', documentId: $json.documentId, title: $json.title } }];"
      },
      "id": "skip-school-return",
      "name": "Return School Skip",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        -160
      ]
    }
  ],
  "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": "Is School Intake?",
            "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
          }
        ]
      ]
    },
    "Is School Intake?": {
      "main": [
        [
          {
            "node": "Return School Skip",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "AI Classify & Extract",
            "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 <admin@paccoco.com>",
        "type": "personal",
        "icon": null,
        "description": null,
        "creatorId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
      }
    }
  ],
  "versionMetadata": {
    "name": "Version 199d2609",
    "description": ""
  }
}
