{
  "name": "Gmail Job Position Classifier",
  "nodes": [
    {
      "parameters": {
        "chatId": "-1003803550245",
        "text": "=const data = $json;\n\nfunction escapeMarkdown(text) {\n  if (!text) return 'N/A';\n  return String(text).replace(/[_*[\\]()~`>#+\\\\-=|{}.!]/g, '\\\\$&');\n}\n\nconst message = `\n📧 *Job Email Classification*\n\n*Category:* ${escapeMarkdown(data.category)}\n*Job Related:* ${data.isJobRelated ? '✅ Yes' : '❌ No'}\n*Company:* ${escapeMarkdown(data.company)}\n*Position:* ${escapeMarkdown(data.position)}\n*Confidence:* ${(data.confidence * 100).toFixed(1)}%\n\n`;\n\nreturn [\n  {\n    json: {\n      text: message\n    }\n  }\n];",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [2592, 912],
      "id": "5781ea8e-788a-4378-a77c-5dac4e3e982f",
      "name": "Send a text message",
      "webhookId": "35199b69-ce18-4841-b316-389a2113e84d",
      "credentials": {
        "telegramApi": {
          "id": "DuCX9mLZUy8nGnlt",
          "name": "Gmail Job Classifier"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1FNgfZxGDwexb86dQ9czR0nK9qNBZ3yULOB",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "Sheet1"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "4012459f-df1d-47b1-b282-4f7d7bcfacf5",
      "name": "Save to Google Sheets1",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.4,
      "position": [2592, 704],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "hHAfsVGhuZEemmJV",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "job-related-check",
              "leftValue": "={{ $json.isJobRelated }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equal"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3e1f3f1c-9cad-4769-9fda-5aa0be6e4f59",
      "name": "Is Job Related?1",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [2336, 944]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst originalItems = $('Extract Email Fields1').all();\nconst results = [];\n\nfor (let i = 0; i < items.length; i++) {\n  const llmData = items[i].json;\n  const emailData = originalItems[i]?.json || {};\n\n  let cls = {\n    isJobRelated: false,\n    category: 'Parse Error',\n    company: '',\n    position: '',\n    confidence: 0\n  };\n\n  try {\n    // LangChain chainLlm returns { output: '...' }\n    // Ollama HTTP fallback returns { response: '...' }\n    const responseText = llmData.output || llmData.response || '';\n    const match = responseText.match(/\\{[\\s\\S]*\\}/);\n    if (match) {\n      const p = JSON.parse(match[0]);\n      cls = {\n        isJobRelated: p.isJobRelated === true,\n        category: p.category || 'Unknown',\n        company: p.company || '',\n        position: p.position || '',\n        confidence: typeof p.confidence === 'number' ? Math.round(p.confidence * 100) / 100 : 0\n      };\n    }\n  } catch(e) {\n    cls.category = 'Parse Error: ' + String(e.message).substring(0, 50);\n  }\n\n  results.push({\n    json: {\n      date: emailData.date || '',\n      direction: emailData.direction || '',\n      from: emailData.from || '',\n      to: emailData.to || '',\n      subject: emailData.subject || '',\n      category: cls.category,\n      company: cls.company,\n      position: cls.position,\n      confidence: cls.confidence,\n      isJobRelated: cls.isJobRelated,\n      bodyText: (emailData.bodyText || '').substring(0, 500),\n      htmlBody: emailData.htmlBody || '',\n      emailId: emailData.emailId || '',\n      threadId: emailData.threadId || '',\n      snippet: emailData.snippet || ''\n    }\n  });\n}\n\nreturn results;"
      },
      "id": "efa64ac8-0722-46c5-b867-983a5a764d47",
      "name": "Parse Classification1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2176, 944]
    },
    {
      "parameters": {
        "model": "llama3.1:latest",
        "options": {}
      },
      "id": "2288c262-3fce-423c-995c-44ff5c516e5d",
      "name": "ollama-model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "position": [2000, 1168],
      "typeVersion": 1,
      "credentials": {
        "ollamaApi": {
          "id": "9k920xXzUND3cX1z",
          "name": "Ollama account"
        }
      }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a precise email classifier for job applications and hiring processes.\n\nAnalyze this email carefully. Determine if it is related to a job position, job application, or hiring/recruitment process.\n\nEmail Details:\n- Direction: {{ $json.direction }} (Inbox = received by me, Sent = I sent this email)\n- From: {{ $json.from }}\n- To: {{ $json.to }}\n- Subject: {{ $json.subject }}\n- Body:\n{{ $json.bodyText }}\n\nJob-related signals to look for (be INCLUSIVE — if there is reasonable doubt it is job-related, set isJobRelated=true with lower confidence):\n- Sent emails: job applications, cover letters, follow-up emails to job postings, thank-you notes after interviews, responses to job offers\n- Received emails: recruiter outreach, interview invitations, application confirmations, offer letters, rejection notices, HR communications\n- Relevant keywords: apply, application, resume, CV, interview, offer, hire, hired, position, role, opportunity, recruiter, talent acquisition, HR, human resources, vacancy, candidate, hiring manager, job description, onboarding\n\nClassification categories:\n- \"Application Sent\" — I submitted a job application or cover letter\n- \"Recruiter Outreach\" — A recruiter or hiring person contacted me about an opening\n- \"Interview Scheduled\" — Interview invitation or scheduling coordination\n- \"Interview Follow-up\" — Any follow-up communication related to an interview\n- \"Offer Received\" — A job offer was extended to me\n- \"Rejection\" — Rejection of my application or after an interview\n- \"Follow-up Sent\" — I followed up on an application or after an interview\n- \"General Job Discussion\" — Job-related communication that does not fit the above\n- \"Not Job Related\" — Clearly unrelated to any job search or hiring activity\n\nReturn ONLY valid JSON with no additional text, no markdown fences:\n{\n  \"isJobRelated\": true,\n  \"category\": \"Application Sent\",\n  \"company\": \"Company Name\",\n  \"position\": \"Job Title\",\n  \"confidence\": 0.9\n}",
        "messages": {
          "messageValues": []
        }
      },
      "id": "5e3706ce-fe80-4094-a464-32b09e5ecdcc",
      "name": "extract-tracking-info",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [2000, 944],
      "typeVersion": 1.4
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst results = [];\n\nconst JOB_KEYWORDS = [\n  'job', 'position', 'role', 'interview', 'apply', 'applying', 'application',\n  'resume', 'cv', 'candidate', 'recruiter', 'recruiting', 'hiring',\n  'offer', 'career', 'opportunity', 'vacancy', 'hr ', 'human resources',\n  'talent', 'onboarding', 'linkedin', 'indeed', 'glassdoor',\n  'cover letter', 'shortlisted', 'assessment', 'screening', 'headhunter',\n  'thank you for applying', 'your application', 'we received your', 'application received',\n  'qa', 'quality assurance', 'test lead', 'team lead'\n];\n\nlet targetPositions = '';\ntry {\n  targetPositions = $('\\u2699\\ufe0f Set Job Profile').first().json.targetPositions || '';\n} catch(e) {\n  targetPositions = '';\n}\n\nconst contextPrefix = targetPositions\n  ? '[Candidate is applying for: ' + targetPositions + ']\\n\\n'\n  : '';\n\nfor (const item of items) {\n  const direction = item.json.direction || '';\n  const subject = item.json.subject || '';\n  const from = item.json.from || '';\n  const bodyText = item.json.bodyText || '';\n\n  // Sent emails: always pass through — user controls what they send\n  if (direction === 'Sent') {\n    results.push({\n      json: {\n        ...item.json,\n        bodyText: contextPrefix + bodyText\n      }\n    });\n    continue;\n  }\n\n  // Inbox emails: require at least one job keyword OR non-Latin text (Hebrew, Arabic, etc.)\n  const searchText = (subject + ' ' + from + ' ' + bodyText).toLowerCase();\n  const hasJobKeyword = JOB_KEYWORDS.some(kw => searchText.includes(kw));\n  const hasNonLatin = /[\\u0590-\\u05FF\\u0600-\\u06FF]/.test(subject + ' ' + bodyText);\n\n  if (!hasJobKeyword && !hasNonLatin) continue; // drop clearly non-job inbox emails\n\n  results.push({\n    json: {\n      ...item.json,\n      bodyText: contextPrefix + bodyText\n    }\n  });\n}\n\nreturn results;"
      },
      "id": "c7d8e9f0-a1b2-4c3d-8e9f-0a1b2c3d4e5f",
      "name": "🔍 Pre-Filter & Enrich",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1856, 944]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst results = [];\n\nfunction decodeBase64(str) {\n  if (!str) return '';\n  try {\n    const b64 = str.replace(/-/g, '+').replace(/_/g, '/');\n    return Buffer.from(b64, 'base64').toString('utf-8');\n  } catch(e) { return ''; }\n}\n\nfunction findPart(parts, mime) {\n  if (!parts || !Array.isArray(parts)) return '';\n  for (const p of parts) {\n    if (p.mimeType === mime && p.body && p.body.data) {\n      return decodeBase64(p.body.data);\n    }\n    if (p.parts) {\n      const found = findPart(p.parts, mime);\n      if (found) return found;\n    }\n  }\n  return '';\n}\n\nfor (const item of items) {\n  const headers = item.json.payload?.headers || [];\n  const h = (name) => (headers.find(x => x.name.toLowerCase() === name.toLowerCase()) || {}).value || '';\n\n  const rawDate = h('Date');\n  let date = '';\n  try { date = rawDate ? new Date(rawDate).toISOString().split('T')[0] : ''; } catch(e) { date = rawDate; }\n\n  const payload = item.json.payload || {};\n  let plainText = '';\n  let htmlBody = '';\n\n  if (payload.parts && payload.parts.length > 0) {\n    plainText = findPart(payload.parts, 'text/plain');\n    htmlBody = findPart(payload.parts, 'text/html');\n  } else if (payload.body && payload.body.data) {\n    const decoded = decodeBase64(payload.body.data);\n    if (payload.mimeType === 'text/html') {\n      htmlBody = decoded;\n    } else {\n      plainText = decoded;\n    }\n  }\n\n  const snippet = (item.json.snippet || '').substring(0, 300);\n  const bodyText = (plainText || snippet).substring(0, 2500);\n\n  results.push({\n    json: {\n      emailId: item.json.id || '',\n      threadId: item.json.threadId || '',\n      direction: item.json.direction || '',\n      from: h('From'),\n      to: h('To'),\n      subject: h('Subject'),\n      date: date,\n      snippet: snippet,\n      bodyText: bodyText,\n      htmlBody: htmlBody.substring(0, 8000)\n    }\n  });\n}\n\nreturn results;"
      },
      "id": "873ad98f-2152-451e-9196-d41417ccd60b",
      "name": "Extract Email Fields1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1728, 944]
    },
    {
      "parameters": {},
      "id": "3dec2d4d-fa14-402b-8eb1-38271b3ea12c",
      "name": "Merge Emails1",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [1536, 944]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "direction-field",
              "name": "direction",
              "value": "Sent",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "47ddf374-5301-480f-85c7-998ad83b3e0b",
      "name": "Tag - Sent1",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1312, 1072]
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "q": "={{ 'in:sent after:' + $json.startDate }}"
        }
      },
      "id": "801f3f01-e403-4f55-9d74-25c14b49f4c3",
      "name": "Gmail - Get Sent1",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [1088, 1072],
      "webhookId": "7944ec19-7d60-4141-8b76-118cfb68f478",
      "credentials": {
        "gmailOAuth2": {
          "id": "gO8EWg5jZuSVkFxz",
          "name": "Gmail account"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "direction-field",
              "name": "direction",
              "value": "Inbox",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "22ad16c5-9a63-4fd0-bc96-4ea651620032",
      "name": "Tag - Inbox1",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1312, 800]
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "q": "={{ 'after:' + $json.startDate + ' -category:promotions -category:social -label:newsletters' }}"
        }
      },
      "id": "37b8d593-f9e9-4b9a-a687-b01a98b16eb4",
      "name": "Gmail - Get Inbox1",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [1088, 800],
      "webhookId": "00738bda-e7df-4f94-a03d-8ee2020c0389",
      "credentials": {
        "gmailOAuth2": {
          "id": "gO8EWg5jZuSVkFxz",
          "name": "Gmail account"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "start-date-pass",
              "name": "startDate",
              "value": "={{ $json.startDate }}",
              "type": "string"
            },
            {
              "id": "target-positions",
              "name": "targetPositions",
              "value": "QA Team Lead, QA Engineer, Quality Assurance Lead, Test Lead, SDET, QA Manager",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
      "name": "⚙️ Set Job Profile",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1088, 944],
      "notes": "EDIT targetPositions: enter the job titles and skills you are applying for. This context is injected into every email analysis."
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "start-date-field",
              "name": "startDate",
              "value": "2026/03/01",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "aebb295d-cf05-4363-b9f9-0369503d71a5",
      "name": "⚙️ Set Start Date",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [880, 944],
      "notes": "EDIT THIS NODE: Change 'startDate' to your desired start date (format: YYYY/MM/DD)"
    },
    {
      "parameters": {},
      "id": "bbb153d3-218e-4f99-9149-ac0764c3b89b",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [640, 944]
    },
    {
      "parameters": {
        "content": "## 📧 Gmail Job Classifier v2\n\n**Fixes in this version:**\n✅ AI prompt now uses actual email data (was broken)\n✅ Full body text extracted (HTML + plain text via base64 decode)\n✅ HTML body saved to Google Sheets for viewing\n✅ Gmail inbox pre-filters promotions/social noise\n✅ LangChain response field fixed (`output` not `response`)\n\n**Setup:**\n• Edit **⚙️ Set Start Date** node with your date\n• Google Sheets: ensure columns exist for `htmlBody`, `bodyText`\n• Confidence threshold: edit the IF node to add `confidence >= 0.6` if needed",
        "height": 280,
        "width": 420,
        "color": 5
      },
      "id": "a2931d5b-7745-4214-83ad-cdd1c2cf5f59",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [576, 544]
    },
    {
      "parameters": {
        "content": "## ✏️ Configure Your Job Profile Here\n\n**⚙️ Set Job Profile** — edit `targetPositions`\nExample: `Software Engineer, React, Node.js, TypeScript`\n\nThis value is injected into every LLM prompt so\nanalysis is personalized to YOUR target roles.\n\n**🔍 Pre-Filter & Enrich**\nDrops inbox emails with zero job keywords\nbefore they reach the LLM (cuts noise).\nAll Sent emails always pass through.\n\n**htmlBody in Google Sheets**\nAdd an `htmlBody` column header to Sheet1.\nPaste any cell value into a browser to render\nthe original email HTML.",
        "height": 300,
        "width": 380,
        "color": 3
      },
      "id": "bb1122cc-3344-5566-7788-99aabbccddee",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [880, 544]
    }
  ],
  "pinData": {},
  "connections": {
    "Is Job Related?1": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save to Google Sheets1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Classification1": {
      "main": [
        [
          {
            "node": "Is Job Related?1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "extract-tracking-info": {
      "main": [
        [
          {
            "node": "Parse Classification1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ollama-model": {
      "ai_languageModel": [
        [
          {
            "node": "extract-tracking-info",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "🔍 Pre-Filter & Enrich": {
      "main": [
        [
          {
            "node": "extract-tracking-info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Email Fields1": {
      "main": [
        [
          {
            "node": "🔍 Pre-Filter & Enrich",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Emails1": {
      "main": [
        [
          {
            "node": "Extract Email Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tag - Sent1": {
      "main": [
        [
          {
            "node": "Merge Emails1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Gmail - Get Sent1": {
      "main": [
        [
          {
            "node": "Tag - Sent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tag - Inbox1": {
      "main": [
        [
          {
            "node": "Merge Emails1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail - Get Inbox1": {
      "main": [
        [
          {
            "node": "Tag - Inbox1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "⚙️ Set Job Profile": {
      "main": [
        [
          {
            "node": "Gmail - Get Inbox1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail - Get Sent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "⚙️ Set Start Date": {
      "main": [
        [
          {
            "node": "⚙️ Set Job Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start": {
      "main": [
        [
          {
            "node": "⚙️ Set Start Date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "850474a8-4dd8-48f0-921f-36a51aa134c5",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "e09d1f0f8b9f78e80ac70b6dc8726dd263b0b6ffc4300b0eee3b58f6da316f29"
  },
  "id": "Bu6ojuMe8kLq4hIt",
  "tags": [
    {
      "updatedAt": "2026-03-17T19:45:12.616Z",
      "createdAt": "2026-03-17T19:45:12.616Z",
      "id": "Aoeg74KWWTIHIqLw",
      "name": "Organizer"
    }
  ]
}
