chore(n8n): restore workflow exports

This commit is contained in:
William Valentin
2026-06-04 13:26:50 -07:00
parent 99a4f93ce7
commit 4815750011
17 changed files with 5158 additions and 0 deletions
@@ -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 <will@wills-portal.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"
}
}
],
"tags": [],
"activeVersion": null
}
@@ -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 <will@wills-portal.com>",
"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"
}
]
}
}
@@ -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 <will@wills-portal.com>",
"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"
}
]
}
}
@@ -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, '<url>')\n .replace(/[0-9a-f]{8,}/gi, '<hex>')\n .replace(/\\b\\d{4,}\\b/g, '<num>')\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 <will@wills-portal.com>",
"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, '<url>')\n .replace(/[0-9a-f]{8,}/gi, '<hex>')\n .replace(/\\b\\d{4,}\\b/g, '<num>')\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"
}
]
}
}
@@ -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 <will@wills-portal.com>",
"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"
}
]
}
}
@@ -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 <will@wills-portal.com>",
"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"
}
]
}
}
@@ -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 <will@wills-portal.com>",
"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"
}
]
}
}
File diff suppressed because one or more lines are too long
@@ -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 <b>, </b>, <code>, </code>, <br>, 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('<think>[\\\\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 <will@wills-portal.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"
}
}
],
"versionMetadata": {
"name": null,
"description": null
}
}
]
@@ -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 <will@wills-portal.com>","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}]
@@ -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 <will@wills-portal.com>","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}]
File diff suppressed because one or more lines are too long
@@ -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 <will@wills-portal.com>","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}]
File diff suppressed because one or more lines are too long
@@ -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 <will@wills-portal.com>","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"versionMetadata":{"name":null,"description":null}}]
@@ -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 <will@wills-portal.com>",
"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"
}
]
}
}
File diff suppressed because one or more lines are too long