{
  "name": "03 - Sort Old Emails with GPT-5.6 Luna",
  "active": false,
  "versionId": "b5cb4ca3-a987-4ca5-9db2-10fd1769c145",
  "nodes": [
    {
      "parameters": {
        "url": "https://graph.microsoft.com/v1.0/me/mailFolders?$top=50&$select=id,displayName",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOutlookOAuth2Api",
        "options": {}
      },
      "id": "bulk-get-folders",
      "name": "Get Folders (Bulk)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        256,
        848
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================\n// ⚙️ SETTINGS - Edit to customize\n// ============================================\n//\n// HOW TO CHANGE:\n// - To disable something, add // at the start of the line\n// - To enable something, remove the // from the start\n// - Only ONE model should be uncommented at a time\n//\n// ============================================\n\n\n// 🤖 REPLICATE AI MODEL\n// Verified model and reasoning configuration for all workflow stages.\nconst MODEL = 'openai/gpt-5.6-luna';\nconst MODEL_LABEL = 'GPT-5.6 Luna';\nconst REASONING_EFFORT = 'none';\nconst BATCH_SIZE = 50;\n\n\n// 🚫 FOLDER BLACKLIST\n// Folders the AI will NEVER sort into.\n// Remove a line to let the AI use that folder.\n\nconst FOLDER_BLACKLIST = [\n  'Inbox',\n  'Archive',\n  'Conversation History',\n  'Deleted Items',\n  'Drafts',\n  'Junk Email',\n  'Junk',\n  'Outbox',\n  'Sent Items',\n  'Notes',\n  'RSS Feeds',\n  'Sync Issues',\n  'Clutter',\n];\n\n\n// 📝 PROMPT\nconst PROMPT = `You are an email triage assistant. Given the available Outlook folders and an incoming email, decide which folder is most appropriate.\n\nAvailable folders: \\${folderNames}\n\nRespond with ONLY a JSON object, no other text:\n{\"folder\": \"<exact folder name>\", \"reason\": \"<1 sentence>\"}\n\nIf no folder is a good fit, use \"_Unknown\" to flag it for manual review. NEVER use \"Inbox\".\n\nFrom: \\${from_name} <\\${from_email}>\nSubject: \\${subject}\nBody: \\${body}`;\n\n\n// 📏 MAX BODY LENGTH (characters)\nconst MAX_BODY = 300;\n\n\nreturn [{ json: {\n  skip: 0,\n  batchSize: BATCH_SIZE,\n  model: MODEL,\n  modelLabel: MODEL_LABEL,\n  reasoningEffort: REASONING_EFFORT,\n  promptTemplate: PROMPT,\n  folderBlacklist: FOLDER_BLACKLIST,\n  maxBody: MAX_BODY\n}}];"
      },
      "id": "bulk-settings",
      "name": "⚙️ Settings (Bulk)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        256,
        672
      ]
    },
    {
      "parameters": {
        "url": "=https://graph.microsoft.com/v1.0/me/mailFolders/Inbox/messages?$top={{ $json.batchSize }}&$skip={{ $json.skip }}&$select=id,subject,bodyPreview,from,conversationId&$orderby=receivedDateTime desc",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOutlookOAuth2Api",
        "options": {}
      },
      "id": "bulk-fetch-emails",
      "name": "Fetch Email Batch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        752,
        736
      ]
    },
    {
      "parameters": {
        "jsCode": "let settings, folders;\ntry {\n  settings = $('⚙️ Settings (Bulk)').first().json;\n  const fd = $('Get Folders (Bulk)').first().json;\n  const blacklist = (settings.folderBlacklist || []).map(f => f.toLowerCase());\n  folders = (fd.value || [])\n    .filter(f => f.displayName && !blacklist.includes(f.displayName.toLowerCase()))\n    .map(f => ({ id: f.id, name: f.displayName }));\n} catch(e) {\n  return [{ json: { error: 'Could not load settings/folders: ' + e.message } }];\n}\n\nconst emailBatch = $('Fetch Email Batch').first().json;\nconst emails = emailBatch.value || [];\nif (emails.length === 0) return [{ json: { done: true } }];\n\nconst folderNames = folders.map(f => f.name).join(', ');\n\nreturn emails.map(email => {\n  const from_email = email.from?.emailAddress?.address || '';\n  const from_name = email.from?.emailAddress?.name || '';\n  const subject = email.subject || '(no subject)';\n  const body = (email.bodyPreview || '').substring(0, settings.maxBody || 300);\n  const prompt = eval('`' + settings.promptTemplate + '`');\n\n  return { json: {\n    prompt,\n    messageId: email.id,\n    subject,\n    from: from_email,\n    model: settings.model,\n    reasoningEffort: settings.reasoningEffort,\n    folders: JSON.stringify(folders)\n  }};\n});\n"
      },
      "id": "bulk-classify",
      "name": "Build Prompts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        736
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "bulk-move-check",
              "leftValue": "={{ $json.shouldMove }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "bulk-should-move",
      "name": "Should Move? (Bulk)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1552,
        736
      ]
    },
    {
      "parameters": {
        "operation": "move",
        "messageId": "={{ $json.messageId }}",
        "folderId": "={{ $json.folderId }}"
      },
      "id": "bulk-move",
      "name": "Move Email (Bulk)",
      "type": "n8n-nodes-base.microsoftOutlook",
      "typeVersion": 2,
      "position": [
        2032,
        720
      ]
    },
    {
      "parameters": {},
      "id": "bulk-skip",
      "name": "Skip (Stay in Inbox)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2032,
        1120
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "id": "bulk-merge",
      "name": "Merge (Bulk)",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.1,
      "position": [
        512,
        736
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.replicate.com/v1/models/openai/gpt-5.6-luna/predictions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Prefer",
              "value": "wait=60"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.replicateRequest }}",
        "options": {
          "timeout": 30000,
          "batching": {
            "batch": {
              "batchSize": 5,
              "batchInterval": 1000
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "id": "bulk-classify-http",
      "name": "Classify Email (API)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1152,
        736
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 6000
    },
    {
      "parameters": {
        "jsCode": "function parseStructured(value) {\n  if (value && typeof value === 'object' && !Array.isArray(value)) {\n    if (value.json_output && typeof value.json_output === 'object') return value.json_output;\n    if (value.output?.json_output && typeof value.output.json_output === 'object') return value.output.json_output;\n    if (typeof value.text !== 'string' && typeof value.output?.text !== 'string') return value;\n  }\n  const source = typeof value?.text === 'string'\n    ? value.text\n    : typeof value?.output?.text === 'string'\n      ? value.output.text\n      : value;\n  const text = Array.isArray(source)\n    ? source.map(part => typeof part === 'string' ? part : JSON.stringify(part)).join('')\n    : String(source ?? '');\n  try { return JSON.parse(text); } catch {}\n  const match = text.match(/\\{[\\s\\S]*\\}/);\n  if (!match) return {};\n  try { return JSON.parse(match[0]); } catch { return {}; }\n}\nconst allInputs = $input.all().map(item => item.json);\nconst allPrompts = $('Build Prompts').all().map(item => item.json);\nreturn allInputs.map((input, index) => {\n  const emailData = allPrompts[index] || {};\n  const parsed = parseStructured(input.output ?? input);\n  let folderName = String(parsed.folder || '_Unknown');\n  if (folderName.toLowerCase() === 'inbox') folderName = '_Unknown';\n  const folders = JSON.parse(emailData.folders || '[]');\n  const match = folders.find(folder => folder.name.toLowerCase() === folderName.toLowerCase());\n  return { json: {\n    folder: folderName,\n    reason: String(parsed.reason || 'No reason returned'),\n    folderId: match?.id || null,\n    messageId: emailData.messageId,\n    subject: emailData.subject,\n    from: emailData.from,\n    shouldMove: Boolean(match?.id),\n    needsCreate: !match?.id\n  } };\n});"
      },
      "id": "bulk-parse",
      "name": "Parse & Match Folder",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1360,
        736
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "needs-create",
              "leftValue": "={{ $json.needsCreate }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "needs-folder-check",
      "name": "Needs Folder?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1776,
        928
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://graph.microsoft.com/v1.0/me/mailFolders",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOutlookOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ displayName: $json.folder }) }}",
        "options": {}
      },
      "id": "create-and-move",
      "name": "Create Folder & Move",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2032,
        912
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://graph.microsoft.com/v1.0/me/messages/{{ $('Needs Folder?').item.json.messageId }}/move",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOutlookOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ destinationId: $json.id }) }}",
        "options": {}
      },
      "id": "move-after-create",
      "name": "Move to New Folder",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2272,
        912
      ]
    },
    {
      "parameters": {},
      "id": "bulk-trigger",
      "name": "Sort Historical Emails",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        736
      ]
    },
    {
      "parameters": {
        "content": "## 3. Historical sorting\n\n**Manual backfill stage.** Fetches a batch of older inbox mail, builds one classification request per message, uses Replicate `openai/gpt-5.6-luna` with **no reasoning**, then moves to an existing folder or creates the required folder before moving.\n\n### Setup after import\n\n1. Select your own Microsoft Outlook OAuth2 credential on every Outlook and Microsoft Graph node.\n2. Create or select a Generic Bearer Auth credential containing your Replicate API token on the GPT-5.6 Luna HTTP Request node.\n3. Review the Settings node and placeholders before activating or running this workflow.",
        "height": 736,
        "width": 2576,
        "color": 6
      },
      "id": "stage-note-historical-sorting",
      "name": "Stage 3 — Historical sorting",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -112,
        544
      ]
    },
    {
      "id": "bulk-classify-http-request-builder",
      "name": "Build Classify Email (API) Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        928,
        736
      ],
      "parameters": {
        "jsCode": "return $input.all().map(item => {\n  const data = item.json;\n  const prompt = data.prompt;\n  return {\n    ...item,\n    json: {\n      ...data,\n      replicateRequest: {\n        input: {\n          prompt,\n          reasoning_effort: \"none\",\n          verbosity: 'low',\n          max_completion_tokens: 2000,\n        },\n      },\n    },\n  };\n});"
      }
    }
  ],
  "connections": {
    "⚙️ Settings (Bulk)": {
      "main": [
        [
          {
            "node": "Merge (Bulk)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Folders (Bulk)": {
      "main": [
        [
          {
            "node": "Merge (Bulk)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge (Bulk)": {
      "main": [
        [
          {
            "node": "Fetch Email Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Email Batch": {
      "main": [
        [
          {
            "node": "Build Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Move? (Bulk)": {
      "main": [
        [
          {
            "node": "Move Email (Bulk)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs Folder?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Prompts": {
      "main": [
        [
          {
            "node": "Build Classify Email (API) Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Email (API)": {
      "main": [
        [
          {
            "node": "Parse & Match Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Match Folder": {
      "main": [
        [
          {
            "node": "Should Move? (Bulk)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Folder?": {
      "main": [
        [
          {
            "node": "Create Folder & Move",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip (Stay in Inbox)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Folder & Move": {
      "main": [
        [
          {
            "node": "Move to New Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort Historical Emails": {
      "main": [
        [
          {
            "node": "Get Folders (Bulk)",
            "type": "main",
            "index": 0
          },
          {
            "node": "⚙️ Settings (Bulk)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Classify Email (API) Request": {
      "main": [
        [
          {
            "node": "Classify Email (API)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
