Add OpenClaw to AI stack

This commit is contained in:
Paccoco
2026-05-10 09:52:48 -05:00
parent 31edbf1970
commit 2048a07abf
6 changed files with 716 additions and 2 deletions

View File

@@ -8,3 +8,6 @@ OPENWEBUI_DB_PASS=changeme
# Scriberr — Hugging Face token for model downloads # Scriberr — Hugging Face token for model downloads
HF_TOKEN=changeme HF_TOKEN=changeme
# OpenClaw — generate with: openssl rand -hex 32
OPENCLAW_GATEWAY_TOKEN=changeme

View File

@@ -128,3 +128,29 @@ services:
- driver: nvidia - driver: nvidia
count: 1 count: 1
capabilities: [gpu] capabilities: [gpu]
openclaw:
container_name: openclaw
image: ghcr.io/openclaw/openclaw:latest
restart: unless-stopped
networks:
- ai-net
- ai-services
- pangolin
ports:
- "18789:18789"
environment:
TZ: ${TZ}
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}
OPENCLAW_DISABLE_BONJOUR: "1"
volumes:
- /mnt/docker-ssd/docker/appdata/openclaw/config:/home/node/.openclaw
- /mnt/docker-ssd/docker/appdata/openclaw/workspace:/home/node/.openclaw/workspace
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:18789/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s

View File

@@ -2,7 +2,7 @@
All homelab services, their hosts, ports, and current status. All homelab services, their hosts, ports, and current status.
*Last updated: 2026-05-09* *Last updated: 2026-05-10*
## Shared Databases (PD) ## Shared Databases (PD)
@@ -32,8 +32,10 @@ All homelab services, their hosts, ports, and current status.
| LiteLLM | PD | 4000 | ✅ Active | | LiteLLM | PD | 4000 | ✅ Active |
| OpenWebUI | PD | 8282 | ✅ Active | | OpenWebUI | PD | 8282 | ✅ Active |
| Qdrant | PD | 6333 (HTTP) / 6334 (gRPC) | ✅ Active | | Qdrant | PD | 6333 (HTTP) / 6334 (gRPC) | ✅ Active |
| Whisper (faster-whisper) | PD | 8786 | ✅ Active | | Whisper (faster-whisper, CPU) | N.O.M.A.D. (10.5.1.16) | 8786 | ✅ Active |
| Whisper (faster-whisper, CUDA large-v3) | Rocinante (10.5.1.112) | 8787 | ✅ Active |
| SearXNG | PD | 8888 | ✅ Active | | SearXNG | PD | 8888 | ✅ Active |
| OpenClaw | PD | 18789 | ✅ Active |
| Ollama — light tier | N.O.M.A.D. (10.5.1.16) | 11434 | ✅ Active | | Ollama — light tier | N.O.M.A.D. (10.5.1.16) | 11434 | ✅ Active |
| Ollama — heavy tier | Rocinante (10.5.1.112) | 11434 | ✅ Active | | Ollama — heavy tier | Rocinante (10.5.1.112) | 11434 | ✅ Active |
| Reranker (TEI) | Serenity (10.5.1.5) | 9787 | ✅ Active | | Reranker (TEI) | Serenity (10.5.1.5) | 9787 | ✅ Active |

View File

@@ -0,0 +1,293 @@
{
"name": "Paperless Inbox Reminder v1.2",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"weeksInterval": 1,
"triggerAtDay": [
1
],
"triggerAtHour": 9,
"triggerAtMinute": 0
}
]
}
},
"id": "41404371-29ed-4660-9989-a5cbd890555b",
"name": "Every Monday 9 AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
]
},
{
"parameters": {
"method": "GET",
"url": "https://paperless.paccoco.com/api/documents/",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Token {{ $env.PAPERLESS_API_TOKEN }}"
}
]
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "is_tagged",
"value": "0"
},
{
"name": "page_size",
"value": "25"
},
{
"name": "ordering",
"value": "created"
},
{
"name": "fields",
"value": "id,title,created,document_type,tags"
}
]
},
"options": {}
},
"id": "fdcd5728-6d27-445f-961b-18f204297469",
"name": "Fetch Inbox Docs",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
224,
0
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst docs = data.results || [];\nconst total = data.count || 0;\n\n// Oldest 5 for the reminder body\nconst oldest = docs.slice(0, 5).map(d => {\n const date = d.created ? d.created.substring(0, 10) : 'unknown date';\n return ` • ${d.title || 'Untitled'} (added ${date})`;\n});\n\nreturn [{\n json: {\n total,\n hasItems: total > 0,\n hasItemsStr: total > 0 ? 'yes' : 'no',\n oldestList: oldest.join('\\n'),\n remaining: Math.max(0, total - 5)\n }\n}];"
},
"id": "8fbe6be0-1ed3-4126-8f21-c3f4efb161c6",
"name": "Parse Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "b206599d-0c79-4a32-9fb9-91e1c41dcff2",
"leftValue": "={{ $json.hasItemsStr }}",
"rightValue": "yes",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "e24344a1-e857-462c-b96a-842169e83a6d",
"name": "Has Inbox Items?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
672,
0
]
},
{
"parameters": {
"jsCode": "const d = $input.first().json;\nconst extra = d.remaining > 0 ? `\\n … and ${d.remaining} more` : '';\nconst body = `${d.total} untagged document${d.total !== 1 ? 's' : ''} in Paperless inbox:\\n${d.oldestList}${extra}\\n\\nhttps://paperless.paccoco.com`;\nreturn [{ json: { title: '📥 Paperless Inbox Needs Attention', message: body, priority: 5 } }];"
},
"id": "897af9c9-4429-445f-8160-a03a14a318a3",
"name": "Format Reminder",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
896,
-120
]
},
{
"parameters": {
"jsCode": "return [{ json: { title: '✅ Paperless Inbox Clear', message: 'No untagged documents — inbox is clean.', priority: 1 } }];"
},
"id": "bd8fe88e-7876-45dd-8344-983674fba79d",
"name": "Format Clear",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
896,
120
]
},
{
"parameters": {
"message": "={{ $json.message }}",
"additionalFields": {
"priority": "={{ $json.priority }}",
"title": "={{ $json.title }}"
},
"options": {
"contentType": "text/plain"
}
},
"id": "74860e7f-42e3-448f-9e46-5cfb6f12cdc2",
"name": "Push to Gotify (Reminder)",
"type": "n8n-nodes-base.gotify",
"typeVersion": 1,
"position": [
1120,
-120
],
"credentials": {
"gotifyApi": {
"id": "SETUP_REQUIRED",
"name": "Reminders"
}
}
},
{
"parameters": {
"message": "={{ $json.message }}",
"additionalFields": {
"priority": "={{ $json.priority }}",
"title": "={{ $json.title }}"
},
"options": {
"contentType": "text/plain"
}
},
"id": "b1dbd39b-87c3-43ec-9cec-f63eabbd0fa5",
"name": "Push to Gotify (Clear)",
"type": "n8n-nodes-base.gotify",
"typeVersion": 1,
"position": [
1120,
120
],
"credentials": {
"gotifyApi": {
"id": "SETUP_REQUIRED",
"name": "Reminders"
}
}
}
],
"connections": {
"Every Monday 9 AM": {
"main": [
[
{
"node": "Fetch Inbox Docs",
"type": "main",
"index": 0
}
]
]
},
"Fetch Inbox Docs": {
"main": [
[
{
"node": "Parse Response",
"type": "main",
"index": 0
}
]
]
},
"Parse Response": {
"main": [
[
{
"node": "Has Inbox Items?",
"type": "main",
"index": 0
}
]
]
},
"Has Inbox Items?": {
"main": [
[
{
"node": "Format Reminder",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Clear",
"type": "main",
"index": 0
}
]
]
},
"Format Reminder": {
"main": [
[
{
"node": "Push to Gotify (Reminder)",
"type": "main",
"index": 0
}
]
]
},
"Format Clear": {
"main": [
[
{
"node": "Push to Gotify (Clear)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"staticData": null,
"meta": null,
"pinData": {},
"tags": [
{
"name": "reminders"
},
{
"name": "paperless"
},
{
"name": "scheduled"
}
],
"id": "2b80579b-8d89-42d5-8b8b-9137f59ea206"
}

View File

@@ -0,0 +1,302 @@
{
"name": "n8n Self-Health Monitor v1.2",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 15
}
]
}
},
"id": "dabcb0ae-d513-410e-b779-4570df5c4a35",
"name": "Every 15 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 0]
},
{
"parameters": {
"method": "GET",
"url": "http://10.5.1.6:5678/api/v1/workflows",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-N8N-API-KEY",
"value": "={{ $env.N8N_API_KEY }}"
}
]
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "limit",
"value": "250"
}
]
},
"options": {}
},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Fetch All Workflows",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [224, 0]
},
{
"parameters": {
"method": "GET",
"url": "http://10.5.1.6:5678/api/v1/executions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-N8N-API-KEY",
"value": "={{ $env.N8N_API_KEY }}"
}
]
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "status",
"value": "error"
},
{
"name": "includeData",
"value": "false"
},
{
"name": "limit",
"value": "50"
}
]
},
"options": {}
},
"id": "806d75e9-b95a-499a-b997-a7f7dc046775",
"name": "Fetch Failed Executions",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [448, 0]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst executions = data.data || [];\nconst oneHourAgo = Date.now() - (60 * 60 * 1000);\n\n// Build workflow ID → name lookup from previously fetched workflows\nconst wfResponse = $('Fetch All Workflows').first().json;\nconst nameMap = {};\n(wfResponse.data || []).forEach(wf => { nameMap[wf.id] = wf.name; });\n\n// Filter to failures in the past hour\nconst recent = executions.filter(e => {\n const ts = new Date(e.startedAt || e.stoppedAt || 0).getTime();\n return ts >= oneHourAgo;\n});\n\n// Map executions to failure objects, resolving names from the lookup\nconst failures = recent.map(e => ({\n executionId: e.id,\n workflowId: e.workflowId,\n workflowName: nameMap[e.workflowId] || e.workflowData?.name || `Workflow ${e.workflowId}`,\n startedAt: e.startedAt,\n stoppedAt: e.stoppedAt,\n errorMessage: e.data?.resultData?.error?.message || 'Unknown error'\n}));\n\n// Deduplicate by workflowName (report once per workflow per check)\nconst seen = new Set();\nconst unique = failures.filter(f => {\n if (seen.has(f.workflowName)) return false;\n seen.add(f.workflowName);\n return true;\n});\n\nreturn [{ json: { hasFailures: unique.length > 0, hasFailuresStr: unique.length > 0 ? 'yes' : 'no', count: unique.length, failures: unique } }];"
},
"id": "f5d47910-5047-4c4f-aa84-a6907aa38551",
"name": "Filter Recent Failures",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [672, 0]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "02ff82df-a31d-491b-9cd1-96874c8ae2fe",
"leftValue": "={{ $json.hasFailuresStr }}",
"rightValue": "yes",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "7c298433-ab89-435e-8955-73da6b986f46",
"name": "Has Failures?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [896, 0]
},
{
"parameters": {
"jsCode": "const d = $input.first().json;\n// Fingerprint = sorted workflow names joined — stable across retriggers\nconst fingerprint = (d.failures || [])\n .map(f => f.workflowName)\n .sort()\n .join('|') || 'unknown';\nreturn [{ json: { ...d, fingerprint } }];"
},
"id": "d3609d45-ba9f-41d3-a9bd-8c558a719f10",
"name": "Build Fingerprint",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1120, -120]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT fingerprint, last_seen FROM n8n_health_dedup WHERE fingerprint = '{{ $json.fingerprint }}' AND last_seen > NOW() - INTERVAL '1 hour' LIMIT 1;",
"options": {}
},
"id": "15a3dd71-bc5b-4396-9534-63518e097438",
"name": "Lookup Dedup",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [1344, -120],
"credentials": {
"postgres": {
"id": "n9svoXemqSZoNNUB",
"name": "Postgres account"
}
},
"continueOnFail": true
},
{
"parameters": {
"jsCode": "const alertData = $('Build Fingerprint').first().json;\nconst rows = $input.all().filter(r => r.json && r.json.fingerprint);\nconst alreadyNotified = rows.length > 0;\nreturn [{ json: { ...alertData, alreadyNotified, shouldNotify: !alreadyNotified, shouldNotifyStr: !alreadyNotified ? 'yes' : 'no' } }];"
},
"id": "a92590e3-7044-4ba3-9b39-f6a9457c5fbe",
"name": "Check Already Notified",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1568, -120]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "aa5db0c3-75a7-4ea4-b29e-c12568ccf320",
"leftValue": "={{ $json.shouldNotifyStr }}",
"rightValue": "yes",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "39ac0eaf-73ce-4fe1-9c4b-d09ac920291d",
"name": "Should Notify?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [1792, -120]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO n8n_health_dedup (fingerprint, last_seen, workflow_names) VALUES ('{{ $json.fingerprint }}', NOW(), '{{ $json.failures.map(f => f.workflowName).join(\", \") }}') ON CONFLICT (fingerprint) DO UPDATE SET last_seen = NOW(), workflow_names = EXCLUDED.workflow_names;",
"options": {}
},
"id": "0c3be155-0c3a-4354-b863-73a3f5bbf598",
"name": "Upsert Dedup Record",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [2016, -120],
"credentials": {
"postgres": {
"id": "n9svoXemqSZoNNUB",
"name": "Postgres account"
}
}
},
{
"parameters": {
"jsCode": "const d = $('Build Fingerprint').first().json;\nconst lines = (d.failures || []).map(f =>\n ` • ${f.workflowName}\\n ${f.errorMessage.substring(0, 120)}`\n);\nconst body = `${d.count} workflow${d.count !== 1 ? 's' : ''} failed in the past hour:\\n\\n${lines.join('\\n')}\\n\\nhttps://n8n.paccoco.com`;\nreturn [{ json: { title: `⚠️ n8n: ${d.count} Workflow Failure${d.count !== 1 ? 's' : ''}`, message: body, priority: 7 } }];"
},
"id": "1c6b42bf-a38e-47b6-82ac-50c43ee9e583",
"name": "Format Notification",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [2240, -120]
},
{
"parameters": {
"message": "={{ $json.message }}",
"additionalFields": {
"priority": "={{ $json.priority }}",
"title": "={{ $json.title }}"
},
"options": {
"contentType": "text/plain"
}
},
"id": "09435c2f-9044-432a-8ca8-dd3ce3262c76",
"name": "Push to Gotify",
"type": "n8n-nodes-base.gotify",
"typeVersion": 1,
"position": [2464, -120],
"credentials": {
"gotifyApi": {
"id": "SETUP_REQUIRED",
"name": "n8n Health"
}
}
}
],
"connections": {
"Every 15 Minutes": {
"main": [[{ "node": "Fetch All Workflows", "type": "main", "index": 0 }]]
},
"Fetch All Workflows": {
"main": [[{ "node": "Fetch Failed Executions", "type": "main", "index": 0 }]]
},
"Fetch Failed Executions": {
"main": [[{ "node": "Filter Recent Failures", "type": "main", "index": 0 }]]
},
"Filter Recent Failures": {
"main": [[{ "node": "Has Failures?", "type": "main", "index": 0 }]]
},
"Has Failures?": {
"main": [
[{ "node": "Build Fingerprint", "type": "main", "index": 0 }],
[]
]
},
"Build Fingerprint": {
"main": [[{ "node": "Lookup Dedup", "type": "main", "index": 0 }]]
},
"Lookup Dedup": {
"main": [[{ "node": "Check Already Notified", "type": "main", "index": 0 }]]
},
"Check Already Notified": {
"main": [[{ "node": "Should Notify?", "type": "main", "index": 0 }]]
},
"Should Notify?": {
"main": [
[{ "node": "Upsert Dedup Record", "type": "main", "index": 0 }],
[]
]
},
"Upsert Dedup Record": {
"main": [[{ "node": "Format Notification", "type": "main", "index": 0 }]]
},
"Format Notification": {
"main": [[{ "node": "Push to Gotify", "type": "main", "index": 0 }]]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"staticData": null,
"meta": null,
"pinData": {},
"tags": [
{ "name": "monitoring" },
{ "name": "scheduled" },
{ "name": "n8n" }
],
"id": "febfda9b-0f4e-40b5-ab84-82ba9b7fcf29"
}

88
n8n-workflows/TESTING-STATUS.md Executable file
View File

@@ -0,0 +1,88 @@
# n8n Workflow Testing Status
_Last updated: 2026-05-10_
---
## ✅ Confirmed Working
| # | Workflow | Version | Notes |
|---|----------|---------|-------|
| 01 | Grafana Alert → AI + Gotify | v1.4 | Dedup + recovery branch confirmed |
| 08 | Class Recording RAG | v1.2 | Paperless + Gotify end-to-end confirmed |
| 09 | Class Transcript Ingest | v1.1 | Gotify confirmed |
| 14 | Paperless Inbox Reminder | v1.2 | Fixed: `Bearer``Token` auth; fixed: `hasItemsStr` bug |
| 15 | n8n Health Monitor | v1.2 | Fixed: workflow names (was showing IDs); Postgres dedup working |
| 16 | NFS Watchdog + Auto-Heal | v1.2 | Confirmed working |
| 07 | Git Commit → AI Summary → Gotify | v1.0 | Confirmed working |
## ⏸ Retired
| # | Workflow | Reason |
|---|----------|--------|
| 04 | Whisper Transcription | Replaced — transcription now runs via Docker on Rocinante (RTX 4090) and sends directly to n8n |
---
## 🔲 Not Yet Tested
### 02 — RAG Pipeline v1.0
**What it does:** Two webhooks — `POST /rag/ingest` chunks + embeds text into Qdrant; `POST /rag/query` embeds query, searches Qdrant, asks LiteLLM, returns answer.
**Pre-test checklist:**
- [ ] `knowledge_base` collection exists in Qdrant (`GET http://10.5.1.6:6333/collections`)
- [ ] `nomic-embed-text` model pulled in Ollama (`ollama list` on PD)
- [ ] **Hardcoded LiteLLM key** in `LiteLLM Answer` node — should use `$env.LITELLM_API_KEY`
**Test approach:** POST `{"text": "some test content", "source": "test"}` to `/rag/ingest`, then POST `{"query": "test content question"}` to `/rag/query` and verify an answer comes back.
---
### 03 — Paperless AI Processing v1.0
**What it does:** Paperless webhook on new document → AI classifies (type/summary/tags) → adds note to doc + sends Gotify + applies tags (parallel branches after `Parse Analysis`).
**Pre-test checklist:**
- [ ] Paperless webhook configured to POST to `https://n8n.paccoco.com/webhook/paperless/new-document` on document add
- [ ] **Hardcoded Paperless token** in `Fetch Document`, `Add Note to Document`, and inline in `Apply Suggested Tags to Paperless` code — should use `$env.PAPERLESS_API_TOKEN`
- [ ] **Hardcoded LiteLLM key** in `AI Classify & Extract` — should use `$env.LITELLM_API_KEY`
- [ ] Gotify credential `ajvRjvj0QldLQYmo` ("Paperless") — already wired ✓
**Test approach:** Upload a test document to Paperless, then manually POST `{"document_id": <id>}` to the webhook to trigger processing. Check Paperless for note + tags, check Gotify for notification.
---
### 05 — Paperless → RAG v1.0
**What it does:** Webhook on `POST /paperless/rag-ingest` → fetches Paperless doc → chunks + embeds content → upserts to Qdrant `knowledge_base` → Gotify notification.
**Pre-test checklist:**
- [ ] `knowledge_base` collection exists in Qdrant (same as 02)
- [ ] **Hardcoded Paperless token** in `Fetch Document` — should use `$env.PAPERLESS_API_TOKEN`
- [ ] Gotify credential `SETUP_REQUIRED` ("Paperless RAG") — needs wiring
**Test approach:** POST `{"document_id": <id>}` to the webhook. Verify chunks appear in Qdrant and Gotify notification fires.
---
### 06 — RSS Digest v1.0
**What it does:** Every 6 hours (+ manual webhook + manual trigger), fetches 33 RSS feeds, parses items, deduplicates via Postgres (`rss_seen_items` table), scores by keyword relevance/urgency, AI-summarizes new items per feed, pushes per-feed digests to Gotify.
**Pre-test checklist:**
- [ ] `rss_seen_items` table exists in Postgres
- [ ] **Hardcoded LiteLLM key** in `AI Summarize Feed` node — should use `$env.LITELLM_API_KEY`
- [ ] Postgres credential `n9svoXemqSZoNNUB` — already wired ✓
- [ ] Gotify credential `ExUx3oqYQyhkqZcp` ("Rss Feed") — already wired ✓
**Test approach:** Use the "When clicking Execute workflow" manual trigger. First run will insert all items as new (expect lots of Gotify notifications). Second run should be silent (all deduped).
---
## 🐛 Known Issues / Tech Debt
| Workflow | Issue | Priority |
|----------|-------|----------|
| 02 | Hardcoded LiteLLM API key | Medium |
| 03 | Hardcoded LiteLLM API key + Paperless token | Medium |
| 05 | Hardcoded Paperless token | Medium |
| 06 | Hardcoded LiteLLM API key | Medium |
| 07 | Hardcoded LiteLLM API key (working but tech debt) | Low |
| 15 | `includeData: false` means error messages show as "Unknown error" | Low |
| 09 v1.0 | Still active in n8n alongside v1.1 — should be deactivated | Low |