{
  "name": "Job Application Processor & Candidate Scorer",
  "nodes": [
    {
      "parameters": {
        "content": "## \ud83d\udccb Job Application Processor\n\n### What this workflow does\n1. Watches Gmail for job applications\n2. Downloads resume attachments\n3. Extracts candidate info from resume\n4. Scores based on experience & skills\n5. Logs to ATS spreadsheet\n6. Sends auto-reply to candidate\n7. Notifies HR team via Slack\n\n### Setup steps\n1. Connect Gmail OAuth2 credentials\n2. Get PDF Vector API key from pdfvector.com/api-keys\n3. Create Google Sheet with columns below\n4. Connect Slack and pick your HR channel\n5. Update required skills in Score Candidate node\n\n### Sheet columns\nName, Email, Phone, Location, Current Title, Years Experience, Score, Status, Skills, Skills Matched, Application Date, Processed Date\n\n### Perfect for\n- HR teams\n- Recruiters\n- Small businesses",
        "height": 480,
        "width": 320,
        "color": 5
      },
      "id": "sticky-main",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "content": "## \ud83c\udfaf Scoring Logic\n\n- Experience: 40 points\n- Education: 30 points\n- Skills match: 30 points\n\n### Status\n- 75+ \u2192 Schedule Interview\n- 50-74 \u2192 Review Further\n- <50 \u2192 Pass\n\nEdit required skills in the\nScore Candidate node.",
        "height": 220,
        "width": 240
      },
      "id": "sticky-scoring",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        704,
        0
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "filters": {
          "includeSpamTrash": false
        }
      },
      "id": "gmail-trigger",
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1,
      "position": [
        128,
        272
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "get",
        "messageId": "={{ $json.id }}",
        "simple": false,
        "options": {
          "downloadAttachments": true
        }
      },
      "id": "gmail-get",
      "name": "Get a message",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        320,
        272
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Rename attachment_0 to data for PDF Vector compatibility\nconst items = $input.all();\n\nfor (const item of items) {\n  if (item.binary && item.binary.attachment_0) {\n    item.binary.data = item.binary.attachment_0;\n    delete item.binary.attachment_0;\n  }\n}\n\nreturn items;"
      },
      "id": "prepare-binary",
      "name": "Prepare Binary File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        528,
        272
      ]
    },
    {
      "parameters": {
        "operation": "extract",
        "inputType": "file",
        "prompt": "Extract candidate resume data as flat fields. fullName, email, phone, location (city and state), linkedIn, yearsOfExperience (number), currentTitle, mostRecentCompany, secondMostRecentCompany, highestDegree (e.g. Bachelor, Master, PhD, Other), educationField, educationInstitution, technicalSkillsList (comma-separated technical skills as single string), softSkillsList (comma-separated soft skills as single string), certificationsList (comma-separated certifications as single string), languagesList (comma-separated languages as single string), keyAchievements (semicolon-separated top 3 achievements as single string).",
        "schema": "{\"type\": \"object\", \"properties\": {\"fullName\": {\"type\": \"string\"}, \"email\": {\"type\": \"string\"}, \"phone\": {\"type\": \"string\"}, \"location\": {\"type\": \"string\"}, \"linkedIn\": {\"type\": \"string\"}, \"yearsOfExperience\": {\"type\": \"number\"}, \"currentTitle\": {\"type\": \"string\"}, \"mostRecentCompany\": {\"type\": \"string\"}, \"secondMostRecentCompany\": {\"type\": \"string\"}, \"highestDegree\": {\"type\": \"string\"}, \"educationField\": {\"type\": \"string\"}, \"educationInstitution\": {\"type\": \"string\"}, \"technicalSkillsList\": {\"type\": \"string\"}, \"softSkillsList\": {\"type\": \"string\"}, \"certificationsList\": {\"type\": \"string\"}, \"languagesList\": {\"type\": \"string\"}, \"keyAchievements\": {\"type\": \"string\"}}, \"additionalProperties\": false}"
      },
      "id": "pdfvector-extract",
      "name": "PDF Vector - Parse Resume",
      "type": "n8n-nodes-pdfvector.pdfVector",
      "typeVersion": 2,
      "position": [
        736,
        272
      ],
      "credentials": {
        "pdfVectorApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const candidate = ($input.first().json?.data || $input.first().json) || {};\nconst emailData = $('Gmail Trigger').item.json;\n\nconst requiredSkills = [\n  'javascript', 'python', 'react', 'node', 'sql',\n  'api', 'git', 'aws', 'docker', 'typescript'\n];\n\n// Experience scoring (40 pts)\nlet expScore = 0;\nconst years = parseFloat(candidate.yearsOfExperience) || 0;\nif (years >= 7)      expScore = 40;\nelse if (years >= 5) expScore = 35;\nelse if (years >= 3) expScore = 25;\nelse if (years >= 1) expScore = 15;\nelse                 expScore = 5;\n\n// Education scoring (30 pts)\nlet eduScore = 10;\nconst degree = (candidate.highestDegree || '').toLowerCase();\nif (degree.includes('phd') || degree.includes('doctorate'))    eduScore = 30;\nelse if (degree.includes('master') || degree.includes('mba'))  eduScore = 25;\nelse if (degree.includes('bachelor'))                           eduScore = 20;\n\n// Skills matching (30 pts)\nconst skillsStr = (candidate.technicalSkillsList || '').toLowerCase();\nlet skillsMatched = 0;\nrequiredSkills.forEach(skill => { if (skillsStr.includes(skill)) skillsMatched++; });\nconst skillsScore = Math.min(skillsMatched * 3, 30);\n\nconst totalScore = expScore + eduScore + skillsScore;\nlet status = 'Pass';\nif (totalScore >= 75)      status = 'Schedule Interview';\nelse if (totalScore >= 50) status = 'Review Further';\n\nreturn [{ json: {\n  fullName:           candidate.fullName           || 'Unknown',\n  email:              candidate.email              || '',\n  phone:              candidate.phone              || 'N/A',\n  location:           candidate.location           || 'N/A',\n  currentTitle:       candidate.currentTitle       || 'N/A',\n  yearsOfExperience:  years,\n  mostRecentCompany:  candidate.mostRecentCompany  || 'N/A',\n  highestDegree:      candidate.highestDegree      || 'N/A',\n  educationField:     candidate.educationField     || 'N/A',\n  skillsList:         candidate.technicalSkillsList || '',\n  certificationsList: candidate.certificationsList || 'N/A',\n  keyAchievements:    candidate.keyAchievements    || 'N/A',\n  applicantEmail: emailData?.from?.value?.[0]?.address || candidate.email || 'unknown',\n  applicationDate: emailData?.date || new Date().toISOString(),\n  score: totalScore,\n  expScore, eduScore, skillsScore,\n  skillsMatched,\n  status,\n  processedAt: new Date().toISOString()\n}}];"
      },
      "id": "score-candidate",
      "name": "Score Candidate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        944,
        272
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SPREADSHEET_ID",
          "mode": "list"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Name": "={{ $json.fullName }}",
            "Email": "={{ $json.email || $json.applicantEmail }}",
            "Phone": "={{ $json.phone || 'N/A' }}",
            "Location": "={{ $json.location || 'N/A' }}",
            "Current Title": "={{ $json.currentTitle || 'N/A' }}",
            "Years Experience": "={{ $json.yearsOfExperience || 0 }}",
            "Score": "={{ $json.score }}/100",
            "Status": "={{ $json.status }}",
            "Skills": "={{ $json.skillsList }}",
            "Skills Matched": "={{ $json.skillsMatched }}/10",
            "Application Date": "={{ $json.applicationDate }}",
            "Processed Date": "={{ $json.processedAt.split('T')[0] }}"
          }
        },
        "options": {}
      },
      "id": "sheets-log",
      "name": "Add to ATS",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1152,
        272
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "sendTo": "={{ $('Score Candidate').item.json.email || $('Score Candidate').item.json.applicantEmail }}",
        "subject": "Thank you for your application",
        "emailType": "text",
        "message": "=Dear {{ $('Score Candidate').item.json.fullName }},\n\nThank you for your interest in joining our team. We have received your application and our team is currently reviewing it.\n\nWe will be in touch within the next few business days regarding next steps.\n\nBest regards,\nThe Hiring Team",
        "options": {}
      },
      "id": "gmail-reply",
      "name": "Send Auto-Reply",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1344,
        272
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "YOUR_SLACK_CHANNEL_ID",
          "mode": "list"
        },
        "text": "=\ud83d\udce5 *New Application Received*\n\n*Candidate:* {{ $('Score Candidate').item.json.fullName }}\n*Current Role:* {{ $('Score Candidate').item.json.currentTitle || 'N/A' }}\n*Experience:* {{ $('Score Candidate').item.json.yearsOfExperience || 0 }} years\n*Score:* {{ $('Score Candidate').item.json.score }}/100\n*Status:* {{ $('Score Candidate').item.json.status }}\n\n*Top Skills:* {{ $('Score Candidate').item.json.skillsList.split(', ').slice(0, 5).join(', ') || 'N/A' }}",
        "otherOptions": {}
      },
      "id": "slack-notify",
      "name": "Notify HR Team",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.1,
      "position": [
        1536,
        272
      ],
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Get a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a message": {
      "main": [
        [
          {
            "node": "Prepare Binary File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Binary File": {
      "main": [
        [
          {
            "node": "PDF Vector - Parse Resume",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PDF Vector - Parse Resume": {
      "main": [
        [
          {
            "node": "Score Candidate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Candidate": {
      "main": [
        [
          {
            "node": "Add to ATS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add to ATS": {
      "main": [
        [
          {
            "node": "Send Auto-Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Auto-Reply": {
      "main": [
        [
          {
            "node": "Notify HR Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "PDF Vector"
    },
    {
      "name": "HR"
    },
    {
      "name": "Recruiting"
    },
    {
      "name": "Resume Parser"
    },
    {
      "name": "ATS"
    }
  ],
  "meta": {
    "templateCredsSetupCompleted": false
  }
}