{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "2edf070f-9e4b-46eb-9b78-77a9bc05ea55",
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        -896,
        208
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "readStatus": "unread"
        },
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8aa5c340-cff7-4714-ac66-90f1b6df2560",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -656,
        64
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "5e9c9bd9-b9a7-4635-91ab-f7a021128c11",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -48,
        336
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f3503d1c-03b7-4204-8b90-34cc9b58d1d8",
      "name": "Get Emails By Sender Email",
      "type": "n8n-nodes-base.gmailTool",
      "position": [
        144,
        336
      ],
      "parameters": {
        "limit": 10,
        "filters": {
          "sender": "={{ $('Loop Over Items').item.json.from.value[0].address }}",
          "receivedBefore": "={{ $('Loop Over Items').item.json.date }}",
          "includeSpamTrash": false
        },
        "operation": "getAll",
        "descriptionType": "manual",
        "toolDescription": "=Fetches previous emails sent by this sender. Always use this tool first. Returned items include subject, body, and previously assigned label."
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "4cd2fddd-e18a-4388-ac8f-9b69d73c172c",
      "name": "Get Emails By Label",
      "type": "n8n-nodes-base.gmailTool",
      "position": [
        304,
        336
      ],
      "parameters": {
        "limit": 10,
        "filters": {
          "q": "=label:{{ $fromAI('Label_Names', ``, 'string', 'The Label Name') }}",
          "includeSpamTrash": false
        },
        "operation": "getAll",
        "descriptionType": "manual",
        "toolDescription": "=Fetches previously labeled emails for a given label. Use only if sender history is insufficient or unclear. Compare content to confirm label matching."
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "795a9f38-d015-45d8-bcd8-425fc6b5e004",
      "name": "Email Classifier Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        112,
        64
      ],
      "parameters": {
        "text": "=You have received a new email. Label it correctly following all system rules.\n\nSubject: {{ $('Loop Over Items').item.json.headers.subject }}\nEmail Body: {{ $('Loop Over Items').item.json.text }}\n\nProcess:\n1. First call \"Get Emails By Sender Email\".\n2. Analyze past labels from this sender (if any).\n3. If needed, call \"Get Emails By Label\" to compare with category examples.\n4. Assign the most appropriate label based on rules.\n5. If none clearly fit, return \"None\".\n\nReturn output ONLY as:\n{\n  \"label\": \"...\"\n}\n",
        "options": {
          "systemMessage": "=You are an Email Classification AI Agent. Your job is to assign the correct label to incoming emails based on their subject and body, while optionally using tools to retrieve contextual history.\n\nYour final output MUST ALWAYS be in this format:\n{\n  \"label\": \"...\"\n}\n\nOnly choose one label, or return \"None\" if no label logically fits.\n---------------------------------------\nLABEL DEFINITIONS\n---------------------------------------\nMeetings:\nEmails containing virtual meetings, appointment scheduling, event details, Zoom/Meet links, or anything referencing a scheduled time.\n\nExpenses:\nBills, invoices, receipts, payment reminders, fees, subscription renewals, upcoming payments.\n\nIncome:\nPayment received, payouts, transfer confirmations, deposit notifications, earnings from platforms.\n\nNotify / Verify:\nAccount notifications, verification codes, login alerts, confirmations, 2FA codes, system alerts (non-promotional).\n\nOrders / Deliveries:\nOrder confirmations, shipping updates, tracking numbers, package delivery notifications.\n\nInquiries:\nGeneral questions, outreach emails, sales proposals, partnership requests, sponsorships, cold emails.\n\nTrash Likely:\nSpam, newsletters, promotions, ads, marketing blasts, unnecessary subscription emails.\n\nNone:\nUse only when absolutely none of the above categories correctly fit.\n\n---------------------------------------\nCLASSIFICATION LOGIC RULES\n---------------------------------------\n1. **ALWAYS start by calling \u201cGet Emails By Sender Email\u201d.**\n   - Fetch all historical emails from this sender.\n   - Read their subjects, bodies, and **previously assigned labels**.\n\n2. **Apply Majority Rule Bias (80% Rule).**\n   - If 80% or more of historical emails from this sender have the same label,\n     strongly prefer that label *if the new email is even loosely consistent.*\n   - If the new email drastically contradicts that label, ignore the bias.\n\n3. **If the sender has NO prior emails:**\n   - Skip sender-based logic.\n   - Classify purely based on subject + body content.\n\n4. **If sender history is not enough to make a confident classification:**\n   - Make a *best guess* label candidate.\n   - Then call \u201cGet Emails By Label\u201d using that candidate to fetch known examples.\n   - Compare and refine your decision.\n\n5. **If still uncertain after comparisons:**\n   - Return:\n     {\n       \"label\": \"None\"\n     }\n\n6. **The final output must ONLY be the JSON label.**\n   No explanations, no tool thoughts, no reasoning.\n\n---------------------------------------\nGENERAL BEHAVIOR\n---------------------------------------\n- Think step-by-step, but only output JSON at the end.\n- Prefer precision over guessing.\n- Never return more than one label.\n"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "e6acfbf9-5306-4091-8dee-dd0916cf0819",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        480,
        336
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"label\": \"The Selected Label Name\"\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "9169d5e2-109a-4a43-bbab-0323450e976f",
      "name": "Add label to thread",
      "type": "n8n-nodes-base.gmail",
      "position": [
        928,
        48
      ],
      "parameters": {
        "labelIds": "={{ [$json.gmailLabelId] }}",
        "resource": "thread",
        "threadId": "={{ $('Loop Over Items').item.json.threadId }}",
        "operation": "addLabels"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "311ca8a2-3c7b-46f2-aa89-8c6f8e40beca",
      "name": "Check Label Existence",
      "type": "n8n-nodes-base.code",
      "position": [
        -400,
        80
      ],
      "parameters": {
        "jsCode": "const labelMap = {\n  \"Label_2420158633459891270\": \"Meetings\",\n  \"Label_2793806992005255116\": \"Inquiries\",\n  \"Label_4191442057272125729\": \"Notify / Verify\",\n  \"Label_4895303893849157619\": \"Expenses\",\n  \"Label_670736358532545568\": \"Orders / Deliveries\",\n  \"Label_6828+1234567890\": \"Trash Likely\"\n};\n\nconst matchedLabel = ($input.first().json.labelIds ?? [])\n  .find(id => labelMap[id]) \n  ? labelMap[($input.first().json.labelIds ?? []).find(id => labelMap[id])]\n  : null;\n\nreturn [{ json: { hasAILabelAssigned: !!matchedLabel, matchedLabel } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4bd26bec-57f3-45ce-a224-1c776bc8b60a",
      "name": "If Not Assigned",
      "type": "n8n-nodes-base.if",
      "position": [
        -192,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "01b63305-1a36-4800-b450-4aea118e6e30",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.hasAILabelAssigned }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8bc95c18-b697-4710-9b79-86023a9c58f6",
      "name": "If not None",
      "type": "n8n-nodes-base.if",
      "position": [
        464,
        64
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1bfd81e3-921c-4e4a-a1a2-70ac049a333b",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.output.label }}",
              "rightValue": "None"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "49ab9a45-b23d-4a81-a4da-f5afca5dd84d",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -896,
        -48
      ],
      "parameters": {
        "limit": 10,
        "simple": false,
        "filters": {},
        "options": {},
        "operation": "getAll"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "f7c194b0-a74b-4252-b418-4c9ebc936800",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1104,
        -48
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "2e244f4c-2b4e-41bb-a453-132e1bb0e5ab",
      "name": "Get Labels Info",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -656,
        608
      ],
      "parameters": {
        "resource": "label",
        "returnAll": true
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "6de8b305-9f91-420e-9186-21c3b542e18e",
      "name": "Convert Label to Label ID",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        48
      ],
      "parameters": {
        "jsCode": "const aiLabel = $input.first().json.output.label; // e.g. \"Meetings\"\n\n// Mapping of AI labels \u2192 Gmail label IDs\nconst labelToId = {\n  \"Meetings\": \"Label_2420158633459891270\",\n  \"Inquiries\": \"Label_2793806992005255116\",\n  \"Notify / Verify\": \"Label_4191442057272125729\",\n  \"Expenses\": \"Label_4895303893849157619\",\n  \"Orders / Deliveries\": \"Label_670736358532545568\",\n  \"Trash Likely\": \"Label_6828+1234567890\"\n};\n\nreturn [\n  {\n    json: {\n      gmailLabelId: labelToId[aiLabel] || null\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "90961646-64c4-4328-80f3-d8f39a5dc656",
      "name": "Remove Inbox label from thread",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1136,
        48
      ],
      "parameters": {
        "labelIds": [
          "INBOX"
        ],
        "resource": "thread",
        "threadId": "={{ $json.messages[0].threadId }}",
        "operation": "removeLabels"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "dda08ae5-40a6-45cc-990c-1a0e5204e998",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1664,
        -224
      ],
      "parameters": {
        "color": 4,
        "width": 372,
        "height": 290,
        "content": "## \ud83c\udfaf WORKFLOW OVERVIEW\n\n**Auto-classifies & labels Gmail using AI**\n\n\u2705 Checks inbox every minute for unread emails\n\u2705 Uses Google Gemini AI to classify emails\n\u2705 Applies labels based on content + sender history\n\u2705 Archives emails (removes from INBOX)\n\u2705 Skips already-labeled emails\n\n**Result:** Zero-inbox email management system"
      },
      "typeVersion": 1
    },
    {
      "id": "0e73909d-025c-4020-97d3-3f96ddaddcb3",
      "name": "Setup Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1664,
        112
      ],
      "parameters": {
        "color": 2,
        "width": 400,
        "height": 684,
        "content": "## \ud83d\udd27 SETUP STEPS (DO THIS FIRST!)\n\n### 1\ufe0f\u20e3 Create Gmail Labels\nCreate these exact labels in Gmail:\n- Meetings\n- Inquiries  \n- Notify / Verify\n- Expenses\n- Orders / Deliveries\n- Trash Likely\n\n### 2\ufe0f\u20e3 Get Your Label IDs\n- Click the Manual Trigger below\n- Run \"Get Labels Info\" node\n- Copy the ID for each label\n- Update the two Code nodes\n\n### 3\ufe0f\u20e3 Configure Credentials\n- Gmail OAuth2 (for email access)\n- Google Gemini API (for AI)\n\n### 4\ufe0f\u20e3 Test Before Activating\n- Send yourself test emails\n- Run manually\n- Verify labels applied correctly"
      },
      "typeVersion": 1
    },
    {
      "id": "6ab0101b-b93d-4332-842d-f1ab764c7632",
      "name": "Gmail Trigger Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        144
      ],
      "parameters": {
        "color": 3,
        "width": 280,
        "height": 220,
        "content": "## \u26a1 TRIGGER\n\nPolls Gmail every 1 minute\nOnly fetches UNREAD emails\n\n\u2699\ufe0f To change frequency:\nEdit pollTimes \u2192 mode"
      },
      "typeVersion": 1
    },
    {
      "id": "061329fd-d9b1-4316-90af-003b201e3a5b",
      "name": "Label Check Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -448,
        -304
      ],
      "parameters": {
        "color": 6,
        "width": 380,
        "height": 340,
        "content": "## \ud83d\udee1\ufe0f SKIP IF ALREADY LABELED\n\n**Check Label Existence** node:\n- Looks at email's current labels\n- Checks if any AI label already exists\n- Returns hasAILabelAssigned: true/false\n\n**If Not Assigned** node:\n- Only continues if hasAILabelAssigned = false\n- Prevents re-processing same email\n\n\u26a0\ufe0f **IMPORTANT:** Update labelMap in this Code node with YOUR label IDs!"
      },
      "typeVersion": 1
    },
    {
      "id": "c1b365b3-792f-4739-b5df-498d74788665",
      "name": "AI Agent Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        -624
      ],
      "parameters": {
        "color": 4,
        "width": 420,
        "height": 632,
        "content": "## \ud83e\udde0 AI CLASSIFICATION ENGINE\n\n**How it works:**\n\n1\ufe0f\u20e3 **Get Emails By Sender** (always called first)\n   - Fetches sender's email history\n   - Checks previously assigned labels\n   \n2\ufe0f\u20e3 **80% RULE** (Majority Bias)\n   - If 80%+ of sender's emails = same label\n   - Strongly prefer that label (if email matches)\n   \n3\ufe0f\u20e3 **Get Emails By Label** (if uncertain)\n   - Fetches examples from suspected category\n   - Compares current email to examples\n   \n4\ufe0f\u20e3 **Returns JSON:**\n   ```json\n   { \"label\": \"Category Name\" }\n   ```\n   or\n   ```json\n   { \"label\": \"None\" }\n   ```\n\n\ud83d\udca1 **Pro Tip:** The AI learns from sender patterns!\nIf someone always sends invoices, future emails\nfrom them will likely be labeled \"Expenses\""
      },
      "typeVersion": 1
    },
    {
      "id": "15599558-75c6-4717-803e-468b0161f405",
      "name": "Sender Tool Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        528
      ],
      "parameters": {
        "color": 5,
        "width": 300,
        "height": 272,
        "content": "## \ud83d\udd0d TOOL: Get Sender History\n\nFetches up to 10 previous emails\nfrom the same sender address\n\nFilters:\n- Received BEFORE current email\n- From same sender\n- Includes subject, body, labels"
      },
      "typeVersion": 1
    },
    {
      "id": "34cf4327-bbfa-4b9b-afff-a1e9f2b60c11",
      "name": "Label Tool Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        512
      ],
      "parameters": {
        "color": 5,
        "width": 300,
        "height": 288,
        "content": "## \ud83d\udd0d TOOL: Get Label Examples\n\nFetches up to 10 emails\nfrom a specific label category\n\nUse when:\n- Sender has no history\n- Need to compare examples\n- Uncertain about classification"
      },
      "typeVersion": 1
    },
    {
      "id": "5c914b27-389e-4c75-aae8-588324780722",
      "name": "Label Application Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        -336
      ],
      "parameters": {
        "color": 2,
        "width": 564,
        "height": 360,
        "content": "## \u2705 LABEL APPLICATION\n\n**If not None** \u2192 Only continues if AI returned a category\n\n**Convert Label to Label ID:**\n- Maps AI label name \u2192 Gmail label ID\n- Example: \"Meetings\" \u2192 \"Label_2420158633459891270\"\n\n\u26a0\ufe0f **CRITICAL:** Update labelToId mapping with YOUR label IDs!\n\n**Add label to thread** \u2192 Applies the label\n\n**Remove label from thread** \u2192 Archives (removes INBOX)"
      },
      "typeVersion": 1
    },
    {
      "id": "44109984-8de2-495b-85f5-7c3e27b8d2b0",
      "name": "Get Label IDs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        800
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 444,
        "content": "## \ud83d\udd11 HOW TO GET YOUR LABEL IDs\n\n### Use Manual Trigger\n1. Click \"When clicking 'Execute workflow'\"\n2. Click \"Execute Node\" on \"Get Labels Info\"\n3. View output - copy each label's ID\n\n### Label ID Format:\n```\nLabel_1234567890123456789\n```\n\n### Update These Nodes:\n1. **Check Label Existence** (line 5)\n2. **Convert Label to Label ID** (line 5)"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "If not None": {
      "main": [
        [
          {
            "node": "Convert Label to Label ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Not Assigned": {
      "main": [
        [
          {
            "node": "Email Classifier Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Check Label Existence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get many messages": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add label to thread": {
      "main": [
        [
          {
            "node": "Remove Inbox label from thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Emails By Label": {
      "ai_tool": [
        [
          {
            "node": "Email Classifier Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Check Label Existence": {
      "main": [
        [
          {
            "node": "If Not Assigned",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Classifier Agent": {
      "main": [
        [
          {
            "node": "If not None",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Email Classifier Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Email Classifier Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Convert Label to Label ID": {
      "main": [
        [
          {
            "node": "Add label to thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Emails By Sender Email": {
      "ai_tool": [
        [
          {
            "node": "Email Classifier Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Remove Inbox label from thread": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}