fix(n8n): harden swarm automation workflows

This commit is contained in:
William Valentin
2026-05-13 17:21:39 -07:00
parent ff28a7c1ad
commit 867f879d4a
9 changed files with 3659 additions and 30 deletions
+2 -2
View File
@@ -323,8 +323,8 @@ services:
- N8N_PROTOCOL=http
- N8N_EDITOR_BASE_URL=http://localhost:18808
- WEBHOOK_URL=http://localhost:18808/
- TZ=UTC
- GENERIC_TIMEZONE=UTC
- TZ=America/Los_Angeles
- GENERIC_TIMEZONE=America/Los_Angeles
- N8N_SECURE_COOKIE=false
volumes:
- n8n-agent-data:/home/node/.n8n
File diff suppressed because one or more lines are too long
@@ -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"
}
]
}
}
@@ -1,5 +1,5 @@
{
"updatedAt": "2026-05-12T17:06:07.191Z",
"updatedAt": "2026-05-14T00:18:01.110Z",
"createdAt": "2026-05-12T16:59:40.394Z",
"id": "G9ylNbHbnJ6fWX2C",
"name": "n8n Failure Digest",
@@ -99,7 +99,7 @@
{
"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 [];\nconst lines = [];\nlines.push(`\ud83d\udea8 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');\nreturn [{ json: { text: lines.join('\\n'), totalFailures: failures.length, groups: sorted, generatedAt: new Date(now).toISOString() } }];"
"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,
@@ -115,7 +115,8 @@
"chatId": "8367012007",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "Markdown"
"parse_mode": "",
"disable_web_page_preview": true
}
},
"type": "n8n-nodes-base.telegram",
@@ -253,10 +254,9 @@
}
},
"meta": null,
"pinData": null,
"versionId": "c198c473-6fed-4fa8-b203-9465bb084e89",
"activeVersionId": "c198c473-6fed-4fa8-b203-9465bb084e89",
"versionCounter": 19,
"versionId": "2d85e3bf-d8cf-4274-bf61-5377241897da",
"activeVersionId": "2d85e3bf-d8cf-4274-bf61-5377241897da",
"versionCounter": 36,
"triggerCount": 1,
"shared": [
{
@@ -279,9 +279,9 @@
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-05-13T21:29:18.280Z",
"createdAt": "2026-05-13T21:29:18.280Z",
"versionId": "c198c473-6fed-4fa8-b203-9465bb084e89",
"updatedAt": "2026-05-14T00:18:01.111Z",
"createdAt": "2026-05-14T00:18:01.111Z",
"versionId": "2d85e3bf-d8cf-4274-bf61-5377241897da",
"workflowId": "G9ylNbHbnJ6fWX2C",
"nodes": [
{
@@ -376,7 +376,7 @@
{
"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 [];\nconst lines = [];\nlines.push(`\ud83d\udea8 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');\nreturn [{ json: { text: lines.join('\\n'), totalFailures: failures.length, groups: sorted, generatedAt: new Date(now).toISOString() } }];"
"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,
@@ -392,7 +392,8 @@
"chatId": "8367012007",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "Markdown"
"parse_mode": "",
"disable_web_page_preview": true
}
},
"type": "n8n-nodes-base.telegram",
@@ -516,19 +517,19 @@
]
}
},
"authors": "import",
"authors": "will will",
"name": null,
"description": null,
"autosaved": false,
"workflowPublishHistory": [
{
"createdAt": "2026-05-13T21:30:20.994Z",
"id": 1429,
"createdAt": "2026-05-14T00:18:01.158Z",
"id": 1491,
"workflowId": "G9ylNbHbnJ6fWX2C",
"versionId": "c198c473-6fed-4fa8-b203-9465bb084e89",
"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"
}
]
}
}
@@ -1,5 +1,5 @@
{
"updatedAt": "2026-05-13T21:40:33.847Z",
"updatedAt": "2026-05-14T00:04:59.343Z",
"createdAt": "2026-05-13T21:40:33.847Z",
"id": "PlZywwqL8MRNEAN6",
"name": "Evening Digest",
@@ -161,7 +161,7 @@
"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 }] }) }}",
"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": {
@@ -183,7 +183,7 @@
{
"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}];"
"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",
@@ -396,7 +396,8 @@
"settings": {
"executionOrder": "v1",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false
"availableInMCP": false,
"timezone": "America/Los_Angeles"
},
"staticData": {
"node:Daily 9PM Schedule": {
@@ -407,7 +408,7 @@
"pinData": null,
"versionId": "afb71f4d-6ac3-434d-b659-de003d47c339",
"activeVersionId": "afb71f4d-6ac3-434d-b659-de003d47c339",
"versionCounter": 4,
"versionCounter": 11,
"triggerCount": 1,
"shared": [
{
@@ -589,7 +590,7 @@
"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 }] }) }}",
"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": {
@@ -611,7 +612,7 @@
{
"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}];"
"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",
@@ -833,7 +834,39 @@
"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
File diff suppressed because one or more lines are too long