From 733b32b6cd7d0d29ba4fd583e074bf47e5802ce4 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 13 May 2026 14:30:28 -0700 Subject: [PATCH] fix(n8n): update IMAP Inbox Triage workflow container URLs from stale 192.168.153.130 to Docker bridge gateway 172.19.0.1 - Judge with Local LLM: http://172.19.0.1:18806/v1/chat/completions - Write Email to Vault: http://172.19.0.1:27123/vault/... - Workflow 9sFwRyUDz51csAp7 deactivated, updated, and reactivated --- swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json diff --git a/swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json b/swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json new file mode 100644 index 0000000..60e59dc --- /dev/null +++ b/swarm-common/n8n-workflows/9sFwRyUDz51csAp7.json @@ -0,0 +1 @@ +{"updatedAt":"2026-05-13T21:29:35.440Z","createdAt":"2026-03-18T05:20:48.223Z","id":"9sFwRyUDz51csAp7","name":"IMAP Inbox Triage + Obsidian Notes","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":15}]}},"id":"n1","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1,"position":[240,304]},{"parameters":{"jsCode":"// DEFINITE NOISE - never worth seeing\nconst NOISE_SENDERS = [\n 'discord', 'plex', 'spotify', 'youtube',\n 'lodge at redmond ridge', 'flex +',\n 'seattle jeep',\n 'no-reply', 'noreply', 'do-not-reply', 'donotreply',\n 'newsletter', 'marketing',\n];\nconst NOISE_SUBJECTS = [\n 'bulletin board', 'daily digest', 'weekly digest',\n 'most watchlisted', 'newsletter',\n 'mentioned you in',\n 'looking to see what your car',\n 'take your favorite music',\n 'introducing the take',\n];\n\n// DEFINITE SIGNAL - always pass through, skip LLM\nconst SIGNAL_PATTERNS = [\n 'login attempt', 'unauthorized', 'unusual sign',\n 'invoice', 'payment due', 'receipt',\n 'urgent', 'action required',\n 'password reset', 'verify your',\n 'github', 'gitea',\n];\n\nconst items = $input.all();\nif (items.length === 0) return [];\n\n// Ignore schedule/no-email pass-through items from polling mode\nconst emailish = items.filter(item => {\n const j = item.json || {};\n return !!(j.from || j.subject || j.text || j.textPlain || j.textHtml || j.html || j.headers || j.messageId);\n});\nif (emailish.length === 0) return [];\n\n\nconst definiteSignal = [];\nconst needsJudgement = [];\n\nfor (const item of items) {\n const from = (item.json.from || '').toLowerCase();\n const subject = (item.json.subject || '').toLowerCase();\n const combined = from + ' ' + subject;\n\n // Definite signal - fast path, no LLM needed\n if (SIGNAL_PATTERNS.some(p => combined.includes(p))) {\n definiteSignal.push({ ...item.json, _stage1: 'definite_signal', _account: item.json._account || 'unknown' });\n continue;\n }\n\n // Definite noise - drop\n const isNoise = \n NOISE_SENDERS.some(n => combined.includes(n)) ||\n NOISE_SUBJECTS.some(n => new RegExp(n, 'i').test(combined));\n if (isNoise) continue;\n\n // Everything else - send to LLM for judgement\n needsJudgement.push({ ...item.json, _stage1: 'needs_judgement', _account: item.json._account || 'unknown' });\n}\n\n// Return all items for next node; tag them so we can route\nconst all = [...definiteSignal, ...needsJudgement];\nif (all.length === 0) return [{ json: { _empty: true } }];\nreturn all.map(j => ({ json: j }));"},"id":"n2","name":"Stage 1 - Static Filter","type":"n8n-nodes-base.code","typeVersion":2,"position":[464,304]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose"},"conditions":[{"id":"c1","leftValue":"={{ $json._empty }}","rightValue":true,"operator":{"type":"boolean","operation":"notEquals"}}],"combinator":"and"},"options":{}},"id":"n3","name":"Any Left?","type":"n8n-nodes-base.if","typeVersion":2,"position":[688,304]},{"parameters":{"conditions":{"string":[{"value1":"={{ $json._stage1 }}","value2":"needs_judgement"}]}},"id":"n4","name":"Needs LLM Judgement?","type":"n8n-nodes-base.if","typeVersion":1,"position":[912,208]},{"parameters":{"method":"POST","url":"http://172.19.0.1:18806/v1/chat/completions","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"contentType":"raw","rawContentType":"application/json","body":"={\"model\": \"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\", \"temperature\": 0, \"max_tokens\": 60, \"messages\": [{\"role\": \"system\", \"content\": \"You are an email triage assistant for a software developer. Emails may be in any language \\u2014 translate mentally before judging. Reply with JSON only: {\\\"signal\\\": true|false, \\\"priority\\\": 1|2|3, \\\"reason\\\": \\\"one short phrase\\\"}. Priority: 1=act now, 2=read today, 3=FYI. Signal=false means drop silently. Always mark security alerts (login attempts, account access, suspicious activity) as signal priority 1, regardless of language.\"}, {\"role\": \"user\", \"content\": \"From: {{ $json.from }}\\nSubject: {{ $json.subject }}\"}]}","options":{"response":{"response":{"responseFormat":"json"}},"timeout":15000}},"id":"n5","name":"Judge with Local LLM","type":"n8n-nodes-base.httpRequest","typeVersion":4,"position":[1120,128]},{"parameters":{"jsCode":"const item = $input.first();\nconst inputItem = $('Needs LLM Judgement?').first();\n\ntry {\n let content = '';\n const j = item.json || {};\n\n if (j.choices && j.choices[0] && j.choices[0].message) {\n content = j.choices[0].message.content || '';\n } else if (j._readableState && j._readableState.buffer && j._readableState.buffer[0] && j._readableState.buffer[0].data) {\n const bytes = j._readableState.buffer[0].data;\n const raw = Buffer.from(bytes).toString('utf8');\n const parsed = JSON.parse(raw);\n content = parsed.choices[0].message.content || '';\n }\n\n content = content.trim();\n if (!content) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_empty', _priority: 3, _reason: 'no llm response' } }];\n }\n\n // Strip markdown code fences\n const cleaned = content.replace(/^[^\\{]*/, '').replace(/[^\\}]*$/, '').trim();\n const result = JSON.parse(cleaned);\n\n if (!result.signal) return [];\n\n return [{ json: { ...inputItem.json, _stage2: 'llm_signal', _priority: result.priority || 3, _reason: result.reason || '' } }];\n} catch(e) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_parse_error', _priority: 3, _reason: 'parse error: ' + e.message } }];\n}"},"id":"n6","name":"Parse LLM Result","type":"n8n-nodes-base.code","typeVersion":2,"position":[1344,128]},{"parameters":{"jsCode":"const results = [];\nfor (const item of $input.all()) {\n const j = item.json || {};\n results.push({\n json: {\n from: String(j.from || ''),\n subject: String(j.subject || ''),\n date: String(j.date || ''),\n textPlain: String(j.textPlain || j.text || '').substring(0, 500),\n messageId: String(j.messageId || ''),\n _account: String(j._account || 'unknown'),\n _stage1: 'definite_signal',\n _stage2: 'definite_signal',\n _priority: 1,\n _reason: 'pattern match'\n }\n });\n}\nreturn results;"},"id":"n7","name":"Tag Definite Signal","type":"n8n-nodes-base.code","typeVersion":2,"position":[1120,304]},{"parameters":{"aggregate":"aggregateAllItemData","destinationFieldName":"messages","options":{}},"id":"n8","name":"Merge All Signal","type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[1568,208]},{"parameters":{"jsCode":"const messages = ($input.first().json.messages || [])\n .sort((a, b) => (a._priority || 3) - (b._priority || 3));\n\nif (messages.length === 0) return [];\n\nconst PRIORITY_EMOJI = { 1: '🔴', 2: '🟡', 3: '🔵' };\n\nconst lines = messages.map((m, i) => {\n const from = (m.from || '(unknown)').replace(/<[^>]+>/g, '').trim().substring(0, 50);\n const subject = (m.subject || '(no subject)').trim().substring(0, 75);\n const emoji = PRIORITY_EMOJI[m._priority] || '🔵';\n const reason = m._reason && m._reason !== 'pattern match' ? ` — _${m._reason}_` : '';\n const acct = m._account && m._account !== 'unknown' ? ` [${m._account}]` : '';\n return `${emoji} ${subject}\\n ${from}${acct}${reason}`;\n});\n\nconst text = `📬 *${messages.length} new email${messages.length > 1 ? 's' : ''}*\\n\\n${lines.join('\\n\\n')}`;\nreturn [{ json: { text } }];"},"id":"n9","name":"Format & Send","type":"n8n-nodes-base.code","typeVersion":2,"position":[1792,128]},{"parameters":{"chatId":"8367012007","text":"={{ $json.text }}","additionalFields":{"parse_mode":"Markdown"}},"id":"n10","name":"Send to Telegram","type":"n8n-nodes-base.telegram","typeVersion":1,"position":[2000,128],"webhookId":"795a0fc5-c932-4265-bd0d-095dd410f8a8","credentials":{"telegramApi":{"id":"aox4dyIWVSRdcH5z","name":"Telegram Bot (OpenClaw)"}}},{"parameters":{},"id":"n11","name":"Silent Stop","type":"n8n-nodes-base.noOp","typeVersion":1,"position":[688,464]},{"parameters":{"jsCode":"const wrapper = $input.first().json;\nconst messages = wrapper.messages || [];\nconst results = [];\n\nfor (const item of messages) {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const subject = (item.subject || 'No Subject').replace(/[\\/\\\\?%*:|\"<>]/g, '-').substring(0, 80);\n const from = (item.from || 'unknown').replace(/<[^>]+>/g, '').trim();\n const snippet = (item.textPlain || '').substring(0, 500);\n const priority = item._priority || 3;\n const reason = item._reason || '';\n const PRIORITY_LABEL = {1: 'high', 2: 'medium', 3: 'low'};\n const PRIORITY_TAG = {1: 'priority-high', 2: 'priority-medium', 3: 'priority-low'};\n const frontmatter = '---\\ntitle: \"' + subject + '\"\\narea: notes\\ntags: [email, imap, ' + PRIORITY_TAG[priority] + ']\\ncreated: ' + date + '\\nupdated: ' + date + '\\nstatus: active\\nfrom: \"' + from + '\"\\npriority: ' + PRIORITY_LABEL[priority] + '\\nsignal_reason: \"' + reason + '\"\\n---';\n const content = frontmatter + '\\n\\n# ' + subject + '\\n\\n**From:** ' + from + '\\n**Date:** ' + date + '\\n**Priority:** ' + PRIORITY_LABEL[priority] + (reason ? ' — ' + reason : '') + '\\n\\n## Snippet\\n\\n' + snippet + '\\n\\n## Notes\\n\\n_Add notes here_\\n';\n results.push({ json: { path: 'Notes/' + date + ' ' + subject + '.md', content, subject, from, priority, date } });\n}\nreturn results;"},"id":"n12","name":"Format Email Notes","type":"n8n-nodes-base.code","typeVersion":2,"position":[1792,304]},{"parameters":{"method":"PUT","url":"=http://172.19.0.1:27123/vault/{{ encodeURIComponent($json.path).replace(/%2F/g, \"/\") }}","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{ $json.content }}","options":{"response":{"response":{"neverError":true}}}},"id":"n13","name":"Write Email to Vault","type":"n8n-nodes-base.httpRequest","typeVersion":4,"position":[2000,304],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}},"onError":"continueRegularOutput"},{"parameters":{"options":{}},"id":"n1a","name":"Read Unseen Emails","type":"n8n-nodes-base.emailReadImap","typeVersion":2,"position":[352,656],"credentials":{"imap":{"id":"5qGEXTjFtPUZL8BB","name":"wills_portal IMAP"}}}],"connections":{"Stage 1 - Static Filter":{"main":[[{"node":"Any Left?","type":"main","index":0}]]},"Any Left?":{"main":[[{"node":"Needs LLM Judgement?","type":"main","index":0}],[{"node":"Silent Stop","type":"main","index":0}]]},"Needs LLM Judgement?":{"main":[[{"node":"Judge with Local LLM","type":"main","index":0}],[{"node":"Tag Definite Signal","type":"main","index":0}]]},"Judge with Local LLM":{"main":[[{"node":"Parse LLM Result","type":"main","index":0}]]},"Parse LLM Result":{"main":[[{"node":"Merge All Signal","type":"main","index":0}]]},"Tag Definite Signal":{"main":[[{"node":"Merge All Signal","type":"main","index":0}]]},"Merge All Signal":{"main":[[{"node":"Format & Send","type":"main","index":0},{"node":"Format Email Notes","type":"main","index":0}]]},"Format & Send":{"main":[[{"node":"Send to Telegram","type":"main","index":0}]]},"Format Email Notes":{"main":[[{"node":"Write Email to Vault","type":"main","index":0}]]},"Schedule Trigger":{"main":[[{"node":"Read Unseen Emails","type":"main","index":0}]]},"Read Unseen Emails":{"main":[[{"node":"Stage 1 - Static Filter","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false,"saveDataSuccessExecution":"all","saveDataErrorExecution":"all","saveManualExecutions":true},"staticData":{"node:Schedule Trigger":{"recurrenceRules":[]},"node:Read Unseen Emails":{},"node:Read wills_portal":{"lastMessageUid":8770},"node:Read squareffect":{},"node:Schedule wills_portal":{"recurrenceRules":[]},"node:Schedule squareffect":{"recurrenceRules":[]},"node:Email Trigger":{}},"meta":null,"versionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","activeVersionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","versionCounter":3814,"triggerCount":2,"shared":[{"updatedAt":"2026-03-18T05:20:48.224Z","createdAt":"2026-03-18T05:20:48.224Z","role":"workflow:owner","workflowId":"9sFwRyUDz51csAp7","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"tags":[{"updatedAt":"2026-03-19T04:40:29.921Z","createdAt":"2026-03-19T04:40:29.921Z","id":"R9u3nhZlt6Vanvus","name":"telegram"},{"updatedAt":"2026-03-19T04:40:29.892Z","createdAt":"2026-03-19T04:40:29.892Z","id":"VfqIkUpiu2YMBSHw","name":"obsidian-sync"},{"updatedAt":"2026-03-19T04:40:29.877Z","createdAt":"2026-03-19T04:40:29.877Z","id":"qu6qwIegC1LgLKoA","name":"email-triage"},{"updatedAt":"2026-03-19T04:40:29.909Z","createdAt":"2026-03-19T04:40:29.909Z","id":"r3vsVtTwe9UfLrGi","name":"imap"},{"updatedAt":"2026-03-19T04:40:29.926Z","createdAt":"2026-03-19T04:40:29.926Z","id":"zKN5N7wCrUuKB7rV","name":"llm"}],"activeVersion":{"updatedAt":"2026-05-13T21:29:35.443Z","createdAt":"2026-05-13T21:29:35.443Z","versionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","workflowId":"9sFwRyUDz51csAp7","nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":15}]}},"id":"n1","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1,"position":[240,304]},{"parameters":{"jsCode":"// DEFINITE NOISE - never worth seeing\nconst NOISE_SENDERS = [\n 'discord', 'plex', 'spotify', 'youtube',\n 'lodge at redmond ridge', 'flex +',\n 'seattle jeep',\n 'no-reply', 'noreply', 'do-not-reply', 'donotreply',\n 'newsletter', 'marketing',\n];\nconst NOISE_SUBJECTS = [\n 'bulletin board', 'daily digest', 'weekly digest',\n 'most watchlisted', 'newsletter',\n 'mentioned you in',\n 'looking to see what your car',\n 'take your favorite music',\n 'introducing the take',\n];\n\n// DEFINITE SIGNAL - always pass through, skip LLM\nconst SIGNAL_PATTERNS = [\n 'login attempt', 'unauthorized', 'unusual sign',\n 'invoice', 'payment due', 'receipt',\n 'urgent', 'action required',\n 'password reset', 'verify your',\n 'github', 'gitea',\n];\n\nconst items = $input.all();\nif (items.length === 0) return [];\n\n// Ignore schedule/no-email pass-through items from polling mode\nconst emailish = items.filter(item => {\n const j = item.json || {};\n return !!(j.from || j.subject || j.text || j.textPlain || j.textHtml || j.html || j.headers || j.messageId);\n});\nif (emailish.length === 0) return [];\n\n\nconst definiteSignal = [];\nconst needsJudgement = [];\n\nfor (const item of items) {\n const from = (item.json.from || '').toLowerCase();\n const subject = (item.json.subject || '').toLowerCase();\n const combined = from + ' ' + subject;\n\n // Definite signal - fast path, no LLM needed\n if (SIGNAL_PATTERNS.some(p => combined.includes(p))) {\n definiteSignal.push({ ...item.json, _stage1: 'definite_signal', _account: item.json._account || 'unknown' });\n continue;\n }\n\n // Definite noise - drop\n const isNoise = \n NOISE_SENDERS.some(n => combined.includes(n)) ||\n NOISE_SUBJECTS.some(n => new RegExp(n, 'i').test(combined));\n if (isNoise) continue;\n\n // Everything else - send to LLM for judgement\n needsJudgement.push({ ...item.json, _stage1: 'needs_judgement', _account: item.json._account || 'unknown' });\n}\n\n// Return all items for next node; tag them so we can route\nconst all = [...definiteSignal, ...needsJudgement];\nif (all.length === 0) return [{ json: { _empty: true } }];\nreturn all.map(j => ({ json: j }));"},"id":"n2","name":"Stage 1 - Static Filter","type":"n8n-nodes-base.code","typeVersion":2,"position":[464,304]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose"},"conditions":[{"id":"c1","leftValue":"={{ $json._empty }}","rightValue":true,"operator":{"type":"boolean","operation":"notEquals"}}],"combinator":"and"},"options":{}},"id":"n3","name":"Any Left?","type":"n8n-nodes-base.if","typeVersion":2,"position":[688,304]},{"parameters":{"conditions":{"string":[{"value1":"={{ $json._stage1 }}","value2":"needs_judgement"}]}},"id":"n4","name":"Needs LLM Judgement?","type":"n8n-nodes-base.if","typeVersion":1,"position":[912,208]},{"parameters":{"method":"POST","url":"http://172.19.0.1:18806/v1/chat/completions","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"contentType":"raw","rawContentType":"application/json","body":"={\"model\": \"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\", \"temperature\": 0, \"max_tokens\": 60, \"messages\": [{\"role\": \"system\", \"content\": \"You are an email triage assistant for a software developer. Emails may be in any language \\u2014 translate mentally before judging. Reply with JSON only: {\\\"signal\\\": true|false, \\\"priority\\\": 1|2|3, \\\"reason\\\": \\\"one short phrase\\\"}. Priority: 1=act now, 2=read today, 3=FYI. Signal=false means drop silently. Always mark security alerts (login attempts, account access, suspicious activity) as signal priority 1, regardless of language.\"}, {\"role\": \"user\", \"content\": \"From: {{ $json.from }}\\nSubject: {{ $json.subject }}\"}]}","options":{"response":{"response":{"responseFormat":"json"}},"timeout":15000}},"id":"n5","name":"Judge with Local LLM","type":"n8n-nodes-base.httpRequest","typeVersion":4,"position":[1120,128]},{"parameters":{"jsCode":"const item = $input.first();\nconst inputItem = $('Needs LLM Judgement?').first();\n\ntry {\n let content = '';\n const j = item.json || {};\n\n if (j.choices && j.choices[0] && j.choices[0].message) {\n content = j.choices[0].message.content || '';\n } else if (j._readableState && j._readableState.buffer && j._readableState.buffer[0] && j._readableState.buffer[0].data) {\n const bytes = j._readableState.buffer[0].data;\n const raw = Buffer.from(bytes).toString('utf8');\n const parsed = JSON.parse(raw);\n content = parsed.choices[0].message.content || '';\n }\n\n content = content.trim();\n if (!content) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_empty', _priority: 3, _reason: 'no llm response' } }];\n }\n\n // Strip markdown code fences\n const cleaned = content.replace(/^[^\\{]*/, '').replace(/[^\\}]*$/, '').trim();\n const result = JSON.parse(cleaned);\n\n if (!result.signal) return [];\n\n return [{ json: { ...inputItem.json, _stage2: 'llm_signal', _priority: result.priority || 3, _reason: result.reason || '' } }];\n} catch(e) {\n return [{ json: { ...inputItem.json, _stage2: 'llm_parse_error', _priority: 3, _reason: 'parse error: ' + e.message } }];\n}"},"id":"n6","name":"Parse LLM Result","type":"n8n-nodes-base.code","typeVersion":2,"position":[1344,128]},{"parameters":{"jsCode":"const results = [];\nfor (const item of $input.all()) {\n const j = item.json || {};\n results.push({\n json: {\n from: String(j.from || ''),\n subject: String(j.subject || ''),\n date: String(j.date || ''),\n textPlain: String(j.textPlain || j.text || '').substring(0, 500),\n messageId: String(j.messageId || ''),\n _account: String(j._account || 'unknown'),\n _stage1: 'definite_signal',\n _stage2: 'definite_signal',\n _priority: 1,\n _reason: 'pattern match'\n }\n });\n}\nreturn results;"},"id":"n7","name":"Tag Definite Signal","type":"n8n-nodes-base.code","typeVersion":2,"position":[1120,304]},{"parameters":{"aggregate":"aggregateAllItemData","destinationFieldName":"messages","options":{}},"id":"n8","name":"Merge All Signal","type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[1568,208]},{"parameters":{"jsCode":"const messages = ($input.first().json.messages || [])\n .sort((a, b) => (a._priority || 3) - (b._priority || 3));\n\nif (messages.length === 0) return [];\n\nconst PRIORITY_EMOJI = { 1: '🔴', 2: '🟡', 3: '🔵' };\n\nconst lines = messages.map((m, i) => {\n const from = (m.from || '(unknown)').replace(/<[^>]+>/g, '').trim().substring(0, 50);\n const subject = (m.subject || '(no subject)').trim().substring(0, 75);\n const emoji = PRIORITY_EMOJI[m._priority] || '🔵';\n const reason = m._reason && m._reason !== 'pattern match' ? ` — _${m._reason}_` : '';\n const acct = m._account && m._account !== 'unknown' ? ` [${m._account}]` : '';\n return `${emoji} ${subject}\\n ${from}${acct}${reason}`;\n});\n\nconst text = `📬 *${messages.length} new email${messages.length > 1 ? 's' : ''}*\\n\\n${lines.join('\\n\\n')}`;\nreturn [{ json: { text } }];"},"id":"n9","name":"Format & Send","type":"n8n-nodes-base.code","typeVersion":2,"position":[1792,128]},{"parameters":{"chatId":"8367012007","text":"={{ $json.text }}","additionalFields":{"parse_mode":"Markdown"}},"id":"n10","name":"Send to Telegram","type":"n8n-nodes-base.telegram","typeVersion":1,"position":[2000,128],"webhookId":"795a0fc5-c932-4265-bd0d-095dd410f8a8","credentials":{"telegramApi":{"id":"aox4dyIWVSRdcH5z","name":"Telegram Bot (OpenClaw)"}}},{"parameters":{},"id":"n11","name":"Silent Stop","type":"n8n-nodes-base.noOp","typeVersion":1,"position":[688,464]},{"parameters":{"jsCode":"const wrapper = $input.first().json;\nconst messages = wrapper.messages || [];\nconst results = [];\n\nfor (const item of messages) {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const subject = (item.subject || 'No Subject').replace(/[\\/\\\\?%*:|\"<>]/g, '-').substring(0, 80);\n const from = (item.from || 'unknown').replace(/<[^>]+>/g, '').trim();\n const snippet = (item.textPlain || '').substring(0, 500);\n const priority = item._priority || 3;\n const reason = item._reason || '';\n const PRIORITY_LABEL = {1: 'high', 2: 'medium', 3: 'low'};\n const PRIORITY_TAG = {1: 'priority-high', 2: 'priority-medium', 3: 'priority-low'};\n const frontmatter = '---\\ntitle: \"' + subject + '\"\\narea: notes\\ntags: [email, imap, ' + PRIORITY_TAG[priority] + ']\\ncreated: ' + date + '\\nupdated: ' + date + '\\nstatus: active\\nfrom: \"' + from + '\"\\npriority: ' + PRIORITY_LABEL[priority] + '\\nsignal_reason: \"' + reason + '\"\\n---';\n const content = frontmatter + '\\n\\n# ' + subject + '\\n\\n**From:** ' + from + '\\n**Date:** ' + date + '\\n**Priority:** ' + PRIORITY_LABEL[priority] + (reason ? ' — ' + reason : '') + '\\n\\n## Snippet\\n\\n' + snippet + '\\n\\n## Notes\\n\\n_Add notes here_\\n';\n results.push({ json: { path: 'Notes/' + date + ' ' + subject + '.md', content, subject, from, priority, date } });\n}\nreturn results;"},"id":"n12","name":"Format Email Notes","type":"n8n-nodes-base.code","typeVersion":2,"position":[1792,304]},{"parameters":{"method":"PUT","url":"=http://172.19.0.1:27123/vault/{{ encodeURIComponent($json.path).replace(/%2F/g, \"/\") }}","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","sendBody":true,"contentType":"raw","rawContentType":"text/markdown","body":"={{ $json.content }}","options":{"response":{"response":{"neverError":true}}}},"id":"n13","name":"Write Email to Vault","type":"n8n-nodes-base.httpRequest","typeVersion":4,"position":[2000,304],"credentials":{"httpHeaderAuth":{"id":"465Swz2b71O2KRAK","name":"Obsidian Local REST API"}},"onError":"continueRegularOutput"},{"parameters":{"options":{}},"id":"n1a","name":"Read Unseen Emails","type":"n8n-nodes-base.emailReadImap","typeVersion":2,"position":[352,656],"credentials":{"imap":{"id":"5qGEXTjFtPUZL8BB","name":"wills_portal IMAP"}}}],"connections":{"Stage 1 - Static Filter":{"main":[[{"node":"Any Left?","type":"main","index":0}]]},"Any Left?":{"main":[[{"node":"Needs LLM Judgement?","type":"main","index":0}],[{"node":"Silent Stop","type":"main","index":0}]]},"Needs LLM Judgement?":{"main":[[{"node":"Judge with Local LLM","type":"main","index":0}],[{"node":"Tag Definite Signal","type":"main","index":0}]]},"Judge with Local LLM":{"main":[[{"node":"Parse LLM Result","type":"main","index":0}]]},"Parse LLM Result":{"main":[[{"node":"Merge All Signal","type":"main","index":0}]]},"Tag Definite Signal":{"main":[[{"node":"Merge All Signal","type":"main","index":0}]]},"Merge All Signal":{"main":[[{"node":"Format & Send","type":"main","index":0},{"node":"Format Email Notes","type":"main","index":0}]]},"Format & Send":{"main":[[{"node":"Send to Telegram","type":"main","index":0}]]},"Format Email Notes":{"main":[[{"node":"Write Email to Vault","type":"main","index":0}]]},"Schedule Trigger":{"main":[[{"node":"Read Unseen Emails","type":"main","index":0}]]},"Read Unseen Emails":{"main":[[{"node":"Stage 1 - Static Filter","type":"main","index":0}]]}},"authors":"will will","name":null,"description":null,"autosaved":false,"workflowPublishHistory":[{"createdAt":"2026-05-13T21:29:37.830Z","id":1426,"workflowId":"9sFwRyUDz51csAp7","versionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","event":"activated","userId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"},{"createdAt":"2026-05-13T21:29:47.863Z","id":1428,"workflowId":"9sFwRyUDz51csAp7","versionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","event":"activated","userId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"},{"createdAt":"2026-05-13T21:29:35.811Z","id":1425,"workflowId":"9sFwRyUDz51csAp7","versionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","event":"deactivated","userId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"},{"createdAt":"2026-05-13T21:29:46.033Z","id":1427,"workflowId":"9sFwRyUDz51csAp7","versionId":"4c4e4dab-f40b-48aa-9bd9-8db805bbf1e4","event":"deactivated","userId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}]}} \ No newline at end of file