{
  "nodes": [
    {
      "id": "c59b29e6-72dd-43fd-8b97-1c21e1e8245e",
      "name": "Sticky Note - How It Works",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        -160
      ],
      "parameters": {
        "color": 5,
        "width": 500,
        "height": 864,
        "content": "## \ud83c\udfaf SMART JOB HUNTER + COVER LETTER GENERATOR\n\n### \ud83d\ude80 What This Workflow Does:\n\n1. **User Submission**\n   - User upload CV (PDF) + preferences (location, job type, salary, etc.)\n   - Triggered by Form node\n\n2. **Extract CV**\n   - PDF text extracted\n   - Stored in `cvData`\n\n3. **Prepare Data**\n   - Collects all form fields (location, work setting, job type, salary, email)\n   - Normalized into JSON variables\n\n4. **Job Hunter Agent**\n   - Uses SerpAPI + Gemini LLM\n   - System prompt forces **ONLY 1 job result**\n   - Returns structured JSON with cover letter\n\n5. **Parse Agent Output**\n   - Cleans JSON (removes ```json fences)\n   - Ensures valid JSON for downstream\n\n6. **Generate HTML Email**\n   - Builds clean email template\n   - Inserts candidate profile + job match + cover letter + application tips\n\n7. **Send via Gmail**\n   - Sends final email to user\u2019s address\n   - Subject: *Your Job MATCH!*\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7567fd6d-6d09-4a01-bfed-115d812c7793",
      "name": "Sticky Note - Setup Guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        176
      ],
      "parameters": {
        "color": 4,
        "width": 380,
        "height": 528,
        "content": "## \u2699\ufe0f SETUP GUIDE (Updated)\n\n### 1. Google Gemini (Required)\n* Go to [Google AI Studio](https://aistudio.google.com/)\n* Create Gemini API key\n* Add it to n8n credentials (`Google Palm / Gemini`)\n* Model used: **Gemini 2.5 Pro**\n\n### 2. SerpAPI (Required)\n* Go to [SerpAPI Dashboard](https://serpapi.com/)\n* Create API key\n* Add it to n8n credentials (`SerpAPI`)\n* Used by workflow to fetch **Google Jobs search results**\n\n### 3. Form Configuration\n* Form webhook auto-generated by n8n\n* Share form link directly with users\n* Or embed form in your website\n"
      },
      "typeVersion": 1
    },
    {
      "id": "9a344bbc-419f-464e-b27a-347003947206",
      "name": "Sticky Note - Pro Tips",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -464,
        176
      ],
      "parameters": {
        "color": 3,
        "width": 360,
        "height": 528,
        "content": "## \ud83d\udee0\ufe0f Customization Tips\n\n- **Prompt**:\n  Adjust system message in Job Hunter Agent to refine tone/length of cover letter.\n\n- **Email Branding**:\n  Update footer in HTML email (currently: *Generated by khmuhtadin.com*).\n\n- **Job Filters**:\n  Extend Prepare Data node to pass blacklist, industries, or company size.\n\n- **LLM Model**:\n  Currently using Google Gemini (Gemini 2.5 Pro).\n  Switch to GPT-4o or Claude 3.5 Sonnet if available for better reasoning.\n\n- **Attachments**:\n  Optionally attach parsed CV summary as PDF for user reference.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "fc4e4c43-2c67-4b96-a52c-40177d3f830e",
      "name": "On form submission",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -1696,
        -128
      ],
      "parameters": {
        "options": {},
        "formTitle": "\ud83c\udfaf Smart Job Hunter + Cover Letter Generator",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "CV/Resume (PDF)",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            },
            {
              "fieldLabel": "Preferred Location",
              "placeholder": "e.g., Jakarta, Bandung, Remote, Anywhere",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Job Type",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Full Time"
                  },
                  {
                    "option": "Part Time"
                  },
                  {
                    "option": "Freelance"
                  },
                  {
                    "option": "Contract"
                  },
                  {
                    "option": "Internship"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Work Setting",
              "fieldOptions": {
                "values": [
                  {
                    "option": "On Site"
                  },
                  {
                    "option": "Remote"
                  },
                  {
                    "option": "Hybrid"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "Minimum Salary Expectation (Optional)",
              "placeholder": "e.g., 10 juta, 15-20 juta, negotiable"
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "Additional Preferences (Optional)",
              "placeholder": "e.g., startup culture, specific companies, must have BPJS"
            },
            {
              "fieldType": "email",
              "fieldLabel": "Email",
              "placeholder": "We will send the results to this email",
              "requiredField": true
            }
          ]
        },
        "formDescription": "Upload your CV and let AI find perfect jobs with custom cover letters!"
      },
      "typeVersion": 2.3
    },
    {
      "id": "6d023047-6245-4075-a5a9-d92bd3a6930f",
      "name": "Extract CV from PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -1472,
        -128
      ],
      "parameters": {
        "options": {},
        "operation": "pdf",
        "binaryPropertyName": "CV_Resume__PDF_"
      },
      "typeVersion": 1
    },
    {
      "id": "8cdb4903-c0fb-4f7e-a109-4677bfc168c5",
      "name": "Prepare Data",
      "type": "n8n-nodes-base.set",
      "position": [
        -1248,
        -128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ce5f3a96-c63d-40fa-b02c-8d763bfa2ed1",
              "name": "cvData",
              "type": "string",
              "value": "={{ $json.text }}"
            },
            {
              "id": "51623c31-dacf-4828-b1ac-f03f67e00d54",
              "name": "location",
              "type": "string",
              "value": "={{ $('On form submission').item.json['Preferred Location'] }}"
            },
            {
              "id": "068b13d5-6941-415d-935b-4936e42df913",
              "name": "workSetting",
              "type": "string",
              "value": "={{ $('On form submission').item.json['Work Setting'] }}"
            },
            {
              "id": "e1636d57-472b-4886-b631-5deb45b103a3",
              "name": "jobType",
              "type": "string",
              "value": "={{ $('On form submission').item.json['Job Type'] }}"
            },
            {
              "id": "salary-expectation",
              "name": "salaryExpectation",
              "type": "string",
              "value": "={{ $('On form submission').item.json['Minimum Salary Expectation (Optional)'] || 'Not specified' }}"
            },
            {
              "id": "additional-prefs",
              "name": "additionalPreferences",
              "type": "string",
              "value": "={{ $('On form submission').item.json['Additional Preferences (Optional)'] || 'None' }}"
            },
            {
              "id": "user-email",
              "name": "userEmail",
              "type": "string",
              "value": "={{ $('On form submission').item.json['Email'] }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bf80ac9b-076e-4c12-a22f-d34f64d81ab6",
      "name": "Job Hunter Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -1024,
        -128
      ],
      "parameters": {
        "text": "={{ \n`ANALYZE CV AND FIND JOBS:\n\nCV Content:\n${$json.cvData}\n\nUser Preferences:\n- Location: ${$json.location}\n- Work Setting: ${$json.workSetting}\n- Job Type: ${$json.jobType}\n- Salary: ${$json.salaryExpectation}\n- Additional: ${$json.additionalPreferences}\n\nTASKS:\n1. Extract key info from CV (name, skills, experience, achievements)\n2. Search for 3 best matching jobs using tools\n3. Generate personalized cover letter for each job\n4. Format everything as structured JSON for email template`\n}}",
        "options": {
          "systemMessage": "=You are an expert Job Hunter specializing in the Indonesian job market.  \nUse Decodo Scrapper and SerpAPI to find a real job.  \n\n## YOUR MISSION:\n1. Analyze the CV to extract skills, experience, and achievements.  \n2. Search for exactly 1 best matching job using the tools.  \n   - Return the first high-quality match you find.  \n   - Do not attempt multiple searches or retries.  \n3. Generate one personalized cover letter for that job (250\u2013350 words).  \n4. Output everything as structured JSON.\n\n## OUTPUT FORMAT:\nYou MUST output a valid JSON object with this exact structure:\n\n{\n  \"candidateName\": \"[Name from CV]\",\n  \"candidateExperience\": \"[X years in Y field]\",\n  \"candidateSkills\": \"[Top 3 skills]\",\n  \"candidateLevel\": \"[Junior/Mid/Senior]\",\n  \"candidateStrength\": \"[Key differentiator]\",\n  \"searchSummary\": \"[Platform searched and result]\",\n  \"job\": {\n    \"title\": \"[Job title]\",\n    \"company\": \"[Company name]\",\n    \"location\": \"[City/Remote]\",\n    \"salary\": \"[Range or estimate]\",\n    \"postedDate\": \"[X days ago]\",\n    \"applyUrl\": \"[URL]\",\n    \"whyMatch\": [\"Reason 1\",\"Reason 2\",\"Reason 3\"],\n    \"coverLetter\": \"Dear Hiring Manager at [Company],\\n\\n[250-350 word letter]\\n\\nBest regards,\\n[Name]\"\n  },\n  \"applicationTips\": {\n    \"bestTime\": \"Monday-Wednesday, 9-11 AM\",\n    \"keywords\": \"[List of keywords from the job]\",\n    \"followUp\": \"After 1 week\",\n    \"interviewPrep\": \"[Focus areas]\"\n  }\n}\n\n## IMPORTANT:\n- Use real job data from the tools (no imaginary jobs).  \n- Stop after finding **one** matching job.  \n- Output ONLY valid JSON, no extra text.\n"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "a832cc9c-de91-48ae-8352-c33c47a903e7",
      "name": "Generate HTML Email",
      "type": "n8n-nodes-base.code",
      "position": [
        -496,
        -128
      ],
      "parameters": {
        "jsCode": "const jobData = $json;\nconst job = jobData.job || null;\n\n// Build HTML email\nconst htmlEmail = `\n<!DOCTYPE html>\n<html lang=\"id\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Your Job Match</title>\n</head>\n<body style=\"margin:0; padding:0; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color:#f4f7fa;\">\n  <table role=\"presentation\" width=\"100%\" style=\"background-color:#f4f7fa;\">\n    <tr>\n      <td align=\"center\" style=\"padding:40px 20px;\">\n        <table role=\"presentation\" width=\"600\" style=\"background:#fff; border-radius:10px; box-shadow:0 4px 6px rgba(0,0,0,0.1);\">\n\n          <!-- Header -->\n          <tr>\n            <td style=\"background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); padding:40px; border-radius:10px 10px 0 0; text-align:center;\">\n              <h1 style=\"margin:0; color:#fff; font-size:28px;\">\ud83c\udfaf Your Job Match Is Ready!</h1>\n            </td>\n          </tr>\n\n          <!-- Profile Summary -->\n          <tr>\n            <td style=\"padding:30px;\">\n              <div style=\"background:#f8f9fa; border-radius:8px; padding:20px; margin-bottom:30px;\">\n                <h2 style=\"margin:0 0 15px 0;\">\ud83d\udc64 Profile Summary</h2>\n                <p><strong>Name:</strong> ${jobData.candidateName || 'N/A'}</p>\n                <p><strong>Experience:</strong> ${jobData.candidateExperience || 'N/A'}</p>\n                <p><strong>Skills:</strong> ${(jobData.candidateSkills || []).join(', ')}</p>\n                <p><strong>Level:</strong> ${jobData.candidateLevel || 'N/A'}</p>\n              </div>\n\n              <!-- Job Match -->\n              ${job ? `\n              <div style=\"border:1px solid #e0e0e0; border-radius:8px; padding:20px; margin-bottom:25px;\">\n                <div style=\"background:linear-gradient(135deg,#f093fb 0%,#f5576c 100%); color:white; padding:8px 16px; border-radius:20px; font-weight:600; font-size:14px; display:inline-block; margin-bottom:15px;\">\n                  \ud83e\udd47 BEST MATCH\n                </div>\n\n                <h3>${job.title}</h3>\n                <p>\ud83c\udfe2 ${job.company} | \ud83d\udccd ${job.location}</p>\n                <p>\ud83d\udcb0 ${job.salary} | \ud83d\udcc5 Posted ${job.postedDate}</p>\n\n                <div style=\"background:#f8f9fa; padding:12px; border-radius:6px; margin:15px 0;\">\n                  <p style=\"margin:0 0 8px 0; font-weight:600;\">Why this matches:</p>\n                  <ul>${(job.whyMatch || []).map(r => `<li>${r}</li>`).join('')}</ul>\n                </div>\n\n                <a href=\"${job.applyUrl}\" style=\"display:inline-block; background:#00a8e8; color:white; padding:10px 24px; text-decoration:none; border-radius:5px; font-weight:600;\">\n                  Apply Now \u2192\n                </a>\n\n                <!-- Cover Letter -->\n                <div style=\"margin-top:20px; padding-top:20px; border-top:1px solid #e0e0e0;\">\n                  <h4>\ud83d\udc8c Your Cover Letter</h4>\n                  <div style=\"background:#fff; border:1px solid #e0e0e0; padding:20px; border-radius:6px; white-space:pre-line; line-height:1.6;\">\n                    ${job.coverLetter}\n                  </div>\n                </div>\n              </div>\n              ` : '<p>No job found.</p>'}\n\n              <!-- Tips -->\n              ${jobData.applicationTips ? `\n              <div style=\"background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); color:white; border-radius:8px; padding:20px;\">\n                <h3 style=\"margin:0 0 15px 0;\">\ud83d\udca1 Application Tips</h3>\n                <p>\ud83d\udcc5 Best Time: ${jobData.applicationTips.bestTime}</p>\n                <p>\ud83c\udfaf Keywords: ${(jobData.applicationTips.keywords || []).join(', ')}</p>\n                <p>\ud83d\udce7 Follow-up: ${jobData.applicationTips.followUp}</p>\n              </div>\n              ` : ''}\n            </td>\n          </tr>\n\n          <!-- Footer -->\n          <tr>\n            <td style=\"background:#f8f9fa; padding:30px; text-align:center; border-radius:0 0 10px 10px;\">\n              <p style=\"margin:0; color:#999; font-size:12px;\">\n                Generated by khmuhtadin.com | ${new Date().toLocaleDateString()}\n              </p>\n            </td>\n          </tr>\n\n        </table>\n      </td>\n    </tr>\n  </table>\n</body>\n</html>\n`;\n\n// Ambil email user dari workflow sebelumnya\nconst userEmail = $('Prepare Data').first().json.userEmail;\n\nreturn {\n  htmlEmail,\n  userEmail,\n  jobData\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c19e2109-d175-4cfa-ba46-05c68b16e1fe",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -304,
        -128
      ],
      "parameters": {
        "sendTo": "={{ $('On form submission').item.json.Email }}",
        "message": "={{ $json.htmlEmail }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "=Your Job MATCH!"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "6f06950e-20f9-4255-9e8f-7dda28f2098a",
      "name": "Parse Agent Output",
      "type": "n8n-nodes-base.code",
      "position": [
        -704,
        -128
      ],
      "parameters": {
        "jsCode": "let raw = $json.output || \"\";\n\n// Clean markdown fences if they exist\nraw = raw.replace(/```json/g, '').replace(/```/g, '').trim();\n\nlet parsed;\ntry {\n  parsed = JSON.parse(raw);\n} catch (e) {\n  parsed = { error: \"Failed to parse JSON\", raw };\n}\n\nreturn parsed;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9eb446ce-0550-4ef3-af32-70fab8960230",
      "name": "SerpAPI",
      "type": "@n8n/n8n-nodes-langchain.toolSerpApi",
      "position": [
        -880,
        32
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "serpApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "47e51c15-ff71-4c11-aece-715cbfb2a466",
      "name": "Sticky Note - How It Works1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1328,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 528,
        "content": "## \u26a0\ufe0f Known Limitations\n\n- **Job Sources**:\n  Currently only SerpAPI (Google Jobs) + Decodo supported.\n  Can add LinkedIn, Jobstreet, Indeed with API/connector.\n\n- **Result Count**:\n  Hard-limited to **1 job** per run (by system prompt).\n  Change prompt if you want multiple jobs.\n\n- **Parsing**:\n  If AI fails to return valid JSON \u2192 fallback is `{ error: ... }`.\n  Email will show \"No job found\".\n\n- **Email Output**:\n  Gmail API required in n8n.\n  Make sure OAuth2 refresh tokens are valid.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4772dd55-822b-4a7a-b44d-5d819ebda152",
      "name": "Gemini 2.5 Pro",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -1024,
        32
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.5-pro"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5eb23275-86a1-44c7-853d-0c93ef186b93",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2752,
        160
      ],
      "parameters": {
        "width": 432,
        "height": 912,
        "content": "![Inbox 1](https://raw.githubusercontent.com/khmuhtadin/n8n-template/main/Website/%20JOB%20Hunter/inbox1.PNG)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "9f305cd6-2748-4da7-83b4-0349a88fe138",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2288,
        160
      ],
      "parameters": {
        "width": 432,
        "height": 912,
        "content": "![Inbox 2](https://raw.githubusercontent.com/khmuhtadin/n8n-template/main/Website/%20JOB%20Hunter/inbox%202.PNG)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "338e805a-11e4-4a65-9b5c-e9b85569516d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1840,
        160
      ],
      "parameters": {
        "width": 432,
        "height": 912,
        "content": "![Inbox 3](https://raw.githubusercontent.com/khmuhtadin/n8n-template/main/Website/%20JOB%20Hunter/inbox3.PNG)"
      },
      "typeVersion": 1
    },
    {
      "id": "88baa7d5-494a-4715-b903-790c36239ad4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1328,
        960
      ],
      "parameters": {
        "color": 3,
        "width": 370,
        "height": 100,
        "content": "## \u2615 Appreciate This Workflow?\n\nSupport the creator by sending coffee:\n\n**PayPal:** [paypal.me/khmuhtadin](https://paypal.me/khmuhtadin)\n\nThank you! \ud83d\ude80"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "SerpAPI": {
      "ai_tool": [
        [
          {
            "node": "Job Hunter Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Data": {
      "main": [
        [
          {
            "node": "Job Hunter Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini 2.5 Pro": {
      "ai_languageModel": [
        [
          {
            "node": "Job Hunter Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Job Hunter Agent": {
      "main": [
        [
          {
            "node": "Parse Agent Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "node": "Extract CV from PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Agent Output": {
      "main": [
        [
          {
            "node": "Generate HTML Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract CV from PDF": {
      "main": [
        [
          {
            "node": "Prepare Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate HTML Email": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}