{
  "name": "03 - Sort Old Gmail with GPT-5.6 Luna",
  "nodes": [
    {
      "parameters": {
        "content": "## 3. Backfill existing messages by position\n\n**Manual stage.** Edit `startEmail` and `endEmail` in `Gmail Backfill Settings`, then run intentionally. Positions are one-based and inclusive: `1–50` means the most recent through the 50th most recent; `51–100` means the next fifty. The Gmail node fetches through `endEmail`, then the bundle node keeps only the requested range. Messages with no matching user label remain unchanged. It never creates system labels, archives, deletes, marks read, or removes labels.",
        "height": 470,
        "width": 2920,
        "color": 6
      },
      "id": "s3-note",
      "name": "Stage 3 — Backfill Gmail labels",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -160,
        400
      ]
    },
    {
      "parameters": {},
      "id": "s3-trigger",
      "name": "Backfill Existing Gmail Messages",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -80,
        576
      ]
    },
    {
      "parameters": {
        "jsCode": "const startEmail = 1; // First email to process: 1 is the most recent; 51 starts with the 51st most recent.\nconst endEmail = 50; // Last email to process, included: 50 gives 1–50; 100 gives 51–100 when startEmail is 51.\nconst searchQuery = 'in:inbox -in:spam -in:trash';\n\nif (!Number.isInteger(startEmail) || !Number.isInteger(endEmail) || startEmail < 1 || endEmail < startEmail) {\n  throw new Error('startEmail and endEmail must be whole numbers, with 1 <= startEmail <= endEmail');\n}\n\nreturn [{ json: { startEmail, endEmail, fetchLimit: endEmail, searchQuery } }];"
      },
      "id": "s3-settings",
      "name": "Gmail Backfill Settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        176,
        576
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "resource": "message",
        "operation": "getAll",
        "returnAll": false,
        "limit": "={{ $json.fetchLimit }}",
        "simple": true,
        "filters": {
          "includeSpamTrash": false,
          "q": "={{ $json.searchQuery }}",
          "readStatus": "both"
        },
        "options": {}
      },
      "id": "s3-get-messages",
      "name": "Get Gmail Backfill Batch",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        432,
        576
      ]
    },
    {
      "parameters": {
        "jsCode": "const fetchedMessages = $input.all().map(item => item.json);\nconst settings = $('Gmail Backfill Settings').first().json;\nconst messages = fetchedMessages.slice(settings.startEmail - 1, settings.endEmail);\nreturn [{ json: { messages, settings, fetchedMessageCount: fetchedMessages.length } }];"
      },
      "id": "s3-bundle",
      "name": "Bundle Gmail Backfill Batch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        688,
        576
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "resource": "label",
        "operation": "getAll",
        "returnAll": true
      },
      "id": "s3-get-labels",
      "name": "Get Gmail User Labels (Backfill)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        944,
        576
      ]
    },
    {
      "parameters": {
        "jsCode": "const messages = $('Bundle Gmail Backfill Batch').first().json.messages || [];\nconst userLabels = $input.all().map(item => item.json).filter(label => label.type === 'user' && label.id && label.name);\nconst allowed = userLabels.map(label => ({ id: label.id, name: label.name }));\nreturn messages.map(message => ({ json: {\n  messageId: message.id,\n  allowedLabels: allowed,\n  prompt: [\n    'Classify this existing Gmail message into at most one existing user label.',\n    'Return ONLY valid JSON: {\"label\":\"exact existing label name or empty string\",\"reason\":\"short reason\"}.',\n    'Use only an exact name from ALLOWED_USER_LABELS. Never choose or invent a Gmail system label.',\n    'If no label clearly fits, return an empty label and leave the message unchanged in Inbox.',\n    'Do not archive, delete, mark read, or remove labels.',\n    'ALLOWED_USER_LABELS: ' + JSON.stringify(allowed.map(label => label.name)),\n    'MESSAGE: ' + JSON.stringify({ from: message.From || message.from || '', subject: message.Subject || message.subject || '(no subject)', snippet: String(message.snippet || '').slice(0, 1200), currentLabels: (message.labels || []).map(label => label.name) }),\n  ].join('\\n'),\n} }));"
      },
      "id": "s3-build-prompts",
      "name": "Build Gmail Backfill Prompts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1216,
        576
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.replicate.com/v1/models/openai/gpt-5.6-luna/predictions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Prefer",
              "value": "wait=60"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.replicateRequest }}",
        "options": {
          "timeout": 120000,
          "batching": {
            "batch": {
              "batchSize": 5,
              "batchInterval": 1000
            }
          }
        }
      },
      "id": "s3-ai",
      "name": "Classify Existing Gmail Message",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1504,
        576
      ],
      "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 sources = $('Build Gmail Backfill Prompts').all();\nconst responses = $input.all();\nif (responses.length !== sources.length) {\n  throw new Error(`Expected ${sources.length} classification responses but received ${responses.length}`);\n}\nreturn responses.map((item, index) => {\n  const source = sources[index].json;\n  const response = item.json;\n  const parsed = parseStructured(response.output ?? response);\n  const requested = String(parsed.label || '').trim();\n  const reason = String(parsed.reason || '').trim();\n  const label = (source.allowedLabels || []).find(labelItem => labelItem.name.toLowerCase() === requested.toLowerCase());\n  return {\n    ...item,\n    json: { messageId: source.messageId, labelId: label?.id || '', labelName: label?.name || '', reason },\n    pairedItem: { item: index },\n  };\n});"
      },
      "id": "s3-match",
      "name": "Match Existing Gmail Label (Backfill)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1776,
        576
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "83364c1c-78fb-4c3f-863b-77f2adc9f367",
              "leftValue": "={{ $json.labelId }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "s3-if",
      "name": "Has Matching Gmail Label? (Backfill)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2032,
        576
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "resource": "message",
        "operation": "addLabels",
        "messageId": "={{ $json.messageId }}",
        "labelIds": "={{ [$json.labelId] }}"
      },
      "id": "s3-add-label",
      "name": "Add Gmail Label to Existing Message",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2304,
        512
      ]
    },
    {
      "parameters": {},
      "id": "s3-noop",
      "name": "Leave Existing Message in Inbox",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2304,
        672
      ]
    },
    {
      "id": "s3-ai-request-builder",
      "name": "Build Classify Existing Gmail Message Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        576
      ],
      "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": {
    "Backfill Existing Gmail Messages": {
      "main": [
        [
          {
            "node": "Gmail Backfill Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail Backfill Settings": {
      "main": [
        [
          {
            "node": "Get Gmail Backfill Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Gmail Backfill Batch": {
      "main": [
        [
          {
            "node": "Bundle Gmail Backfill Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bundle Gmail Backfill Batch": {
      "main": [
        [
          {
            "node": "Get Gmail User Labels (Backfill)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Gmail User Labels (Backfill)": {
      "main": [
        [
          {
            "node": "Build Gmail Backfill Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Gmail Backfill Prompts": {
      "main": [
        [
          {
            "node": "Build Classify Existing Gmail Message Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Existing Gmail Message": {
      "main": [
        [
          {
            "node": "Match Existing Gmail Label (Backfill)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Existing Gmail Label (Backfill)": {
      "main": [
        [
          {
            "node": "Has Matching Gmail Label? (Backfill)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Matching Gmail Label? (Backfill)": {
      "main": [
        [
          {
            "node": "Add Gmail Label to Existing Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leave Existing Message in Inbox",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Classify Existing Gmail Message Request": {
      "main": [
        [
          {
            "node": "Classify Existing Gmail Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false,
    "timezone": "Australia/Sydney"
  },
  "versionId": "32e80008-14c7-41fb-9087-e70e7f92b9f6",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
