{
  "id": "G85axj7PbbvVP1aK",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "M365 Smart Inbox + Security Monitor",
  "tags": [],
  "nodes": [
    {
      "id": "b11b4d26-869b-4582-8087-08f45e1a8f7a",
      "name": "Every 30 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        9024,
        1312
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9c37e58a-0fa9-40ad-8325-4662f7dcffdc",
      "name": "Get Unread Emails",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        9248,
        1312
      ],
      "parameters": {
        "options": {},
        "operation": "getAll"
      },
      "typeVersion": 2
    },
    {
      "id": "d134b604-05e6-4b41-a71a-650606eea70b",
      "name": "Any Emails?",
      "type": "n8n-nodes-base.if",
      "position": [
        9472,
        1312
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-1",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $input.all().length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "3b836a42-ab5a-4641-935e-df6cb61b0a28",
      "name": "No New Emails",
      "type": "n8n-nodes-base.noOp",
      "position": [
        9696,
        1408
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c97e7acf-c019-4052-be4f-e3a8bd06d11a",
      "name": "One Email at a Time",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        9696,
        1216
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "b6a6cd6a-8541-4fc2-8407-645d83dce9c0",
      "name": "Extract Task from Email",
      "type": "n8n-nodes-base.code",
      "position": [
        10096,
        1312
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const email = $input.item.json;\nconst keywords = ($vars['ACTION_KEYWORDS'] || 'ACTION REQUIRED,URGENT,TASK:,PLEASE REVIEW,CRITICAL').split(',').map(k => k.trim().toLowerCase());\nconst subject = (email.subject || '').toLowerCase();\nconst rawBody = (email.body?.content || '');\nconst cleanBody = rawBody.replace(/<(style|script)[^>]*>[\\s\\S]*?<\\/\\1>/gi, ' ').replace(/<[^>]*>/g, ' ').replace(/&nbsp;/gi, ' ').replace(/\\s+/g, ' ').trim();\nconst bodyText = cleanBody.toLowerCase();\nconst combined = subject + ' ' + bodyText;\nconst matchedKeyword = keywords.find(kw => combined.includes(kw));\nif (!matchedKeyword) { return { json: { emailId: email.id, matchedKeyword: '' } }; }\nconst isUrgent = ['urgent', 'critical'].some(k => combined.includes(k));\nconst priority = isUrgent ? 1 : 5;\n// Fix 4: timezone from workflow variable with env fallback\nconst tz = $vars['REPORT_TIMEZONE'] || process.env.REPORT_TIMEZONE || 'Europe/Helsinki';\nconst helsinkiNow = new Date(new Date().toLocaleString('en-US', { timeZone: tz }));\nlet dueDate = null;\nconst isoMatch = cleanBody.match(/\\b(\\d{4}-\\d{2}-\\d{2})\\b/);\nconst structuredMatch = cleanBody.match(/(due:|by:)\\s*(\\d{1,2}[.\\/\\-]\\d{1,2}[.\\/\\-]\\d{2,4})/i);\nconst relativeMatch = cleanBody.match(/\\b(today|tomorrow|eod|end of day|end of week|friday|monday)\\b/i);\nif (isoMatch) { dueDate = isoMatch[1] + 'T23:59:59Z'; }\nelse if (structuredMatch) { const raw = structuredMatch[2]; const parts = raw.split(/[.\\/\\-]/); if (parts.length === 3) { const y = parts[0].length === 4 ? parts[0] : '20' + parts[2]; const m = (parts[0].length === 4 ? parts[1] : parts[1]).padStart(2, '0'); const d = (parts[0].length === 4 ? parts[2] : parts[0]).padStart(2, '0'); dueDate = `${y}-${m}-${d}T23:59:59Z`; } }\nelse if (relativeMatch) { const phrase = relativeMatch[1].toLowerCase(); const target = new Date(helsinkiNow); if (phrase === 'tomorrow') { target.setDate(target.getDate() + 1); } else if (phrase === 'end of week' || phrase === 'friday') { const day = target.getDay(); target.setDate(target.getDate() + ((5 - day + 7) % 7 || 7)); } else if (phrase === 'monday') { const day = target.getDay(); target.setDate(target.getDate() + ((1 - day + 7) % 7 || 7)); } target.setHours(23, 59, 59, 0); const utcOffset = new Date(target.toLocaleString('en-US', { timeZone: tz })) - target; const utcTarget = new Date(target.getTime() - utcOffset); dueDate = utcTarget.toISOString(); }\nconst taskTitle = `[EMAIL] ${email.subject || 'No Subject'}`;\nconst senderName = email.from?.emailAddress?.name || 'Unknown';\nconst senderEmail = email.from?.emailAddress?.address || '';\nconst received = email.receivedDateTime || new Date().toISOString();\nconst bodyPreview = cleanBody.substring(0, 500);\nconst taskNotes = `From: ${senderName} <${senderEmail}>\\nReceived: ${received}\\nKeyword trigger: ${matchedKeyword.toUpperCase()}\\n\\n${bodyPreview}`;\nreturn { json: { emailId: email.id, emailSubject: email.subject, senderName, senderEmail, receivedDateTime: email.receivedDateTime, taskTitle, taskNotes, priority, dueDate, matchedKeyword, isUrgent, importance: email.importance || 'normal' } };"
      },
      "typeVersion": 2
    },
    {
      "id": "03d94575-53de-45e8-aa94-95af1a60029f",
      "name": "Keyword Match Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        10336,
        1312
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-2",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.matchedKeyword }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "460c031d-e9ae-4c63-99fc-71f627de4f9c",
      "name": "Create Planner Task",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        10848,
        1184
      ],
      "parameters": {
        "url": "https://graph.microsoft.com/v1.0/planner/tasks",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ planId: $vars.PLANNER_PLAN_ID, bucketId: $vars.PLANNER_BUCKET_ID, title: $json.taskTitle, priority: $json.priority, dueDateTime: $json.dueDate || null, assignments: { [$vars.TASK_ASSIGNEE_USER_ID]: { '@odata.type': '#microsoft.graph.plannerAssignment', orderHint: ' !' } } }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "15d60d2c-6886-4e64-a9f1-8db4ad273964",
      "name": "Set Task Details (Notes)",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        11072,
        1184
      ],
      "parameters": {
        "url": "={{ `https://graph.microsoft.com/v1.0/planner/tasks/${$json.id}/details` }}",
        "method": "PATCH",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ description: $json.taskNotes }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "If-Match",
              "value": "*"
            }
          ]
        },
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "73ae3ecb-f00f-4f24-8592-c1521b16a15a",
      "name": "Post Teams Notification",
      "type": "n8n-nodes-base.microsoftTeams",
      "onError": "continueRegularOutput",
      "position": [
        11296,
        1184
      ],
      "parameters": {
        "teamId": "={{ $vars.TEAMS_TEAM_ID }}",
        "message": "=<b>\ud83d\udccb New Task Created from Email</b><br><b>Subject:</b> {{ $('Extract Task from Email').item.json.emailSubject }}<br><b>From:</b> {{ $('Extract Task from Email').item.json.senderName }} ({{ $('Extract Task from Email').item.json.senderEmail }})<br><b>Keyword:</b> {{ $('Extract Task from Email').item.json.matchedKeyword.toUpperCase() }}<br><b>Priority:</b> {{ $('Extract Task from Email').item.json.priority === 1 ? '\ud83d\udd34 Urgent' : '\ud83d\udfe1 Normal' }}<br><b>Due:</b> {{ $('Extract Task from Email').item.json.dueDate || 'Not set' }}",
        "options": {},
        "resource": "channelMessage",
        "channelId": "={{ $vars.TEAMS_CHANNEL_ID }}",
        "messageType": "html"
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "e85d996f-2df9-4569-a2e1-2e29a8e40aa2",
      "name": "Reply, Categorize & Mark Read",
      "type": "n8n-nodes-base.microsoftOutlook",
      "onError": "continueRegularOutput",
      "position": [
        11520,
        1184
      ],
      "parameters": {
        "options": {},
        "messageId": "={{ $('Extract Task from Email').item.json.emailId }}",
        "operation": "reply"
      },
      "typeVersion": 2,
      "continueOnFail": true
    },
    {
      "id": "e53e66b6-52dc-4300-8959-7f16b4b9424a",
      "name": "Mark as Read After Reply",
      "type": "n8n-nodes-base.microsoftOutlook",
      "onError": "continueRegularOutput",
      "position": [
        11744,
        1184
      ],
      "parameters": {
        "messageId": "={{ $('Extract Task from Email').item.json.emailId }}",
        "operation": "update",
        "updateFields": {
          "isRead": true,
          "categories": [
            "Task Created"
          ]
        }
      },
      "typeVersion": 2,
      "continueOnFail": true
    },
    {
      "id": "d4c22886-6225-4e23-b92f-b160f316aeaf",
      "name": "Done (Email Processed)",
      "type": "n8n-nodes-base.noOp",
      "position": [
        11968,
        1184
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "dacdf7e3-b1c9-400c-8e9a-1950cfb5e2c2",
      "name": "Error Capture (Poison Pill Guard)",
      "type": "n8n-nodes-base.code",
      "position": [
        12336,
        1184
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const emailId = $('Extract Task from Email').item.json?.emailId || 'unknown';\nconst subject = $('Extract Task from Email').item.json?.emailSubject || 'unknown';\nconst nodesToCheck = ['Create Planner Task','Set Task Details (Notes)','Post Teams Notification','Reply, Categorize & Mark Read','Mark as Read After Reply'];\nlet errorSummary = null;\nfor (const nodeName of nodesToCheck) {\n  try { const result = $node[nodeName]?.outputData?.main?.[0]?.[0]; if (result?.error) { errorSummary = { failedNode: nodeName, error: result.error.message, emailId, subject }; break; } } catch(e) { }\n}\nreturn { json: { emailId, subject, hadError: !!errorSummary, errorSummary } };"
      },
      "typeVersion": 2
    },
    {
      "id": "6c265314-75e8-40e7-9fa8-13197e0063fa",
      "name": "Had Error?",
      "type": "n8n-nodes-base.if",
      "position": [
        12560,
        1184
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-haderror",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.hadError }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ec05fed4-0fe8-4198-a064-ac2a6d7d2252",
      "name": "Mark as Read (No Action)",
      "type": "n8n-nodes-base.microsoftOutlook",
      "onError": "continueRegularOutput",
      "position": [
        12560,
        1328
      ],
      "parameters": {
        "messageId": "={{ $('One Email at a Time').item.json.id }}",
        "operation": "update",
        "updateFields": {
          "isRead": true
        }
      },
      "typeVersion": 2,
      "continueOnFail": true
    },
    {
      "id": "d12b9b98-7872-4094-b7a0-f893f4b6f9f9",
      "name": "API Rate Limit Throttle",
      "type": "n8n-nodes-base.wait",
      "position": [
        12784,
        1264
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1
    },
    {
      "id": "91f8d1f1-1e6f-4d06-b871-a9a444097ae8",
      "name": "Workflow Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        9088,
        2640
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "4747d467-b318-482a-9b47-2d8f0ac42d0b",
      "name": "Send Error Notification Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        9312,
        2640
      ],
      "parameters": {
        "url": "={{ `https://graph.microsoft.com/v1.0/users/${$vars.MONITORED_MAILBOX}/sendMail` }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ message: { subject: `\u274c n8n Error: M365 Smart Inbox + Security Monitor [${$json.execution.id}]`, body: { contentType: 'HTML', content: `<p>The <b>M365 Smart Inbox + Security Monitor</b> workflow has encountered an error.</p><table><tr><td><b>Execution ID:</b></td><td>${$json.execution.id}</td></tr><tr><td><b>Failing Node:</b></td><td>${$json.execution.lastNodeExecuted}</td></tr><tr><td><b>Error:</b></td><td>${$json.execution.error.message}</td></tr><tr><td><b>Time:</b></td><td>${new Date().toISOString()}</td></tr></table>` }, toRecipients: [{ emailAddress: { address: 'it-admin@company.fi' } }] }, saveToSentItems: false }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "5050a533-a12a-48d9-acd0-4f42d77cee1f",
      "name": "Every Monday at 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        9024,
        2112
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "4e305dfa-e38f-4312-991f-2e282a7505ea",
      "name": "Get Latest Secure Score",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        9248,
        2112
      ],
      "parameters": {
        "url": "https://graph.microsoft.com/v1.0/security/secureScores?$top=1&$orderby=createdDateTime desc",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "8677609b-7d6e-4493-8bb3-d00df50e2a7e",
      "name": "Calculate Security Score Percentage",
      "type": "n8n-nodes-base.code",
      "position": [
        9472,
        2112
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const response = $input.item.json;\nconst scoreData = (response.value && response.value[0]) ? response.value[0] : response;\nconst currentScore = scoreData.currentScore || 0;\nconst maxScore = scoreData.maxScore || 1;\nconst percentage = ((currentScore / maxScore) * 100).toFixed(1);\n// Fix 4: timezone from workflow variable with env fallback\nconst tz = $vars['REPORT_TIMEZONE'] || process.env.REPORT_TIMEZONE || 'Europe/Helsinki';\nconst scanDate = scoreData.createdDateTime ? new Date(scoreData.createdDateTime).toLocaleDateString('fi-FI', { timeZone: tz }) : new Date().toLocaleDateString('fi-FI', { timeZone: tz });\nconst isHealthy = parseFloat(percentage) >= 80.0;\nreturn { json: { scanDate, currentScore, maxScore, percentage, isHealthy, activeUserCount: scoreData.activeUserCount || 'Unknown' } };"
      },
      "typeVersion": 2
    },
    {
      "id": "c46ffcb2-ab90-4cca-9f1d-b88cca641173",
      "name": "Is Score Below 80%?",
      "type": "n8n-nodes-base.if",
      "position": [
        9696,
        2112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-3",
              "operator": {
                "type": "boolean",
                "operation": "false"
              },
              "leftValue": "={{ $json.isHealthy }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "44d08945-a5b4-452d-bbcc-387dd255f241",
      "name": "Create Security Planner Task",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        10912,
        2000
      ],
      "parameters": {
        "url": "https://graph.microsoft.com/v1.0/planner/tasks",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ planId: $vars.PLANNER_PLAN_ID, bucketId: $vars.PLANNER_BUCKET_ID, title: `\ud83d\udea8 Security Score Alert: ${$('Calculate Security Score Percentage').item.json.percentage}%`, priority: 1, dueDateTime: new Date(Date.now() + 86400000).toISOString(), assignments: { [$vars.TASK_ASSIGNEE_USER_ID]: { '@odata.type': '#microsoft.graph.plannerAssignment', orderHint: ' !' } } }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "2280259d-3284-4a28-ad87-c4eef06604bf",
      "name": "Set Security Task Details",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        11136,
        2000
      ],
      "parameters": {
        "url": "={{ `https://graph.microsoft.com/v1.0/planner/tasks/${$json.id}/details` }}",
        "method": "PATCH",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ description: `The tenant security score has dropped to ${$('Calculate Security Score Percentage').item.json.percentage}%. Please review the Secure Score Control Profiles in the Defender portal.\\n\\nCurrent Score: ${$('Calculate Security Score Percentage').item.json.currentScore}\\nMax Score: ${$('Calculate Security Score Percentage').item.json.maxScore}\\nScan Date: ${$('Calculate Security Score Percentage').item.json.scanDate}` }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "If-Match",
              "value": "*"
            }
          ]
        },
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "01230ec5-f7eb-47c2-98e1-887aa316448c",
      "name": "Post Security Teams Alert",
      "type": "n8n-nodes-base.microsoftTeams",
      "position": [
        11376,
        2112
      ],
      "parameters": {
        "teamId": "={{ $vars.TEAMS_TEAM_ID }}",
        "message": "=<b>\ud83d\udea8 Security Score Alert</b><br><b>Score:</b> {{ $('Calculate Security Score Percentage').item.json.percentage }}% ({{ $('Calculate Security Score Percentage').item.json.currentScore }} / {{ $('Calculate Security Score Percentage').item.json.maxScore }})<br><b>Scan Date:</b> {{ $('Calculate Security Score Percentage').item.json.scanDate }}<br><b>Action required:</b> Review Secure Score Control Profiles in the Microsoft Defender portal.",
        "options": {},
        "resource": "channelMessage",
        "channelId": "={{ $vars.TEAMS_CHANNEL_ID }}",
        "messageType": "html"
      },
      "typeVersion": 1
    },
    {
      "id": "017e1406-1d88-4b13-af95-a6b8543ca5d1",
      "name": "Post Security Teams Summary",
      "type": "n8n-nodes-base.microsoftTeams",
      "position": [
        10112,
        2208
      ],
      "parameters": {
        "teamId": "={{ $vars.TEAMS_TEAM_ID }}",
        "message": "=<b>\u2705 Security Score: Healthy</b><br><b>Score:</b> {{ $json.percentage }}% ({{ $json.currentScore }} / {{ $json.maxScore }})<br><b>Scan Date:</b> {{ $json.scanDate }}<br>No action required. Tenant security posture is above the 80% threshold.",
        "options": {},
        "resource": "channelMessage",
        "channelId": "={{ $vars.TEAMS_CHANNEL_ID }}",
        "messageType": "html"
      },
      "typeVersion": 1
    },
    {
      "id": "b2b830bc-6656-4cef-b0bf-d0f3f05176fe",
      "name": "Get Existing Planner Tasks",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        10112,
        2016
      ],
      "parameters": {
        "url": "={{ `https://graph.microsoft.com/v1.0/planner/buckets/${$vars.PLANNER_BUCKET_ID}/tasks` }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "80ec9664-b675-4d5e-af8f-89a56b8e3c21",
      "name": "Alert Task Already Open?",
      "type": "n8n-nodes-base.if",
      "position": [
        10336,
        2016
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-dedup",
              "operator": {
                "type": "boolean",
                "operation": "false"
              },
              "leftValue": "={{ $json.value.some(t => t.title.startsWith('\ud83d\udea8 Security Score Alert') && t.percentComplete < 100) }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "2f0bd7a0-8f5f-4459-b8cf-bf887bcdc0a5",
      "name": "Resolve Existing Task ID",
      "type": "n8n-nodes-base.code",
      "position": [
        10912,
        2224
      ],
      "parameters": {
        "jsCode": "const tasks = $input.all()[0].json.value || [];\nconst existing = tasks.find(t => t.title.startsWith('\ud83d\udea8 Security Score Alert') && t.percentComplete < 100);\nconst taskId = existing?.id || null;\nconst scoreData = $('Calculate Security Score Percentage').first().json;\nreturn { json: { existingTaskId: taskId, percentage: scoreData.percentage, currentScore: scoreData.currentScore, maxScore: scoreData.maxScore, scanDate: scoreData.scanDate } };"
      },
      "typeVersion": 2
    },
    {
      "id": "3e4375dc-63eb-4d15-a16c-1428d74fce9d",
      "name": "Update Existing Alert Task",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        11136,
        2224
      ],
      "parameters": {
        "url": "={{ `https://graph.microsoft.com/v1.0/planner/tasks/${$json.existingTaskId}/details` }}",
        "method": "PATCH",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ description: `[UPDATED ${new Date().toISOString()}]\\nScore is still below threshold: ${$json.percentage}%.\\n\\nCurrent Score: ${$json.currentScore}\\nMax Score: ${$json.maxScore}\\nScan Date: ${$json.scanDate}\\n\\nPlease review the Secure Score Control Profiles in the Defender portal.` }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "If-Match",
              "value": "*"
            }
          ]
        },
        "nodeCredentialType": "microsoftGraphOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "740bc510-4e01-44bf-9690-064184673b25",
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8272,
        912
      ],
      "parameters": {
        "color": 2,
        "width": 640,
        "height": 956,
        "content": "## M365 Smart Inbox + Security Monitor\n\n### Email-to-Task Parser + Security Score Monitor\n\n### How it works\nTwo independent automation pipelines on a single Microsoft Graph OAuth2 credential:\n\n**Pipeline 1 \u2014 Email-to-Task Parser (every 30 min)**\n1. Scans Outlook for unread emails every 30 minutes\n2. Parses each email for configurable action keywords (URGENT, ACTION REQUIRED, TASK:, etc.)\n3. Extracts due dates from ISO format, structured `due:` prefixes, or relative phrases (today, tomorrow, end of week)\n4. Creates a Microsoft Planner task with correct priority, due date, and sender notes\n5. Posts a Teams notification, sends an auto-reply, and marks the email as read\n6. Poison Pill Guard + Had Error? node checks for silent node failures before looping\n\n**Pipeline 2 \u2014 Security Score Monitor (every Monday 08:00)**\n1. Fetches the latest Microsoft Secure Score from the Graph API\n2. Calculates the score as a percentage\n3. If below 80%: checks for an existing open alert task to avoid duplicates, then creates or updates a priority-1 Planner task with a 24-hour due date, and posts a Teams alert\n4. If healthy: posts a green summary to the Teams channel\n\n### Setup steps\n- [ ] Connect Microsoft Graph OAuth2 credential (covers Outlook, Planner, Teams)\n- [ ] Set `PLANNER_PLAN_ID` workflow variable\n- [ ] Set `PLANNER_BUCKET_ID` workflow variable\n- [ ] Set `TASK_ASSIGNEE_USER_ID` workflow variable\n- [ ] Set `TEAMS_TEAM_ID` workflow variable\n- [ ] Set `TEAMS_CHANNEL_ID` workflow variable\n- [ ] Set `MONITORED_MAILBOX` workflow variable (UPN of the mailbox to scan)\n- [ ] Set `ACTION_KEYWORDS` workflow variable (comma-separated, defaults to: ACTION REQUIRED,URGENT,TASK:,PLEASE REVIEW,CRITICAL)\n- [ ] Set `REPORT_TIMEZONE` workflow variable (defaults to: Europe/Helsinki)\n- [ ] Update error email recipient in Send Error Notification Email node\n\n### Required credentials\nMicrosoft Graph OAuth2 (covers all M365 nodes)\n\n### Required Graph API permissions\n`Mail.ReadWrite` `Tasks.ReadWrite` `ChannelMessage.Send` `SecurityEvents.Read.All`"
      },
      "typeVersion": 1
    },
    {
      "id": "a87c4a38-610a-430a-bc26-5d892a9534f9",
      "name": "Pipeline 1 Label",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8976,
        912
      ],
      "parameters": {
        "color": 5,
        "width": 4032,
        "height": 80,
        "content": "# \u2709\ufe0f Pipeline 1: Email-to-Task Parser"
      },
      "typeVersion": 1
    },
    {
      "id": "799ea09c-9844-4f8d-8923-b699348c1044",
      "name": "Pipeline 2 Label",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8976,
        1744
      ],
      "parameters": {
        "color": 5,
        "width": 2668,
        "height": 80,
        "content": "# \ud83d\udd10 Pipeline 2: Security Score Monitor"
      },
      "typeVersion": 1
    },
    {
      "id": "e13bfa78-6de6-4181-b9a0-1b5c745fdba0",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8976,
        1040
      ],
      "parameters": {
        "color": 7,
        "width": 912,
        "height": 628,
        "content": "## Email Ingestion\nPolls Outlook every 30 minutes for unread emails. Exits cleanly if inbox is empty. Splits emails into a sequential batch loop for one-at-a-time processing."
      },
      "typeVersion": 1
    },
    {
      "id": "c09c975b-8106-442b-b46d-f57bbd30f1b5",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9952,
        1040
      ],
      "parameters": {
        "color": 7,
        "width": 752,
        "height": 628,
        "content": "## Keyword Extraction & Date Parsing\nStrips HTML from email body, then scans subject and body for configurable action keywords. Extracts due dates from ISO format, structured `due:`/`by:` prefixes, or relative phrases (today, tomorrow, end of week, Friday). Emails with no keyword match are marked read and skipped. Timezone is read from the `REPORT_TIMEZONE` workflow variable with a `process.env` fallback."
      },
      "typeVersion": 1
    },
    {
      "id": "2dc55b9f-9826-41fc-8fd4-ae2a391d5a95",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        10768,
        1040
      ],
      "parameters": {
        "color": 7,
        "width": 1396,
        "height": 628,
        "content": "## Task Creation Chain\nCreates a Planner task via Graph API with title, priority, due date, and assignee. Sets the task description with sender details and a 500-char body preview. Posts an HTML Teams notification with subject, sender, keyword, priority, and due date. Sends an HTML auto-reply to the sender. Marks the email as read and tags it with the `Task Created` category via a dedicated update node \u2014 this is the fix that prevents the infinite re-processing loop. All nodes run with Continue on Fail so a single API error does not crash the batch."
      },
      "typeVersion": 1
    },
    {
      "id": "56313c70-6763-405a-8862-238d20cbede4",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        12224,
        1040
      ],
      "parameters": {
        "color": 7,
        "width": 784,
        "height": 628,
        "content": "## Poison Pill Guard & Rate Limit\nChecks upstream nodes for silent failures after each email is processed. The `Had Error?` IF node routes confirmed failures to a dead-end branch (wire to an admin alert node as needed) and clean executions to the rate limit wait. Waits 2 seconds between emails to respect Graph API limits, then loops back to the batch splitter for the next email."
      },
      "typeVersion": 1
    },
    {
      "id": "2c63afd7-412a-4d9b-a9ce-d03fa7a32865",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8976,
        1872
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 524,
        "content": "## Score Fetch & Calculation\nRuns every Monday at 08:00. Fetches the latest Secure Score from the Microsoft Graph Security API and calculates it as a percentage of the maximum achievable score. Threshold is 80% \u2014 below triggers the alert path, at or above posts a healthy summary to Teams. Timezone is read from the `REPORT_TIMEZONE` workflow variable with a `process.env` fallback."
      },
      "typeVersion": 1
    },
    {
      "id": "7efea3ce-95d9-4e3b-ac4c-6686430ea6e9",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9904,
        1872
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 528,
        "content": "## Alert Routing & Deduplication\nChecks whether an open Security Score Alert task already exists in the Planner bucket. If one is open, resolves its task ID and updates the description with the latest score. If no open task exists, creates a new one. Prevents duplicate tasks accumulating across weekly runs."
      },
      "typeVersion": 1
    },
    {
      "id": "20f59e87-680d-40ec-b35d-770d40f55755",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        10704,
        1872
      ],
      "parameters": {
        "color": 7,
        "width": 940,
        "height": 528,
        "content": "## Security Task & Teams Alert\nCreates or updates a priority-1 Planner task with a 24-hour due date and a description linking to the Defender portal. Posts an HTML Teams alert with the current score, maximum score, and scan date. If the score is healthy, posts a green summary instead."
      },
      "typeVersion": 1
    },
    {
      "id": "8d0a9314-4546-494f-943b-4b470464c03a",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8976,
        2448
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 404,
        "content": "## Global Error Failsafe\nIndependent trigger that catches any catastrophic workflow failure and sends a structured HTML error email via the Graph API with the execution ID, failed node name, error message, and timestamp."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "1febc07a-3003-455b-b8b0-60a06f14241b",
  "connections": {
    "Had Error?": {
      "main": [
        [],
        [
          {
            "node": "API Rate Limit Throttle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any Emails?": {
      "main": [
        [
          {
            "node": "One Email at a Time",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No New Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 30 Minutes": {
      "main": [
        [
          {
            "node": "Get Unread Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Unread Emails": {
      "main": [
        [
          {
            "node": "Any Emails?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Planner Task": {
      "main": [
        [
          {
            "node": "Set Task Details (Notes)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday at 8AM": {
      "main": [
        [
          {
            "node": "Get Latest Secure Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Score Below 80%?": {
      "main": [
        [
          {
            "node": "Get Existing Planner Tasks",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post Security Teams Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One Email at a Time": {
      "main": [
        [
          {
            "node": "Extract Task from Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keyword Match Found?": {
      "main": [
        [
          {
            "node": "Create Planner Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark as Read (No Action)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Done (Email Processed)": {
      "main": [
        [
          {
            "node": "Error Capture (Poison Pill Guard)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Error Trigger": {
      "main": [
        [
          {
            "node": "Send Error Notification Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API Rate Limit Throttle": {
      "main": [
        [
          {
            "node": "One Email at a Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Task from Email": {
      "main": [
        [
          {
            "node": "Keyword Match Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Latest Secure Score": {
      "main": [
        [
          {
            "node": "Calculate Security Score Percentage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Teams Notification": {
      "main": [
        [
          {
            "node": "Reply, Categorize & Mark Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Task Already Open?": {
      "main": [
        [
          {
            "node": "Create Security Planner Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Resolve Existing Task ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as Read (No Action)": {
      "main": [
        [
          {
            "node": "API Rate Limit Throttle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as Read After Reply": {
      "main": [
        [
          {
            "node": "Done (Email Processed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Existing Task ID": {
      "main": [
        [
          {
            "node": "Update Existing Alert Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Task Details (Notes)": {
      "main": [
        [
          {
            "node": "Post Teams Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Security Task Details": {
      "main": [
        [
          {
            "node": "Post Security Teams Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Existing Planner Tasks": {
      "main": [
        [
          {
            "node": "Alert Task Already Open?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Existing Alert Task": {
      "main": [
        [
          {
            "node": "Post Security Teams Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Security Planner Task": {
      "main": [
        [
          {
            "node": "Set Security Task Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reply, Categorize & Mark Read": {
      "main": [
        [
          {
            "node": "Mark as Read After Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Capture (Poison Pill Guard)": {
      "main": [
        [
          {
            "node": "Had Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Security Score Percentage": {
      "main": [
        [
          {
            "node": "Is Score Below 80%?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}