{
  "name": "moss proactive engine",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/10 * * * *"
            }
          ]
        }
      },
      "id": "node-schedule",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "value": "YOUR_CALENDAR_ID",
          "mode": "list",
          "cachedResultName": "YOUR_CALENDAR_ID"
        },
        "limit": 20,
        "timeMin": "={{ $now.startOf('day') }}",
        "timeMax": "={{ $now.endOf('day') }}",
        "options": {
          "orderBy": "startTime"
        }
      },
      "alwaysOutputData": true,
      "id": "node-get-events",
      "name": "get upcoming events",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        220,
        0
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// moss proactive engine \u2014 signal processor\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Signal schema:\n//   type:               signal category\n//   id:                 dedup key (type:source_id:YYYY-MM-DD)\n//   urgency:            'immediate' | 'soon'\n//   title:              human-readable title\n//   detail:             supporting info for AI\n//   starts_in_minutes:  (calendar only) minutes until start\n//   suggested_actions:  ['notify'] \u2014 extend here for future action types\n//   action_context:     arbitrary data for future action handlers\n//                       e.g. location \u2192 route query, doc_url \u2192 doc prep\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst https = require('https');\nconst fs = require('fs');\n\n// TODOIST_TOKEN: fill in on VPS directly, never commit real token\nconst TODOIST_TOKEN = 'YOUR_TODOIST_API_TOKEN';\nconst CHAT_ID = $env['MOSS_OWNER_CHAT_ID'] || '';\nif (!CHAT_ID) throw new Error('MOSS_OWNER_CHAT_ID not set in environment');\n\nconst DEDUP_PATH = '/home/node/.n8n/moss_notified.json';\nconst now = new Date();\nconst today = now.toISOString().slice(0, 10);\n\n// \u2500\u2500 Load & prune dedup log \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet notified = {};\ntry {\n  notified = JSON.parse(fs.readFileSync(DEDUP_PATH, 'utf8'));\n  // Remove entries older than 2 days to keep file small\n  for (const key of Object.keys(notified)) {\n    if (notified[key] < today) delete notified[key];\n  }\n} catch(e) { notified = {}; }\n\n// \u2500\u2500 Calendar signals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Notify for events starting in 5\u201320 min\n// Window accounts for 10-min polling interval\nconst calItems = $input.all().filter(i => i.json && i.json.summary);\nconst calSignals = [];\n\nfor (const item of calItems) {\n  const e = item.json;\n  const startStr = e.start?.dateTime || e.start?.date;\n  if (!startStr) continue;\n  const start = new Date(startStr);\n  const minutesUntil = (start - now) / 60000;\n\n  if (minutesUntil >= 5 && minutesUntil <= 20) {\n    const key = `calendar_remind:${e.id}:${today}`;\n    if (!notified[key]) {\n      calSignals.push({\n        type: 'calendar_remind',\n        id: key,\n        urgency: 'immediate',\n        title: e.summary,\n        detail: [e.location, e.description].filter(Boolean).join(' | ').slice(0, 200),\n        starts_in_minutes: Math.round(minutesUntil),\n        suggested_actions: ['notify'],\n        // action_context \u2014 future hooks:\n        //   location \u2192 trigger route query sub-workflow\n        //   doc_url  \u2192 trigger document prep sub-workflow\n        action_context: {\n          event_id: e.id,\n          location: e.location || null,\n          doc_url: null  // placeholder: parse from description in future\n        }\n      });\n    }\n  }\n}\n\n// \u2500\u2500 Todoist signals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction getTasks(filter) {\n  return new Promise((resolve) => {\n    const opts = {\n      hostname: 'api.todoist.com',\n      path: '/api/v1/tasks?filter=' + encodeURIComponent(filter),\n      method: 'GET',\n      headers: { 'Authorization': 'Bearer ' + TODOIST_TOKEN }\n    };\n    const req = https.request(opts, res => {\n      let raw = '';\n      res.on('data', c => raw += c);\n      res.on('end', () => { try { resolve(JSON.parse(raw)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', () => resolve({}));\n    req.end();\n  });\n}\n\nconst [overdueData, todayData] = await Promise.all([\n  getTasks('overdue'),\n  getTasks('today & !overdue')\n]);\n\nconst taskSignals = [];\n\n// Overdue tasks \u2014 one reminder per task per day\nfor (const t of (overdueData.results || [])) {\n  const key = `task_overdue:${t.id}:${today}`;\n  if (!notified[key]) {\n    taskSignals.push({\n      type: 'task_overdue',\n      id: key,\n      urgency: 'soon',\n      title: t.content,\n      detail: t.due?.string || '',\n      suggested_actions: ['notify'],\n      action_context: { task_id: t.id }\n    });\n  }\n}\n\n// Today's tasks with a specific due time, starting soon (within 30 min)\nfor (const t of (todayData.results || [])) {\n  if (!t.due?.datetime) continue;\n  const dueTime = new Date(t.due.datetime);\n  const minutesUntil = (dueTime - now) / 60000;\n  if (minutesUntil >= 0 && minutesUntil <= 30) {\n    const key = `task_due_soon:${t.id}:${today}`;\n    if (!notified[key]) {\n      taskSignals.push({\n        type: 'task_due_soon',\n        id: key,\n        urgency: 'immediate',\n        title: t.content,\n        detail: `due in ${Math.round(minutesUntil)} min`,\n        suggested_actions: ['notify'],\n        action_context: { task_id: t.id }\n      });\n    }\n  }\n}\n\n// \u2500\u2500 FUTURE SIGNAL SOURCES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Add new signal collectors here following the same pattern.\n// Example stubs (not yet implemented):\n//\n// const routeSignals = await gatherRouteSignals(calSignals);\n//   \u2192 for events with location, check commute time, alert if need to leave soon\n//\n// const docPrepSignals = await gatherDocPrepSignals(calSignals);\n//   \u2192 for events with doc_url in description, prep a summary before the meeting\n//\n// Push additional signals into the array below.\n// The AI Agent and action router downstream will handle them.\n\nconst signals = [...calSignals, ...taskSignals];\n\n// Only emit today's overdue tasks in first morning run (before 10am)\n// to avoid spamming overdue tasks all day.\n// After 10am, only re-surface overdue if they're also due today with a time.\nconst hour = now.getHours();\nconst filteredSignals = signals.filter(s => {\n  if (s.type === 'task_overdue' && hour >= 10) return false;\n  return true;\n});\n\nreturn [{\n  json: {\n    chatId: CHAT_ID,\n    signals: filteredSignals,\n    hasSignals: filteredSignals.length > 0,\n    notified,\n    today\n  }\n}];"
      },
      "id": "node-process-signals",
      "name": "process signals",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.hasSignals }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "node-if-signals",
      "name": "has signals?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        700,
        0
      ]
    },
    {
      "parameters": {
        "agent": "conversationalAgent",
        "promptType": "define",
        "text": "=\u6839\u636e\u4ee5\u4e0b\u5f85\u63a8\u9001\u4fe1\u53f7\uff0c\u751f\u6210\u4e00\u6761\u7b80\u6d01\u7684\u4e3b\u52a8\u63d0\u9192\u6d88\u606f\u53d1\u7ed9\u7528\u6237\u3002\n\n\u4fe1\u53f7\u5217\u8868\uff08JSON\uff09\uff1a\n{{ JSON.stringify($json.signals, null, 2) }}\n\n\u5f53\u524d\u65f6\u95f4\uff1a{{ $now.format('HH:mm') }}\n\n\u683c\u5f0f\u8981\u6c42\uff1a\n- \u6309\u7d27\u8feb\u7a0b\u5ea6\u6392\u5e8f\uff0c\u6700\u7d27\u6025\u7684\u653e\u6700\u524d\n- calendar_remind\uff1a\u8bf4\u6e05\u695a\u51e0\u5206\u949f\u540e\u5f00\u59cb\uff0c\u6709\u5730\u70b9\u5c31\u5e26\u4e0a\n- task_overdue\uff1a\u7b80\u77ed\u5217\u51fa\uff0c\u4e0d\u8981\u5570\u55e6\n- task_due_soon\uff1a\u8bf4\u6e05\u695a\u8fd8\u6709\u51e0\u5206\u949f\u622a\u6b62\n- \u591a\u6761\u4fe1\u53f7\u5408\u5e76\u6210\u4e00\u6761\u6d88\u606f\uff0c\u4e0d\u8981\u9010\u6761\u53d1\n- \u63a7\u5236\u5728 150 \u5b57\u4ee5\u5185",
        "options": {
          "systemMessage": "\u4f60\u662f moss\uff0c\u7528\u6237\u7684\u79c1\u4eba AI \u52a9\u7406\uff0c\u5973\u6027\u3002\u73b0\u5728\u662f\u4e3b\u52a8\u63a8\u9001\u6a21\u5f0f\u2014\u2014\u4f60\u4e3b\u52a8\u627e\u7528\u6237\uff0c\u4e0d\u662f\u56de\u590d\u7528\u6237\u3002\n\n\u98ce\u683c\uff1a\n- \u7b80\u6d01\u3001\u6709\u6e29\u5ea6\uff0c\u4e0d\u505a\u4f5c\n- \u76f4\u63a5\u8bf4\u91cd\u70b9\uff0c\u4e0d\u5e9f\u8bdd\n- \u7528\u4e2d\u6587\n- \u4e0d\u8981\u52a0\u4efb\u4f55\u5f00\u573a\u767d\uff0c\u76f4\u63a5\u8bf4\u4e8b"
        }
      },
      "id": "node-ai-agent",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.7,
      "position": [
        960,
        -120
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatDeepSeek",
      "typeVersion": 1,
      "position": [
        960,
        80
      ],
      "id": "node-deepseek",
      "name": "DeepSeek Chat Model",
      "credentials": {
        "deepSeekApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "={{ $('process signals').item.json.chatId }}",
        "text": "={{ $json.output }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "Markdown"
        }
      },
      "id": "node-telegram-send",
      "name": "Send Alert",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1200,
        -120
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst DEDUP_PATH = '/home/node/.n8n/moss_notified.json';\n\nconst { signals, notified, today } = $('process signals').item.json;\n\n// Mark all pushed signals as notified\nfor (const s of signals) {\n  notified[s.id] = today;\n}\n\nfs.writeFileSync(DEDUP_PATH, JSON.stringify(notified, null, 2));\n\nreturn [{ json: { saved: signals.length, keys: signals.map(s => s.id) } }];"
      },
      "id": "node-save-dedup",
      "name": "save dedup log",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        -120
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "get upcoming events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get upcoming events": {
      "main": [
        [
          {
            "node": "process signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "process signals": {
      "main": [
        [
          {
            "node": "has signals?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "has signals?": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Send Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DeepSeek Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Send Alert": {
      "main": [
        [
          {
            "node": "save dedup log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}