feat: add Evening Digest n8n workflow (replaces Nightly Obsidian Vault Sync)

- New workflow PlZywwqL8MRNEAN6: Evening Digest
  - Daily 9PM America/Los_Angeles schedule trigger
  - Collects: n8n success/error executions, swarm health, new Obsidian notes
  - LLM synthesis via local gemma-4-26B for digest generation
  - Delivers to Telegram, Discord, and Obsidian vault
  - All collection nodes have continueOnFail for resilience

- Deactivated old Nightly Obsidian Vault Sync (75JCevkdgkyCr2qH)
- Exported both workflows + updated Failure Digest export
This commit is contained in:
William Valentin
2026-05-13 14:41:46 -07:00
parent b3eefc4d14
commit 9b11016340
3 changed files with 1468 additions and 1 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
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,839 @@
{
"updatedAt": "2026-05-13T21:40:33.847Z",
"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: \ud83d\udd27 Executions Summary, \u26a0\ufe0f Failures, \ud83d\udcdd New Notes, \ud83c\udfe5 Infrastructure Health, \ud83d\udccb 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 = '\ud83c\udf19 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
},
"staticData": {
"node:Daily 9PM Schedule": {
"recurrenceRules": []
}
},
"meta": null,
"pinData": null,
"versionId": "afb71f4d-6ac3-434d-b659-de003d47c339",
"activeVersionId": "afb71f4d-6ac3-434d-b659-de003d47c339",
"versionCounter": 4,
"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: \ud83d\udd27 Executions Summary, \u26a0\ufe0f Failures, \ud83d\udcdd New Notes, \ud83c\udfe5 Infrastructure Health, \ud83d\udccb 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 = '\ud83c\udf19 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"
}
]
}
}