[ { "updatedAt": "2026-05-20T16:30:18.000Z", "createdAt": "2026-05-13T21:41:17.798Z", "id": "g3IdGZCK1EtTsv9T", "name": "Morning Brief", "description": null, "active": true, "isArchived": false, "nodes": [ { "parameters": { "rule": { "interval": [ { "field": "cronExpression", "expression": "30 6 * * *" } ] } }, "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.3, "position": [ 0, 0 ], "id": "16110cb5-e50a-4d99-a613-448057221422", "name": "Daily 06:30 PT" }, { "parameters": { "method": "GET", "url": "http://wttr.in/Seattle?format=j1", "options": { "timeout": 10000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 300, -400 ], "id": "a119dfe9-46db-43ca-98b2-f0690bc0f6f5", "name": "Weather", "continueOnFail": true }, { "parameters": { "method": "GET", "url": "http://172.19.0.1:18809/health", "options": { "timeout": 10000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 300, -250 ], "id": "05f60eba-ab11-4fe0-b761-d1ca9ae557d4", "name": "Swarm Health", "continueOnFail": true }, { "parameters": { "method": "GET", "url": "http://127.0.0.1:5678/healthz", "options": { "timeout": 10000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 300, -100 ], "id": "4b5c3f4c-7f11-4e0c-9c56-3b8596a1d25d", "name": "n8n Health", "continueOnFail": true }, { "parameters": { "method": "GET", "url": "http://172.19.0.1:18804/health/liveliness", "options": { "timeout": 10000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 300, 50 ], "id": "a8e4e45c-60a1-4f90-8ecc-49782d7be900", "name": "LiteLLM Health", "continueOnFail": true }, { "parameters": { "method": "GET", "url": "http://127.0.0.1:5678/api/v1/executions", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "workflowId", "value": "9sFwRyUDz51csAp7" }, { "name": "limit", "value": "5" }, { "name": "status", "value": "success" } ] }, "options": { "timeout": 15000 }, "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth" }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 300, 200 ], "id": "c688abdf-9b63-43b4-81da-7c81388b73f8", "name": "Email Highlights", "continueOnFail": true, "credentials": { "httpHeaderAuth": { "id": "UPAHgUJVRqZQceL4", "name": "n8n Public API (Failure Digest)" } } }, { "parameters": { "method": "GET", "url": "=https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin={{ $now.format('yyyy-MM-dd') }}T00:00:00-07:00&timeMax={{ $now.plus({days:1}).format('yyyy-MM-dd') }}T23:59:59-07:00&singleEvents=true&orderBy=startTime", "authentication": "oAuth2", "options": { "timeout": 10000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 300, 350 ], "id": "d3c5a4ce-9f81-4da8-8dc8-7256bd96285b", "name": "Calendar", "credentials": { "oAuth2Api": { "id": "458fY4bs1z49OTeZ", "name": "Google OAuth" } }, "continueOnFail": true }, { "parameters": { "mode": "runOnceForAllItems", "jsCode": "\nfunction getSafe(nodeName) {\n try {\n const items = $(nodeName).all();\n if (items && items.length > 0 && items[0].json) {\n return items[0].json;\n }\n } catch (e) {}\n return { error: 'Node failed or returned no data' };\n}\n\nfunction parseMaybeJson(value) {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch (e) {\n return { error: 'Weather JSON parse failed', raw: value.slice(0, 200) };\n }\n}\n\nconst weather = parseMaybeJson(getSafe('Weather'));\nconst swarmHealth = getSafe('Swarm Health');\nconst n8nHealth = getSafe('n8n Health');\nconst litellmHealth = getSafe('LiteLLM Health');\nconst emailData = getSafe('Email Highlights');\nconst calendar = getSafe('Calendar');\n\n// Extract weather summary\nlet weatherSummary = {};\nif (weather.current_condition && weather.current_condition[0]) {\n const c = weather.current_condition[0];\n weatherSummary = {\n temp_F: c.FeelsLikeF || c.temp_F,\n description: c.weatherDesc ? c.weatherDesc[0].value : 'unknown',\n humidity: c.humidity,\n wind_mph: c.windspeedMiles\n };\n} else {\n weatherSummary = { error: weather.error || 'Weather data unavailable' };\n}\n\n// Count healthy/unhealthy containers\nlet infraSummary = { healthy: 0, unhealthy: 0, details: [] };\nif (Array.isArray(swarmHealth)) {\n for (const c of swarmHealth) {\n if (c.health === 'healthy' || c.status === 'running') {\n infraSummary.healthy++;\n } else {\n infraSummary.unhealthy++;\n }\n infraSummary.details.push({ name: c.name || c.Names, status: c.status, health: c.health });\n }\n} else if (swarmHealth.containers && Array.isArray(swarmHealth.containers)) {\n for (const c of swarmHealth.containers) {\n if (c.health === 'healthy' || c.status === 'running') {\n infraSummary.healthy++;\n } else {\n infraSummary.unhealthy++;\n }\n infraSummary.details.push({ name: c.name, status: c.status, health: c.health });\n }\n} else if (swarmHealth.error) {\n infraSummary = { error: 'Swarm health endpoint unavailable' };\n}\n\nconst n8nOk = (n8nHealth && !n8nHealth.error);\nconst litellmOk = (litellmHealth && !litellmHealth.error);\n\n// Extract email info from execution data\nlet emailHighlights = [];\nif (emailData && emailData.data && Array.isArray(emailData.data)) {\n for (const exec of emailData.data.slice(0, 5)) {\n emailHighlights.push({\n id: exec.id,\n finished: exec.stoppedAt || 'unknown'\n });\n }\n}\n\n// Calendar events\nlet calendarEvents = [];\nif (calendar && calendar.items && Array.isArray(calendar.items)) {\n for (const ev of calendar.items.slice(0, 10)) {\n calendarEvents.push({\n summary: ev.summary || '(no title)',\n start: (ev.start && (ev.start.dateTime || ev.start.date)) || 'unknown',\n end: (ev.end && (ev.end.dateTime || ev.end.date)) || 'unknown'\n });\n }\n}\n\nconst dataForLLM = {\n date: new Date().toISOString().split('T')[0],\n weather: weatherSummary,\n infrastructure: {\n swarm: infraSummary,\n n8n: n8nOk ? 'healthy' : 'unhealthy',\n litellm: litellmOk ? 'healthy' : 'unhealthy'\n },\n email: emailHighlights.length > 0 ? emailHighlights : [{ info: 'No recent email triage data' }],\n calendar: calendarEvents.length > 0 ? calendarEvents : [{ info: 'Calendar unavailable or no events today' }]\n};\n\nreturn [{ json: { dataJson: JSON.stringify(dataForLLM, null, 2) } }];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 650, 0 ], "id": "1d2b39db-3649-4316-8ce9-b5c83c981017", "name": "Merge Data" }, { "parameters": { "method": "POST", "url": "http://172.19.0.1:18806/v1/chat/completions", "sendBody": true, "specifyBody": "json", "jsonBody": "= {\"model\":\"gemma-4-26B-A4B-it-UD-IQ2_M.gguf\",\"messages\":[{\"role\":\"system\",\"content\":\"You are Will's personal morning brief formatter.\\n\\nReturn ONLY the final Telegram-ready brief. Do not include reasoning, drafts, constraint checks, self-corrections, notes, analysis, or labels like \\\"Details\\\", \\\"Drafting\\\", \\\"Final Polish\\\", or \\\"Self-Correction\\\".\\n\\nUse the same readable style as the old Zap brief: plain Telegram text with lightweight Markdown, not HTML.\\n- Use emojis in section headings.\\n- Use **Heading** for bold headings if useful.\\n- Never output HTML/XML tags: no , , , ,
, or similar.\\n- Use bullet lines starting with \\\"• \\\"; use numbered lines only for Action Items.\\n- Keep it scannable, concise, and under 250 words.\\n- Required sections in order:\\n ☀️ Morning Brief: Month D, YYYY\\n 🌥️ Weather\\n ⚙️ Infrastructure Status\\n 🛠️ Action Item (only if something needs attention)\\n 📧 Email Summary\\n 📅 Calendar\\n- If data is missing, say so in one sentence and move on.\\n- For infrastructure, if any service is unhealthy, call it out clearly and make it the action item.\\n\"},{\"role\":\"user\",\"content\":{{ JSON.stringify(\"Here is today's raw data. Produce only the final brief, not your analysis.\\n\" + $json.dataJson) }}}],\"temperature\":0.1,\"max_tokens\":500}", "options": { "timeout": 60000 }, "contentType": "json" }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 950, 0 ], "id": "f2eb23d3-bf07-46d8-8556-2ba6a0185f5a", "name": "Synthesize with LLM", "continueOnFail": false }, { "parameters": { "mode": "runOnceForAllItems", "jsCode": "const response = $input.first().json;\nlet brief = '';\n\nif (response.choices && response.choices[0] && response.choices[0].message) {\n brief = response.choices[0].message.content || '';\n} else if (typeof response === 'string') {\n brief = response;\n} else {\n brief = 'Morning brief synthesis failed.';\n}\n\nbrief = String(brief);\n\n// Remove hidden reasoning/code blocks and formatting that direct delivery shows literally.\nbrief = brief.replace(new RegExp('[\\\\s\\\\S]*?<\\\\/think>', 'gi'), '');\nbrief = brief.replace(new RegExp('```[\\\\s\\\\S]*?```', 'g'), '');\nbrief = brief.replace(new RegExp('<\\\\/?(?:b|strong|code|i|em)>', 'gi'), '');\nbrief = brief.replace(new RegExp('<[^>]+>', 'g'), '');\nbrief = brief.replace(/[\\*`_~]/g, '');\n\n// If the model leaked drafting/meta sections, keep only the last final-brief-looking block.\nconst markers = ['17 Morning Brief:', 'Morning Brief:', 'Weather'];\nlet bestIndex = -1;\nfor (const marker of markers) {\n const idx = brief.lastIndexOf(marker);\n if (idx > bestIndex) bestIndex = idx;\n}\nif (bestIndex > 0) brief = brief.slice(bestIndex);\n\nbrief = brief\n .split('\\n')\n .filter(line => !/^\\s*(Details|Header|Section \\d+|Drafting|Constraint Check|Self-Correction|Final Polish|Refining for|Final:|Plan:)/i.test(line))\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\nconst today = new Date().toISOString().split('T')[0];\nconst yamlFrontmatter = '---\\ncreated: ' + today + '\\ntype: morning-brief\\ntags: [daily, brief]\\n---\\n\\n';\n\nreturn [{\n json: {\n brief,\n briefWithFrontmatter: yamlFrontmatter + '# Morning Brief - ' + today + '\\n\\n' + brief,\n date: today\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1250, 0 ], "id": "0adac542-7d95-4002-a3e2-080442cfd9e3", "name": "Extract Brief" }, { "parameters": { "method": "POST", "url": "http://172.19.0.1:8644/webhooks/morning-brief-atlas", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "X-Gitlab-Token", "value": "iKjtyz9ZXp6qOu6HeFagQYVzkav01rNVi4hBuFCx0VY" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "contentType": "json", "specifyBody": "json", "jsonBody": "= {\"brief\": {{ JSON.stringify($json.brief) }}}", "options": { "timeout": 30000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1550, -150 ], "id": "8242ada9-20c8-4689-b00c-3cd2787b2eb5", "name": "Send via Atlas", "continueOnFail": true }, { "parameters": { "method": "PUT", "url": "=http://172.19.0.1:27123/vault/Notes/{{ $json.date }} Morning Brief.md", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "text/markdown" } ] }, "sendBody": true, "contentType": "raw", "rawContentType": "text/markdown", "body": "={{ $json.briefWithFrontmatter }}", "options": { "timeout": 10000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1550, 150 ], "id": "0f1fd6a2-86c0-4d3f-a948-32ce701d9f9f", "name": "Save to Obsidian", "credentials": { "httpHeaderAuth": { "id": "465Swz2b71O2KRAK", "name": "Obsidian Local REST API" } }, "continueOnFail": true } ], "connections": { "Daily 06:30 PT": { "main": [ [ { "node": "Weather", "type": "main", "index": 0 } ] ] }, "Weather": { "main": [ [ { "node": "Swarm Health", "type": "main", "index": 0 } ] ] }, "Swarm Health": { "main": [ [ { "node": "n8n Health", "type": "main", "index": 0 } ] ] }, "n8n Health": { "main": [ [ { "node": "LiteLLM Health", "type": "main", "index": 0 } ] ] }, "LiteLLM Health": { "main": [ [ { "node": "Email Highlights", "type": "main", "index": 0 } ] ] }, "Email Highlights": { "main": [ [ { "node": "Calendar", "type": "main", "index": 0 } ] ] }, "Calendar": { "main": [ [ { "node": "Merge Data", "type": "main", "index": 0 } ] ] }, "Merge Data": { "main": [ [ { "node": "Synthesize with LLM", "type": "main", "index": 0 } ] ] }, "Synthesize with LLM": { "main": [ [ { "node": "Extract Brief", "type": "main", "index": 0 } ] ] }, "Extract Brief": { "main": [ [ { "node": "Send via Atlas", "type": "main", "index": 0 }, { "node": "Save to Obsidian", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "timezone": "America/Los_Angeles", "callerPolicy": "workflowsFromSameOwner", "availableInMCP": false }, "staticData": { "node:Daily 06:30 PT": { "recurrenceRules": [] } }, "meta": null, "pinData": null, "versionId": "6f6dd1b7-c08b-4ca9-a49d-274d59a7205c", "activeVersionId": "6f6dd1b7-c08b-4ca9-a49d-274d59a7205c", "versionCounter": 75, "triggerCount": 1, "tags": [], "shared": [ { "updatedAt": "2026-05-13T21:41:17.800Z", "createdAt": "2026-05-13T21:41:17.800Z", "role": "workflow:owner", "workflowId": "g3IdGZCK1EtTsv9T", "projectId": "WGdp8QunI1tHpjXa", "project": { "updatedAt": "2026-03-11T21:08:10.005Z", "createdAt": "2026-03-11T21:05:11.541Z", "id": "WGdp8QunI1tHpjXa", "name": "will will ", "type": "personal", "icon": null, "description": null, "creatorId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" } } ], "versionMetadata": { "name": null, "description": null } } ]