diff --git a/swarm-common/n8n-workflows/morning-brief.json b/swarm-common/n8n-workflows/morning-brief.json new file mode 100644 index 0000000..0f10485 --- /dev/null +++ b/swarm-common/n8n-workflows/morning-brief.json @@ -0,0 +1 @@ +[{"updatedAt":"2026-05-13T21:41:17.798Z","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"}]},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-N8N-API-KEY","value":"={{ $env.N8N_API_KEY }}"}]},"options":{"timeout":15000}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[300,200],"id":"c688abdf-9b63-43b4-81da-7c81388b73f8","name":"Email Highlights","continueOnFail":true},{"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,"pinData":null,"versionId":"2e269319-d17c-4d15-8500-43d8e2bbf0b9","activeVersionId":"2e269319-d17c-4d15-8500-43d8e2bbf0b9","versionCounter":4,"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}}] \ No newline at end of file