From 481575001130dea81876c492f200833b7ea53580 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 4 Jun 2026 13:26:50 -0700 Subject: [PATCH] chore(n8n): restore workflow exports --- .../n8n-workflows/75JCevkdgkyCr2qH.json | 95 ++ .../n8n-workflows/9sFwRyUDz51csAp7.json | 957 ++++++++++++++++++ .../n8n-workflows/El1BHJZ56JlzhrRZ.json | 486 +++++++++ .../n8n-workflows/G9ylNbHbnJ6fWX2C.json | 535 ++++++++++ .../n8n-workflows/GSmzuA5dgGgyRg5v.json | 485 +++++++++ .../n8n-workflows/PlZywwqL8MRNEAN6.json | 872 ++++++++++++++++ .../n8n-workflows/QRCCdHNXZUHc2Oz4.json | 362 +++++++ .../agentmon-health-watchdog.json | 147 +++ swarm-common/n8n-workflows/morning-brief.json | 453 +++++++++ .../obsidian-6SKSZWZwuJNwuO2P.json | 1 + .../obsidian-LF3i86l3NkxpayxL.json | 1 + .../obsidian-Ori3Bu5u5ODtxxyD.json | 1 + .../obsidian-PCtD3PuQjzKLyEEE.json | 1 + .../obsidian-UWLMOQQVxbTX6Sis.json | 1 + .../obsidian-YZyJ5G0Ur8D6TlM8.json | 1 + .../rag-and-embedding-health-watchdog.json | 345 +++++++ .../n8n-workflows/swarm-health-watchdog.json | 415 ++++++++ 17 files changed, 5158 insertions(+) create mode 100644 swarm-common/n8n-workflows/75JCevkdgkyCr2qH.json create mode 100644 swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json create mode 100644 swarm-common/n8n-workflows/El1BHJZ56JlzhrRZ.json create mode 100644 swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json create mode 100644 swarm-common/n8n-workflows/GSmzuA5dgGgyRg5v.json create mode 100644 swarm-common/n8n-workflows/PlZywwqL8MRNEAN6.json create mode 100644 swarm-common/n8n-workflows/QRCCdHNXZUHc2Oz4.json create mode 100644 swarm-common/n8n-workflows/agentmon-health-watchdog.json create mode 100644 swarm-common/n8n-workflows/morning-brief.json create mode 100644 swarm-common/n8n-workflows/obsidian-6SKSZWZwuJNwuO2P.json create mode 100644 swarm-common/n8n-workflows/obsidian-LF3i86l3NkxpayxL.json create mode 100644 swarm-common/n8n-workflows/obsidian-Ori3Bu5u5ODtxxyD.json create mode 100644 swarm-common/n8n-workflows/obsidian-PCtD3PuQjzKLyEEE.json create mode 100644 swarm-common/n8n-workflows/obsidian-UWLMOQQVxbTX6Sis.json create mode 100644 swarm-common/n8n-workflows/obsidian-YZyJ5G0Ur8D6TlM8.json create mode 100644 swarm-common/n8n-workflows/rag-and-embedding-health-watchdog.json create mode 100644 swarm-common/n8n-workflows/swarm-health-watchdog.json diff --git a/swarm-common/n8n-workflows/75JCevkdgkyCr2qH.json b/swarm-common/n8n-workflows/75JCevkdgkyCr2qH.json new file mode 100644 index 0000000..af6047f --- /dev/null +++ b/swarm-common/n8n-workflows/75JCevkdgkyCr2qH.json @@ -0,0 +1,95 @@ +{ + "updatedAt": "2026-05-01T20:02:33.035Z", + "createdAt": "2026-03-27T23:10:47.862Z", + "id": "75JCevkdgkyCr2qH", + "name": "Nightly Obsidian Vault Sync", + "description": null, + "active": false, + "isArchived": false, + "nodes": [ + { + "id": "schedule-node", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 240, + 300 + ], + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "45 23 * * *" + } + ] + } + } + }, + { + "id": "nightly-sync", + "name": "Generate Nightly Vault Sync", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 520, + 300 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst http = this.helpers.httpRequest;\nconst OBS_BASE = 'http://192.168.153.130:27123';\nconst OBS_KEY = '698cfc8b00b93c41480e7e1cb84d77b75176be87507256a5fae9a5b53b5a20cb';\nconst MODEL = 'gemma-4-26B-A4B-it-UD-IQ2_M.gguf';\nconst TZ = 'America/Los_Angeles';\n\nconst enc = (p) => encodeURIComponent(p).replace(/%2F/g, '/');\nconst getDate = () => new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()).replaceAll('/', '-');\n\nconst notePaths = [\n 'Infrastructure/Architecture.md',\n 'Infrastructure/Automation/n8n Workflows.md',\n 'Infrastructure/Automation/Cron Jobs.md',\n 'Infrastructure/Services/Docker Services.md'\n];\n\nconst obsHeaders = {\n 'Authorization': `Bearer ${OBS_KEY}`,\n 'User-Agent': 'n8n-nightly-vault-sync'\n};\n\nconst notes = {};\nfor (const p of notePaths) {\n notes[p] = await http({\n method: 'GET',\n url: `${OBS_BASE}/vault/${enc(p)}`,\n headers: obsHeaders,\n timeout: 15000,\n });\n}\n\nconst n8nHealth = await http({\n method: 'GET',\n url: 'http://192.168.153.130:18808/healthz',\n json: true,\n timeout: 10000,\n});\n\nconst modelInfo = await http({\n method: 'GET',\n url: 'http://192.168.153.130:18806/v1/models',\n json: true,\n timeout: 10000,\n});\n\nconst prompt = [\n 'Write a concise nightly operational sync note for an Obsidian shared vault.',\n 'Return markdown body only. No code fences.',\n 'Start with heading: # Nightly Vault Sync',\n 'Then sections: ## Summary, ## Current State, ## Follow-ups',\n 'Keep it factual, low-noise, and under 250 words.',\n 'Mention that this is an automated nightly note generated by n8n using the local LLM.',\n '',\n 'Live health:',\n `- n8n health: ${JSON.stringify(n8nHealth)}`,\n `- local model ids: ${JSON.stringify((modelInfo.data || []).map(m => m.id))}`,\n '',\n 'Source note contents:',\n ...notePaths.flatMap(p => [`\\n--- ${p} ---`, String(notes[p]).slice(0, 1800)])\n].join('\\n');\n\nconst llm = await http({\n method: 'POST',\n url: 'http://192.168.153.130:18806/v1/chat/completions',\n headers: { 'Content-Type': 'application/json' },\n body: {\n model: MODEL,\n temperature: 0.2,\n max_tokens: 260,\n messages: [\n {\n role: 'system',\n content: 'You create concise nightly operations notes for an Obsidian vault. Prefer concrete facts from the provided sources. If there are unresolved drifts or follow-ups, mention them briefly. Do not invent incidents.'\n },\n { role: 'user', content: prompt }\n ]\n },\n json: true,\n timeout: 60000,\n});\n\nlet body = (((llm || {}).choices || [])[0] || {}).message?.content || '';\nbody = body.replace(/^```(?:markdown)?\\s*/i, '').replace(/```\\s*$/i, '').trim();\nif (!body) {\n body = '# Nightly Vault Sync\\n\\n## Summary\\n\\nAutomated nightly note ran, but the local LLM returned an empty response.\\n\\n## Current State\\n\\n- n8n health: ok\\n- local model endpoint reachable\\n\\n## Follow-ups\\n\\n- Check the local LLM response path if this repeats.';\n}\n\nconst date = getDate();\nconst notePath = `Notes/${date} Nightly Vault Sync.md`;\nconst full = `---\\ntitle: Nightly Vault Sync\\narea: infrastructure\\ntags: [infrastructure, obsidian, automation, nightly, assistant]\\ncreated: ${date}\\nupdated: ${date}\\nstatus: active\\nrelated: [[Infrastructure/Architecture]], [[Infrastructure/Automation/n8n Workflows]], [[Infrastructure/Automation/Cron Jobs]], [[Infrastructure/Services/Docker Services]]\\n---\\n\\n${body}\\n`;\n\nawait http({\n method: 'PUT',\n url: `${OBS_BASE}/vault/${enc(notePath)}`,\n headers: { ...obsHeaders, 'Content-Type': 'text/markdown' },\n body: full,\n timeout: 20000,\n});\n\nreturn [{ json: { notePath, model: MODEL, sourceNotes: notePaths, n8nHealth, modelCount: (modelInfo.data || []).length } }];\n" + } + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Generate Nightly Vault Sync", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": { + "node:Schedule Trigger": { + "recurrenceRules": [] + } + }, + "meta": null, + "pinData": null, + "versionId": "9585256a-29c2-444a-aa55-0eaf259f032d", + "activeVersionId": null, + "versionCounter": 66, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-03-27T23:10:47.871Z", + "createdAt": "2026-03-27T23:10:47.871Z", + "role": "workflow:owner", + "workflowId": "75JCevkdgkyCr2qH", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": null +} \ No newline at end of file diff --git a/swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json b/swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json new file mode 100644 index 0000000..0c9e106 --- /dev/null +++ b/swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json @@ -0,0 +1,957 @@ +{ + "updatedAt": "2026-05-14T00:02:05.677Z", + "createdAt": "2026-03-18T05:20:48.223Z", + "id": "9sFwRyUDz51csAp7", + "name": "IMAP Inbox Triage + Obsidian Notes", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 15 + } + ] + } + }, + "id": "n1", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 240, + 304 + ] + }, + { + "parameters": { + "jsCode": "// DEFINITE NOISE - never worth seeing\nconst NOISE_SENDERS = [\n 'discord', 'plex', 'spotify', 'youtube',\n 'lodge at redmond ridge', 'flex +',\n 'seattle jeep',\n 'no-reply', 'noreply', 'do-not-reply', 'donotreply',\n 'newsletter', 'marketing',\n];\nconst NOISE_SUBJECTS = [\n 'bulletin board', 'daily digest', 'weekly digest',\n 'most watchlisted', 'newsletter',\n 'mentioned you in',\n 'looking to see what your car',\n 'take your favorite music',\n 'introducing the take',\n];\n\n// DEFINITE SIGNAL - always pass through, skip LLM\nconst SIGNAL_PATTERNS = [\n 'login attempt', 'unauthorized', 'unusual sign',\n 'invoice', 'payment due', 'receipt',\n 'urgent', 'action required',\n 'password reset', 'verify your',\n 'github', 'gitea',\n];\n\nconst items = $input.all();\nif (items.length === 0) return [];\n\n// Ignore schedule/no-email pass-through items from polling mode\nconst emailish = items.filter(item => {\n const j = item.json || {};\n return !!(j.from || j.subject || j.text || j.textPlain || j.textHtml || j.html || j.headers || j.messageId);\n});\nif (emailish.length === 0) return [];\n\n\nconst definiteSignal = [];\nconst needsJudgement = [];\n\nfor (const item of items) {\n const from = (item.json.from || '').toLowerCase();\n const subject = (item.json.subject || '').toLowerCase();\n const combined = from + ' ' + subject;\n\n // Definite signal - fast path, no LLM needed\n if (SIGNAL_PATTERNS.some(p => combined.includes(p))) {\n definiteSignal.push({ ...item.json, _stage1: 'definite_signal', _account: item.json._account || 'unknown' });\n continue;\n }\n\n // Definite noise - drop\n const isNoise = \n NOISE_SENDERS.some(n => combined.includes(n)) ||\n NOISE_SUBJECTS.some(n => new RegExp(n, 'i').test(combined));\n if (isNoise) continue;\n\n // Everything else - send to LLM for judgement\n needsJudgement.push({ ...item.json, _stage1: 'needs_judgement', _account: item.json._account || 'unknown' });\n}\n\n// Return all items for next node; tag them so we can route\nconst all = [...definiteSignal, ...needsJudgement];\nif (all.length === 0) return [{ json: { _empty: true } }];\nreturn all.map(j => ({ json: j }));" + }, + "id": "n2", + "name": "Stage 1 - Static Filter", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 464, + 304 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json._empty }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "notEquals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "n3", + "name": "Any Left?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 688, + 304 + ] + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{ $json._stage1 }}", + "value2": "needs_judgement" + } + ] + } + }, + "id": "n4", + "name": "Needs LLM Judgement?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 912, + 208 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={\"model\": \"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\", \"temperature\": 0, \"max_tokens\": 256, \"messages\": [{\"role\": \"system\", \"content\": \"You are an email triage assistant for a software developer. Emails may be in any language \\u2014 translate mentally before judging. Reply with JSON only: {\\\"signal\\\": true|false, \\\"priority\\\": 1|2|3, \\\"reason\\\": \\\"one short phrase\\\"}. Priority: 1=act now, 2=read today, 3=FYI. Signal=false means drop silently. Always mark security alerts (login attempts, account access, suspicious activity) as signal priority 1, regardless of language.\"}, {\"role\": \"user\", \"content\": \"From: {{ $json.from }}\\nSubject: {{ $json.subject }}\"}]}", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + }, + "timeout": 15000 + } + }, + "id": "n5", + "name": "Judge with Local LLM", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 1120, + 128 + ] + }, + { + "parameters": { + "jsCode": "const item = $input.first();\nconst inputItem = $('Needs LLM Judgement?').first();\n\ntry {\n let content = '';\n const j = item.json || {};\n\n if (j.choices && j.choices[0] && j.choices[0].message) {\n content = j.choices[0].message.content || '';\n } else if (j._readableState && j._readableState.buffer && j._readableState.buffer[0] && j._readableState.buffer[0].data) {\n const bytes = j._readableState.buffer[0].data;\n const raw = Buffer.from(bytes).toString('utf8');\n const parsed = JSON.parse(raw);\n content = parsed.choices[0].message.content || '';\n }\n\n content = content.trim();\n if (!content) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_empty', _priority: 3, _reason: 'no llm response' } }];\n }\n\n // Strip markdown code fences\n const cleaned = content.replace(/^[^\\{]*/, '').replace(/[^\\}]*$/, '').trim();\n const result = JSON.parse(cleaned);\n\n if (!result.signal) return [];\n\n return [{ json: { ...inputItem.json, _stage2: 'llm_signal', _priority: result.priority || 3, _reason: result.reason || '' } }];\n} catch(e) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_parse_error', _priority: 3, _reason: 'parse error: ' + e.message } }];\n}" + }, + "id": "n6", + "name": "Parse LLM Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1344, + 128 + ] + }, + { + "parameters": { + "jsCode": "const results = [];\nfor (const item of $input.all()) {\n const j = item.json || {};\n results.push({\n json: {\n from: String(j.from || ''),\n subject: String(j.subject || ''),\n date: String(j.date || ''),\n textPlain: String(j.textPlain || j.text || '').substring(0, 500),\n messageId: String(j.messageId || ''),\n _account: String(j._account || 'unknown'),\n _stage1: 'definite_signal',\n _stage2: 'definite_signal',\n _priority: 1,\n _reason: 'pattern match'\n }\n });\n}\nreturn results;" + }, + "id": "n7", + "name": "Tag Definite Signal", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 304 + ] + }, + { + "parameters": { + "aggregate": "aggregateAllItemData", + "destinationFieldName": "messages", + "options": {} + }, + "id": "n8", + "name": "Merge All Signal", + "type": "n8n-nodes-base.aggregate", + "typeVersion": 1, + "position": [ + 1568, + 208 + ] + }, + { + "parameters": { + "jsCode": "const messages = ($input.first().json.messages || [])\n .sort((a, b) => (a._priority || 3) - (b._priority || 3));\n\nif (messages.length === 0) return [];\n\nconst PRIORITY_EMOJI = { 1: '🔴', 2: '🟡', 3: '🔵' };\n\nconst lines = messages.map((m, i) => {\n const from = (m.from || '(unknown)').replace(/<[^>]+>/g, '').trim().substring(0, 50);\n const subject = (m.subject || '(no subject)').trim().substring(0, 75);\n const emoji = PRIORITY_EMOJI[m._priority] || '🔵';\n const reason = m._reason && m._reason !== 'pattern match' ? ` — _${m._reason}_` : '';\n const acct = m._account && m._account !== 'unknown' ? ` [${m._account}]` : '';\n return `${emoji} ${subject}\\n ${from}${acct}${reason}`;\n});\n\nconst text = `📬 *${messages.length} new email${messages.length > 1 ? 's' : ''}*\\n\\n${lines.join('\\n\\n')}`;\nreturn [{ json: { text } }];" + }, + "id": "n9", + "name": "Format & Send", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1792, + 128 + ] + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "n10", + "name": "Send to Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 2000, + 128 + ], + "webhookId": "795a0fc5-c932-4265-bd0d-095dd410f8a8", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": {}, + "id": "n11", + "name": "Silent Stop", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 688, + 464 + ] + }, + { + "parameters": { + "jsCode": "const wrapper = $input.first().json;\nconst messages = wrapper.messages || [];\nconst results = [];\n\nfor (const item of messages) {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const subject = (item.subject || 'No Subject').replace(/[\\/\\\\?%*:|\"<>]/g, '-').substring(0, 80);\n const from = (item.from || 'unknown').replace(/<[^>]+>/g, '').trim();\n const snippet = (item.textPlain || '').substring(0, 500);\n const priority = item._priority || 3;\n const reason = item._reason || '';\n const PRIORITY_LABEL = {1: 'high', 2: 'medium', 3: 'low'};\n const PRIORITY_TAG = {1: 'priority-high', 2: 'priority-medium', 3: 'priority-low'};\n const frontmatter = '---\\ntitle: \"' + subject + '\"\\narea: notes\\ntags: [email, imap, ' + PRIORITY_TAG[priority] + ']\\ncreated: ' + date + '\\nupdated: ' + date + '\\nstatus: active\\nfrom: \"' + from + '\"\\npriority: ' + PRIORITY_LABEL[priority] + '\\nsignal_reason: \"' + reason + '\"\\n---';\n const content = frontmatter + '\\n\\n# ' + subject + '\\n\\n**From:** ' + from + '\\n**Date:** ' + date + '\\n**Priority:** ' + PRIORITY_LABEL[priority] + (reason ? ' — ' + reason : '') + '\\n\\n## Snippet\\n\\n' + snippet + '\\n\\n## Notes\\n\\n_Add notes here_\\n';\n results.push({ json: { path: 'Notes/' + date + ' ' + subject + '.md', content, subject, from, priority, date } });\n}\nreturn results;" + }, + "id": "n12", + "name": "Format Email Notes", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1792, + 304 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "=http://172.19.0.1:27123/vault/{{ encodeURIComponent($json.path).replace(/%2F/g, \"/\") }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.content }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "id": "n13", + "name": "Write Email to Vault", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 2000, + 304 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + }, + "onError": "continueRegularOutput" + }, + { + "parameters": { + "options": {} + }, + "id": "n1a", + "name": "Read Unseen Emails", + "type": "n8n-nodes-base.emailReadImap", + "typeVersion": 2, + "position": [ + 352, + 656 + ], + "credentials": { + "imap": { + "id": "5qGEXTjFtPUZL8BB", + "name": "wills_portal IMAP" + } + } + } + ], + "connections": { + "Stage 1 - Static Filter": { + "main": [ + [ + { + "node": "Any Left?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Any Left?": { + "main": [ + [ + { + "node": "Needs LLM Judgement?", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Silent Stop", + "type": "main", + "index": 0 + } + ] + ] + }, + "Needs LLM Judgement?": { + "main": [ + [ + { + "node": "Judge with Local LLM", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Tag Definite Signal", + "type": "main", + "index": 0 + } + ] + ] + }, + "Judge with Local LLM": { + "main": [ + [ + { + "node": "Parse LLM Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse LLM Result": { + "main": [ + [ + { + "node": "Merge All Signal", + "type": "main", + "index": 0 + } + ] + ] + }, + "Tag Definite Signal": { + "main": [ + [ + { + "node": "Merge All Signal", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge All Signal": { + "main": [ + [ + { + "node": "Format & Send", + "type": "main", + "index": 0 + }, + { + "node": "Format Email Notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format & Send": { + "main": [ + [ + { + "node": "Send to Telegram", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Email Notes": { + "main": [ + [ + { + "node": "Write Email to Vault", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Read Unseen Emails", + "type": "main", + "index": 0 + } + ] + ] + }, + "Read Unseen Emails": { + "main": [ + [ + { + "node": "Stage 1 - Static Filter", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "saveDataSuccessExecution": "all", + "saveDataErrorExecution": "all", + "saveManualExecutions": true + }, + "staticData": { + "node:Schedule Trigger": { + "recurrenceRules": [] + }, + "node:Read Unseen Emails": {}, + "node:Read wills_portal": { + "lastMessageUid": 8770 + }, + "node:Read squareffect": {}, + "node:Schedule wills_portal": { + "recurrenceRules": [] + }, + "node:Schedule squareffect": { + "recurrenceRules": [] + }, + "node:Email Trigger": {} + }, + "meta": null, + "pinData": {}, + "versionId": "8b39192f-1924-42d0-a421-afe88cdee3cf", + "activeVersionId": "8b39192f-1924-42d0-a421-afe88cdee3cf", + "versionCounter": 3824, + "triggerCount": 2, + "shared": [ + { + "updatedAt": "2026-03-18T05:20:48.224Z", + "createdAt": "2026-03-18T05:20:48.224Z", + "role": "workflow:owner", + "workflowId": "9sFwRyUDz51csAp7", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [ + { + "updatedAt": "2026-03-19T04:40:29.921Z", + "createdAt": "2026-03-19T04:40:29.921Z", + "id": "R9u3nhZlt6Vanvus", + "name": "telegram" + }, + { + "updatedAt": "2026-03-19T04:40:29.892Z", + "createdAt": "2026-03-19T04:40:29.892Z", + "id": "VfqIkUpiu2YMBSHw", + "name": "obsidian-sync" + }, + { + "updatedAt": "2026-03-19T04:40:29.877Z", + "createdAt": "2026-03-19T04:40:29.877Z", + "id": "qu6qwIegC1LgLKoA", + "name": "email-triage" + }, + { + "updatedAt": "2026-03-19T04:40:29.909Z", + "createdAt": "2026-03-19T04:40:29.909Z", + "id": "r3vsVtTwe9UfLrGi", + "name": "imap" + }, + { + "updatedAt": "2026-03-19T04:40:29.926Z", + "createdAt": "2026-03-19T04:40:29.926Z", + "id": "zKN5N7wCrUuKB7rV", + "name": "llm" + } + ], + "activeVersion": { + "updatedAt": "2026-05-14T00:02:05.678Z", + "createdAt": "2026-05-14T00:02:05.678Z", + "versionId": "8b39192f-1924-42d0-a421-afe88cdee3cf", + "workflowId": "9sFwRyUDz51csAp7", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 15 + } + ] + } + }, + "id": "n1", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 240, + 304 + ] + }, + { + "parameters": { + "jsCode": "// DEFINITE NOISE - never worth seeing\nconst NOISE_SENDERS = [\n 'discord', 'plex', 'spotify', 'youtube',\n 'lodge at redmond ridge', 'flex +',\n 'seattle jeep',\n 'no-reply', 'noreply', 'do-not-reply', 'donotreply',\n 'newsletter', 'marketing',\n];\nconst NOISE_SUBJECTS = [\n 'bulletin board', 'daily digest', 'weekly digest',\n 'most watchlisted', 'newsletter',\n 'mentioned you in',\n 'looking to see what your car',\n 'take your favorite music',\n 'introducing the take',\n];\n\n// DEFINITE SIGNAL - always pass through, skip LLM\nconst SIGNAL_PATTERNS = [\n 'login attempt', 'unauthorized', 'unusual sign',\n 'invoice', 'payment due', 'receipt',\n 'urgent', 'action required',\n 'password reset', 'verify your',\n 'github', 'gitea',\n];\n\nconst items = $input.all();\nif (items.length === 0) return [];\n\n// Ignore schedule/no-email pass-through items from polling mode\nconst emailish = items.filter(item => {\n const j = item.json || {};\n return !!(j.from || j.subject || j.text || j.textPlain || j.textHtml || j.html || j.headers || j.messageId);\n});\nif (emailish.length === 0) return [];\n\n\nconst definiteSignal = [];\nconst needsJudgement = [];\n\nfor (const item of items) {\n const from = (item.json.from || '').toLowerCase();\n const subject = (item.json.subject || '').toLowerCase();\n const combined = from + ' ' + subject;\n\n // Definite signal - fast path, no LLM needed\n if (SIGNAL_PATTERNS.some(p => combined.includes(p))) {\n definiteSignal.push({ ...item.json, _stage1: 'definite_signal', _account: item.json._account || 'unknown' });\n continue;\n }\n\n // Definite noise - drop\n const isNoise = \n NOISE_SENDERS.some(n => combined.includes(n)) ||\n NOISE_SUBJECTS.some(n => new RegExp(n, 'i').test(combined));\n if (isNoise) continue;\n\n // Everything else - send to LLM for judgement\n needsJudgement.push({ ...item.json, _stage1: 'needs_judgement', _account: item.json._account || 'unknown' });\n}\n\n// Return all items for next node; tag them so we can route\nconst all = [...definiteSignal, ...needsJudgement];\nif (all.length === 0) return [{ json: { _empty: true } }];\nreturn all.map(j => ({ json: j }));" + }, + "id": "n2", + "name": "Stage 1 - Static Filter", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 464, + 304 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json._empty }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "notEquals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "n3", + "name": "Any Left?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 688, + 304 + ] + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{ $json._stage1 }}", + "value2": "needs_judgement" + } + ] + } + }, + "id": "n4", + "name": "Needs LLM Judgement?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 912, + 208 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={\"model\": \"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\", \"temperature\": 0, \"max_tokens\": 256, \"messages\": [{\"role\": \"system\", \"content\": \"You are an email triage assistant for a software developer. Emails may be in any language \\u2014 translate mentally before judging. Reply with JSON only: {\\\"signal\\\": true|false, \\\"priority\\\": 1|2|3, \\\"reason\\\": \\\"one short phrase\\\"}. Priority: 1=act now, 2=read today, 3=FYI. Signal=false means drop silently. Always mark security alerts (login attempts, account access, suspicious activity) as signal priority 1, regardless of language.\"}, {\"role\": \"user\", \"content\": \"From: {{ $json.from }}\\nSubject: {{ $json.subject }}\"}]}", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + }, + "timeout": 15000 + } + }, + "id": "n5", + "name": "Judge with Local LLM", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 1120, + 128 + ] + }, + { + "parameters": { + "jsCode": "const item = $input.first();\nconst inputItem = $('Needs LLM Judgement?').first();\n\ntry {\n let content = '';\n const j = item.json || {};\n\n if (j.choices && j.choices[0] && j.choices[0].message) {\n content = j.choices[0].message.content || '';\n } else if (j._readableState && j._readableState.buffer && j._readableState.buffer[0] && j._readableState.buffer[0].data) {\n const bytes = j._readableState.buffer[0].data;\n const raw = Buffer.from(bytes).toString('utf8');\n const parsed = JSON.parse(raw);\n content = parsed.choices[0].message.content || '';\n }\n\n content = content.trim();\n if (!content) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_empty', _priority: 3, _reason: 'no llm response' } }];\n }\n\n // Strip markdown code fences\n const cleaned = content.replace(/^[^\\{]*/, '').replace(/[^\\}]*$/, '').trim();\n const result = JSON.parse(cleaned);\n\n if (!result.signal) return [];\n\n return [{ json: { ...inputItem.json, _stage2: 'llm_signal', _priority: result.priority || 3, _reason: result.reason || '' } }];\n} catch(e) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_parse_error', _priority: 3, _reason: 'parse error: ' + e.message } }];\n}" + }, + "id": "n6", + "name": "Parse LLM Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1344, + 128 + ] + }, + { + "parameters": { + "jsCode": "const results = [];\nfor (const item of $input.all()) {\n const j = item.json || {};\n results.push({\n json: {\n from: String(j.from || ''),\n subject: String(j.subject || ''),\n date: String(j.date || ''),\n textPlain: String(j.textPlain || j.text || '').substring(0, 500),\n messageId: String(j.messageId || ''),\n _account: String(j._account || 'unknown'),\n _stage1: 'definite_signal',\n _stage2: 'definite_signal',\n _priority: 1,\n _reason: 'pattern match'\n }\n });\n}\nreturn results;" + }, + "id": "n7", + "name": "Tag Definite Signal", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 304 + ] + }, + { + "parameters": { + "aggregate": "aggregateAllItemData", + "destinationFieldName": "messages", + "options": {} + }, + "id": "n8", + "name": "Merge All Signal", + "type": "n8n-nodes-base.aggregate", + "typeVersion": 1, + "position": [ + 1568, + 208 + ] + }, + { + "parameters": { + "jsCode": "const messages = ($input.first().json.messages || [])\n .sort((a, b) => (a._priority || 3) - (b._priority || 3));\n\nif (messages.length === 0) return [];\n\nconst PRIORITY_EMOJI = { 1: '🔴', 2: '🟡', 3: '🔵' };\n\nconst lines = messages.map((m, i) => {\n const from = (m.from || '(unknown)').replace(/<[^>]+>/g, '').trim().substring(0, 50);\n const subject = (m.subject || '(no subject)').trim().substring(0, 75);\n const emoji = PRIORITY_EMOJI[m._priority] || '🔵';\n const reason = m._reason && m._reason !== 'pattern match' ? ` — _${m._reason}_` : '';\n const acct = m._account && m._account !== 'unknown' ? ` [${m._account}]` : '';\n return `${emoji} ${subject}\\n ${from}${acct}${reason}`;\n});\n\nconst text = `📬 *${messages.length} new email${messages.length > 1 ? 's' : ''}*\\n\\n${lines.join('\\n\\n')}`;\nreturn [{ json: { text } }];" + }, + "id": "n9", + "name": "Format & Send", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1792, + 128 + ] + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "n10", + "name": "Send to Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 2000, + 128 + ], + "webhookId": "795a0fc5-c932-4265-bd0d-095dd410f8a8", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": {}, + "id": "n11", + "name": "Silent Stop", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 688, + 464 + ] + }, + { + "parameters": { + "jsCode": "const wrapper = $input.first().json;\nconst messages = wrapper.messages || [];\nconst results = [];\n\nfor (const item of messages) {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const subject = (item.subject || 'No Subject').replace(/[\\/\\\\?%*:|\"<>]/g, '-').substring(0, 80);\n const from = (item.from || 'unknown').replace(/<[^>]+>/g, '').trim();\n const snippet = (item.textPlain || '').substring(0, 500);\n const priority = item._priority || 3;\n const reason = item._reason || '';\n const PRIORITY_LABEL = {1: 'high', 2: 'medium', 3: 'low'};\n const PRIORITY_TAG = {1: 'priority-high', 2: 'priority-medium', 3: 'priority-low'};\n const frontmatter = '---\\ntitle: \"' + subject + '\"\\narea: notes\\ntags: [email, imap, ' + PRIORITY_TAG[priority] + ']\\ncreated: ' + date + '\\nupdated: ' + date + '\\nstatus: active\\nfrom: \"' + from + '\"\\npriority: ' + PRIORITY_LABEL[priority] + '\\nsignal_reason: \"' + reason + '\"\\n---';\n const content = frontmatter + '\\n\\n# ' + subject + '\\n\\n**From:** ' + from + '\\n**Date:** ' + date + '\\n**Priority:** ' + PRIORITY_LABEL[priority] + (reason ? ' — ' + reason : '') + '\\n\\n## Snippet\\n\\n' + snippet + '\\n\\n## Notes\\n\\n_Add notes here_\\n';\n results.push({ json: { path: 'Notes/' + date + ' ' + subject + '.md', content, subject, from, priority, date } });\n}\nreturn results;" + }, + "id": "n12", + "name": "Format Email Notes", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1792, + 304 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "=http://172.19.0.1:27123/vault/{{ encodeURIComponent($json.path).replace(/%2F/g, \"/\") }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.content }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "id": "n13", + "name": "Write Email to Vault", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 2000, + 304 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + }, + "onError": "continueRegularOutput" + }, + { + "parameters": { + "options": {} + }, + "id": "n1a", + "name": "Read Unseen Emails", + "type": "n8n-nodes-base.emailReadImap", + "typeVersion": 2, + "position": [ + 352, + 656 + ], + "credentials": { + "imap": { + "id": "5qGEXTjFtPUZL8BB", + "name": "wills_portal IMAP" + } + } + } + ], + "connections": { + "Stage 1 - Static Filter": { + "main": [ + [ + { + "node": "Any Left?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Any Left?": { + "main": [ + [ + { + "node": "Needs LLM Judgement?", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Silent Stop", + "type": "main", + "index": 0 + } + ] + ] + }, + "Needs LLM Judgement?": { + "main": [ + [ + { + "node": "Judge with Local LLM", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Tag Definite Signal", + "type": "main", + "index": 0 + } + ] + ] + }, + "Judge with Local LLM": { + "main": [ + [ + { + "node": "Parse LLM Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse LLM Result": { + "main": [ + [ + { + "node": "Merge All Signal", + "type": "main", + "index": 0 + } + ] + ] + }, + "Tag Definite Signal": { + "main": [ + [ + { + "node": "Merge All Signal", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge All Signal": { + "main": [ + [ + { + "node": "Format & Send", + "type": "main", + "index": 0 + }, + { + "node": "Format Email Notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format & Send": { + "main": [ + [ + { + "node": "Send to Telegram", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Email Notes": { + "main": [ + [ + { + "node": "Write Email to Vault", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Read Unseen Emails", + "type": "main", + "index": 0 + } + ] + ] + }, + "Read Unseen Emails": { + "main": [ + [ + { + "node": "Stage 1 - Static Filter", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T00:02:07.948Z", + "id": 1469, + "workflowId": "9sFwRyUDz51csAp7", + "versionId": "8b39192f-1924-42d0-a421-afe88cdee3cf", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:02:06.050Z", + "id": 1468, + "workflowId": "9sFwRyUDz51csAp7", + "versionId": "8b39192f-1924-42d0-a421-afe88cdee3cf", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/El1BHJZ56JlzhrRZ.json b/swarm-common/n8n-workflows/El1BHJZ56JlzhrRZ.json new file mode 100644 index 0000000..111a286 --- /dev/null +++ b/swarm-common/n8n-workflows/El1BHJZ56JlzhrRZ.json @@ -0,0 +1,486 @@ +{ + "updatedAt": "2026-05-14T00:03:13.116Z", + "createdAt": "2026-05-12T17:56:05.279Z", + "id": "El1BHJZ56JlzhrRZ", + "name": "Voice Memo Capture (Audio URL + Local Whisper)", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "voice-memo", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -980, + 0 + ], + "id": "9f1da0a8-32db-4e67-a6e4-18cf8b4d42ee", + "name": "Webhook - Voice Memo", + "webhookId": "06796590-13b3-4347-9582-1ac92719c95d" + }, + { + "parameters": { + "jsCode": "const body = $json.body ?? $json;\n\nconst audio_url = String(body.audio_url || body.url || '').trim();\nconst telegram_file_id = String(body.telegram_file_id || body.file_id || '').trim();\nconst discord_audio_url = String(body.discord_audio_url || '').trim();\nconst audio_base64 = String(body.audio_base64 || '').trim();\nconst audio_format = String(body.audio_format || body.format || 'ogg').trim();\nconst language = String(body.language || 'en').trim();\nconst title = String(body.title || 'Voice Memo').trim();\nconst tags = Array.isArray(body.tags) ? body.tags : String(body.tags || 'voice,memo').split(',').map(s => s.trim()).filter(Boolean);\nconst include_tts = body.include_tts === true || body.tts_readback === true;\nconst voice = String(body.voice || body.tts_voice || 'af_heart').trim();\nif (!audio_url && !telegram_file_id && !discord_audio_url && !audio_base64) {\n throw new Error('POST JSON must include audio_url, telegram_file_id, discord_audio_url, or audio_base64');\n}\nreturn [{ json: { audio_url, telegram_file_id, discord_audio_url, audio_base64, audio_format, language, title, tags, include_tts, voice } }];" + }, + "id": "vm-normalize-v2", + "name": "Normalize Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -680, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18813/process", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ audio_url: $json.audio_url, telegram_file_id: $json.telegram_file_id, discord_audio_url: $json.discord_audio_url, title: $json.title, tags: $json.tags, include_tts: $json.include_tts, voice: $json.voice }) }}", + "options": { + "timeout": 180000, + "fullResponse": false + } + }, + "id": "vm-process-v2", + "name": "Process Voice Memo", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -460, + 0 + ] + }, + { + "parameters": { + "jsCode": "const input = $('Normalize Input').first().json;\nconst proc = $input.first().json;\n\nfunction slugify(s) { return String(s || 'voice-memo').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'voice-memo'; }\nfunction yaml(s) { return String(s ?? '').split('\\n').join(' ').replaceAll('\"', '\\\\\"'); }\n\nconst date = new Date(proc.created_at || Date.now());\nconst ymd = date.toISOString().slice(0,10);\nconst notePath = `Voice Memos/${ymd}-${slugify(proc.title || input.title)}.md`;\n\nconst title = proc.title || input.title || 'Voice Memo';\nconst tags = proc.tags || input.tags || ['voice', 'memo'];\nconst tagLines = tags.map(t => ` - ${yaml(t)}`).join('\\n');\nconst sourceType = proc.source_type || input.source || 'unknown';\nconst sourceUrl = input.source_url || '';\n\nlet audioNote = '';\nif (proc.tts_audio_url) {\n audioNote = `\\n## Audio Summary\\n\\n> Listen to the AI-generated summary: ${proc.tts_audio_url}\\n`;\n}\n\nconst markdown = `---\\ntitle: \"${yaml(title)}\"\\nsource: \"${yaml(sourceUrl)}\"\\nsource_type: \"${sourceType}\"\\ncreated: \"${date.toISOString()}\"\\ntags:\\n${tagLines}\\n---\\n\\n# ${title}\\n\\n## Summary\\n\\n${(proc.summary || '').trim()}\\n${audioNote}\\n## Transcript\\n\\n${proc.transcript || 'No transcript available.'}\\n`;\n\nreturn [{ json: { ...input, notePath, markdown, title, tts_audio_url: proc.tts_audio_url || null } }];\n" + }, + "id": "vm-build-obsidian-v2", + "name": "Build Obsidian Note", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -240, + 0 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.notePath).replace(/%2F/g, '/')}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "text/markdown" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{$json.markdown}}", + "options": { + "timeout": 30000 + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" + }, + "id": "vm-write-obsidian-v2", + "name": "Write Note to Obsidian", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 0 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ \"Voice memo captured (\" + $json.source_type + \"): \" + $json.title + \"\\nObsidian: \" + $json.notePath + ($json.tts_audio_url ? \"\\nAudio summary: \" + $json.tts_audio_url : \"\") }}", + "additionalFields": {} + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1160, + -80 + ], + "id": "41bf5a55-2047-400a-87c7-44744a0f2a42", + "name": "Send Telegram Notification", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ JSON.stringify({ ok: true, notePath: $json.notePath, title: $json.title, source_type: $json.source_type, tts_audio_url: $json.tts_audio_url || null }) }}" + }, + "id": "vm-respond-v2", + "name": "Respond", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 460, + 0 + ] + } + ], + "connections": { + "Webhook - Voice Memo": { + "main": [ + [ + { + "node": "Normalize Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Input": { + "main": [ + [ + { + "node": "Process Voice Memo", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Voice Memo": { + "main": [ + [ + { + "node": "Build Obsidian Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Obsidian Note": { + "main": [ + [ + { + "node": "Write Note to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + }, + "Write Note to Obsidian": { + "main": [ + [ + { + "node": "Send Telegram Notification", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send Telegram Notification": { + "main": [ + [ + { + "node": "Respond", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "timezone": "America/Los_Angeles", + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "none", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": null, + "meta": null, + "pinData": null, + "versionId": "4511e901-afab-493e-9b17-99a9d9865147", + "activeVersionId": "4511e901-afab-493e-9b17-99a9d9865147", + "versionCounter": 38, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-12T17:56:05.281Z", + "createdAt": "2026-05-12T17:56:05.281Z", + "role": "workflow:owner", + "workflowId": "El1BHJZ56JlzhrRZ", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-05-14T00:03:13.117Z", + "createdAt": "2026-05-14T00:03:13.117Z", + "versionId": "4511e901-afab-493e-9b17-99a9d9865147", + "workflowId": "El1BHJZ56JlzhrRZ", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "voice-memo", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -980, + 0 + ], + "id": "9f1da0a8-32db-4e67-a6e4-18cf8b4d42ee", + "name": "Webhook - Voice Memo", + "webhookId": "06796590-13b3-4347-9582-1ac92719c95d" + }, + { + "parameters": { + "jsCode": "const body = $json.body ?? $json;\n\nconst audio_url = String(body.audio_url || body.url || '').trim();\nconst telegram_file_id = String(body.telegram_file_id || body.file_id || '').trim();\nconst discord_audio_url = String(body.discord_audio_url || '').trim();\nconst audio_base64 = String(body.audio_base64 || '').trim();\nconst audio_format = String(body.audio_format || body.format || 'ogg').trim();\nconst language = String(body.language || 'en').trim();\nconst title = String(body.title || 'Voice Memo').trim();\nconst tags = Array.isArray(body.tags) ? body.tags : String(body.tags || 'voice,memo').split(',').map(s => s.trim()).filter(Boolean);\nconst include_tts = body.include_tts === true || body.tts_readback === true;\nconst voice = String(body.voice || body.tts_voice || 'af_heart').trim();\nif (!audio_url && !telegram_file_id && !discord_audio_url && !audio_base64) {\n throw new Error('POST JSON must include audio_url, telegram_file_id, discord_audio_url, or audio_base64');\n}\nreturn [{ json: { audio_url, telegram_file_id, discord_audio_url, audio_base64, audio_format, language, title, tags, include_tts, voice } }];" + }, + "id": "vm-normalize-v2", + "name": "Normalize Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -680, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18813/process", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ audio_url: $json.audio_url, telegram_file_id: $json.telegram_file_id, discord_audio_url: $json.discord_audio_url, title: $json.title, tags: $json.tags, include_tts: $json.include_tts, voice: $json.voice }) }}", + "options": { + "timeout": 180000, + "fullResponse": false + } + }, + "id": "vm-process-v2", + "name": "Process Voice Memo", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -460, + 0 + ] + }, + { + "parameters": { + "jsCode": "const input = $('Normalize Input').first().json;\nconst proc = $input.first().json;\n\nfunction slugify(s) { return String(s || 'voice-memo').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'voice-memo'; }\nfunction yaml(s) { return String(s ?? '').split('\\n').join(' ').replaceAll('\"', '\\\\\"'); }\n\nconst date = new Date(proc.created_at || Date.now());\nconst ymd = date.toISOString().slice(0,10);\nconst notePath = `Voice Memos/${ymd}-${slugify(proc.title || input.title)}.md`;\n\nconst title = proc.title || input.title || 'Voice Memo';\nconst tags = proc.tags || input.tags || ['voice', 'memo'];\nconst tagLines = tags.map(t => ` - ${yaml(t)}`).join('\\n');\nconst sourceType = proc.source_type || input.source || 'unknown';\nconst sourceUrl = input.source_url || '';\n\nlet audioNote = '';\nif (proc.tts_audio_url) {\n audioNote = `\\n## Audio Summary\\n\\n> Listen to the AI-generated summary: ${proc.tts_audio_url}\\n`;\n}\n\nconst markdown = `---\\ntitle: \"${yaml(title)}\"\\nsource: \"${yaml(sourceUrl)}\"\\nsource_type: \"${sourceType}\"\\ncreated: \"${date.toISOString()}\"\\ntags:\\n${tagLines}\\n---\\n\\n# ${title}\\n\\n## Summary\\n\\n${(proc.summary || '').trim()}\\n${audioNote}\\n## Transcript\\n\\n${proc.transcript || 'No transcript available.'}\\n`;\n\nreturn [{ json: { ...input, notePath, markdown, title, tts_audio_url: proc.tts_audio_url || null } }];\n" + }, + "id": "vm-build-obsidian-v2", + "name": "Build Obsidian Note", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -240, + 0 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.notePath).replace(/%2F/g, '/')}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "text/markdown" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{$json.markdown}}", + "options": { + "timeout": 30000 + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" + }, + "id": "vm-write-obsidian-v2", + "name": "Write Note to Obsidian", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 0 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ \"Voice memo captured (\" + $json.source_type + \"): \" + $json.title + \"\\nObsidian: \" + $json.notePath + ($json.tts_audio_url ? \"\\nAudio summary: \" + $json.tts_audio_url : \"\") }}", + "additionalFields": {} + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1160, + -80 + ], + "id": "41bf5a55-2047-400a-87c7-44744a0f2a42", + "name": "Send Telegram Notification", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ JSON.stringify({ ok: true, notePath: $json.notePath, title: $json.title, source_type: $json.source_type, tts_audio_url: $json.tts_audio_url || null }) }}" + }, + "id": "vm-respond-v2", + "name": "Respond", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 460, + 0 + ] + } + ], + "connections": { + "Webhook - Voice Memo": { + "main": [ + [ + { + "node": "Normalize Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Input": { + "main": [ + [ + { + "node": "Process Voice Memo", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Voice Memo": { + "main": [ + [ + { + "node": "Build Obsidian Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Obsidian Note": { + "main": [ + [ + { + "node": "Write Note to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + }, + "Write Note to Obsidian": { + "main": [ + [ + { + "node": "Send Telegram Notification", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send Telegram Notification": { + "main": [ + [ + { + "node": "Respond", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T00:03:13.146Z", + "id": 1475, + "workflowId": "El1BHJZ56JlzhrRZ", + "versionId": "4511e901-afab-493e-9b17-99a9d9865147", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:03:13.139Z", + "id": 1474, + "workflowId": "El1BHJZ56JlzhrRZ", + "versionId": "4511e901-afab-493e-9b17-99a9d9865147", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json b/swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json new file mode 100644 index 0000000..98b775e --- /dev/null +++ b/swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json @@ -0,0 +1,535 @@ +{ + "updatedAt": "2026-05-14T00:18:01.110Z", + "createdAt": "2026-05-12T16:59:40.394Z", + "id": "G9ylNbHbnJ6fWX2C", + "name": "n8n Failure Digest", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -920, + -120 + ], + "id": "a673b342-0e9e-44ae-a470-0a7ba93d135e", + "name": "Manual Trigger" + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 10 * * * *" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -920, + 80 + ], + "id": "6b8a395f-eadd-479d-980d-6f744f411c7d", + "name": "Hourly Schedule" + }, + { + "parameters": { + "url": "http://127.0.0.1:5678/api/v1/executions?status=error&limit=100", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -660, + 0 + ], + "id": "afbf364e-4aca-4c7f-a43a-62a5e0b05d3b", + "name": "List Failed Executions", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const data = Array.isArray($json.data) ? $json.data : [];\nconst windowMinutes = 65;\nconst cutoff = Date.now() - windowMinutes * 60 * 1000;\nconst selfName = 'n8n Failure Digest';\nconst seen = new Set();\nconst out = [];\nfor (const ex of data) {\n const status = String(ex.status || '').toLowerCase();\n if (!['error', 'crashed'].includes(status)) continue;\n const t = Date.parse(ex.stoppedAt || ex.startedAt || ex.createdAt || '');\n if (Number.isFinite(t) && t < cutoff) continue;\n const id = String(ex.id || '');\n if (!id || seen.has(id)) continue;\n seen.add(id);\n out.push({ json: { id, status, startedAt: ex.startedAt, stoppedAt: ex.stoppedAt, workflowId: ex.workflowId, windowMinutes } });\n}\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -420, + 0 + ], + "id": "00f4d7aa-3890-4eb4-bcb4-64afd7675767", + "name": "Recent Failure IDs" + }, + { + "parameters": { + "url": "=http://127.0.0.1:5678/api/v1/executions/{{$json.id}}?includeData=true", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -180, + 0 + ], + "id": "4de4125e-75d6-4896-93d1-1ce20dce2db8", + "name": "Fetch Failure Details", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const items = $input.all();\nconst windowMinutes = 65;\nconst now = Date.now();\nconst selfName = 'n8n Failure Digest';\nfunction arr(v) { return Array.isArray(v) ? v : (v == null ? [] : [v]); }\nfunction msg(err) {\n if (!err) return 'Unknown error';\n return String(err.message || err.description || err.name || err.code || JSON.stringify(err)).trim() || 'Unknown error';\n}\nfunction errType(err) { return String(err?.name || err?.type || err?.code || err?.httpCode || 'Error'); }\nfunction sig(s) {\n return String(s).split('\\n')[0]\n .replace(/https?:\\/\\/\\S+/g, '')\n .replace(/[0-9a-f]{8,}/gi, '')\n .replace(/\\b\\d{4,}\\b/g, '')\n .slice(0, 180);\n}\nfunction findErr(ex) {\n const rd = ex.data?.resultData || {};\n if (rd.error) return { node: rd.error.node?.name || rd.error.node || rd.lastNodeExecuted || 'unknown', error: rd.error };\n const runData = rd.runData || {};\n for (const [nodeName, attempts] of Object.entries(runData)) {\n for (const attempt of arr(attempts).slice().reverse()) {\n if (attempt?.error) return { node: nodeName, error: attempt.error };\n }\n }\n return { node: rd.lastNodeExecuted || 'unknown', error: ex.error || {} };\n}\nconst failures = [];\nfor (const item of items) {\n const ex = item.json || {};\n const workflowName = ex.workflowData?.name || ex.workflow?.name || `Workflow ${ex.workflowId || 'unknown'}`;\n if (workflowName === selfName) continue;\n const found = findErr(ex);\n const message = msg(found.error);\n const when = ex.stoppedAt || ex.startedAt || ex.createdAt || new Date(now).toISOString();\n failures.push({\n id: ex.id,\n workflowId: ex.workflowId || ex.workflowData?.id || 'unknown',\n workflowName,\n node: found.node || 'unknown',\n errorType: errType(found.error),\n message,\n signature: sig(message),\n when,\n status: ex.status || 'unknown',\n });\n}\nconst groups = new Map();\nfor (const f of failures) {\n const key = `${f.workflowId}\\u0000${f.node}\\u0000${f.errorType}\\u0000${f.signature}`;\n if (!groups.has(key)) groups.set(key, { workflowName: f.workflowName, workflowId: f.workflowId, node: f.node, errorType: f.errorType, signature: f.signature, count: 0, ids: [], latest: f.when });\n const g = groups.get(key);\n g.count++;\n if (g.ids.length < 8) g.ids.push(f.id);\n if (String(f.when) > String(g.latest)) g.latest = f.when;\n}\nconst sorted = [...groups.values()].sort((a,b) => b.count - a.count || String(b.latest).localeCompare(String(a.latest))).slice(0, 12);\nif (!sorted.length) return [];\nfunction telegramSafe(s) { return String(s || '').replace(/[\\u0000-\\u001f\\u007f]/g, ' ').slice(0, 3500); }\nconst lines = [];\nlines.push(`🚨 n8n Failure Digest: ${failures.length} failed execution(s) in the last ${windowMinutes} min`);\nlines.push('');\nsorted.forEach((g, i) => {\n lines.push(`${i+1}. ${g.workflowName}`);\n lines.push(` Node: ${g.node}`);\n lines.push(` ${g.count}x ${g.errorType}: ${g.signature}`);\n lines.push(` Execs: ${g.ids.join(', ')} | latest ${g.latest}`);\n});\nlines.push('');\nlines.push('Open n8n: http://127.0.0.1:18808');\n// Telegram node defaults to legacy Markdown, so escape characters that\n// commonly occur in workflow/node/error names (notably underscores).\nfunction telegramMarkdownSafe(s) { return String(s).replace(/([_*`\\[])/g, '\\\\$1'); }\nconst text = telegramMarkdownSafe(lines.join('\\n'));\nreturn [{ json: { text, totalFailures: failures.length, groups: sorted, generatedAt: new Date(now).toISOString() } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 80, + 0 + ], + "id": "f6b4eab8-7017-43e6-97c8-dce63873e097", + "name": "Build Digest" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "", + "disable_web_page_preview": true + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 340, + 0 + ], + "id": "cf49d05d-5d81-404b-a751-ce56794985a9", + "name": "Send Telegram Digest", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://discord.com/api/v10/channels/1494453542243532932/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ content: $json.text.substring(0, 2000) }) }}", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + } + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 340, + 200 + ], + "id": "6c3086e4-0869-4003-94c3-66b4975f94e9", + "name": "Send Discord Digest", + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "List Failed Executions", + "type": "main", + "index": 0 + } + ] + ] + }, + "Hourly Schedule": { + "main": [ + [ + { + "node": "List Failed Executions", + "type": "main", + "index": 0 + } + ] + ] + }, + "List Failed Executions": { + "main": [ + [ + { + "node": "Recent Failure IDs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Recent Failure IDs": { + "main": [ + [ + { + "node": "Fetch Failure Details", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Failure Details": { + "main": [ + [ + { + "node": "Build Digest", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Digest": { + "main": [ + [ + { + "node": "Send Telegram Digest", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord Digest", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "timezone": "America/Los_Angeles", + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "none", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": { + "node:Hourly Schedule": { + "recurrenceRules": [] + } + }, + "meta": null, + "versionId": "2d85e3bf-d8cf-4274-bf61-5377241897da", + "activeVersionId": "2d85e3bf-d8cf-4274-bf61-5377241897da", + "versionCounter": 36, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-12T16:59:40.395Z", + "createdAt": "2026-05-12T16:59:40.395Z", + "role": "workflow:owner", + "workflowId": "G9ylNbHbnJ6fWX2C", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-05-14T00:18:01.111Z", + "createdAt": "2026-05-14T00:18:01.111Z", + "versionId": "2d85e3bf-d8cf-4274-bf61-5377241897da", + "workflowId": "G9ylNbHbnJ6fWX2C", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -920, + -120 + ], + "id": "a673b342-0e9e-44ae-a470-0a7ba93d135e", + "name": "Manual Trigger" + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 10 * * * *" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -920, + 80 + ], + "id": "6b8a395f-eadd-479d-980d-6f744f411c7d", + "name": "Hourly Schedule" + }, + { + "parameters": { + "url": "http://127.0.0.1:5678/api/v1/executions?status=error&limit=100", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -660, + 0 + ], + "id": "afbf364e-4aca-4c7f-a43a-62a5e0b05d3b", + "name": "List Failed Executions", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const data = Array.isArray($json.data) ? $json.data : [];\nconst windowMinutes = 65;\nconst cutoff = Date.now() - windowMinutes * 60 * 1000;\nconst selfName = 'n8n Failure Digest';\nconst seen = new Set();\nconst out = [];\nfor (const ex of data) {\n const status = String(ex.status || '').toLowerCase();\n if (!['error', 'crashed'].includes(status)) continue;\n const t = Date.parse(ex.stoppedAt || ex.startedAt || ex.createdAt || '');\n if (Number.isFinite(t) && t < cutoff) continue;\n const id = String(ex.id || '');\n if (!id || seen.has(id)) continue;\n seen.add(id);\n out.push({ json: { id, status, startedAt: ex.startedAt, stoppedAt: ex.stoppedAt, workflowId: ex.workflowId, windowMinutes } });\n}\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -420, + 0 + ], + "id": "00f4d7aa-3890-4eb4-bcb4-64afd7675767", + "name": "Recent Failure IDs" + }, + { + "parameters": { + "url": "=http://127.0.0.1:5678/api/v1/executions/{{$json.id}}?includeData=true", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -180, + 0 + ], + "id": "4de4125e-75d6-4896-93d1-1ce20dce2db8", + "name": "Fetch Failure Details", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const items = $input.all();\nconst windowMinutes = 65;\nconst now = Date.now();\nconst selfName = 'n8n Failure Digest';\nfunction arr(v) { return Array.isArray(v) ? v : (v == null ? [] : [v]); }\nfunction msg(err) {\n if (!err) return 'Unknown error';\n return String(err.message || err.description || err.name || err.code || JSON.stringify(err)).trim() || 'Unknown error';\n}\nfunction errType(err) { return String(err?.name || err?.type || err?.code || err?.httpCode || 'Error'); }\nfunction sig(s) {\n return String(s).split('\\n')[0]\n .replace(/https?:\\/\\/\\S+/g, '')\n .replace(/[0-9a-f]{8,}/gi, '')\n .replace(/\\b\\d{4,}\\b/g, '')\n .slice(0, 180);\n}\nfunction findErr(ex) {\n const rd = ex.data?.resultData || {};\n if (rd.error) return { node: rd.error.node?.name || rd.error.node || rd.lastNodeExecuted || 'unknown', error: rd.error };\n const runData = rd.runData || {};\n for (const [nodeName, attempts] of Object.entries(runData)) {\n for (const attempt of arr(attempts).slice().reverse()) {\n if (attempt?.error) return { node: nodeName, error: attempt.error };\n }\n }\n return { node: rd.lastNodeExecuted || 'unknown', error: ex.error || {} };\n}\nconst failures = [];\nfor (const item of items) {\n const ex = item.json || {};\n const workflowName = ex.workflowData?.name || ex.workflow?.name || `Workflow ${ex.workflowId || 'unknown'}`;\n if (workflowName === selfName) continue;\n const found = findErr(ex);\n const message = msg(found.error);\n const when = ex.stoppedAt || ex.startedAt || ex.createdAt || new Date(now).toISOString();\n failures.push({\n id: ex.id,\n workflowId: ex.workflowId || ex.workflowData?.id || 'unknown',\n workflowName,\n node: found.node || 'unknown',\n errorType: errType(found.error),\n message,\n signature: sig(message),\n when,\n status: ex.status || 'unknown',\n });\n}\nconst groups = new Map();\nfor (const f of failures) {\n const key = `${f.workflowId}\\u0000${f.node}\\u0000${f.errorType}\\u0000${f.signature}`;\n if (!groups.has(key)) groups.set(key, { workflowName: f.workflowName, workflowId: f.workflowId, node: f.node, errorType: f.errorType, signature: f.signature, count: 0, ids: [], latest: f.when });\n const g = groups.get(key);\n g.count++;\n if (g.ids.length < 8) g.ids.push(f.id);\n if (String(f.when) > String(g.latest)) g.latest = f.when;\n}\nconst sorted = [...groups.values()].sort((a,b) => b.count - a.count || String(b.latest).localeCompare(String(a.latest))).slice(0, 12);\nif (!sorted.length) return [];\nfunction telegramSafe(s) { return String(s || '').replace(/[\\u0000-\\u001f\\u007f]/g, ' ').slice(0, 3500); }\nconst lines = [];\nlines.push(`🚨 n8n Failure Digest: ${failures.length} failed execution(s) in the last ${windowMinutes} min`);\nlines.push('');\nsorted.forEach((g, i) => {\n lines.push(`${i+1}. ${g.workflowName}`);\n lines.push(` Node: ${g.node}`);\n lines.push(` ${g.count}x ${g.errorType}: ${g.signature}`);\n lines.push(` Execs: ${g.ids.join(', ')} | latest ${g.latest}`);\n});\nlines.push('');\nlines.push('Open n8n: http://127.0.0.1:18808');\n// Telegram node defaults to legacy Markdown, so escape characters that\n// commonly occur in workflow/node/error names (notably underscores).\nfunction telegramMarkdownSafe(s) { return String(s).replace(/([_*`\\[])/g, '\\\\$1'); }\nconst text = telegramMarkdownSafe(lines.join('\\n'));\nreturn [{ json: { text, totalFailures: failures.length, groups: sorted, generatedAt: new Date(now).toISOString() } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 80, + 0 + ], + "id": "f6b4eab8-7017-43e6-97c8-dce63873e097", + "name": "Build Digest" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "", + "disable_web_page_preview": true + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 340, + 0 + ], + "id": "cf49d05d-5d81-404b-a751-ce56794985a9", + "name": "Send Telegram Digest", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://discord.com/api/v10/channels/1494453542243532932/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ content: $json.text.substring(0, 2000) }) }}", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + } + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 340, + 200 + ], + "id": "6c3086e4-0869-4003-94c3-66b4975f94e9", + "name": "Send Discord Digest", + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "List Failed Executions", + "type": "main", + "index": 0 + } + ] + ] + }, + "Hourly Schedule": { + "main": [ + [ + { + "node": "List Failed Executions", + "type": "main", + "index": 0 + } + ] + ] + }, + "List Failed Executions": { + "main": [ + [ + { + "node": "Recent Failure IDs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Recent Failure IDs": { + "main": [ + [ + { + "node": "Fetch Failure Details", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Failure Details": { + "main": [ + [ + { + "node": "Build Digest", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Digest": { + "main": [ + [ + { + "node": "Send Telegram Digest", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord Digest", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T00:18:01.158Z", + "id": 1491, + "workflowId": "G9ylNbHbnJ6fWX2C", + "versionId": "2d85e3bf-d8cf-4274-bf61-5377241897da", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/GSmzuA5dgGgyRg5v.json b/swarm-common/n8n-workflows/GSmzuA5dgGgyRg5v.json new file mode 100644 index 0000000..0d141c0 --- /dev/null +++ b/swarm-common/n8n-workflows/GSmzuA5dgGgyRg5v.json @@ -0,0 +1,485 @@ +{ + "updatedAt": "2026-05-14T00:01:22.299Z", + "createdAt": "2026-05-12T17:48:01.214Z", + "id": "GSmzuA5dgGgyRg5v", + "name": "Web-to-Notes Capture (Local LLM + Obsidian)", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "web-to-notes", + "responseMode": "responseNode", + "options": {} + }, + "id": "02979a5e-67e7-43ae-8c9f-4694a5b36e56", + "name": "Webhook - Capture URL", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -900, + 0 + ], + "webhookId": "7958ecbc-c714-41d5-a829-882447ab95f8" + }, + { + "parameters": { + "jsCode": "const body = $json.body ?? $json;\nconst url = String(body.url || body.link || '').trim();\nif (!url || !/^https?:\\/\\//i.test(url)) throw new Error('POST JSON must include url starting with http:// or https://');\nconst title = String(body.title || '').trim();\nconst notes = String(body.notes || body.note || body.comment || '').trim();\nconst tags = Array.isArray(body.tags) ? body.tags : String(body.tags || 'web-capture').split(',').map(s => s.trim()).filter(Boolean);\nreturn [{ json: { url, title, notes, tags, capturedAt: new Date().toISOString() } }];" + }, + "id": "22ba0ac9-af51-4469-a8bd-b3d3c1dd049b", + "name": "Normalize Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -680, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ model: \"gemma-4-26b\", messages: [{ role: \"system\", content: \"You are a concise summarizer. Extract key points, claims, and notable details. Format as clear markdown with a summary section and key points list.\" }, { role: \"user\", content: `Summarize this ${$json.content_type || \"web\"} content titled \"${$json.title || \"untitled\"}\":\\n\\n${($json.text || \"\").slice(0, 8000)}` }], temperature: 0.3, max_tokens: 1600 }) }}", + "options": { + "timeout": 120000 + } + }, + "id": "2ea254be-4a88-426a-97ff-16a80196b462", + "name": "Summarize with llama.cpp", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 0 + ], + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const extracted = $('Extract Content').first().json;\nconst input = $('Normalize Input').first().json;\n\nlet summary = '';\ntry { summary = $json.choices?.[0]?.message?.content || $json.body?.choices?.[0]?.message?.content || ''; } catch (e) {}\n// Dedent summary (LLM sometimes returns indented markdown)\nsummary = summary.split('\\n').map(l => l.replace(/^\\s{4}/, '')).join('\\n').trim();\nif (!summary) summary = 'LLM summary unavailable.\\n\\nContent excerpt:\\n\\n> ' + (extracted.text || '').slice(0, 1200);\n\nconst contentType = extracted.content_type || 'web';\nconst title = extracted.title || input.title || 'Untitled';\nconst sourceUrl = extracted.metadata?.source_url || input.url;\nconst notes = input.notes || '';\nconst tags = input.tags || ['web-capture'];\n\nif (contentType === 'youtube') tags.push('youtube', 'video-transcript');\nelse if (contentType === 'pdf') tags.push('pdf', 'document');\n\nconst meta = extracted.metadata || {};\nlet metaSection = '';\nif (contentType === 'youtube') {\n metaSection = `**Video ID:** ${meta.video_id || 'N/A'} \\n**Transcript Entries:** ${meta.transcript_entries || 0}`;\n} else if (contentType === 'pdf') {\n metaSection = `**Author:** ${meta.author || 'N/A'} \\n**Pages:** ${meta.page_count || 'N/A'}`;\n}\n\nfunction slugify(s) { return String(s || 'untitled').toLowerCase().replace(/https?:\\/\\//,'').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,80) || 'untitled'; }\nfunction yamlSafe(s) { return String(s || '').replace(/'/g, \"''\").replace(/\\n/g, ' '); }\n\nconst date = new Date().toISOString().split('T')[0];\nconst notePath = `Clippings/${date}-${slugify(title)}.md`;\n\nconst frontmatter = [\n '---',\n `title: '${yamlSafe(title)}'`,\n `source_url: ${sourceUrl}`,\n `content_type: ${contentType}`,\n `date: ${date}`,\n `tags: [${tags.map(t => \"'\" + t + \"'\").join(', ')}]`,\n '---',\n].join('\\n');\n\nconst body = [\n frontmatter,\n '',\n `# ${title}`,\n '',\n `> Source: [${title}](${sourceUrl})`,\n ...(metaSection ? ['', metaSection] : []),\n ...(notes ? ['', `## Notes\\n${notes}`] : []),\n '',\n '## Summary',\n '',\n summary,\n '',\n '---',\n `*Captured via Web-to-Notes (${contentType})*`,\n].join('\\n');\n\nreturn [{ json: { notePath, body, title, contentType, sourceUrl } }];\n" + }, + "id": "403dff8b-5789-4018-89ec-69d45569cd25", + "name": "Build Markdown Note", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 220, + 0 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.notePath).replace(/%2F/g, '/')}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "text/markdown" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{$json.body}}", + "options": { + "timeout": 30000 + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" + }, + "id": "1d00b920-985e-415c-b445-4a28674287a0", + "name": "Write Note to Obsidian", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 460, + 0 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{JSON.stringify({ok: true, notePath: $json.notePath, title: $json.title, source: $json.url})}}", + "options": {} + }, + "id": "c3d45b9e-a4d3-43ee-855a-7a76030e8888", + "name": "Respond", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 700, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18812/extract", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ url: $json.url }) }}", + "options": { + "timeout": 120000, + "fullResponse": false + } + }, + "id": "extract-content-v2", + "name": "Extract Content", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -240, + 0 + ] + } + ], + "connections": { + "Webhook - Capture URL": { + "main": [ + [ + { + "node": "Normalize Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Input": { + "main": [ + [ + { + "node": "Extract Content", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Content": { + "main": [ + [ + { + "node": "Summarize with llama.cpp", + "type": "main", + "index": 0 + } + ] + ] + }, + "Summarize with llama.cpp": { + "main": [ + [ + { + "node": "Build Markdown Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Markdown Note": { + "main": [ + [ + { + "node": "Write Note to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + }, + "Write Note to Obsidian": { + "main": [ + [ + { + "node": "Respond", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": null, + "meta": null, + "pinData": null, + "versionId": "f503ca32-52bf-42ef-9dd4-ceecf538ed08", + "activeVersionId": "f503ca32-52bf-42ef-9dd4-ceecf538ed08", + "versionCounter": 30, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-12T17:48:01.217Z", + "createdAt": "2026-05-12T17:48:01.217Z", + "role": "workflow:owner", + "workflowId": "GSmzuA5dgGgyRg5v", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-05-14T00:01:22.300Z", + "createdAt": "2026-05-14T00:01:22.300Z", + "versionId": "f503ca32-52bf-42ef-9dd4-ceecf538ed08", + "workflowId": "GSmzuA5dgGgyRg5v", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "web-to-notes", + "responseMode": "responseNode", + "options": {} + }, + "id": "02979a5e-67e7-43ae-8c9f-4694a5b36e56", + "name": "Webhook - Capture URL", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -900, + 0 + ], + "webhookId": "7958ecbc-c714-41d5-a829-882447ab95f8" + }, + { + "parameters": { + "jsCode": "const body = $json.body ?? $json;\nconst url = String(body.url || body.link || '').trim();\nif (!url || !/^https?:\\/\\//i.test(url)) throw new Error('POST JSON must include url starting with http:// or https://');\nconst title = String(body.title || '').trim();\nconst notes = String(body.notes || body.note || body.comment || '').trim();\nconst tags = Array.isArray(body.tags) ? body.tags : String(body.tags || 'web-capture').split(',').map(s => s.trim()).filter(Boolean);\nreturn [{ json: { url, title, notes, tags, capturedAt: new Date().toISOString() } }];" + }, + "id": "22ba0ac9-af51-4469-a8bd-b3d3c1dd049b", + "name": "Normalize Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -680, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ model: \"gemma-4-26b\", messages: [{ role: \"system\", content: \"You are a concise summarizer. Extract key points, claims, and notable details. Format as clear markdown with a summary section and key points list.\" }, { role: \"user\", content: `Summarize this ${$json.content_type || \"web\"} content titled \"${$json.title || \"untitled\"}\":\\n\\n${($json.text || \"\").slice(0, 8000)}` }], temperature: 0.3, max_tokens: 1600 }) }}", + "options": { + "timeout": 120000 + } + }, + "id": "2ea254be-4a88-426a-97ff-16a80196b462", + "name": "Summarize with llama.cpp", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 0 + ], + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const extracted = $('Extract Content').first().json;\nconst input = $('Normalize Input').first().json;\n\nlet summary = '';\ntry { summary = $json.choices?.[0]?.message?.content || $json.body?.choices?.[0]?.message?.content || ''; } catch (e) {}\n// Dedent summary (LLM sometimes returns indented markdown)\nsummary = summary.split('\\n').map(l => l.replace(/^\\s{4}/, '')).join('\\n').trim();\nif (!summary) summary = 'LLM summary unavailable.\\n\\nContent excerpt:\\n\\n> ' + (extracted.text || '').slice(0, 1200);\n\nconst contentType = extracted.content_type || 'web';\nconst title = extracted.title || input.title || 'Untitled';\nconst sourceUrl = extracted.metadata?.source_url || input.url;\nconst notes = input.notes || '';\nconst tags = input.tags || ['web-capture'];\n\nif (contentType === 'youtube') tags.push('youtube', 'video-transcript');\nelse if (contentType === 'pdf') tags.push('pdf', 'document');\n\nconst meta = extracted.metadata || {};\nlet metaSection = '';\nif (contentType === 'youtube') {\n metaSection = `**Video ID:** ${meta.video_id || 'N/A'} \\n**Transcript Entries:** ${meta.transcript_entries || 0}`;\n} else if (contentType === 'pdf') {\n metaSection = `**Author:** ${meta.author || 'N/A'} \\n**Pages:** ${meta.page_count || 'N/A'}`;\n}\n\nfunction slugify(s) { return String(s || 'untitled').toLowerCase().replace(/https?:\\/\\//,'').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,80) || 'untitled'; }\nfunction yamlSafe(s) { return String(s || '').replace(/'/g, \"''\").replace(/\\n/g, ' '); }\n\nconst date = new Date().toISOString().split('T')[0];\nconst notePath = `Clippings/${date}-${slugify(title)}.md`;\n\nconst frontmatter = [\n '---',\n `title: '${yamlSafe(title)}'`,\n `source_url: ${sourceUrl}`,\n `content_type: ${contentType}`,\n `date: ${date}`,\n `tags: [${tags.map(t => \"'\" + t + \"'\").join(', ')}]`,\n '---',\n].join('\\n');\n\nconst body = [\n frontmatter,\n '',\n `# ${title}`,\n '',\n `> Source: [${title}](${sourceUrl})`,\n ...(metaSection ? ['', metaSection] : []),\n ...(notes ? ['', `## Notes\\n${notes}`] : []),\n '',\n '## Summary',\n '',\n summary,\n '',\n '---',\n `*Captured via Web-to-Notes (${contentType})*`,\n].join('\\n');\n\nreturn [{ json: { notePath, body, title, contentType, sourceUrl } }];\n" + }, + "id": "403dff8b-5789-4018-89ec-69d45569cd25", + "name": "Build Markdown Note", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 220, + 0 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.notePath).replace(/%2F/g, '/')}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "text/markdown" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{$json.body}}", + "options": { + "timeout": 30000 + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" + }, + "id": "1d00b920-985e-415c-b445-4a28674287a0", + "name": "Write Note to Obsidian", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 460, + 0 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{JSON.stringify({ok: true, notePath: $json.notePath, title: $json.title, source: $json.url})}}", + "options": {} + }, + "id": "c3d45b9e-a4d3-43ee-855a-7a76030e8888", + "name": "Respond", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 700, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18812/extract", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ url: $json.url }) }}", + "options": { + "timeout": 120000, + "fullResponse": false + } + }, + "id": "extract-content-v2", + "name": "Extract Content", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -240, + 0 + ] + } + ], + "connections": { + "Webhook - Capture URL": { + "main": [ + [ + { + "node": "Normalize Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Input": { + "main": [ + [ + { + "node": "Extract Content", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Content": { + "main": [ + [ + { + "node": "Summarize with llama.cpp", + "type": "main", + "index": 0 + } + ] + ] + }, + "Summarize with llama.cpp": { + "main": [ + [ + { + "node": "Build Markdown Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Markdown Note": { + "main": [ + [ + { + "node": "Write Note to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + }, + "Write Note to Obsidian": { + "main": [ + [ + { + "node": "Respond", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T00:01:22.328Z", + "id": 1462, + "workflowId": "GSmzuA5dgGgyRg5v", + "versionId": "f503ca32-52bf-42ef-9dd4-ceecf538ed08", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:01:22.316Z", + "id": 1461, + "workflowId": "GSmzuA5dgGgyRg5v", + "versionId": "f503ca32-52bf-42ef-9dd4-ceecf538ed08", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/PlZywwqL8MRNEAN6.json b/swarm-common/n8n-workflows/PlZywwqL8MRNEAN6.json new file mode 100644 index 0000000..b14cdba --- /dev/null +++ b/swarm-common/n8n-workflows/PlZywwqL8MRNEAN6.json @@ -0,0 +1,872 @@ +{ + "updatedAt": "2026-05-14T00:04:59.343Z", + "createdAt": "2026-05-13T21:40:33.847Z", + "id": "PlZywwqL8MRNEAN6", + "name": "Evening Digest", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 21 * * *" + } + ] + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000001", + "name": "Daily 9PM Schedule", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + 0 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "method": "GET", + "url": "http://127.0.0.1:5678/api/v1/executions?status=success&limit=100", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000002", + "name": "n8n Success Executions", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + -200 + ], + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "method": "GET", + "url": "http://127.0.0.1:5678/api/v1/executions?status=error&limit=50", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000003", + "name": "n8n Failed Executions", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + 0 + ], + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "method": "GET", + "url": "http://172.19.0.1:18809/health", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000004", + "name": "Swarm Health", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + 200 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "method": "GET", + "url": "http://172.19.0.1:27123/vault/Notes/", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000005", + "name": "New Obsidian Notes", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + 400 + ], + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// Aggregate all collection results into a structured summary\nconst data = {};\n\n// Process successful executions\ntry {\n const successItems = $input.first()?.json?.data || [];\n const successByWorkflow = {};\n let totalSuccess = 0;\n for (const item of successItems) {\n const wfName = item.workflowData?.name || item.workflowId || 'Unknown';\n successByWorkflow[wfName] = (successByWorkflow[wfName] || 0) + 1;\n totalSuccess++;\n }\n data.successExecutions = { total: totalSuccess, byWorkflow: successByWorkflow };\n} catch(e) {\n data.successExecutions = { total: 0, byWorkflow: {}, error: e.message };\n}\n\n// Process failed executions\ntry {\n // Failed executions come from a separate input\n const failNode = $node['n8n Failed Executions']?.json;\n const failItems = failNode?.data || [];\n const failures = [];\n let totalFail = 0;\n for (const item of failItems) {\n const wfName = item.workflowData?.name || item.workflowId || 'Unknown';\n failures.push({\n workflow: wfName,\n id: item.id,\n stoppedAt: item.stoppedAt\n });\n totalFail++;\n }\n data.failedExecutions = { total: totalFail, failures: failures };\n} catch(e) {\n data.failedExecutions = { total: 0, failures: [], error: e.message };\n}\n\n// Swarm health\ntry {\n data.swarmHealth = $node['Swarm Health']?.json || { status: 'unavailable' };\n} catch(e) {\n data.swarmHealth = { status: 'error', error: e.message };\n}\n\n// New Obsidian notes\ntry {\n const obsResult = $node['New Obsidian Notes']?.json;\n const allFiles = obsResult?.files || [];\n // Filter for today's date in filename\n const today = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'America/Los_Angeles',\n year: 'numeric', month: '2-digit', day: '2-digit'\n }).format(new Date()).replaceAll('/', '-');\n const todayFiles = allFiles.filter(f => {\n const name = typeof f === 'string' ? f : (f.name || f.path || '');\n return name.includes(today);\n });\n data.newNotes = todayFiles.map(f => typeof f === 'string' ? f : (f.name || f.path || JSON.stringify(f)));\n} catch(e) {\n data.newNotes = [];\n data.notesError = e.message;\n}\n\ndata.date = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'America/Los_Angeles',\n year: 'numeric', month: '2-digit', day: '2-digit'\n}).format(new Date()).replaceAll('/', '-');\n\ndata.summary = JSON.stringify(data, null, 2);\n\nreturn [{ json: data }];" + }, + "id": "a1b2c3d4-0001-4000-8000-000000000006", + "name": "Aggregate Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 500, + 100 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ model: 'gemma-4-26B-A4B-it-UD-IQ2_M.gguf', temperature: 0.3, max_tokens: 800, messages: [{ role: 'system', content: 'You are an evening digest assistant. Given data about today\\'s automation runs, failures, new notes, and infrastructure health, produce a concise evening digest under 400 words. Use emojis for section headers. Format for Telegram/Markdown. Sections: 🔧 Executions Summary, ⚠️ Failures, 📝 New Notes, 🏥 Infrastructure Health, 📋 Action Items. Be factual and concise.' }, { role: 'user', content: 'Here is today\\'s data:\\n' + $json.summary }] }) }}", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000007", + "name": "LLM Synthesis", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 740, + 100 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// Extract LLM response text and prepare messages for Telegram/Discord/Obsidian\nlet text = '';\ntry {\n const llmResponse = $input.first()?.json;\n text = llmResponse?.choices?.[0]?.message?.content || '';\n // Strip code fences if present\n text = text.replace(/^```(?:markdown)?\\s*/i, '').replace(/```\\s*$/i, '').trim();\n} catch(e) {\n text = 'Evening digest generation encountered an error.';\n}\n\nif (!text) {\n text = '🌙 Evening Digest\\n\\nNo data collected today. All collection nodes may have failed.';\n}\n\n// Escape special chars for Telegram MarkdownV1\nlet telegramText = text;\n// Replace problematic markdown chars for Telegram\ntelegramText = telegramText.replace(/([_*\\[\\]()~`>#+\\-=|{}.!])/g, (m) => {\n // Keep basic markdown formatting\n if (['*', '_', '`'].includes(m)) return m;\n return '\\\\' + m;\n});\n\nconst today = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'America/Los_Angeles',\n year: 'numeric', month: '2-digit', day: '2-digit'\n}).format(new Date()).replaceAll('/', '-');\n\nreturn [{\n json: {\n text: telegramText,\n discordText: text.substring(0, 2000),\n obsidianContent: `---\\ntitle: Evening Digest\\narea: infrastructure\\ntags: [infrastructure, digest, automation, daily, evening]\\ncreated: ${today}\\nupdated: ${today}\\nstatus: active\\n---\\n\\n# Evening Digest - ${today}\\n\\n${text}\\n`,\n notePath: `Notes/${today} Evening Digest.md`,\n date: today\n }\n}];" + }, + "id": "a1b2c3d4-0001-4000-8000-000000000008", + "name": "Prepare Messages", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 980, + 100 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000009", + "name": "Send Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 1220, + -100 + ], + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://discord.com/api/v10/channels/1494453542243532932/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ content: $json.discordText }) }}", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000010", + "name": "Send Discord", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1220, + 100 + ], + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + }, + { + "parameters": { + "method": "PUT", + "url": "={{ 'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.notePath).replace(/%2F/g, '/') }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.obsidianContent }}", + "options": {} + }, + "id": "a1b2c3d4-0001-4000-8000-000000000011", + "name": "Save to Obsidian", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1220, + 300 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + } + ], + "connections": { + "Daily 9PM Schedule": { + "main": [ + [ + { + "node": "n8n Success Executions", + "type": "main", + "index": 0 + }, + { + "node": "n8n Failed Executions", + "type": "main", + "index": 0 + }, + { + "node": "Swarm Health", + "type": "main", + "index": 0 + }, + { + "node": "New Obsidian Notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "n8n Success Executions": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "n8n Failed Executions": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Swarm Health": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "New Obsidian Notes": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Aggregate Data": { + "main": [ + [ + { + "node": "LLM Synthesis", + "type": "main", + "index": 0 + } + ] + ] + }, + "LLM Synthesis": { + "main": [ + [ + { + "node": "Prepare Messages", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Messages": { + "main": [ + [ + { + "node": "Send Telegram", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord", + "type": "main", + "index": 0 + }, + { + "node": "Save to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timezone": "America/Los_Angeles" + }, + "staticData": { + "node:Daily 9PM Schedule": { + "recurrenceRules": [] + } + }, + "meta": null, + "pinData": null, + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "activeVersionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "versionCounter": 11, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-13T21:40:33.849Z", + "createdAt": "2026-05-13T21:40:33.849Z", + "role": "workflow:owner", + "workflowId": "PlZywwqL8MRNEAN6", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-05-13T21:40:33.854Z", + "createdAt": "2026-05-13T21:40:33.854Z", + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "workflowId": "PlZywwqL8MRNEAN6", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 21 * * *" + } + ] + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000001", + "name": "Daily 9PM Schedule", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + 0 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "method": "GET", + "url": "http://127.0.0.1:5678/api/v1/executions?status=success&limit=100", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000002", + "name": "n8n Success Executions", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + -200 + ], + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "method": "GET", + "url": "http://127.0.0.1:5678/api/v1/executions?status=error&limit=50", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000003", + "name": "n8n Failed Executions", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + 0 + ], + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "method": "GET", + "url": "http://172.19.0.1:18809/health", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000004", + "name": "Swarm Health", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + 200 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "method": "GET", + "url": "http://172.19.0.1:27123/vault/Notes/", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000005", + "name": "New Obsidian Notes", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 240, + 400 + ], + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// Aggregate all collection results into a structured summary\nconst data = {};\n\n// Process successful executions\ntry {\n const successItems = $input.first()?.json?.data || [];\n const successByWorkflow = {};\n let totalSuccess = 0;\n for (const item of successItems) {\n const wfName = item.workflowData?.name || item.workflowId || 'Unknown';\n successByWorkflow[wfName] = (successByWorkflow[wfName] || 0) + 1;\n totalSuccess++;\n }\n data.successExecutions = { total: totalSuccess, byWorkflow: successByWorkflow };\n} catch(e) {\n data.successExecutions = { total: 0, byWorkflow: {}, error: e.message };\n}\n\n// Process failed executions\ntry {\n // Failed executions come from a separate input\n const failNode = $node['n8n Failed Executions']?.json;\n const failItems = failNode?.data || [];\n const failures = [];\n let totalFail = 0;\n for (const item of failItems) {\n const wfName = item.workflowData?.name || item.workflowId || 'Unknown';\n failures.push({\n workflow: wfName,\n id: item.id,\n stoppedAt: item.stoppedAt\n });\n totalFail++;\n }\n data.failedExecutions = { total: totalFail, failures: failures };\n} catch(e) {\n data.failedExecutions = { total: 0, failures: [], error: e.message };\n}\n\n// Swarm health\ntry {\n data.swarmHealth = $node['Swarm Health']?.json || { status: 'unavailable' };\n} catch(e) {\n data.swarmHealth = { status: 'error', error: e.message };\n}\n\n// New Obsidian notes\ntry {\n const obsResult = $node['New Obsidian Notes']?.json;\n const allFiles = obsResult?.files || [];\n // Filter for today's date in filename\n const today = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'America/Los_Angeles',\n year: 'numeric', month: '2-digit', day: '2-digit'\n }).format(new Date()).replaceAll('/', '-');\n const todayFiles = allFiles.filter(f => {\n const name = typeof f === 'string' ? f : (f.name || f.path || '');\n return name.includes(today);\n });\n data.newNotes = todayFiles.map(f => typeof f === 'string' ? f : (f.name || f.path || JSON.stringify(f)));\n} catch(e) {\n data.newNotes = [];\n data.notesError = e.message;\n}\n\ndata.date = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'America/Los_Angeles',\n year: 'numeric', month: '2-digit', day: '2-digit'\n}).format(new Date()).replaceAll('/', '-');\n\ndata.summary = JSON.stringify(data, null, 2);\n\nreturn [{ json: data }];" + }, + "id": "a1b2c3d4-0001-4000-8000-000000000006", + "name": "Aggregate Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 500, + 100 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ model: 'gemma-4-26B-A4B-it-UD-IQ2_M.gguf', temperature: 0.3, max_tokens: 800, messages: [{ role: 'system', content: 'You are an evening digest assistant. Given data about today\\'s automation runs, failures, new notes, and infrastructure health, produce a concise evening digest under 400 words. Use emojis for section headers. Format for Telegram/Markdown. Sections: 🔧 Executions Summary, ⚠️ Failures, 📝 New Notes, 🏥 Infrastructure Health, 📋 Action Items. Be factual and concise.' }, { role: 'user', content: 'Here is today\\'s data:\\n' + $json.summary }] }) }}", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000007", + "name": "LLM Synthesis", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 740, + 100 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// Extract LLM response text and prepare messages for Telegram/Discord/Obsidian\nlet text = '';\ntry {\n const llmResponse = $input.first()?.json;\n text = llmResponse?.choices?.[0]?.message?.content || '';\n // Strip code fences if present\n text = text.replace(/^```(?:markdown)?\\s*/i, '').replace(/```\\s*$/i, '').trim();\n} catch(e) {\n text = 'Evening digest generation encountered an error.';\n}\n\nif (!text) {\n text = '🌙 Evening Digest\\n\\nNo data collected today. All collection nodes may have failed.';\n}\n\n// Escape special chars for Telegram MarkdownV1\nlet telegramText = text;\n// Replace problematic markdown chars for Telegram\ntelegramText = telegramText.replace(/([_*\\[\\]()~`>#+\\-=|{}.!])/g, (m) => {\n // Keep basic markdown formatting\n if (['*', '_', '`'].includes(m)) return m;\n return '\\\\' + m;\n});\n\nconst today = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'America/Los_Angeles',\n year: 'numeric', month: '2-digit', day: '2-digit'\n}).format(new Date()).replaceAll('/', '-');\n\nreturn [{\n json: {\n text: telegramText,\n discordText: text.substring(0, 2000),\n obsidianContent: `---\\ntitle: Evening Digest\\narea: infrastructure\\ntags: [infrastructure, digest, automation, daily, evening]\\ncreated: ${today}\\nupdated: ${today}\\nstatus: active\\n---\\n\\n# Evening Digest - ${today}\\n\\n${text}\\n`,\n notePath: `Notes/${today} Evening Digest.md`,\n date: today\n }\n}];" + }, + "id": "a1b2c3d4-0001-4000-8000-000000000008", + "name": "Prepare Messages", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 980, + 100 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000009", + "name": "Send Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 1220, + -100 + ], + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://discord.com/api/v10/channels/1494453542243532932/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ content: $json.discordText }) }}", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + } + } + }, + "id": "a1b2c3d4-0001-4000-8000-000000000010", + "name": "Send Discord", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1220, + 100 + ], + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + }, + { + "parameters": { + "method": "PUT", + "url": "={{ 'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.notePath).replace(/%2F/g, '/') }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.obsidianContent }}", + "options": {} + }, + "id": "a1b2c3d4-0001-4000-8000-000000000011", + "name": "Save to Obsidian", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1220, + 300 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + } + ], + "connections": { + "Daily 9PM Schedule": { + "main": [ + [ + { + "node": "n8n Success Executions", + "type": "main", + "index": 0 + }, + { + "node": "n8n Failed Executions", + "type": "main", + "index": 0 + }, + { + "node": "Swarm Health", + "type": "main", + "index": 0 + }, + { + "node": "New Obsidian Notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "n8n Success Executions": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "n8n Failed Executions": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Swarm Health": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "New Obsidian Notes": { + "main": [ + [ + { + "node": "Aggregate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Aggregate Data": { + "main": [ + [ + { + "node": "LLM Synthesis", + "type": "main", + "index": 0 + } + ] + ] + }, + "LLM Synthesis": { + "main": [ + [ + { + "node": "Prepare Messages", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Messages": { + "main": [ + [ + { + "node": "Send Telegram", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord", + "type": "main", + "index": 0 + }, + { + "node": "Save to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-13T21:40:40.515Z", + "id": 1432, + "workflowId": "PlZywwqL8MRNEAN6", + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:04:59.370Z", + "id": 1483, + "workflowId": "PlZywwqL8MRNEAN6", + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:04:59.415Z", + "id": 1485, + "workflowId": "PlZywwqL8MRNEAN6", + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:04:59.362Z", + "id": 1482, + "workflowId": "PlZywwqL8MRNEAN6", + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:04:59.388Z", + "id": 1484, + "workflowId": "PlZywwqL8MRNEAN6", + "versionId": "afb71f4d-6ac3-434d-b659-de003d47c339", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/QRCCdHNXZUHc2Oz4.json b/swarm-common/n8n-workflows/QRCCdHNXZUHc2Oz4.json new file mode 100644 index 0000000..e42da03 --- /dev/null +++ b/swarm-common/n8n-workflows/QRCCdHNXZUHc2Oz4.json @@ -0,0 +1,362 @@ +{ + "updatedAt": "2026-05-14T00:01:24.692Z", + "createdAt": "2026-03-18T20:17:45.262Z", + "id": "QRCCdHNXZUHc2Oz4", + "name": "Calendar to Obsidian Notes", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 6 + } + ] + } + }, + "id": "schedule-trigger", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 240, + 304 + ] + }, + { + "parameters": { + "operation": "getAll", + "calendar": { + "__rl": true, + "value": "william.valentin.info@gmail.com", + "mode": "list", + "cachedResultName": "Perso" + }, + "limit": 20, + "options": { + "timeMin": "={{ new Date().toISOString() }}", + "timeMax": "={{ new Date(Date.now() + 7*24*60*60*1000).toISOString() }}", + "singleEvents": true, + "orderBy": "startTime" + } + }, + "id": "get-events", + "name": "Get Upcoming Events", + "type": "n8n-nodes-base.googleCalendar", + "typeVersion": 1, + "position": [ + 464, + 304 + ], + "credentials": { + "googleCalendarOAuth2Api": { + "id": "458fY4bs1z49OTeZ", + "name": "Google Calendar account" + } + }, + "continueOnFail": true, + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "const event = $input.item.json || {};\nconst now = new Date();\nconst today = now.toISOString().slice(0, 10);\nconst hasUsableEvent = event.start && (event.summary || event.id || event.htmlLink);\nif (event.error || event.message || !hasUsableEvent) {\n const detail = String(event.error?.message || event.message || event.error || 'Google Calendar returned no usable event; credentials may need reauthorization.').replace(/`/g, \"'\").slice(0, 1000);\n const content = `---\ntitle: \"Google Calendar sync needs attention\"\narea: notes\ntags: [calendar, automation, degraded]\ncreated: ${today}\nupdated: ${today}\nstatus: needs-reauth\n---\n\n# Google Calendar sync needs attention\n\nThe n8n Calendar to Obsidian workflow could not read Google Calendar events.\n\nLikely cause: expired Google OAuth credentials in n8n.\n\nAction: reauthorize the Google Calendar credential used by workflow QRCCdHNXZUHc2Oz4, then run the workflow manually.\n\nLast observed detail:\n\n> ${detail}\n`;\n return [{ json: { path: `Notes/Calendar Sync Status/${today} Google Calendar Needs Reauth.md`, content, title: 'Google Calendar sync needs attention', date: today, degraded: true } }];\n}\nconst event = $input.item.json;\nconst startRaw = event.start?.dateTime || event.start?.date || \"\";\nconst date = startRaw.split(\"T\")[0];\nconst title = (event.summary || \"Untitled Event\").replace(/[\\/\\\\?%*:|\"<>]/g, \"-\").substring(0, 80);\nconst location = event.location || \"\";\nconst description = event.description || \"\";\nconst attendees = (event.attendees || []).map(a => a.email).join(\", \");\nconst endRaw = event.end?.dateTime || event.end?.date || \"\";\nconst startTime = startRaw.includes(\"T\") ? startRaw.split(\"T\")[1].substring(0,5) : \"All day\";\nconst endTime = endRaw.includes(\"T\") ? endRaw.split(\"T\")[1].substring(0,5) : \"\";\nconst timeStr = endTime ? `${startTime} – ${endTime}` : startTime;\nconst frontmatter = `---\\ntitle: \"${title}\"\\narea: notes\\ntags: [calendar, event]\\ncreated: ${date}\\nupdated: ${date}\\nstatus: active\\nevent_date: ${date}\\nevent_time: \"${timeStr}\"\\n---`;\nconst content = `${frontmatter}\\n\\n# ${title}\\n\\n**Date:** ${date}\\n**Time:** ${timeStr}\\n${location ? `**Location:** ${location}\\n` : \"\"}${attendees ? `**Attendees:** ${attendees}\\n` : \"\"}\\n## Description\\n\\n${description || \"_No description_\"}\\n\\n## Notes\\n\\n_Add notes here_\\n`;\nreturn [{ json: { path: `Notes/${date} ${title}.md`, content, title, date, timeStr } }];" + }, + "id": "format-note", + "name": "Format Event Note", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 688, + 304 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "=http://192.168.153.130:27123/vault/{{ encodeURIComponent($json.path).replace(/%2F/g, \"/\") }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.content }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "id": "write-to-vault", + "name": "Write to Vault", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 912, + 304 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Get Upcoming Events", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Upcoming Events": { + "main": [ + [ + { + "node": "Format Event Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Event Note": { + "main": [ + [ + { + "node": "Write to Vault", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": { + "node:Schedule Trigger": { + "recurrenceRules": [ + 6 + ] + } + }, + "meta": null, + "pinData": {}, + "versionId": "40b22838-7ce4-4632-b186-b78ccda438c4", + "activeVersionId": "40b22838-7ce4-4632-b186-b78ccda438c4", + "versionCounter": 1636, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-03-18T20:17:45.264Z", + "createdAt": "2026-03-18T20:17:45.264Z", + "role": "workflow:owner", + "workflowId": "QRCCdHNXZUHc2Oz4", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [ + { + "updatedAt": "2026-03-19T04:40:29.915Z", + "createdAt": "2026-03-19T04:40:29.915Z", + "id": "GLr9Awuvw8uO7ZRP", + "name": "calendar" + }, + { + "updatedAt": "2026-03-19T04:40:29.892Z", + "createdAt": "2026-03-19T04:40:29.892Z", + "id": "VfqIkUpiu2YMBSHw", + "name": "obsidian-sync" + } + ], + "activeVersion": { + "updatedAt": "2026-05-14T00:01:24.693Z", + "createdAt": "2026-05-14T00:01:24.693Z", + "versionId": "40b22838-7ce4-4632-b186-b78ccda438c4", + "workflowId": "QRCCdHNXZUHc2Oz4", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 6 + } + ] + } + }, + "id": "schedule-trigger", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 240, + 304 + ] + }, + { + "parameters": { + "operation": "getAll", + "calendar": { + "__rl": true, + "value": "william.valentin.info@gmail.com", + "mode": "list", + "cachedResultName": "Perso" + }, + "limit": 20, + "options": { + "timeMin": "={{ new Date().toISOString() }}", + "timeMax": "={{ new Date(Date.now() + 7*24*60*60*1000).toISOString() }}", + "singleEvents": true, + "orderBy": "startTime" + } + }, + "id": "get-events", + "name": "Get Upcoming Events", + "type": "n8n-nodes-base.googleCalendar", + "typeVersion": 1, + "position": [ + 464, + 304 + ], + "credentials": { + "googleCalendarOAuth2Api": { + "id": "458fY4bs1z49OTeZ", + "name": "Google Calendar account" + } + }, + "continueOnFail": true, + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "const event = $input.item.json || {};\nconst now = new Date();\nconst today = now.toISOString().slice(0, 10);\nconst hasUsableEvent = event.start && (event.summary || event.id || event.htmlLink);\nif (event.error || event.message || !hasUsableEvent) {\n const detail = String(event.error?.message || event.message || event.error || 'Google Calendar returned no usable event; credentials may need reauthorization.').replace(/`/g, \"'\").slice(0, 1000);\n const content = `---\ntitle: \"Google Calendar sync needs attention\"\narea: notes\ntags: [calendar, automation, degraded]\ncreated: ${today}\nupdated: ${today}\nstatus: needs-reauth\n---\n\n# Google Calendar sync needs attention\n\nThe n8n Calendar to Obsidian workflow could not read Google Calendar events.\n\nLikely cause: expired Google OAuth credentials in n8n.\n\nAction: reauthorize the Google Calendar credential used by workflow QRCCdHNXZUHc2Oz4, then run the workflow manually.\n\nLast observed detail:\n\n> ${detail}\n`;\n return [{ json: { path: `Notes/Calendar Sync Status/${today} Google Calendar Needs Reauth.md`, content, title: 'Google Calendar sync needs attention', date: today, degraded: true } }];\n}\nconst event = $input.item.json;\nconst startRaw = event.start?.dateTime || event.start?.date || \"\";\nconst date = startRaw.split(\"T\")[0];\nconst title = (event.summary || \"Untitled Event\").replace(/[\\/\\\\?%*:|\"<>]/g, \"-\").substring(0, 80);\nconst location = event.location || \"\";\nconst description = event.description || \"\";\nconst attendees = (event.attendees || []).map(a => a.email).join(\", \");\nconst endRaw = event.end?.dateTime || event.end?.date || \"\";\nconst startTime = startRaw.includes(\"T\") ? startRaw.split(\"T\")[1].substring(0,5) : \"All day\";\nconst endTime = endRaw.includes(\"T\") ? endRaw.split(\"T\")[1].substring(0,5) : \"\";\nconst timeStr = endTime ? `${startTime} – ${endTime}` : startTime;\nconst frontmatter = `---\\ntitle: \"${title}\"\\narea: notes\\ntags: [calendar, event]\\ncreated: ${date}\\nupdated: ${date}\\nstatus: active\\nevent_date: ${date}\\nevent_time: \"${timeStr}\"\\n---`;\nconst content = `${frontmatter}\\n\\n# ${title}\\n\\n**Date:** ${date}\\n**Time:** ${timeStr}\\n${location ? `**Location:** ${location}\\n` : \"\"}${attendees ? `**Attendees:** ${attendees}\\n` : \"\"}\\n## Description\\n\\n${description || \"_No description_\"}\\n\\n## Notes\\n\\n_Add notes here_\\n`;\nreturn [{ json: { path: `Notes/${date} ${title}.md`, content, title, date, timeStr } }];" + }, + "id": "format-note", + "name": "Format Event Note", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 688, + 304 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "=http://192.168.153.130:27123/vault/{{ encodeURIComponent($json.path).replace(/%2F/g, \"/\") }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.content }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "id": "write-to-vault", + "name": "Write to Vault", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 912, + 304 + ], + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + } + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Get Upcoming Events", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Upcoming Events": { + "main": [ + [ + { + "node": "Format Event Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Event Note": { + "main": [ + [ + { + "node": "Write to Vault", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T00:01:24.723Z", + "id": 1466, + "workflowId": "QRCCdHNXZUHc2Oz4", + "versionId": "40b22838-7ce4-4632-b186-b78ccda438c4", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:01:24.711Z", + "id": 1465, + "workflowId": "QRCCdHNXZUHc2Oz4", + "versionId": "40b22838-7ce4-4632-b186-b78ccda438c4", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/agentmon-health-watchdog.json b/swarm-common/n8n-workflows/agentmon-health-watchdog.json new file mode 100644 index 0000000..43be600 --- /dev/null +++ b/swarm-common/n8n-workflows/agentmon-health-watchdog.json @@ -0,0 +1,147 @@ +{ + "name": "Agentmon Health Watchdog", + "active": false, + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -760, + -40 + ], + "id": "dd86a324-8041-4000-92d7-7bcdfa4dfdcb", + "name": "Manual Trigger" + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 5 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + -760, + 160 + ], + "id": "1b25c434-e019-4395-887b-8452f136f543", + "name": "Every 5 Minutes" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst CONFIG = {\n baseUrl: 'http://172.19.0.1:8081',\n hostBaseUrl: 'http://172.19.0.1',\n staleAfterMs: 3 * 60 * 1000,\n failureThreshold: 2,\n reminderEveryFailedRuns: 6,\n requiredServices: [\n 'agentmon-ingest',\n 'agentmon-query',\n 'agentmon-ui',\n 'agentmon-processor',\n 'agentmon-swarm-monitor',\n 'agentmon-db',\n 'agentmon-nats',\n ],\n};\n\nconst httpRequest = this.helpers.httpRequest.bind(this.helpers);\n\nasync function requestJson(url, timeout = 10000) {\n const response = await httpRequest({\n method: 'GET',\n url,\n timeout,\n json: true,\n simple: false,\n resolveWithFullResponse: true,\n returnFullResponse: true,\n ignoreHttpStatusErrors: true,\n });\n const status = response.statusCode || response.status;\n if (status < 200 || status >= 300) {\n throw new Error(`${url} returned HTTP ${status}`);\n }\n return response.body;\n}\n\nasync function requestText(url, timeout = 5000) {\n const response = await httpRequest({\n method: 'GET',\n url,\n timeout,\n json: false,\n simple: false,\n resolveWithFullResponse: true,\n returnFullResponse: true,\n ignoreHttpStatusErrors: true,\n });\n const status = response.statusCode || response.status;\n if (status < 200 || status >= 300) {\n throw new Error(`${url} returned HTTP ${status}`);\n }\n return typeof response.body === 'string' ? response.body : JSON.stringify(response.body);\n}\n\nfunction serviceSummary(svc) {\n if (!svc) return 'missing';\n const bits = [`status=${svc.status || 'unknown'}`, `state=${svc.container_state || 'unknown'}`, `health=${svc.health_state || 'unknown'}`];\n if (svc.http_status !== undefined) bits.push(`http=${svc.http_status}`);\n if (svc.uptime_sec !== undefined) bits.push(`uptime=${svc.uptime_sec}s`);\n return bits.join(' ');\n}\n\nfunction normalizeIssues(issues) {\n const out = [];\n if (!issues || typeof issues !== 'object') return out;\n for (const [key, value] of Object.entries(issues)) {\n if (Array.isArray(value) && value.length) out.push(`${key}: ${value.join(', ')}`);\n else if (value && typeof value === 'object' && Object.keys(value).length) out.push(`${key}: ${JSON.stringify(value)}`);\n else if (value && !Array.isArray(value)) out.push(`${key}: ${String(value)}`);\n }\n return out;\n}\n\nconst now = new Date();\nconst nowIso = now.toISOString();\nconst problems = [];\nconst details = [];\nlet snapshotEvent = null;\nlet services = [];\nlet stats = null;\n\ntry {\n await requestText(`${CONFIG.hostBaseUrl}:8080/healthz`, 5000);\n} catch (error) {\n problems.push(`agentmon-ingest /healthz failed: ${error.message}`);\n}\ntry {\n await requestText(`${CONFIG.hostBaseUrl}:8081/healthz`, 5000);\n} catch (error) {\n problems.push(`agentmon-query /healthz failed: ${error.message}`);\n}\ntry {\n await requestText(`${CONFIG.hostBaseUrl}:8082/healthz`, 5000);\n} catch (error) {\n problems.push(`agentmon-ui /healthz failed: ${error.message}`);\n}\n\ntry {\n const eventsResponse = await requestJson(`${CONFIG.baseUrl}/v1/events?event_type=swarm.snapshot&limit=1`, 10000);\n snapshotEvent = (eventsResponse.events || [])[0];\n if (!snapshotEvent) {\n problems.push('no swarm.snapshot events returned');\n } else {\n const tsRaw = snapshotEvent.ts || snapshotEvent.payload?.event?.ts;\n const ts = new Date(tsRaw);\n const ageMs = now.getTime() - ts.getTime();\n if (!Number.isFinite(ageMs)) {\n problems.push(`latest swarm.snapshot has invalid timestamp: ${tsRaw}`);\n } else if (ageMs > CONFIG.staleAfterMs) {\n problems.push(`latest swarm.snapshot stale: age=${Math.round(ageMs / 1000)}s ts=${tsRaw}`);\n }\n\n const payload = snapshotEvent.payload?.payload || snapshotEvent.payload || {};\n services = payload.services || [];\n const byName = Object.fromEntries(services.map((svc) => [svc.name, svc]));\n for (const issue of normalizeIssues(payload.issues)) {\n problems.push(`swarm issue ${issue}`);\n }\n for (const name of CONFIG.requiredServices) {\n const svc = byName[name];\n if (!svc) {\n problems.push(`required service missing: ${name}`);\n } else if (svc.status !== 'healthy' || svc.container_state !== 'running') {\n problems.push(`required service unhealthy: ${name} (${serviceSummary(svc)})`);\n }\n }\n const unhealthy = services.filter((svc) => svc.status && svc.status !== 'healthy');\n for (const svc of unhealthy.slice(0, 20)) {\n details.push(`${svc.name}: ${serviceSummary(svc)}`);\n }\n }\n} catch (error) {\n problems.push(`swarm.snapshot query failed: ${error.message}`);\n}\n\ntry {\n stats = await requestJson(`${CONFIG.baseUrl}/v1/stats/summary`, 5000);\n} catch (error) {\n problems.push(`stats summary query failed: ${error.message}`);\n}\n\nstaticData.agentmon = staticData.agentmon || { failedRuns: 0, alerted: false };\nconst prev = staticData.agentmon;\nconst healthy = problems.length === 0;\nconst result = {\n checkedAt: nowIso,\n healthy,\n problems,\n details,\n snapshotTs: snapshotEvent?.ts || snapshotEvent?.payload?.event?.ts || null,\n serviceCount: services.length,\n stats,\n};\n\nif (healthy) {\n if (prev.alerted) {\n staticData.agentmon = { failedRuns: 0, alerted: false, lastOk: nowIso };\n return [{ json: { ...result, text: `\u2705 Agentmon Health Watchdog recovered\\n- snapshot=${result.snapshotTs}\\n- services=${result.serviceCount}\\n- checked=${nowIso}` } }];\n }\n staticData.agentmon = { failedRuns: 0, alerted: false, lastOk: nowIso };\n return [];\n}\n\nconst failedRuns = (prev.failedRuns || 0) + 1;\nconst shouldAlert = failedRuns >= CONFIG.failureThreshold && (!prev.alerted || (CONFIG.reminderEveryFailedRuns > 0 && failedRuns % CONFIG.reminderEveryFailedRuns === 0));\nstaticData.agentmon = { failedRuns, alerted: prev.alerted || shouldAlert, lastFailure: nowIso, lastProblems: problems };\n\nif (!shouldAlert) return [];\nconst lines = ['\ud83d\udea8 Agentmon Health Watchdog', `failedChecks=${failedRuns}`, `checked=${nowIso}`];\nfor (const p of problems.slice(0, 12)) lines.push(`- ${p}`);\nif (details.length) {\n lines.push('details:');\n for (const d of details.slice(0, 12)) lines.push(`- ${d}`);\n}\nlines.push('suggested: check `docker logs agentmon-query --tail 100`, `docker logs agentmon-swarm-monitor --tail 100`, and agentmon query `/v1/events?event_type=swarm.snapshot&limit=1`.');\nreturn [{ json: { ...result, failedRuns, text: lines.join('\\n') } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -500, + 60 + ], + "id": "201ffa92-12f9-4b7f-9a0e-7e4df4fbdbe0", + "name": "Check Agentmon Snapshot" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{$json.text}}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + -220, + -40 + ], + "id": "1e160d4e-7614-4479-b470-a3048e08124c", + "name": "Send Telegram Alert", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "method": "POST", + "url": "https://discord.com/api/v10/channels/425781661268049931/messages", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { content: $json.text } }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -220, + 140 + ], + "id": "cf94f111-7824-48a8-8c00-e06cc36cd01e", + "name": "Send Discord Alert", + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Check Agentmon Snapshot", + "type": "main", + "index": 0 + } + ] + ] + }, + "Every 5 Minutes": { + "main": [ + [ + { + "node": "Check Agentmon Snapshot", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Agentmon Snapshot": { + "main": [ + [ + { + "node": "Send Telegram Alert", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord Alert", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "pinData": {}, + "tags": [], + "id": "AgentmonHealthWatchdog" +} \ No newline at end of file diff --git a/swarm-common/n8n-workflows/morning-brief.json b/swarm-common/n8n-workflows/morning-brief.json new file mode 100644 index 0000000..36fe6ae --- /dev/null +++ b/swarm-common/n8n-workflows/morning-brief.json @@ -0,0 +1,453 @@ +[ + { + "updatedAt": "2026-05-20T16:30:18.000Z", + "createdAt": "2026-05-13T21:41:17.798Z", + "id": "g3IdGZCK1EtTsv9T", + "name": "Morning Brief", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "30 6 * * *" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + 0, + 0 + ], + "id": "16110cb5-e50a-4d99-a613-448057221422", + "name": "Daily 06:30 PT" + }, + { + "parameters": { + "method": "GET", + "url": "http://wttr.in/Seattle?format=j1", + "options": { + "timeout": 10000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 300, + -400 + ], + "id": "a119dfe9-46db-43ca-98b2-f0690bc0f6f5", + "name": "Weather", + "continueOnFail": true + }, + { + "parameters": { + "method": "GET", + "url": "http://172.19.0.1:18809/health", + "options": { + "timeout": 10000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 300, + -250 + ], + "id": "05f60eba-ab11-4fe0-b761-d1ca9ae557d4", + "name": "Swarm Health", + "continueOnFail": true + }, + { + "parameters": { + "method": "GET", + "url": "http://127.0.0.1:5678/healthz", + "options": { + "timeout": 10000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 300, + -100 + ], + "id": "4b5c3f4c-7f11-4e0c-9c56-3b8596a1d25d", + "name": "n8n Health", + "continueOnFail": true + }, + { + "parameters": { + "method": "GET", + "url": "http://172.19.0.1:18804/health/liveliness", + "options": { + "timeout": 10000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 300, + 50 + ], + "id": "a8e4e45c-60a1-4f90-8ecc-49782d7be900", + "name": "LiteLLM Health", + "continueOnFail": true + }, + { + "parameters": { + "method": "GET", + "url": "http://127.0.0.1:5678/api/v1/executions", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "workflowId", + "value": "9sFwRyUDz51csAp7" + }, + { + "name": "limit", + "value": "5" + }, + { + "name": "status", + "value": "success" + } + ] + }, + "options": { + "timeout": 15000 + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 300, + 200 + ], + "id": "c688abdf-9b63-43b4-81da-7c81388b73f8", + "name": "Email Highlights", + "continueOnFail": true, + "credentials": { + "httpHeaderAuth": { + "id": "UPAHgUJVRqZQceL4", + "name": "n8n Public API (Failure Digest)" + } + } + }, + { + "parameters": { + "method": "GET", + "url": "=https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin={{ $now.format('yyyy-MM-dd') }}T00:00:00-07:00&timeMax={{ $now.plus({days:1}).format('yyyy-MM-dd') }}T23:59:59-07:00&singleEvents=true&orderBy=startTime", + "authentication": "oAuth2", + "options": { + "timeout": 10000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 300, + 350 + ], + "id": "d3c5a4ce-9f81-4da8-8dc8-7256bd96285b", + "name": "Calendar", + "credentials": { + "oAuth2Api": { + "id": "458fY4bs1z49OTeZ", + "name": "Google OAuth" + } + }, + "continueOnFail": true + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nfunction getSafe(nodeName) {\n try {\n const items = $(nodeName).all();\n if (items && items.length > 0 && items[0].json) {\n return items[0].json;\n }\n } catch (e) {}\n return { error: 'Node failed or returned no data' };\n}\n\nfunction parseMaybeJson(value) {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch (e) {\n return { error: 'Weather JSON parse failed', raw: value.slice(0, 200) };\n }\n}\n\nconst weather = parseMaybeJson(getSafe('Weather'));\nconst swarmHealth = getSafe('Swarm Health');\nconst n8nHealth = getSafe('n8n Health');\nconst litellmHealth = getSafe('LiteLLM Health');\nconst emailData = getSafe('Email Highlights');\nconst calendar = getSafe('Calendar');\n\n// Extract weather summary\nlet weatherSummary = {};\nif (weather.current_condition && weather.current_condition[0]) {\n const c = weather.current_condition[0];\n weatherSummary = {\n temp_F: c.FeelsLikeF || c.temp_F,\n description: c.weatherDesc ? c.weatherDesc[0].value : 'unknown',\n humidity: c.humidity,\n wind_mph: c.windspeedMiles\n };\n} else {\n weatherSummary = { error: weather.error || 'Weather data unavailable' };\n}\n\n// Count healthy/unhealthy containers\nlet infraSummary = { healthy: 0, unhealthy: 0, details: [] };\nif (Array.isArray(swarmHealth)) {\n for (const c of swarmHealth) {\n if (c.health === 'healthy' || c.status === 'running') {\n infraSummary.healthy++;\n } else {\n infraSummary.unhealthy++;\n }\n infraSummary.details.push({ name: c.name || c.Names, status: c.status, health: c.health });\n }\n} else if (swarmHealth.containers && Array.isArray(swarmHealth.containers)) {\n for (const c of swarmHealth.containers) {\n if (c.health === 'healthy' || c.status === 'running') {\n infraSummary.healthy++;\n } else {\n infraSummary.unhealthy++;\n }\n infraSummary.details.push({ name: c.name, status: c.status, health: c.health });\n }\n} else if (swarmHealth.error) {\n infraSummary = { error: 'Swarm health endpoint unavailable' };\n}\n\nconst n8nOk = (n8nHealth && !n8nHealth.error);\nconst litellmOk = (litellmHealth && !litellmHealth.error);\n\n// Extract email info from execution data\nlet emailHighlights = [];\nif (emailData && emailData.data && Array.isArray(emailData.data)) {\n for (const exec of emailData.data.slice(0, 5)) {\n emailHighlights.push({\n id: exec.id,\n finished: exec.stoppedAt || 'unknown'\n });\n }\n}\n\n// Calendar events\nlet calendarEvents = [];\nif (calendar && calendar.items && Array.isArray(calendar.items)) {\n for (const ev of calendar.items.slice(0, 10)) {\n calendarEvents.push({\n summary: ev.summary || '(no title)',\n start: (ev.start && (ev.start.dateTime || ev.start.date)) || 'unknown',\n end: (ev.end && (ev.end.dateTime || ev.end.date)) || 'unknown'\n });\n }\n}\n\nconst dataForLLM = {\n date: new Date().toISOString().split('T')[0],\n weather: weatherSummary,\n infrastructure: {\n swarm: infraSummary,\n n8n: n8nOk ? 'healthy' : 'unhealthy',\n litellm: litellmOk ? 'healthy' : 'unhealthy'\n },\n email: emailHighlights.length > 0 ? emailHighlights : [{ info: 'No recent email triage data' }],\n calendar: calendarEvents.length > 0 ? calendarEvents : [{ info: 'Calendar unavailable or no events today' }]\n};\n\nreturn [{ json: { dataJson: JSON.stringify(dataForLLM, null, 2) } }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 650, + 0 + ], + "id": "1d2b39db-3649-4316-8ce9-b5c83c981017", + "name": "Merge Data" + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18806/v1/chat/completions", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "= {\"model\":\"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\",\"messages\":[{\"role\":\"system\",\"content\":\"You are Will's personal morning brief formatter.\\n\\nReturn ONLY the final Telegram-ready brief. Do not include reasoning, drafts, constraint checks, self-corrections, notes, analysis, or labels like \\\"Details\\\", \\\"Drafting\\\", \\\"Final Polish\\\", or \\\"Self-Correction\\\".\\n\\nUse the same readable style as the old Zap brief: plain Telegram text with lightweight Markdown, not HTML.\\n- Use emojis in section headings.\\n- Use **Heading** for bold headings if useful.\\n- Never output HTML/XML tags: no , , , ,
, or similar.\\n- Use bullet lines starting with \\\"• \\\"; use numbered lines only for Action Items.\\n- Keep it scannable, concise, and under 250 words.\\n- Required sections in order:\\n ☀️ Morning Brief: Month D, YYYY\\n 🌥️ Weather\\n ⚙️ Infrastructure Status\\n 🛠️ Action Item (only if something needs attention)\\n 📧 Email Summary\\n 📅 Calendar\\n- If data is missing, say so in one sentence and move on.\\n- For infrastructure, if any service is unhealthy, call it out clearly and make it the action item.\\n\"},{\"role\":\"user\",\"content\":{{ JSON.stringify(\"Here is today's raw data. Produce only the final brief, not your analysis.\\n\" + $json.dataJson) }}}],\"temperature\":0.1,\"max_tokens\":500}", + "options": { + "timeout": 60000 + }, + "contentType": "json" + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 950, + 0 + ], + "id": "f2eb23d3-bf07-46d8-8556-2ba6a0185f5a", + "name": "Synthesize with LLM", + "continueOnFail": false + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const response = $input.first().json;\nlet brief = '';\n\nif (response.choices && response.choices[0] && response.choices[0].message) {\n brief = response.choices[0].message.content || '';\n} else if (typeof response === 'string') {\n brief = response;\n} else {\n brief = 'Morning brief synthesis failed.';\n}\n\nbrief = String(brief);\n\n// Remove hidden reasoning/code blocks and formatting that direct delivery shows literally.\nbrief = brief.replace(new RegExp('[\\\\s\\\\S]*?<\\\\/think>', 'gi'), '');\nbrief = brief.replace(new RegExp('```[\\\\s\\\\S]*?```', 'g'), '');\nbrief = brief.replace(new RegExp('<\\\\/?(?:b|strong|code|i|em)>', 'gi'), '');\nbrief = brief.replace(new RegExp('<[^>]+>', 'g'), '');\nbrief = brief.replace(/[\\*`_~]/g, '');\n\n// If the model leaked drafting/meta sections, keep only the last final-brief-looking block.\nconst markers = ['17 Morning Brief:', 'Morning Brief:', 'Weather'];\nlet bestIndex = -1;\nfor (const marker of markers) {\n const idx = brief.lastIndexOf(marker);\n if (idx > bestIndex) bestIndex = idx;\n}\nif (bestIndex > 0) brief = brief.slice(bestIndex);\n\nbrief = brief\n .split('\\n')\n .filter(line => !/^\\s*(Details|Header|Section \\d+|Drafting|Constraint Check|Self-Correction|Final Polish|Refining for|Final:|Plan:)/i.test(line))\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\nconst today = new Date().toISOString().split('T')[0];\nconst yamlFrontmatter = '---\\ncreated: ' + today + '\\ntype: morning-brief\\ntags: [daily, brief]\\n---\\n\\n';\n\nreturn [{\n json: {\n brief,\n briefWithFrontmatter: yamlFrontmatter + '# Morning Brief - ' + today + '\\n\\n' + brief,\n date: today\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1250, + 0 + ], + "id": "0adac542-7d95-4002-a3e2-080442cfd9e3", + "name": "Extract Brief" + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:8644/webhooks/morning-brief-atlas", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-Gitlab-Token", + "value": "iKjtyz9ZXp6qOu6HeFagQYVzkav01rNVi4hBuFCx0VY" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "json", + "specifyBody": "json", + "jsonBody": "= {\"brief\": {{ JSON.stringify($json.brief) }}}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1550, + -150 + ], + "id": "8242ada9-20c8-4689-b00c-3cd2787b2eb5", + "name": "Send via Atlas", + "continueOnFail": true + }, + { + "parameters": { + "method": "PUT", + "url": "=http://172.19.0.1:27123/vault/Notes/{{ $json.date }} Morning Brief.md", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "text/markdown" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "text/markdown", + "body": "={{ $json.briefWithFrontmatter }}", + "options": { + "timeout": 10000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1550, + 150 + ], + "id": "0f1fd6a2-86c0-4d3f-a948-32ce701d9f9f", + "name": "Save to Obsidian", + "credentials": { + "httpHeaderAuth": { + "id": "465Swz2b71O2KRAK", + "name": "Obsidian Local REST API" + } + }, + "continueOnFail": true + } + ], + "connections": { + "Daily 06:30 PT": { + "main": [ + [ + { + "node": "Weather", + "type": "main", + "index": 0 + } + ] + ] + }, + "Weather": { + "main": [ + [ + { + "node": "Swarm Health", + "type": "main", + "index": 0 + } + ] + ] + }, + "Swarm Health": { + "main": [ + [ + { + "node": "n8n Health", + "type": "main", + "index": 0 + } + ] + ] + }, + "n8n Health": { + "main": [ + [ + { + "node": "LiteLLM Health", + "type": "main", + "index": 0 + } + ] + ] + }, + "LiteLLM Health": { + "main": [ + [ + { + "node": "Email Highlights", + "type": "main", + "index": 0 + } + ] + ] + }, + "Email Highlights": { + "main": [ + [ + { + "node": "Calendar", + "type": "main", + "index": 0 + } + ] + ] + }, + "Calendar": { + "main": [ + [ + { + "node": "Merge Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge Data": { + "main": [ + [ + { + "node": "Synthesize with LLM", + "type": "main", + "index": 0 + } + ] + ] + }, + "Synthesize with LLM": { + "main": [ + [ + { + "node": "Extract Brief", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Brief": { + "main": [ + [ + { + "node": "Send via Atlas", + "type": "main", + "index": 0 + }, + { + "node": "Save to Obsidian", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "timezone": "America/Los_Angeles", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": { + "node:Daily 06:30 PT": { + "recurrenceRules": [] + } + }, + "meta": null, + "pinData": null, + "versionId": "6f6dd1b7-c08b-4ca9-a49d-274d59a7205c", + "activeVersionId": "6f6dd1b7-c08b-4ca9-a49d-274d59a7205c", + "versionCounter": 75, + "triggerCount": 1, + "tags": [], + "shared": [ + { + "updatedAt": "2026-05-13T21:41:17.800Z", + "createdAt": "2026-05-13T21:41:17.800Z", + "role": "workflow:owner", + "workflowId": "g3IdGZCK1EtTsv9T", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "versionMetadata": { + "name": null, + "description": null + } + } +] diff --git a/swarm-common/n8n-workflows/obsidian-6SKSZWZwuJNwuO2P.json b/swarm-common/n8n-workflows/obsidian-6SKSZWZwuJNwuO2P.json new file mode 100644 index 0000000..949f093 --- /dev/null +++ b/swarm-common/n8n-workflows/obsidian-6SKSZWZwuJNwuO2P.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-14T21:36:33.163Z","createdAt":"2026-05-14T21:36:33.163Z","id":"6SKSZWZwuJNwuO2P","name":"Obsidian Inbox Triage","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{},"id":"a244fdef-bf36-4903-bc52-d37bbc501f64","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0]},{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 18 * * *"}]}},"id":"f7ccf023-35a2-4011-9f35-82b7d9eb804d","name":"Daily 18:00 PT","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[0,180]},{"parameters":{"url":"http://172.19.0.1:27123/vault/Inbox/","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"21e3b6ca-f129-4884-82a4-80c9217cb0f4","name":"List Inbox","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,80],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}},"continueOnFail":true},{"parameters":{"jsCode":"\nconst now = new Date(); const date = now.toISOString().slice(0,10);\nconst input = $input.first().json;\nconst files = (Array.isArray(input.files) ? input.files : []).filter(f => f.endsWith('.md') && !f.includes('Triage'));\nconst lines = files.length ? files.map(f => `- [ ] [[${f.replace(/^Inbox\\//,'').replace(/\\.md$/,'')}]] — classify as Project / Resource / Decision / Runbook / Archive`).join('\\n') : '- No untriaged Inbox markdown files found.';\nconst body = `# Inbox Triage ${date}\n\nGenerated: ${now.toISOString()}\n\n## Inbox items\n\n${lines}\n\n## Promote to Projects\n\n- [ ] \n\n## Promote to Resources\n\n- [ ] \n\n## Promote to Decisions\n\n- [ ] \n\n## Promote to Runbooks\n\n- [ ] \n\n## Archive / Delete / Defer\n\n- [ ] \n`;\nreturn [{json:{path:`Inbox/Triage/${date}.md`, body}}];\n"},"id":"0b4e13c6-47ac-4d89-89b9-65d5da966c07","name":"Build Triage Note","type":"n8n-nodes-base.code","typeVersion":2,"position":[560,80]},{"parameters":{"method":"PUT","url":"={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.path).replace(/%2F/g, '/')}}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"text/markdown"}]},"sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{$json.body}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"dde63028-700e-4abd-af07-cb7af7119c99","name":"Write Triage Note","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[840,80],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}}}],"connections":{"Manual Trigger":{"main":[[{"node":"List Inbox","type":"main","index":0}]]},"Daily 18:00 PT":{"main":[[{"node":"List Inbox","type":"main","index":0}]]},"List Inbox":{"main":[[{"node":"Build Triage Note","type":"main","index":0}]]},"Build Triage Note":{"main":[[{"node":"Write Triage Note","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Daily 18:00 PT":{"recurrenceRules":[]}},"meta":null,"pinData":null,"versionId":"aa7b9bb1-7e61-410a-ae86-594e2325c52b","activeVersionId":"aa7b9bb1-7e61-410a-ae86-594e2325c52b","versionCounter":4,"triggerCount":1,"tags":[],"shared":[{"updatedAt":"2026-05-14T21:36:33.167Z","createdAt":"2026-05-14T21:36:33.167Z","role":"workflow:owner","workflowId":"6SKSZWZwuJNwuO2P","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}] \ No newline at end of file diff --git a/swarm-common/n8n-workflows/obsidian-LF3i86l3NkxpayxL.json b/swarm-common/n8n-workflows/obsidian-LF3i86l3NkxpayxL.json new file mode 100644 index 0000000..b045706 --- /dev/null +++ b/swarm-common/n8n-workflows/obsidian-LF3i86l3NkxpayxL.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-14T21:36:33.215Z","createdAt":"2026-05-14T21:36:33.215Z","id":"LF3i86l3NkxpayxL","name":"Obsidian Chat Summary Capture","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{"httpMethod":"POST","path":"obsidian-chat-summary","responseMode":"responseNode","options":{}},"id":"90069c7c-b6c9-4434-93f4-3b97061e590a","name":"Webhook - Chat Summary","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[0,0],"webhookId":"obsidian-chat-summary"},{"parameters":{"jsCode":"\nconst input = $json.body ?? $json;\nconst now = new Date(); const iso = now.toISOString(); const date = iso.slice(0,10);\nconst type = String(input.type || 'chat').toLowerCase();\nconst folderMap = {meeting:'Meetings', call:'Meetings', zoom:'Meetings', teams:'Meetings', decision:'Decisions', runbook:'Runbooks', project:'Projects', resource:'Resources', daily:'Daily'};\nconst folder = folderMap[type] || 'Inbox/Chat Summaries';\nfunction clean(s){ return String(s||'Untitled Summary').replace(/[\\\\/:*?\"<>|#\\[\\]]/g,'').replace(/\\s+/g,' ').trim().slice(0,120) || 'Untitled Summary'; }\nconst title = clean(input.title || input.subject || `${type} summary`);\nconst summary = input.summary || input.text || input.content || '';\nconst content = input.markdown || input.content || summary;\nconst tags = Array.isArray(input.tags) ? input.tags : String(input.tags || '').split(',').map(s=>s.trim()).filter(Boolean);\nconst body = `---\ntitle: ${JSON.stringify(title)}\ntype: ${JSON.stringify(type)}\nsource: ${JSON.stringify(input.source || input.platform || 'webhook')}\ncreated: ${JSON.stringify(iso)}\ntags: ${JSON.stringify(['automation/n8n','chat-summary',...tags])}\n---\n\n# ${title}\n\n## Summary\n\n${summary || '_No summary provided._'}\n\n## Notes\n\n${content || '_No content provided._'}\n\n## Metadata\n\n\\`\\`\\`json\n${JSON.stringify(input.metadata || {}, null, 2)}\n\\`\\`\\`\n`;\nreturn [{json:{path:`${folder}/${date} - ${title}.md`, body, title, folder, type}}];\n"},"id":"e80e092c-4fc4-4159-ac7c-fc570ef1c761","name":"Prepare Chat Note","type":"n8n-nodes-base.code","typeVersion":2,"position":[280,0]},{"parameters":{"method":"PUT","url":"={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.path).replace(/%2F/g, '/')}}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"text/markdown"}]},"sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{$json.body}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"a616c85b-9898-42bc-866f-077037c07a41","name":"Write Chat Note","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[560,0],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}}},{"parameters":{"respondWith":"json","responseBody":"={{JSON.stringify({ok:true, path:$('Prepare Chat Note').first().json.path, title:$('Prepare Chat Note').first().json.title, folder:$('Prepare Chat Note').first().json.folder})}}","options":{}},"id":"6944cd52-8614-4658-8907-b54a44fc01fa","name":"Respond","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[840,0]}],"connections":{"Webhook - Chat Summary":{"main":[[{"node":"Prepare Chat Note","type":"main","index":0}]]},"Prepare Chat Note":{"main":[[{"node":"Write Chat Note","type":"main","index":0}]]},"Write Chat Note":{"main":[[{"node":"Respond","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"49e05c18-c8d6-4eac-b507-1833840d57fe","activeVersionId":"49e05c18-c8d6-4eac-b507-1833840d57fe","versionCounter":3,"triggerCount":1,"tags":[],"shared":[{"updatedAt":"2026-05-14T21:36:33.223Z","createdAt":"2026-05-14T21:36:33.223Z","role":"workflow:owner","workflowId":"LF3i86l3NkxpayxL","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}] \ No newline at end of file diff --git a/swarm-common/n8n-workflows/obsidian-Ori3Bu5u5ODtxxyD.json b/swarm-common/n8n-workflows/obsidian-Ori3Bu5u5ODtxxyD.json new file mode 100644 index 0000000..c37ad27 --- /dev/null +++ b/swarm-common/n8n-workflows/obsidian-Ori3Bu5u5ODtxxyD.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-14T21:36:33.279Z","createdAt":"2026-05-14T21:36:33.279Z","id":"Ori3Bu5u5ODtxxyD","name":"Obsidian URL to Note","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{"httpMethod":"POST","path":"obsidian-url-to-note","responseMode":"responseNode","options":{}},"id":"70c67af3-3642-404e-bca9-8024f1ae2c4f","name":"Webhook - URL to Note","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[0,0],"webhookId":"obsidian-url-to-note"},{"parameters":{"jsCode":"\nconst input = $json.body ?? $json;\nconst url = String(input.url || input.link || '').trim();\nif (!/^https?:\\/\\//i.test(url)) throw new Error('POST JSON must include url starting with http:// or https://');\nreturn [{json:{url, folder: input.folder || 'Resources/Web Clips', tags: Array.isArray(input.tags)?input.tags:[], notes: input.notes || input.note || ''}}];\n"},"id":"9de66ad1-e538-455d-890a-be9b75a769d1","name":"Validate URL","type":"n8n-nodes-base.code","typeVersion":2,"position":[240,0]},{"parameters":{"method":"POST","url":"http://172.19.0.1:18812/extract","sendBody":true,"specifyBody":"json","jsonBody":"={{JSON.stringify({url:$json.url})}}","options":{"timeout":120000}},"id":"78ac965f-38c0-4821-b0ac-9a22a3b4d034","name":"Extract Content","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[500,0]},{"parameters":{"jsCode":"\nconst original = $('Validate URL').first().json;\nconst ex = $json;\nconst title = ex.title || ex.metadata?.title || original.url;\nconst text = String(ex.markdown || ex.content || ex.text || ex.article || JSON.stringify(ex)).slice(0,60000);\nreturn [{json:{...original, extractedTitle:title, extractedText:text}}];\n"},"id":"51b95641-61c9-4d5e-8d1c-1b166f0a7dc0","name":"Prepare LLM Input","type":"n8n-nodes-base.code","typeVersion":2,"position":[760,0]},{"parameters":{"method":"POST","url":"http://172.19.0.1:18806/v1/chat/completions","sendBody":true,"specifyBody":"json","jsonBody":"={JSON.stringify({model:\"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\", temperature:0.2, max_tokens:2048, messages:[{role:'system', content:'Convert extracted web content into concise Obsidian resource notes. Return only valid JSON with keys: title, summary, key_points, tags, note.'}, {role:'user', content:'URL: '+$json.url+'\\nTitle: '+$json.extractedTitle+'\\n\\nContent:\\n'+$json.extractedText}]})}","options":{"timeout":240000}},"id":"57f10533-1217-4486-bc79-33343782f54c","name":"Summarize with Local LLM","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1020,0],"continueOnFail":true},{"parameters":{"jsCode":"\nconst original = $('Prepare LLM Input').first().json;\nlet content = $json.choices?.[0]?.message?.content || '';\ncontent = String(content).replace(/^```json\\s*/i,'').replace(/^```\\s*/i,'').replace(/```$/,'').trim();\nlet parsed; try { parsed = JSON.parse(content); } catch(e) { parsed = {title: original.extractedTitle, summary: content || 'Summary unavailable.', key_points: [], note: original.extractedText.slice(0,8000), tags: []}; }\nfunction clean(s){ return String(s||'Untitled Resource').replace(/[\\\\/:*?\"<>|#\\[\\]]/g,'').replace(/\\s+/g,' ').trim().slice(0,120) || 'Untitled Resource'; }\nconst now = new Date(); const iso = now.toISOString(); const date = iso.slice(0,10);\nconst title = clean(parsed.title || original.extractedTitle);\nconst tags = Array.from(new Set(['automation/n8n','resource','web-clip',...(original.tags||[]),...(parsed.tags||[])]));\nconst points = Array.isArray(parsed.key_points) ? parsed.key_points.map(p=>`- ${p}`).join('\\n') : String(parsed.key_points||'');\nconst body = `---\ntitle: ${JSON.stringify(title)}\ntype: resource\nsource: ${JSON.stringify(original.url)}\ncreated: ${JSON.stringify(iso)}\ntags: ${JSON.stringify(tags)}\n---\n\n# ${title}\n\nSource: ${original.url}\n\n## Summary\n\n${parsed.summary || '_No summary generated._'}\n\n## Key points\n\n${points || '_No key points generated._'}\n\n## Notes\n\n${original.notes || ''}\n\n## Extracted note\n\n${parsed.note || ''}\n`;\nreturn [{json:{path:`${original.folder}/${date} - ${title}.md`, body, title, url: original.url}}];\n"},"id":"0e9af8e2-5ab0-4eac-9bd1-5578f946814f","name":"Build Resource Note","type":"n8n-nodes-base.code","typeVersion":2,"position":[1280,0]},{"parameters":{"method":"PUT","url":"={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.path).replace(/%2F/g, '/')}}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"text/markdown"}]},"sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{$json.body}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"725068d4-175e-462b-9050-fd42b229f8df","name":"Write Resource Note","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1540,0],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}}},{"parameters":{"respondWith":"json","responseBody":"={{JSON.stringify({ok:true,path:$('Build Resource Note').first().json.path,title:$('Build Resource Note').first().json.title,url:$('Build Resource Note').first().json.url})}}","options":{}},"id":"d07a2d9e-b9e3-455a-bcdf-2604ab6db3a9","name":"Respond","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1800,0]}],"connections":{"Webhook - URL to Note":{"main":[[{"node":"Validate URL","type":"main","index":0}]]},"Validate URL":{"main":[[{"node":"Extract Content","type":"main","index":0}]]},"Extract Content":{"main":[[{"node":"Prepare LLM Input","type":"main","index":0}]]},"Prepare LLM Input":{"main":[[{"node":"Summarize with Local LLM","type":"main","index":0}]]},"Summarize with Local LLM":{"main":[[{"node":"Build Resource Note","type":"main","index":0}]]},"Build Resource Note":{"main":[[{"node":"Write Resource Note","type":"main","index":0}]]},"Write Resource Note":{"main":[[{"node":"Respond","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"70bd9e5f-b04f-4ba1-b6d1-82cfece2bc2f","activeVersionId":"70bd9e5f-b04f-4ba1-b6d1-82cfece2bc2f","versionCounter":3,"triggerCount":1,"tags":[],"shared":[{"updatedAt":"2026-05-14T21:36:33.285Z","createdAt":"2026-05-14T21:36:33.285Z","role":"workflow:owner","workflowId":"Ori3Bu5u5ODtxxyD","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}] \ No newline at end of file diff --git a/swarm-common/n8n-workflows/obsidian-PCtD3PuQjzKLyEEE.json b/swarm-common/n8n-workflows/obsidian-PCtD3PuQjzKLyEEE.json new file mode 100644 index 0000000..1bd5788 --- /dev/null +++ b/swarm-common/n8n-workflows/obsidian-PCtD3PuQjzKLyEEE.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-14T21:36:33.045Z","createdAt":"2026-05-14T21:36:33.045Z","id":"PCtD3PuQjzKLyEEE","name":"Obsidian Health + Reindex","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{},"id":"f9152036-4ee6-48cf-9f71-fd59ce617c52","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0]},{"parameters":{"rule":{"interval":[{"field":"hours","hoursInterval":1}]}},"id":"7845e784-c35b-4912-9d72-2463a06d95d2","name":"Hourly Health Schedule","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[0,180]},{"parameters":{"url":"http://172.19.0.1:27123/","options":{"timeout":10000}},"id":"4976f00c-3539-4d3a-a87d-f7f3ac1adf19","name":"Check Obsidian REST","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,80],"continueOnFail":true},{"parameters":{"method":"POST","url":"http://172.19.0.1:18810/reindex","options":{"timeout":300000}},"id":"8abf0596-3af6-4d56-b4d0-5284f13998ae","name":"Trigger Obsidian Reindex","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[560,80],"continueOnFail":true},{"parameters":{"method":"POST","url":"http://172.19.0.1:18814/check","options":{"timeout":240000}},"id":"248b4109-2d60-43bc-b598-cb766edde11f","name":"Run RAG Embedding Check","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[840,80],"continueOnFail":true},{"parameters":{"jsCode":"\nconst now = new Date().toISOString();\nconst reindex = $('Trigger Obsidian Reindex').first().json;\nconst rag = $('Run RAG Embedding Check').first().json;\nconst rest = $('Check Obsidian REST').first().json;\nconst ok = Boolean(rest.status === 'OK' || rest.manifest || rest.statusCode) && Boolean(rag.ok !== false) && Boolean(reindex.ok !== false);\nconst body = `# Obsidian Automation Health\n\nUpdated: ${now}\n\n## Status\n\n- Overall: ${ok ? 'OK' : 'Needs attention'}\n- Obsidian REST: ${rest.status || rest.statusCode || 'responded'}\n- Reindex trigger: ${JSON.stringify(reindex).slice(0, 500)}\n- RAG/embedding check: ${JSON.stringify(rag).slice(0, 1000)}\n\nThis note is automatically overwritten by n8n.\n`;\nreturn [{ json: { ok, path: 'Resources/Obsidian Automation Health.md', body } }];\n"},"id":"e67008ad-0d9e-4546-a180-3d4223b8d05c","name":"Build Health Note","type":"n8n-nodes-base.code","typeVersion":2,"position":[1120,80]},{"parameters":{"method":"PUT","url":"={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.path).replace(/%2F/g, '/')}}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"text/markdown"}]},"sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{$json.body}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"d86d8942-966a-48fd-ad99-cf23408f2ae4","name":"Write Health Note","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1400,80],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}}}],"connections":{"Manual Trigger":{"main":[[{"node":"Check Obsidian REST","type":"main","index":0}]]},"Hourly Health Schedule":{"main":[[{"node":"Check Obsidian REST","type":"main","index":0}]]},"Check Obsidian REST":{"main":[[{"node":"Trigger Obsidian Reindex","type":"main","index":0}]]},"Trigger Obsidian Reindex":{"main":[[{"node":"Run RAG Embedding Check","type":"main","index":0}]]},"Run RAG Embedding Check":{"main":[[{"node":"Build Health Note","type":"main","index":0}]]},"Build Health Note":{"main":[[{"node":"Write Health Note","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Hourly Health Schedule":{"recurrenceRules":[]}},"meta":null,"pinData":null,"versionId":"2de2a0d3-ab17-47b5-b2ee-a9c5c20969cd","activeVersionId":"2de2a0d3-ab17-47b5-b2ee-a9c5c20969cd","versionCounter":4,"triggerCount":1,"tags":[],"shared":[{"updatedAt":"2026-05-14T21:36:33.056Z","createdAt":"2026-05-14T21:36:33.056Z","role":"workflow:owner","workflowId":"PCtD3PuQjzKLyEEE","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}] \ No newline at end of file diff --git a/swarm-common/n8n-workflows/obsidian-UWLMOQQVxbTX6Sis.json b/swarm-common/n8n-workflows/obsidian-UWLMOQQVxbTX6Sis.json new file mode 100644 index 0000000..2a02eac --- /dev/null +++ b/swarm-common/n8n-workflows/obsidian-UWLMOQQVxbTX6Sis.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-14T21:36:33.337Z","createdAt":"2026-05-14T21:36:33.337Z","id":"UWLMOQQVxbTX6Sis","name":"Obsidian Weekly Decision Runbook Extractor","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{},"id":"f0b1fc1f-e1d9-4529-a1a2-b04bb942472f","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0]},{"parameters":{"rule":{"interval":[{"field":"weeks","triggerAtDay":[1],"triggerAtHour":8,"triggerAtMinute":0}]}},"id":"259798b5-2bff-4ec3-8b75-39353c053576","name":"Weekly Monday 08:00 PT","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[0,180]},{"parameters":{"jsCode":"\nreturn ['we decided','decision','runbook','procedure','rollback','workaround','root cause','next time'].map(query => ({json:{query}}));\n"},"id":"b8e28ca5-1d19-4e53-97c2-c3b1c88b2102","name":"Build Search Queries","type":"n8n-nodes-base.code","typeVersion":2,"position":[280,80]},{"parameters":{"method":"POST","url":"={{'http://172.19.0.1:27123/search/simple/?query=' + encodeURIComponent($json.query) + '&contextLength=300'}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"ef1d4e75-5a9d-42bf-8c46-646b94c16da3","name":"Search Obsidian","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[560,80],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}},"continueOnFail":true},{"parameters":{"jsCode":"\nconst now = new Date(); const date = now.toISOString().slice(0,10);\nconst rows=[];\nfor (const item of $input.all()) {\n const arr = Array.isArray(item.json) ? item.json : (Array.isArray(item.json.body) ? item.json.body : (Array.isArray(item.json.results) ? item.json.results : []));\n for (const r of arr) {\n const filename = r.filename || r.path || 'Unknown';\n const matches = Array.isArray(r.matches) ? r.matches : [];\n for (const m of matches) rows.push({filename, context:String(m.context||'').replace(/\\s+/g,' ').trim()});\n }\n}\nconst seen = new Set();\nconst uniq = rows.filter(r => { const k=r.filename+'|'+r.context.slice(0,100); if (seen.has(k)) return false; seen.add(k); return true; }).slice(0,60);\nconst decisionRe = /\\b(decision|decided|choose|chosen|because|tradeoff|approved|rejected)\\b/i;\nconst runbookRe = /\\b(runbook|procedure|steps|incident|fix|workaround|deploy|rollback|recovery|restart|root cause|next time)\\b/i;\nfunction section(title, arr, kind){\n if (!arr.length) return `## ${title}\\n\\n_No candidates found._\\n`;\n return `## ${title}\\n\\n` + arr.slice(0,20).map((r,i)=>`### ${i+1}. ${r.filename}\\n\\n- Suggested action: create/update a ${kind}.\\n- Source: [[${r.filename.replace(/\\.md$/,'')}]]\\n- Evidence: ${r.context.slice(0,700)}\\n`).join('\\n');\n}\nconst decisions=uniq.filter(r=>decisionRe.test(r.context));\nconst runbooks=uniq.filter(r=>runbookRe.test(r.context));\nconst body = `# Decision / Runbook Suggestions ${date}\\n\\nGenerated: ${now.toISOString()}\\n\\nReview candidates and promote useful items into durable Decision or Runbook notes. This note is overwritten weekly.\\n\\n${section('Decision Candidates', decisions, 'Decision note')}\\n\\n${section('Runbook Candidates', runbooks, 'Runbook note')}\\n\\n## Raw Summary\\n\\n- Total candidates: ${uniq.length}\\n- Decision candidates: ${decisions.length}\\n- Runbook candidates: ${runbooks.length}\\n`;\nreturn [{json:{path:'Decisions/Runbook Suggestions.md', body, total:uniq.length}}];\n","mode":"runOnceForAllItems"},"id":"6862ae15-2c5d-4805-9009-b3e72861be8e","name":"Build Suggestions Note","type":"n8n-nodes-base.code","typeVersion":2,"position":[840,80]},{"parameters":{"method":"PUT","url":"={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.path).replace(/%2F/g, '/')}}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"text/markdown"}]},"sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{$json.body}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"cbab2fb9-c980-4c92-8450-f36885727a86","name":"Write Suggestions Note","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1120,80],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}}}],"connections":{"Manual Trigger":{"main":[[{"node":"Build Search Queries","type":"main","index":0}]]},"Weekly Monday 08:00 PT":{"main":[[{"node":"Build Search Queries","type":"main","index":0}]]},"Build Search Queries":{"main":[[{"node":"Search Obsidian","type":"main","index":0}]]},"Search Obsidian":{"main":[[{"node":"Build Suggestions Note","type":"main","index":0}]]},"Build Suggestions Note":{"main":[[{"node":"Write Suggestions Note","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Weekly Monday 08:00 PT":{"recurrenceRules":[]}},"meta":null,"pinData":null,"versionId":"d8e576e0-22e5-455a-95ce-1f20b443cc61","activeVersionId":"d8e576e0-22e5-455a-95ce-1f20b443cc61","versionCounter":4,"triggerCount":1,"tags":[],"shared":[{"updatedAt":"2026-05-14T21:36:33.340Z","createdAt":"2026-05-14T21:36:33.340Z","role":"workflow:owner","workflowId":"UWLMOQQVxbTX6Sis","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}] \ No newline at end of file diff --git a/swarm-common/n8n-workflows/obsidian-YZyJ5G0Ur8D6TlM8.json b/swarm-common/n8n-workflows/obsidian-YZyJ5G0Ur8D6TlM8.json new file mode 100644 index 0000000..f1d1a35 --- /dev/null +++ b/swarm-common/n8n-workflows/obsidian-YZyJ5G0Ur8D6TlM8.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-14T21:36:33.117Z","createdAt":"2026-05-14T21:36:33.117Z","id":"YZyJ5G0Ur8D6TlM8","name":"Obsidian Daily Review","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{},"id":"01121020-b53b-4f27-8ad2-f6e1ddb656c4","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0]},{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"30 7 * * *"}]}},"id":"4d6ec3bb-2953-43a8-bbed-e9a54199622d","name":"Daily 07:30 PT","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[0,180]},{"parameters":{"jsCode":"\nconst now = new Date();\nconst date = now.toISOString().slice(0,10);\nconst body = `---\ntype: daily-review\ndate: ${date}\ntags: [type/daily-review, automation/n8n]\n---\n\n# Daily Review ${date}\n\n## Top priorities\n\n- [ ] \n- [ ] \n- [ ] \n\n## Inbox sweep\n\n- [ ] Review [[Inbox]]\n- [ ] Promote useful captures into [[Projects Home]], [[Resources Home]], [[Decisions Home]], or [[Runbooks Home]]\n\n## Open loops\n\n- [ ] Check [[Projects Home]]\n- [ ] Check [[Meetings Home]] action items\n- [ ] Check [[Runbooks Home]] for procedures that need updates\n\n## Notes / log\n\n- \n\n## End-of-day reflection\n\n- What moved forward?\n- What is blocked?\n- What should start tomorrow?\n`;\nreturn [{ json: { path: `Daily/Reviews/${date} Daily Review.md`, body } }];\n"},"id":"8ffb36c5-de40-4811-8f92-61d9dde9982c","name":"Build Daily Review","type":"n8n-nodes-base.code","typeVersion":2,"position":[280,80]},{"parameters":{"method":"PUT","url":"={{'http://172.19.0.1:27123/vault/' + encodeURIComponent($json.path).replace(/%2F/g, '/')}}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"text/markdown"}]},"sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{$json.body}}","options":{"timeout":30000},"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"id":"5e2226bb-3c34-4f17-b968-039ddc1dfe35","name":"Write Daily Review","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[560,80],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}}}],"connections":{"Manual Trigger":{"main":[[{"node":"Build Daily Review","type":"main","index":0}]]},"Daily 07:30 PT":{"main":[[{"node":"Build Daily Review","type":"main","index":0}]]},"Build Daily Review":{"main":[[{"node":"Write Daily Review","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Daily 07:30 PT":{"recurrenceRules":[]}},"meta":null,"pinData":null,"versionId":"2d2a2217-4772-42eb-80ce-622ed419d209","activeVersionId":"2d2a2217-4772-42eb-80ce-622ed419d209","versionCounter":4,"triggerCount":1,"tags":[],"shared":[{"updatedAt":"2026-05-14T21:36:33.120Z","createdAt":"2026-05-14T21:36:33.120Z","role":"workflow:owner","workflowId":"YZyJ5G0Ur8D6TlM8","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}] \ No newline at end of file diff --git a/swarm-common/n8n-workflows/rag-and-embedding-health-watchdog.json b/swarm-common/n8n-workflows/rag-and-embedding-health-watchdog.json new file mode 100644 index 0000000..4f3e362 --- /dev/null +++ b/swarm-common/n8n-workflows/rag-and-embedding-health-watchdog.json @@ -0,0 +1,345 @@ +{ + "updatedAt": "2026-05-14T18:49:58.205Z", + "createdAt": "2026-05-14T18:49:04.674Z", + "id": "SwKaPtYqUJrakpFu", + "name": "RAG and Embedding Health Watchdog", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": {}, + "id": "bca0ccac-1102-4b45-a9e3-a52f06352376", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 0, + 100 + ] + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 6 + } + ] + } + }, + "id": "3f5e4d1e-7e90-43d1-ae01-97dde40fbf28", + "name": "Every 6 Hours", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + -80 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18814/check", + "options": { + "timeout": 240000 + } + }, + "id": "52e14b9f-4ab4-4906-9ed7-0dbe10762c26", + "name": "Run RAG Health Check", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 260, + 20 + ] + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst data = $input.first().json;\nconst now = new Date().toISOString();\nconst nl = String.fromCharCode(10);\nconst prev = staticData.ragEmbedding || { failedRuns: 0, alerted: false };\n\nif (data.ok) {\n const wasAlerted = prev.alerted;\n staticData.ragEmbedding = { failedRuns: 0, alerted: false, lastOk: now, lastStatus: data.status, durationMs: data.durationMs };\n if (!wasAlerted) return [];\n return [{ json: { text: ['\u2705 RAG/Embedding health recovered', `- status=ok; duration=${data.durationMs}ms`, `checked=${now}`].join(nl), data } }];\n}\n\nconst failedRuns = (prev.failedRuns || 0) + 1;\nconst shouldAlert = !prev.alerted || failedRuns % 4 === 0;\nstaticData.ragEmbedding = { failedRuns, alerted: prev.alerted || shouldAlert, lastFailure: now, lastStatus: data.status, exitCode: data.exitCode, output: data.output };\nif (!shouldAlert) return [];\n\nconst output = (data.output || 'No output from checker').trim();\nconst lines = [\n '\ud83d\udea8 RAG/Embedding Health Watchdog',\n `- failedRuns=${failedRuns}; status=${data.status}; exit=${data.exitCode}; duration=${data.durationMs}ms`,\n output,\n 'fix=check systemctl --user status rag-embedding-health.service; then inspect Ollama 18807, ChromaDB, and Obsidian reindex 18810.',\n `checked=${now}`,\n];\nreturn [{ json: { text: lines.join(nl), data } }];" + }, + "id": "6b435e3e-2efc-43da-b565-d5ecb819af1f", + "name": "Alert on Failure or Recovery", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 520, + 20 + ] + }, + { + "parameters": { + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "method": "POST", + "url": "https://discord.com/api/v10/channels/1494453542243532932/messages", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { content: $json.text } }}", + "options": {} + }, + "id": "1ebabe7e-2dbc-4fa6-a63c-3d869314a5cf", + "name": "Send Discord Ops Alert", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 800, + 20 + ], + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Run RAG Health Check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Every 6 Hours": { + "main": [ + [ + { + "node": "Run RAG Health Check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Run RAG Health Check": { + "main": [ + [ + { + "node": "Alert on Failure or Recovery", + "type": "main", + "index": 0 + } + ] + ] + }, + "Alert on Failure or Recovery": { + "main": [ + [ + { + "node": "Send Discord Ops Alert", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": { + "node:Every 6 Hours": { + "recurrenceRules": [] + }, + "global": { + "ragEmbedding": { + "failedRuns": 0, + "alerted": false, + "lastOk": "2026-05-14T18:50:22.108Z", + "lastStatus": "ok", + "durationMs": 13239 + } + } + }, + "meta": null, + "versionId": "b6be4349-5960-40cd-b857-bd6c9c6c717f", + "activeVersionId": "b6be4349-5960-40cd-b857-bd6c9c6c717f", + "versionCounter": 9, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-14T18:49:04.685Z", + "createdAt": "2026-05-14T18:49:04.685Z", + "role": "workflow:owner", + "workflowId": "SwKaPtYqUJrakpFu", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-05-14T18:49:58.207Z", + "createdAt": "2026-05-14T18:49:58.207Z", + "versionId": "b6be4349-5960-40cd-b857-bd6c9c6c717f", + "workflowId": "SwKaPtYqUJrakpFu", + "nodes": [ + { + "parameters": {}, + "id": "bca0ccac-1102-4b45-a9e3-a52f06352376", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 0, + 100 + ] + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 6 + } + ] + } + }, + "id": "3f5e4d1e-7e90-43d1-ae01-97dde40fbf28", + "name": "Every 6 Hours", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + -80 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://172.19.0.1:18814/check", + "options": { + "timeout": 240000 + } + }, + "id": "52e14b9f-4ab4-4906-9ed7-0dbe10762c26", + "name": "Run RAG Health Check", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 260, + 20 + ] + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst data = $input.first().json;\nconst now = new Date().toISOString();\nconst nl = String.fromCharCode(10);\nconst prev = staticData.ragEmbedding || { failedRuns: 0, alerted: false };\n\nif (data.ok) {\n const wasAlerted = prev.alerted;\n staticData.ragEmbedding = { failedRuns: 0, alerted: false, lastOk: now, lastStatus: data.status, durationMs: data.durationMs };\n if (!wasAlerted) return [];\n return [{ json: { text: ['\u2705 RAG/Embedding health recovered', `- status=ok; duration=${data.durationMs}ms`, `checked=${now}`].join(nl), data } }];\n}\n\nconst failedRuns = (prev.failedRuns || 0) + 1;\nconst shouldAlert = !prev.alerted || failedRuns % 4 === 0;\nstaticData.ragEmbedding = { failedRuns, alerted: prev.alerted || shouldAlert, lastFailure: now, lastStatus: data.status, exitCode: data.exitCode, output: data.output };\nif (!shouldAlert) return [];\n\nconst output = (data.output || 'No output from checker').trim();\nconst lines = [\n '\ud83d\udea8 RAG/Embedding Health Watchdog',\n `- failedRuns=${failedRuns}; status=${data.status}; exit=${data.exitCode}; duration=${data.durationMs}ms`,\n output,\n 'fix=check systemctl --user status rag-embedding-health.service; then inspect Ollama 18807, ChromaDB, and Obsidian reindex 18810.',\n `checked=${now}`,\n];\nreturn [{ json: { text: lines.join(nl), data } }];" + }, + "id": "6b435e3e-2efc-43da-b565-d5ecb819af1f", + "name": "Alert on Failure or Recovery", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 520, + 20 + ] + }, + { + "parameters": { + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "method": "POST", + "url": "https://discord.com/api/v10/channels/1494453542243532932/messages", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { content: $json.text } }}", + "options": {} + }, + "id": "1ebabe7e-2dbc-4fa6-a63c-3d869314a5cf", + "name": "Send Discord Ops Alert", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 800, + 20 + ], + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Run RAG Health Check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Every 6 Hours": { + "main": [ + [ + { + "node": "Run RAG Health Check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Run RAG Health Check": { + "main": [ + [ + { + "node": "Alert on Failure or Recovery", + "type": "main", + "index": 0 + } + ] + ] + }, + "Alert on Failure or Recovery": { + "main": [ + [ + { + "node": "Send Discord Ops Alert", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T18:49:58.274Z", + "id": 1516, + "workflowId": "SwKaPtYqUJrakpFu", + "versionId": "b6be4349-5960-40cd-b857-bd6c9c6c717f", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +} diff --git a/swarm-common/n8n-workflows/swarm-health-watchdog.json b/swarm-common/n8n-workflows/swarm-health-watchdog.json new file mode 100644 index 0000000..097db4b --- /dev/null +++ b/swarm-common/n8n-workflows/swarm-health-watchdog.json @@ -0,0 +1,415 @@ +{ + "updatedAt": "2026-05-14T00:32:57.803Z", + "createdAt": "2026-05-12T17:48:01.214Z", + "id": "lDKocSFXBQWQrDd3", + "name": "Swarm Health Watchdog", + "description": "Every 15 minutes, checks core swarm endpoints from inside n8n. Alerts after two consecutive failures and reports recoveries to Telegram and Discord.", + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -620, + -100 + ], + "id": "3759f3cd-fa90-49b6-ad08-322d21f3d727", + "name": "Manual Trigger" + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 15 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -620, + 100 + ], + "id": "9d209ddb-8da7-48ad-850c-ec0e452760ca", + "name": "Every 15 Minutes" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst CONFIG = {\n timeoutMs: 60000,\n failureThreshold: 2,\n reminderEveryFailedRuns: 6,\n};\nconst services = [\n { key: 'brave', name: 'Brave MCP', port: 18802, url: 'http://172.19.0.1:18802/mcp', ok: [200, 400, 404, 405, 406], docker: 'brave-search' },\n { key: 'searxng', name: 'SearXNG', port: 18803, url: 'http://172.19.0.1:18803/search?q=health&format=json', ok: [200], docker: 'searxng' },\n { key: 'litellm', name: 'LiteLLM', port: 18804, url: 'http://172.19.0.1:18804/health/liveliness', ok: [200], docker: 'litellm' },\n { key: 'kokoro', name: 'Kokoro TTS', port: 18805, url: 'http://172.19.0.1:18805/health', ok: [200], docker: 'kokoro-tts' },\n { key: 'llamacpp', name: 'llama.cpp', port: 18806, url: 'http://172.19.0.1:18806/health', ok: [200] },\n { key: 'ollama', name: 'Ollama embeddings', port: 18807, url: 'http://172.19.0.1:18807/api/version', ok: [200] },\n { key: 'n8n', name: 'n8n', port: 18808, url: 'http://127.0.0.1:5678/healthz', ok: [200], docker: 'n8n-agent' },\n { key: 'whisper', name: 'Whisper NPU', port: 18816, url: 'http://172.19.0.1:18816/', ok: [200, 404], docker: 'whisper-server-npu' },\n];\n\nconst httpRequest = this.helpers.httpRequest.bind(this.helpers);\n\nfunction responseLike(response) {\n const status = response.statusCode || response.status;\n const body = response.body === undefined || response.body === null ? '' : response.body;\n return {\n status,\n ok: status >= 200 && status < 300,\n async text() {\n return typeof body === 'string' ? body : JSON.stringify(body);\n },\n async json() {\n if (typeof body === 'string') return JSON.parse(body);\n return body;\n },\n };\n}\n\nasync function fetchWithTimeout(url, options = {}, timeoutMs = CONFIG.timeoutMs) {\n const method = options.method || 'GET';\n try {\n const response = await httpRequest({\n method,\n url,\n timeout: timeoutMs,\n json: false,\n simple: false,\n resolveWithFullResponse: true,\n returnFullResponse: true,\n ignoreHttpStatusErrors: true,\n });\n return responseLike(response);\n } catch (error) {\n if (error.response) return responseLike(error.response);\n throw error;\n }\n}\n\n// Fetch Docker container health from host-side endpoint\nlet dockerHealth = {};\ntry {\n const dhRes = await fetchWithTimeout('http://172.19.0.1:18809/health', { method: 'GET' }, 3000);\n if (dhRes.ok) {\n const dhData = await dhRes.json();\n for (const c of (dhData.containers || [])) {\n dockerHealth[c.name] = c;\n }\n }\n} catch (_) {\n // Docker health endpoint unavailable - continue without it\n}\n\nasync function check(svc) {\n const started = Date.now();\n try {\n const res = await fetchWithTimeout(svc.url, { method: 'GET' }, CONFIG.timeoutMs);\n const ms = Date.now() - started;\n const body = await res.text().catch(() => '');\n return {\n ...svc,\n healthy: svc.ok.includes(res.status),\n status: res.status,\n ms,\n detail: body.slice(0, 160).replace(/\\s+/g, ' ').trim(),\n docker: svc.docker ? (dockerHealth[svc.docker] || { name: svc.docker, status: 'unknown', health: 'unknown', restarts: -1 }) : null,\n };\n } catch (error) {\n return {\n ...svc,\n healthy: false,\n status: 'error',\n ms: Date.now() - started,\n detail: error.message,\n docker: svc.docker ? (dockerHealth[svc.docker] || { name: svc.docker, status: 'unknown', health: 'unknown', restarts: -1 }) : null,\n };\n }\n}\n\nfunction suggestedFix(r) {\n const dh = r.docker;\n const dockerInfo = dh ? ` [docker: ${dh.status}/${dh.health} restarts=${dh.restarts}]` : '';\n if (r.key === 'llamacpp') return 'systemctl status llama-server.service; restart only if it is down.' + dockerInfo;\n if (r.key === 'ollama') return 'systemctl --user status ollama.service; verify port 18807 and nomic-embed-text.' + dockerInfo;\n if (r.key === 'n8n') return 'docker logs n8n-agent --tail 100; check database/API health.' + dockerInfo;\n if (['searxng','litellm','kokoro','whisper','brave'].includes(r.key)) return `cd ~/lab/swarm && docker compose ps; inspect ${r.name} logs.${dockerInfo}`;\n return 'Check service logs and port listener.' + dockerInfo;\n}\n\nconst results = await Promise.all(services.map(check));\nconst now = new Date().toISOString();\nstaticData.services = staticData.services || {};\nconst alerts = [];\nconst recoveries = [];\nfor (const r of results) {\n const prev = staticData.services[r.key] || { failedRuns: 0, alerted: false };\n if (r.healthy) {\n if (prev.alerted) recoveries.push({ ...r, previousFailedRuns: prev.failedRuns });\n staticData.services[r.key] = { failedRuns: 0, alerted: false, lastOk: now, lastStatus: r.status, lastDetail: r.detail };\n } else {\n const failedRuns = (prev.failedRuns || 0) + 1;\n const shouldAlert = failedRuns >= CONFIG.failureThreshold && (!prev.alerted || (CONFIG.reminderEveryFailedRuns > 0 && failedRuns % CONFIG.reminderEveryFailedRuns === 0));\n staticData.services[r.key] = { failedRuns, alerted: prev.alerted || shouldAlert, lastFailure: now, lastStatus: r.status, lastDetail: r.detail };\n if (shouldAlert) alerts.push({ ...r, failedRuns, suggestedFix: suggestedFix(r) });\n }\n}\n\nif (!alerts.length && !recoveries.length) return [];\nlet lines = [];\nif (alerts.length) {\n lines.push('\\u{1F6A8} Swarm Health Watchdog');\n for (const a of alerts) {\n const dh = a.docker;\n const dockerStr = dh ? ` | docker:${dh.status}/${dh.health}/restarts=${dh.restarts}` : '';\n lines.push(`- ${a.name} :${a.port} failed ${a.failedRuns} checks; status=${a.status}${dockerStr}; detail=${a.detail || 'n/a'}; fix=${a.suggestedFix}`);\n }\n}\nif (recoveries.length) {\n lines.push('\\u2705 Swarm service recovered');\n for (const r of recoveries) {\n const dh = r.docker;\n const dockerStr = dh ? ` | docker:${dh.status}/${dh.health}` : '';\n lines.push(`- ${r.name} :${r.port} healthy again; status=${r.status}; latency=${r.ms}ms${dockerStr}`);\n }\n}\nlines.push(`checked=${now}`);\nreturn [{ json: { text: lines.join('\\n'), alerts, recoveries, results, dockerHealth, checkedAt: now } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -340, + 0 + ], + "id": "b3f76d53-204b-45bb-9a48-8cf20262319d", + "name": "Check Swarm Services" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{$json.text}}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + -80, + -80 + ], + "id": "32d7ad9f-80bb-4acf-b546-89f04db32a6a", + "name": "Send Telegram Alert", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "method": "POST", + "url": "https://discord.com/api/v10/channels/425781661268049931/messages", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { content: $json.text } }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -80, + 100 + ], + "id": "7eb589f5-6e50-4e1e-8a37-391f06785ad87", + "name": "Send Discord Alert", + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Check Swarm Services", + "type": "main", + "index": 0 + } + ] + ] + }, + "Every 15 Minutes": { + "main": [ + [ + { + "node": "Check Swarm Services", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Swarm Services": { + "main": [ + [ + { + "node": "Send Telegram Alert", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord Alert", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "timezone": "America/Los_Angeles", + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "staticData": { + "node:Every 15 Minutes": { + "recurrenceRules": [] + }, + "global": { + "services": { + "brave": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "searxng": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "litellm": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "kokoro": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "llamacpp": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "ollama": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "n8n": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + }, + "whisper": { + "failedRuns": 4, + "alerted": true, + "lastFailure": "2026-05-14T00:30:40.067Z", + "lastStatus": "error", + "lastDetail": "fetch is not defined" + } + } + } + }, + "meta": null, + "versionId": "eec5521b-fb44-44ea-b238-aff842560f98", + "activeVersionId": "eec5521b-fb44-44ea-b238-aff842560f98", + "versionCounter": 52, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-12T17:39:10.124Z", + "createdAt": "2026-05-12T17:39:10.124Z", + "role": "workflow:owner", + "workflowId": "lDKocSFXBQWQrDd3", + "projectId": "WGdp8QunI1tHpjXa", + "project": { + "updatedAt": "2026-03-11T21:08:10.005Z", + "createdAt": "2026-03-11T21:05:11.541Z", + "id": "WGdp8QunI1tHpjXa", + "name": "will will ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-05-14T00:09:09.316Z", + "createdAt": "2026-05-14T00:09:09.316Z", + "versionId": "eec5521b-fb44-44ea-b238-aff842560f98", + "workflowId": "lDKocSFXBQWQrDd3", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -620, + -100 + ], + "id": "3759f3cd-fa90-49b6-ad08-322d21f3d727", + "name": "Manual Trigger" + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 15 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -620, + 100 + ], + "id": "9d209ddb-8da7-48ad-850c-ec0e452760ca", + "name": "Every 15 Minutes" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst CONFIG = {\n timeoutMs: 5000,\n failureThreshold: 2,\n reminderEveryFailedRuns: 6,\n};\nconst services = [\n { key: 'brave', name: 'Brave MCP', port: 18802, url: 'http://172.19.0.1:18802/mcp', ok: [200, 400, 404, 405, 406], docker: 'brave-search' },\n { key: 'searxng', name: 'SearXNG', port: 18803, url: 'http://172.19.0.1:18803/search?q=health&format=json', ok: [200], docker: 'searxng' },\n { key: 'litellm', name: 'LiteLLM', port: 18804, url: 'http://172.19.0.1:18804/health/liveliness', ok: [200], docker: 'litellm' },\n { key: 'kokoro', name: 'Kokoro TTS', port: 18805, url: 'http://172.19.0.1:18805/health', ok: [200], docker: 'kokoro-tts' },\n { key: 'llamacpp', name: 'llama.cpp', port: 18806, url: 'http://172.19.0.1:18806/health', ok: [200] },\n { key: 'ollama', name: 'Ollama embeddings', port: 18807, url: 'http://172.19.0.1:18807/api/version', ok: [200] },\n { key: 'n8n', name: 'n8n', port: 18808, url: 'http://127.0.0.1:5678/healthz', ok: [200], docker: 'n8n-agent' },\n { key: 'whisper', name: 'Whisper NPU', port: 18816, url: 'http://172.19.0.1:18816/', ok: [200, 404], docker: 'whisper-server-npu' },\n];\n\nasync function fetchWithTimeout(url, options = {}, timeoutMs = CONFIG.timeoutMs) {\n let timer;\n const timeoutPromise = new Promise((_, reject) => {\n timer = setTimeout(() => reject(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs);\n });\n try {\n return await Promise.race([fetch(url, options), timeoutPromise]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n// Fetch Docker container health from host-side endpoint\nlet dockerHealth = {};\ntry {\n const dhRes = await fetchWithTimeout('http://172.19.0.1:18809/health', { method: 'GET' }, 3000);\n if (dhRes.ok) {\n const dhData = await dhRes.json();\n for (const c of (dhData.containers || [])) {\n dockerHealth[c.name] = c;\n }\n }\n} catch (_) {\n // Docker health endpoint unavailable - continue without it\n}\n\nasync function check(svc) {\n const started = Date.now();\n try {\n const res = await fetchWithTimeout(svc.url, { method: 'GET' }, CONFIG.timeoutMs);\n const ms = Date.now() - started;\n const body = await res.text().catch(() => '');\n return {\n ...svc,\n healthy: svc.ok.includes(res.status),\n status: res.status,\n ms,\n detail: body.slice(0, 160).replace(/\\s+/g, ' ').trim(),\n docker: svc.docker ? (dockerHealth[svc.docker] || { name: svc.docker, status: 'unknown', health: 'unknown', restarts: -1 }) : null,\n };\n } catch (error) {\n return {\n ...svc,\n healthy: false,\n status: 'error',\n ms: Date.now() - started,\n detail: error.message,\n docker: svc.docker ? (dockerHealth[svc.docker] || { name: svc.docker, status: 'unknown', health: 'unknown', restarts: -1 }) : null,\n };\n }\n}\n\nfunction suggestedFix(r) {\n const dh = r.docker;\n const dockerInfo = dh ? ` [docker: ${dh.status}/${dh.health} restarts=${dh.restarts}]` : '';\n if (r.key === 'llamacpp') return 'systemctl status llama-server.service; restart only if it is down.' + dockerInfo;\n if (r.key === 'ollama') return 'systemctl --user status ollama.service; verify port 18807 and nomic-embed-text.' + dockerInfo;\n if (r.key === 'n8n') return 'docker logs n8n-agent --tail 100; check database/API health.' + dockerInfo;\n if (['searxng','litellm','kokoro','whisper','brave'].includes(r.key)) return `cd ~/lab/swarm && docker compose ps; inspect ${r.name} logs.${dockerInfo}`;\n return 'Check service logs and port listener.' + dockerInfo;\n}\n\nconst results = await Promise.all(services.map(check));\nconst now = new Date().toISOString();\nstaticData.services = staticData.services || {};\nconst alerts = [];\nconst recoveries = [];\nfor (const r of results) {\n const prev = staticData.services[r.key] || { failedRuns: 0, alerted: false };\n if (r.healthy) {\n if (prev.alerted) recoveries.push({ ...r, previousFailedRuns: prev.failedRuns });\n staticData.services[r.key] = { failedRuns: 0, alerted: false, lastOk: now, lastStatus: r.status, lastDetail: r.detail };\n } else {\n const failedRuns = (prev.failedRuns || 0) + 1;\n const shouldAlert = failedRuns >= CONFIG.failureThreshold && (!prev.alerted || (CONFIG.reminderEveryFailedRuns > 0 && failedRuns % CONFIG.reminderEveryFailedRuns === 0));\n staticData.services[r.key] = { failedRuns, alerted: prev.alerted || shouldAlert, lastFailure: now, lastStatus: r.status, lastDetail: r.detail };\n if (shouldAlert) alerts.push({ ...r, failedRuns, suggestedFix: suggestedFix(r) });\n }\n}\n\nif (!alerts.length && !recoveries.length) return [];\nlet lines = [];\nif (alerts.length) {\n lines.push('\\u{1F6A8} Swarm Health Watchdog');\n for (const a of alerts) {\n const dh = a.docker;\n const dockerStr = dh ? ` | docker:${dh.status}/${dh.health}/restarts=${dh.restarts}` : '';\n lines.push(`- ${a.name} :${a.port} failed ${a.failedRuns} checks; status=${a.status}${dockerStr}; detail=${a.detail || 'n/a'}; fix=${a.suggestedFix}`);\n }\n}\nif (recoveries.length) {\n lines.push('\\u2705 Swarm service recovered');\n for (const r of recoveries) {\n const dh = r.docker;\n const dockerStr = dh ? ` | docker:${dh.status}/${dh.health}` : '';\n lines.push(`- ${r.name} :${r.port} healthy again; status=${r.status}; latency=${r.ms}ms${dockerStr}`);\n }\n}\nlines.push(`checked=${now}`);\nreturn [{ json: { text: lines.join('\\n'), alerts, recoveries, results, dockerHealth, checkedAt: now } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -340, + 0 + ], + "id": "b3f76d53-204b-45bb-9a48-8cf20262319d", + "name": "Check Swarm Services" + }, + { + "parameters": { + "chatId": "8367012007", + "text": "={{$json.text}}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + -80, + -80 + ], + "id": "32d7ad9f-80bb-4acf-b546-89f04db32a6a", + "name": "Send Telegram Alert", + "credentials": { + "telegramApi": { + "id": "aox4dyIWVSRdcH5z", + "name": "Telegram Bot (OpenClaw)" + } + } + }, + { + "parameters": { + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "method": "POST", + "url": "https://discord.com/api/v10/channels/425781661268049931/messages", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { content: $json.text } }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -80, + 100 + ], + "id": "7eb589f5-6e50-4e1e-8a37-391f06785ad87", + "name": "Send Discord Alert", + "credentials": { + "httpHeaderAuth": { + "id": "UgPqYcoCNNIgr55m", + "name": "Discord Bot Auth" + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Check Swarm Services", + "type": "main", + "index": 0 + } + ] + ] + }, + "Every 15 Minutes": { + "main": [ + [ + { + "node": "Check Swarm Services", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Swarm Services": { + "main": [ + [ + { + "node": "Send Telegram Alert", + "type": "main", + "index": 0 + }, + { + "node": "Send Discord Alert", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "will will", + "name": null, + "description": null, + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-05-14T00:09:09.344Z", + "id": 1489, + "workflowId": "lDKocSFXBQWQrDd3", + "versionId": "eec5521b-fb44-44ea-b238-aff842560f98", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:32:57.833Z", + "id": 1495, + "workflowId": "lDKocSFXBQWQrDd3", + "versionId": "eec5521b-fb44-44ea-b238-aff842560f98", + "event": "activated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + }, + { + "createdAt": "2026-05-14T00:32:57.790Z", + "id": 1494, + "workflowId": "lDKocSFXBQWQrDd3", + "versionId": "eec5521b-fb44-44ea-b238-aff842560f98", + "event": "deactivated", + "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + } + ] + } +}