diff --git a/swarm-common/n8n-workflows/morning-brief.json b/swarm-common/n8n-workflows/morning-brief.json index 4a9ec65..36fe6ae 100644 --- a/swarm-common/n8n-workflows/morning-brief.json +++ b/swarm-common/n8n-workflows/morning-brief.json @@ -1,466 +1,12 @@ -{ - "updatedAt": "2026-05-14T00:21:08.433Z", - "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\nconst weather = 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 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 a personal morning brief assistant. Given raw data about weather, infrastructure, email, and calendar, produce a concise morning brief under 400 words. Use emojis for section headers. Format for Telegram using HTML (use for bold, for code). Keep it scannable and actionable. If any section data is missing or shows errors, note it briefly and move on.\"}, {\"role\": \"user\", \"content\": \"Here is today's data:\\n{{ $json.dataJson }}\"}], \"temperature\": 0.3, \"max_tokens\": 800}", - "options": { - "timeout": 60000 - } - }, - "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": "\nconst 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 = 'Error: LLM synthesis failed. Raw data: ' + JSON.stringify(response).substring(0, 500);\n}\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: brief,\n briefWithFrontmatter: yamlFrontmatter + '# Morning Brief - ' + today + '\\n\\n' + brief,\n date: today\n }\n}];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1250, - 0 - ], - "id": "0adac542-7d95-4002-a3e2-080442cfd9e3", - "name": "Extract Brief" - }, - { - "parameters": { - "chatId": "8367012007", - "text": "={{ $json.brief }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1550, - -150 - ], - "id": "8242ada9-20c8-4689-b00c-3cd2787b2eb5", - "name": "Send Telegram", - "credentials": { - "telegramApi": { - "id": "aox4dyIWVSRdcH5z", - "name": "Telegram Bot (OpenClaw)" - } - }, - "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 - }, - { - "node": "Swarm Health", - "type": "main", - "index": 0 - }, - { - "node": "n8n Health", - "type": "main", - "index": 0 - }, - { - "node": "LiteLLM Health", - "type": "main", - "index": 0 - }, - { - "node": "Email Highlights", - "type": "main", - "index": 0 - }, - { - "node": "Calendar", - "type": "main", - "index": 0 - } - ] - ] - }, - "Weather": { - "main": [ - [ - { - "node": "Merge Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Swarm Health": { - "main": [ - [ - { - "node": "Merge Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "n8n Health": { - "main": [ - [ - { - "node": "Merge Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "LiteLLM Health": { - "main": [ - [ - { - "node": "Merge Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Email Highlights": { - "main": [ - [ - { - "node": "Merge Data", - "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 Telegram", - "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, - "versionId": "c23946c7-29cf-42dc-985c-c1b818b8d010", - "activeVersionId": "c23946c7-29cf-42dc-985c-c1b818b8d010", - "versionCounter": 10, - "triggerCount": 1, - "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" - } - } - ], - "tags": [], - "activeVersion": { - "updatedAt": "2026-05-14T00:21:08.434Z", - "createdAt": "2026-05-14T00:21:08.434Z", - "versionId": "c23946c7-29cf-42dc-985c-c1b818b8d010", - "workflowId": "g3IdGZCK1EtTsv9T", +[ + { + "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": { @@ -625,7 +171,7 @@ { "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\nconst weather = 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 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" + "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, @@ -642,10 +188,11 @@ "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 a personal morning brief assistant. Given raw data about weather, infrastructure, email, and calendar, produce a concise morning brief under 400 words. Use emojis for section headers. Format for Telegram using HTML (use for bold, for code). Keep it scannable and actionable. If any section data is missing or shows errors, note it briefly and move on.\"}, {\"role\": \"user\", \"content\": \"Here is today's data:\\n{{ $json.dataJson }}\"}], \"temperature\": 0.3, \"max_tokens\": 800}", + "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, @@ -660,7 +207,7 @@ { "parameters": { "mode": "runOnceForAllItems", - "jsCode": "\nconst 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 = 'Error: LLM synthesis failed. Raw data: ' + JSON.stringify(response).substring(0, 500);\n}\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: brief,\n briefWithFrontmatter: yamlFrontmatter + '# Morning Brief - ' + today + '\\n\\n' + brief,\n date: today\n }\n}];\n" + "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, @@ -673,26 +220,37 @@ }, { "parameters": { - "chatId": "8367012007", - "text": "={{ $json.brief }}", - "additionalFields": { - "parse_mode": "HTML" + "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.telegram", - "typeVersion": 1.2, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, "position": [ 1550, -150 ], "id": "8242ada9-20c8-4689-b00c-3cd2787b2eb5", - "name": "Send Telegram", - "credentials": { - "telegramApi": { - "id": "aox4dyIWVSRdcH5z", - "name": "Telegram Bot (OpenClaw)" - } - }, + "name": "Send via Atlas", "continueOnFail": true }, { @@ -741,31 +299,6 @@ "node": "Weather", "type": "main", "index": 0 - }, - { - "node": "Swarm Health", - "type": "main", - "index": 0 - }, - { - "node": "n8n Health", - "type": "main", - "index": 0 - }, - { - "node": "LiteLLM Health", - "type": "main", - "index": 0 - }, - { - "node": "Email Highlights", - "type": "main", - "index": 0 - }, - { - "node": "Calendar", - "type": "main", - "index": 0 } ] ] @@ -774,7 +307,7 @@ "main": [ [ { - "node": "Merge Data", + "node": "Swarm Health", "type": "main", "index": 0 } @@ -785,7 +318,7 @@ "main": [ [ { - "node": "Merge Data", + "node": "n8n Health", "type": "main", "index": 0 } @@ -796,7 +329,7 @@ "main": [ [ { - "node": "Merge Data", + "node": "LiteLLM Health", "type": "main", "index": 0 } @@ -807,7 +340,7 @@ "main": [ [ { - "node": "Merge Data", + "node": "Email Highlights", "type": "main", "index": 0 } @@ -818,7 +351,7 @@ "main": [ [ { - "node": "Merge Data", + "node": "Calendar", "type": "main", "index": 0 } @@ -862,7 +395,7 @@ "main": [ [ { - "node": "Send Telegram", + "node": "Send via Atlas", "type": "main", "index": 0 }, @@ -875,19 +408,46 @@ ] } }, - "authors": "will will", - "name": null, - "description": null, - "autosaved": false, - "workflowPublishHistory": [ - { - "createdAt": "2026-05-14T00:21:08.466Z", - "id": 1493, - "workflowId": "g3IdGZCK1EtTsv9T", - "versionId": "c23946c7-29cf-42dc-985c-c1b818b8d010", - "event": "activated", - "userId": "5ad50ead-6e6a-4d12-ab5b-e5db15835bb5" + "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 + } } -} +]