{
  "updatedAt": "2026-05-22T21:55:07.597Z",
  "createdAt": "2026-05-13T16:06:39.571Z",
  "id": "FIhrfDfL1NgakDwl",
  "name": "Paperless Intake Triage → Doris Review Queue",
  "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/intake-triage",
        "options": {}
      },
      "id": "paperless-webhook",
      "name": "Paperless Intake Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -980,
        0
      ],
      "webhookId": "706f9c22-fbe4-4f6a-87d7-168a6eb96b97"
    },
    {
      "parameters": {
        "jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || {};\nconst query = input.query || {};\nconst params = input.params || {};\nconst headers = input.headers || {};\nconst candidates = [\n  body.document_id,\n  body.id,\n  body.doc_id,\n  body.pk,\n  body.document_id?.id,\n  body.document?.id,\n  body.document,\n  query.document_id,\n  query.id,\n  query.doc_id,\n  params.document_id,\n  params.id,\n  headers['x-paperless-document-id'],\n  input.document_id,\n  input.id\n].filter(v => v !== undefined && v !== null && v !== '');\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n  throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n  throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: String(documentId), received_at: new Date().toISOString(), webhook_payload_keys: Object.keys(body) } }];\n"
      },
      "id": "extract-doc-id",
      "name": "Extract Document ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -760,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '') + '/api/documents/' + ($json.document_id ? ($json.document_id + '/') : '?ordering=-added&page_size=1')}}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "timeout": 30000
        }
      },
      "id": "fetch-document",
      "name": "Fetch Paperless Document",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -540,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst response = $json;\nconst doc = response.results ? response.results[0] : response;\nif (!doc || !doc.id) throw new Error('No document found in Paperless API response');\nconst truncate = (s, n=9000) => String(s || '').replace(/\\s+/g, ' ').trim().slice(0, n);\nconst archiveUrl = `${($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '')}/documents/${doc.id}/details`;\nconst payload = {\n  id: doc.id,\n  title: doc.title || 'Untitled',\n  original_filename: doc.original_filename || doc.original_file_name || doc.archive_filename || null,\n  created: doc.created || null,\n  added: doc.added || doc.created_date || null,\n  correspondent: doc.correspondent_detail?.name || doc.correspondent_name || doc.correspondent || null,\n  document_type: doc.document_type_detail?.name || doc.document_type_name || doc.document_type || null,\n  tags: (doc.tags_detail || doc.tags || []).map(t => typeof t === 'string' ? t : (t.name || t.id)).filter(Boolean),\n  archive_url: archiveUrl,\n  content: truncate(doc.content || doc.text || '')\n};\nconst schema = `Return STRICT JSON only with exactly these keys: summary, suggested_title, document_type, suggested_correspondent, suggested_tags, due_date, needs_review, urgency, reason, confidence. document_type must be one of bill, receipt, school, medical, tax, insurance, legal, manual, letter, other. due_date is YYYY-MM-DD or null. urgency is none, low, medium, high. confidence is high, medium, low.`;\nreturn [{ json: { document: payload, llm_messages: [\n  { role: 'system', content: 'You triage Paperless-NGX documents for a human review-first dashboard. Never invent due dates. Be conservative. ' + schema },\n  { role: 'user', content: JSON.stringify(payload) }\n] } }];\n"
      },
      "id": "prepare-triage",
      "name": "Prepare Compact Triage Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -320,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{($env.LITELLM_BASE_URL || 'http://10.5.30.6:4000').replace(/\\/$/, '') + '/v1/chat/completions'}}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $env.PAPERLESS_TRIAGE_MODEL || 'medium', temperature: 0.1, response_format: { type: 'json_object' }, messages: $json.llm_messages }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "llm-triage",
      "name": "LiteLLM Triage JSON",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -100,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nfunction parseTriage(input) {\n  let content = input.choices?.[0]?.message?.content ?? input.text ?? input;\n  if (typeof content !== 'string') content = JSON.stringify(content);\n  content = content.trim().replace(/^```json\\s*/i, '').replace(/```$/,'').trim();\n  return JSON.parse(content);\n}\nconst sourceDoc = $('Prepare Compact Triage Payload').first().json.document;\nconst t = parseTriage($json);\nconst allowedTypes = new Set(['bill','receipt','school','medical','tax','insurance','legal','manual','letter','other']);\nconst out = {\n  summary: String(t.summary || '').slice(0, 800),\n  suggested_title: String(t.suggested_title || sourceDoc.title || sourceDoc.original_filename || `Document ${sourceDoc.id}`).slice(0, 200),\n  document_type: allowedTypes.has(t.document_type) ? t.document_type : 'other',\n  suggested_correspondent: t.suggested_correspondent ? String(t.suggested_correspondent).slice(0, 160) : null,\n  suggested_tags: Array.isArray(t.suggested_tags) ? [...new Set(t.suggested_tags.map(x => String(x).trim().toLowerCase()).filter(Boolean))].slice(0, 12) : [],\n  due_date: /^\\d{4}-\\d{2}-\\d{2}$/.test(String(t.due_date || '')) ? String(t.due_date) : null,\n  needs_review: Boolean(t.needs_review),\n  urgency: ['none','low','medium','high'].includes(t.urgency) ? t.urgency : 'medium',\n  reason: String(t.reason || 'No reason supplied; review required.').slice(0, 500),\n  confidence: ['high','medium','low'].includes(t.confidence) ? t.confidence : 'low'\n};\nconst riskTypes = new Set(['legal','medical','school','tax','insurance']);\nconst billSignals = /\b(bill|payment|invoice|amount due|pay by|past due|statement)\b/i.test(`${out.summary} ${out.suggested_title} ${sourceDoc.content || ''}`);\nconst forcedReview = riskTypes.has(out.document_type) || Boolean(out.due_date) || billSignals || out.confidence === 'low' || ['medium','high'].includes(out.urgency);\nout.needs_review = out.needs_review || forcedReview;\nif (forcedReview && !/review/i.test(out.reason)) out.reason = `${out.reason} Safety gate requires human review.`;\nconst applySafe = String($env.PAPERLESS_TRIAGE_APPLY_SAFE || 'false').toLowerCase() === 'true';\nconst eligible_for_safe_apply = !out.needs_review && out.confidence === 'high' && ['none','low'].includes(out.urgency) && !riskTypes.has(out.document_type) && !out.due_date;\nconst reviewItem = {\n  id: sourceDoc.id,\n  title: sourceDoc.title,\n  original_filename: sourceDoc.original_filename,\n  added: sourceDoc.added,\n  archive_url: sourceDoc.archive_url,\n  current: { correspondent: sourceDoc.correspondent, document_type: sourceDoc.document_type, tags: sourceDoc.tags },\n  triage: out,\n  safety: { forced_review: forcedReview, eligible_for_safe_apply, apply_safe_enabled: applySafe },\n  queued_at: new Date().toISOString()\n};\nreturn [{ json: { document: sourceDoc, triage: out, review_item: reviewItem, should_apply_safe: applySafe && eligible_for_safe_apply, should_notify: out.urgency === 'high' } }];\n"
      },
      "id": "parse-safety",
      "name": "Parse JSON + Safety Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        120,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\n// Requires n8n Code node filesystem access: NODE_FUNCTION_ALLOW_BUILTIN=fs,path\nconst fs = require('fs');\nconst path = require('path');\nconst queuePath = $env.PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH || '/data/paperless_review.json';\nconst maxItems = Number($env.PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS || 200);\nfs.mkdirSync(path.dirname(queuePath), { recursive: true });\nlet queue = [];\ntry { queue = JSON.parse(fs.readFileSync(queuePath, 'utf8')); } catch (_) { queue = []; }\nif (!Array.isArray(queue)) queue = [];\nconst item = $json.review_item;\nqueue = [item, ...queue.filter(x => String(x.id) !== String(item.id))].slice(0, maxItems);\nconst tmp = queuePath + '.tmp';\nfs.writeFileSync(tmp, JSON.stringify(queue, null, 2) + '\\n', 'utf8');\nfs.renameSync(tmp, queuePath);\nreturn [{ json: { ...$json, queue_path: queuePath, queue_count: queue.length } }];\n"
      },
      "id": "write-queue",
      "name": "Write Doris Review Queue JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        340,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.should_apply_safe}}",
              "value2": true
            }
          ]
        }
      },
      "id": "if-safe-apply",
      "name": "Apply Safe Updates Enabled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        560,
        -80
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '') + '/api/documents/' + $json.document.id + '/'}}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ title: $json.triage.suggested_title }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "safe-title-update",
      "name": "Safe Title Update Only",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        780,
        -160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.should_notify && !!$env.GOTIFY_URL && !!$env.GOTIFY_TOKEN}}",
              "value2": true
            }
          ]
        }
      },
      "id": "if-urgent",
      "name": "Urgent Notification Configured?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        560,
        140
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$env.GOTIFY_URL.replace(/\\/$/, '') + '/message'}}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Gotify-Key",
              "value": "={{$env.GOTIFY_TOKEN}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ title: 'Urgent Paperless document', message: `${$json.triage.suggested_title}\n${$json.triage.summary}\n${$json.document.archive_url}`, priority: 8 }) }}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "gotify-urgent",
      "name": "Send Urgent Gotify",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        780,
        140
      ]
    }
  ],
  "connections": {
    "Paperless Intake Webhook": {
      "main": [
        [
          {
            "node": "Extract Document ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Document ID": {
      "main": [
        [
          {
            "node": "Fetch Paperless Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Paperless Document": {
      "main": [
        [
          {
            "node": "Prepare Compact Triage Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Compact Triage Payload": {
      "main": [
        [
          {
            "node": "LiteLLM Triage JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LiteLLM Triage JSON": {
      "main": [
        [
          {
            "node": "Parse JSON + Safety Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON + Safety Gate": {
      "main": [
        [
          {
            "node": "Write Doris Review Queue JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Doris Review Queue JSON": {
      "main": [
        [
          {
            "node": "Apply Safe Updates Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Safe Updates Enabled?": {
      "main": [
        [
          {
            "node": "Safe Title Update Only",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Urgent Notification Configured?": {
      "main": [
        [
          {
            "node": "Send Urgent Gotify",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": null,
  "pinData": null,
  "versionId": "26b3e6dd-3e8b-4bd0-a38d-f2d09990447f",
  "activeVersionId": "26b3e6dd-3e8b-4bd0-a38d-f2d09990447f",
  "versionCounter": 36,
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2026-05-13T16:06:39.571Z",
      "createdAt": "2026-05-13T16:06:39.571Z",
      "role": "workflow:owner",
      "workflowId": "FIhrfDfL1NgakDwl",
      "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"
      }
    }
  ],
  "tags": [],
  "activeVersion": {
    "updatedAt": "2026-05-22T21:55:07.599Z",
    "createdAt": "2026-05-22T21:55:07.599Z",
    "versionId": "26b3e6dd-3e8b-4bd0-a38d-f2d09990447f",
    "workflowId": "FIhrfDfL1NgakDwl",
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "paperless/intake-triage",
          "options": {}
        },
        "id": "paperless-webhook",
        "name": "Paperless Intake Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -980,
          0
        ],
        "webhookId": "706f9c22-fbe4-4f6a-87d7-168a6eb96b97"
      },
      {
        "parameters": {
          "jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || {};\nconst query = input.query || {};\nconst params = input.params || {};\nconst headers = input.headers || {};\nconst candidates = [\n  body.document_id,\n  body.id,\n  body.doc_id,\n  body.pk,\n  body.document_id?.id,\n  body.document?.id,\n  body.document,\n  query.document_id,\n  query.id,\n  query.doc_id,\n  params.document_id,\n  params.id,\n  headers['x-paperless-document-id'],\n  input.document_id,\n  input.id\n].filter(v => v !== undefined && v !== null && v !== '');\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n  throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n  throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: String(documentId), received_at: new Date().toISOString(), webhook_payload_keys: Object.keys(body) } }];\n"
        },
        "id": "extract-doc-id",
        "name": "Extract Document ID",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -760,
          0
        ]
      },
      {
        "parameters": {
          "url": "={{($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '') + '/api/documents/' + ($json.document_id ? ($json.document_id + '/') : '?ordering=-added&page_size=1')}}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
              },
              {
                "name": "Accept",
                "value": "application/json"
              }
            ]
          },
          "options": {
            "timeout": 30000
          }
        },
        "id": "fetch-document",
        "name": "Fetch Paperless Document",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -540,
          0
        ]
      },
      {
        "parameters": {
          "jsCode": "\nconst response = $json;\nconst doc = response.results ? response.results[0] : response;\nif (!doc || !doc.id) throw new Error('No document found in Paperless API response');\nconst truncate = (s, n=9000) => String(s || '').replace(/\\s+/g, ' ').trim().slice(0, n);\nconst archiveUrl = `${($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '')}/documents/${doc.id}/details`;\nconst payload = {\n  id: doc.id,\n  title: doc.title || 'Untitled',\n  original_filename: doc.original_filename || doc.original_file_name || doc.archive_filename || null,\n  created: doc.created || null,\n  added: doc.added || doc.created_date || null,\n  correspondent: doc.correspondent_detail?.name || doc.correspondent_name || doc.correspondent || null,\n  document_type: doc.document_type_detail?.name || doc.document_type_name || doc.document_type || null,\n  tags: (doc.tags_detail || doc.tags || []).map(t => typeof t === 'string' ? t : (t.name || t.id)).filter(Boolean),\n  archive_url: archiveUrl,\n  content: truncate(doc.content || doc.text || '')\n};\nconst schema = `Return STRICT JSON only with exactly these keys: summary, suggested_title, document_type, suggested_correspondent, suggested_tags, due_date, needs_review, urgency, reason, confidence. document_type must be one of bill, receipt, school, medical, tax, insurance, legal, manual, letter, other. due_date is YYYY-MM-DD or null. urgency is none, low, medium, high. confidence is high, medium, low.`;\nreturn [{ json: { document: payload, llm_messages: [\n  { role: 'system', content: 'You triage Paperless-NGX documents for a human review-first dashboard. Never invent due dates. Be conservative. ' + schema },\n  { role: 'user', content: JSON.stringify(payload) }\n] } }];\n"
        },
        "id": "prepare-triage",
        "name": "Prepare Compact Triage Payload",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -320,
          0
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{($env.LITELLM_BASE_URL || 'http://10.5.30.6:4000').replace(/\\/$/, '') + '/v1/chat/completions'}}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $env.PAPERLESS_TRIAGE_MODEL || 'medium', temperature: 0.1, response_format: { type: 'json_object' }, messages: $json.llm_messages }) }}",
          "options": {
            "timeout": 60000
          }
        },
        "id": "llm-triage",
        "name": "LiteLLM Triage JSON",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -100,
          0
        ]
      },
      {
        "parameters": {
          "jsCode": "\nfunction parseTriage(input) {\n  let content = input.choices?.[0]?.message?.content ?? input.text ?? input;\n  if (typeof content !== 'string') content = JSON.stringify(content);\n  content = content.trim().replace(/^```json\\s*/i, '').replace(/```$/,'').trim();\n  return JSON.parse(content);\n}\nconst sourceDoc = $('Prepare Compact Triage Payload').first().json.document;\nconst t = parseTriage($json);\nconst allowedTypes = new Set(['bill','receipt','school','medical','tax','insurance','legal','manual','letter','other']);\nconst out = {\n  summary: String(t.summary || '').slice(0, 800),\n  suggested_title: String(t.suggested_title || sourceDoc.title || sourceDoc.original_filename || `Document ${sourceDoc.id}`).slice(0, 200),\n  document_type: allowedTypes.has(t.document_type) ? t.document_type : 'other',\n  suggested_correspondent: t.suggested_correspondent ? String(t.suggested_correspondent).slice(0, 160) : null,\n  suggested_tags: Array.isArray(t.suggested_tags) ? [...new Set(t.suggested_tags.map(x => String(x).trim().toLowerCase()).filter(Boolean))].slice(0, 12) : [],\n  due_date: /^\\d{4}-\\d{2}-\\d{2}$/.test(String(t.due_date || '')) ? String(t.due_date) : null,\n  needs_review: Boolean(t.needs_review),\n  urgency: ['none','low','medium','high'].includes(t.urgency) ? t.urgency : 'medium',\n  reason: String(t.reason || 'No reason supplied; review required.').slice(0, 500),\n  confidence: ['high','medium','low'].includes(t.confidence) ? t.confidence : 'low'\n};\nconst riskTypes = new Set(['legal','medical','school','tax','insurance']);\nconst billSignals = /\b(bill|payment|invoice|amount due|pay by|past due|statement)\b/i.test(`${out.summary} ${out.suggested_title} ${sourceDoc.content || ''}`);\nconst forcedReview = riskTypes.has(out.document_type) || Boolean(out.due_date) || billSignals || out.confidence === 'low' || ['medium','high'].includes(out.urgency);\nout.needs_review = out.needs_review || forcedReview;\nif (forcedReview && !/review/i.test(out.reason)) out.reason = `${out.reason} Safety gate requires human review.`;\nconst applySafe = String($env.PAPERLESS_TRIAGE_APPLY_SAFE || 'false').toLowerCase() === 'true';\nconst eligible_for_safe_apply = !out.needs_review && out.confidence === 'high' && ['none','low'].includes(out.urgency) && !riskTypes.has(out.document_type) && !out.due_date;\nconst reviewItem = {\n  id: sourceDoc.id,\n  title: sourceDoc.title,\n  original_filename: sourceDoc.original_filename,\n  added: sourceDoc.added,\n  archive_url: sourceDoc.archive_url,\n  current: { correspondent: sourceDoc.correspondent, document_type: sourceDoc.document_type, tags: sourceDoc.tags },\n  triage: out,\n  safety: { forced_review: forcedReview, eligible_for_safe_apply, apply_safe_enabled: applySafe },\n  queued_at: new Date().toISOString()\n};\nreturn [{ json: { document: sourceDoc, triage: out, review_item: reviewItem, should_apply_safe: applySafe && eligible_for_safe_apply, should_notify: out.urgency === 'high' } }];\n"
        },
        "id": "parse-safety",
        "name": "Parse JSON + Safety Gate",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          120,
          0
        ]
      },
      {
        "parameters": {
          "jsCode": "\n// Requires n8n Code node filesystem access: NODE_FUNCTION_ALLOW_BUILTIN=fs,path\nconst fs = require('fs');\nconst path = require('path');\nconst queuePath = $env.PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH || '/data/paperless_review.json';\nconst maxItems = Number($env.PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS || 200);\nfs.mkdirSync(path.dirname(queuePath), { recursive: true });\nlet queue = [];\ntry { queue = JSON.parse(fs.readFileSync(queuePath, 'utf8')); } catch (_) { queue = []; }\nif (!Array.isArray(queue)) queue = [];\nconst item = $json.review_item;\nqueue = [item, ...queue.filter(x => String(x.id) !== String(item.id))].slice(0, maxItems);\nconst tmp = queuePath + '.tmp';\nfs.writeFileSync(tmp, JSON.stringify(queue, null, 2) + '\\n', 'utf8');\nfs.renameSync(tmp, queuePath);\nreturn [{ json: { ...$json, queue_path: queuePath, queue_count: queue.length } }];\n"
        },
        "id": "write-queue",
        "name": "Write Doris Review Queue JSON",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          340,
          0
        ]
      },
      {
        "parameters": {
          "conditions": {
            "boolean": [
              {
                "value1": "={{$json.should_apply_safe}}",
                "value2": true
              }
            ]
          }
        },
        "id": "if-safe-apply",
        "name": "Apply Safe Updates Enabled?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.2,
        "position": [
          560,
          -80
        ]
      },
      {
        "parameters": {
          "method": "PATCH",
          "url": "={{($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '') + '/api/documents/' + $json.document.id + '/'}}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ title: $json.triage.suggested_title }) }}",
          "options": {
            "timeout": 30000
          }
        },
        "id": "safe-title-update",
        "name": "Safe Title Update Only",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          780,
          -160
        ]
      },
      {
        "parameters": {
          "conditions": {
            "boolean": [
              {
                "value1": "={{$json.should_notify && !!$env.GOTIFY_URL && !!$env.GOTIFY_TOKEN}}",
                "value2": true
              }
            ]
          }
        },
        "id": "if-urgent",
        "name": "Urgent Notification Configured?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.2,
        "position": [
          560,
          140
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{$env.GOTIFY_URL.replace(/\\/$/, '') + '/message'}}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "X-Gotify-Key",
                "value": "={{$env.GOTIFY_TOKEN}}"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ title: 'Urgent Paperless document', message: `${$json.triage.suggested_title}\n${$json.triage.summary}\n${$json.document.archive_url}`, priority: 8 }) }}",
          "options": {
            "timeout": 15000
          }
        },
        "id": "gotify-urgent",
        "name": "Send Urgent Gotify",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          780,
          140
        ]
      }
    ],
    "connections": {
      "Paperless Intake Webhook": {
        "main": [
          [
            {
              "node": "Extract Document ID",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Extract Document ID": {
        "main": [
          [
            {
              "node": "Fetch Paperless Document",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Fetch Paperless Document": {
        "main": [
          [
            {
              "node": "Prepare Compact Triage Payload",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Prepare Compact Triage Payload": {
        "main": [
          [
            {
              "node": "LiteLLM Triage JSON",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "LiteLLM Triage JSON": {
        "main": [
          [
            {
              "node": "Parse JSON + Safety Gate",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Parse JSON + Safety Gate": {
        "main": [
          [
            {
              "node": "Write Doris Review Queue JSON",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Write Doris Review Queue JSON": {
        "main": [
          [
            {
              "node": "Apply Safe Updates Enabled?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Apply Safe Updates Enabled?": {
        "main": [
          [
            {
              "node": "Safe Title Update Only",
              "type": "main",
              "index": 0
            }
          ],
          []
        ]
      },
      "Urgent Notification Configured?": {
        "main": [
          [
            {
              "node": "Send Urgent Gotify",
              "type": "main",
              "index": 0
            }
          ],
          []
        ]
      }
    },
    "authors": "Wilfred Fizzlepoof",
    "name": null,
    "description": null,
    "autosaved": false,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-05-22T21:55:07.641Z",
        "id": 323,
        "workflowId": "FIhrfDfL1NgakDwl",
        "versionId": "26b3e6dd-3e8b-4bd0-a38d-f2d09990447f",
        "event": "deactivated",
        "userId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
      },
      {
        "createdAt": "2026-05-22T21:55:07.661Z",
        "id": 324,
        "workflowId": "FIhrfDfL1NgakDwl",
        "versionId": "26b3e6dd-3e8b-4bd0-a38d-f2d09990447f",
        "event": "activated",
        "userId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
      }
    ]
  }
}
