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