This workflow corresponds to n8n.io template #12304 — we link there as the canonical source.
This workflow follows the Agent → Gmail recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "E4VbFv0h7EActCFd",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Find High-Quality Remote Jobs with AI, Decodo, and Google Sheets",
"tags": [],
"nodes": [
{
"id": "e375362b-8709-4a26-8d75-64d7957fb1be",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2032,
-352
],
"parameters": {
"height": 992,
"content": "## What this workflow does\n\nThis workflow automates the discovery, evaluation, and notification of job opportunities based on a candidate\u2019s professional profile.\n\nIt fetches remote job listings, compares each role against a structured candidate profile stored in Google Sheets, and uses AI to evaluate real alignment in terms of skills, seniority, salary, industry, and role complexity.\n\nOnly the most relevant opportunities are kept, stored in Google Sheets, and delivered via email as a Top 5 shortlist.\n[Decodo \u2013 Web Scraper for n8n](https://visit.decodo.com/raqXGD)\n\n## How to configure (quick setup)\n1. Define the candidate profile in Google Sheets (skills, salary expectations, preferences).\n2. Configure credentials (Google Sheets, Gmail, decodo, AI provider).\n3. Set the matching threshold (e.g. skill match \u2265 90%).\n4. Run the workflow manually or on a schedule.\n\n## Output\n- Structured job match results in Google Sheets \n- Automated email with the Top 5 best-matched job opportunities"
},
"typeVersion": 1
},
{
"id": "994716c3-8656-4544-aafb-363fae6c58c0",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1536,
0
],
"parameters": {
"color": 7,
"width": 384,
"height": 80,
"content": "## 1. Candidate profile input\nReads the candidate profile from Google Sheets."
},
"typeVersion": 1
},
{
"id": "59034560-8896-42a8-8f7f-73fcbae0a70c",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-400,
-368
],
"parameters": {
"color": 7,
"width": 480,
"height": 144,
"content": "## 2. Data collection & AI scoring\n- Fetches remote job listings using Decodo.\n- Extracts structured job data \n- Combines each job with the candidate profile.\n- An AI agent evaluates fit based on skills, salary, and industry."
},
"typeVersion": 1
},
{
"id": "5a03fe43-a4d8-4fb0-ae0a-2e20e057e948",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
880,
288
],
"parameters": {
"color": 7,
"width": 432,
"content": "## 3.Results & notifications\n- Filters jobs based on a minimum fit score.\n- Saves qualified matches to Google Sheets for tracking.\n- Builds an HTML email with the Top 5 job matches.\n-Sends the summary via Gmail."
},
"typeVersion": 1
},
{
"id": "5f690d8b-1d28-4c33-ad4c-d72713acfd3f",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-56,
16
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "gpt-4o-mini"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "bdbdc8da-d05d-4265-92e4-119ece6bdf3a",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
848,
-672
],
"parameters": {
"width": 832,
"height": 368,
"content": "## Output \n"
},
"typeVersion": 1
},
{
"id": "b027fa9e-ebe3-4db1-9bc5-1c70463d431f",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1696,
112
],
"parameters": {
"width": 624,
"height": 224,
"content": "## Input\n"
},
"typeVersion": 1
},
{
"id": "2999fdf8-d3f1-4e18-8c57-3b3c078c8555",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1056,
-112
],
"parameters": {
"width": 768,
"height": 352,
"content": "## Output\n"
},
"typeVersion": 1
},
{
"id": "2ae9e8c4-c04c-41e1-ba48-8ef0a5aab82d",
"name": "Daily trigger (job scan)",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1472,
-208
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3
},
{
"id": "062c51ef-1452-4c80-948b-58fe78ead883",
"name": "Load candidate profile (Google Sheets)",
"type": "n8n-nodes-base.googleSheets",
"position": [
-1248,
-208
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1TpKh77W5nLLwcLn8KU6LU58Y_GhEr9Cc2eJW1q9p4sk/edit#gid=0",
"cachedResultName": "Candidate_profile"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1TpKh77W5nLLwcLn8KU6LU58Y_GhEr9Cc2eJW1q9p4sk/edit?usp=drivesdk",
"cachedResultName": "jobs_matching"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "a56fcd39-e13c-49c2-b07a-3911235e405b",
"name": "Fetch RemoteOK HTML (Decodo)",
"type": "@decodo/n8n-nodes-decodo.decodo",
"position": [
-1024,
-136
],
"parameters": {
"url": "https://remoteok.com/remote-technical-jobs?location=Worldwide&min_salary=120000"
},
"credentials": {
"decodoApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ef3c6f5c-0ace-4b35-85b5-7c37c581cdd7",
"name": "Extract JobPosting JSON",
"type": "n8n-nodes-base.code",
"position": [
-800,
-136
],
"parameters": {
"jsCode": "// V1.5 - Still WAF-safe: no regex, no <tr parsing.\n// Extracts all JSON-LD blocks and enriches with salary + locations.\n// Company/url may be missing because those are not always in JSON-LD.\n\nvar first = $input.first();\nvar html = (first && first.json && first.json.results && first.json.results[0]) ? (first.json.results[0].content || '') : '';\n\nvar OPEN = '<script type=\"application/ld+json\">';\nvar CLOSE = '</script>';\n\nvar out = [];\nvar pos = 0;\n\nwhile (true) {\n var a = html.indexOf(OPEN, pos);\n if (a === -1) break;\n\n var b = html.indexOf(CLOSE, a);\n if (b === -1) break;\n\n var jsonText = html.substring(a + OPEN.length, b).trim();\n pos = b + CLOSE.length;\n\n var data;\n try {\n data = JSON.parse(jsonText);\n } catch (e) {\n continue;\n }\n\n if (!data || data['@type'] !== 'JobPosting') continue;\n\n // Salary (safe, no ??, no ?.)\n var salaryValue = (data.baseSalary && data.baseSalary.value) ? data.baseSalary.value : {};\n var minSalary = (salaryValue && salaryValue.minValue !== undefined && salaryValue.minValue !== null) ? salaryValue.minValue : null;\n var maxSalary = (salaryValue && salaryValue.maxValue !== undefined && salaryValue.maxValue !== null) ? salaryValue.maxValue : null;\n var currency = (data.baseSalary && data.baseSalary.currency) ? data.baseSalary.currency : null;\n\n // Locations\n var locations = [];\n if (data.jobLocation && Array.isArray(data.jobLocation)) {\n for (var i = 0; i < data.jobLocation.length; i++) {\n var loc = data.jobLocation[i];\n var addr = (loc && loc.address) ? loc.address : {};\n var place = addr.addressCountry || addr.addressRegion || addr.addressLocality || null;\n if (place) locations.push(place);\n }\n }\n\n out.push({\n json: {\n title: data.title || null,\n company: (data.hiringOrganization && data.hiringOrganization.name) ? data.hiringOrganization.name : null,\n datePosted: data.datePosted || null,\n minSalary: minSalary,\n maxSalary: maxSalary,\n currency: currency,\n employmentType: data.employmentType || null,\n locationType: data.jobLocationType || null,\n locations: locations,\n url: data.url || null,\n description: data.description || null\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "c2065e0d-6312-4a97-86bc-1f4db4371d16",
"name": "Merge profile + job list",
"type": "n8n-nodes-base.merge",
"position": [
-576,
-208
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "34037fb5-64ba-4208-9198-7daf10bfd659",
"name": "Create candidate-job pairs",
"type": "n8n-nodes-base.code",
"position": [
-352,
-208
],
"parameters": {
"jsCode": "// Safe version (no arrow, no spread, no optional, no nullish)\nvar candidate = (items && items[0]) ? items[0].json : {};\nvar out = [];\n\nfor (var i = 1; i < items.length; i++) {\n out.push({\n json: {\n candidate: candidate,\n job: items[i].json\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "8c6557cc-cf04-43be-ac24-a822d47ddbf5",
"name": "Score fit with AI (salary + skills + industry)",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-128,
-208
],
"parameters": {
"text": "=You will receive a JSON object with all job posting fields and all candidate profile fields already merged.\n\n### INPUT JSON:\n{{ JSON.stringify($json) }}\n\nUsing ONLY this JSON, evaluate how well the job matches the candidate profile.\n\nReturn ONLY this JSON structure:\n\n{\n \"fit_score\": <0-100>,\n \"salary_score\": <0-100>,\n \"skill_match_percentage\": <0-100>,\n \"industry_match\": \"<high/medium/low/unknown>\",\n \"industry_match_score\": <0-100>,\n \"missing_skills\": [],\n \"matching_skills\": [],\n \"salary_alignment\": \"<above/below/matching/unknown>\",\n \"location_match\": \"<yes/no/maybe/unknown>\",\n \"job_complexity_score\": <0-100>,\n \"seniority_match\": \"<yes/no/maybe/unknown>\",\n \"alignment_explanation\": \"<3-6 short factual sentences>\",\n \"final_recommendation\": \"<yes/no/maybe>\"\n}\n\n### RULES:\n- Use the System Prompt scoring weights:\n - 40% salary alignment\n - 40% skills match\n - 20% industry match\n- Salary:\n - Compare job minSalary/maxSalary/currency vs candidate salary_expectation.\n - If salary is missing, set salary_alignment to \"unknown\".\n- Skills:\n - matching_skills = skills in candidate \"skills\" that appear in job \"description\".\n - missing_skills = job-required skills inferred from description not present in candidate \"skills\".\n- Industry:\n - Compare job description/company against candidate \"preferred_industries\".\n - Use \"no_go\" as negative signal.\n- seniority_match is informational only and must NOT impact the fit_score.\n- job_complexity_score must reflect difficulty inferred from \"description\".\n- Output MUST be valid JSON. No commentary, no explanation outside JSON.\n",
"options": {
"systemMessage": "You are a Job Matching Engine. \nYour role is to evaluate how well a job posting matches a candidate's personal profile.\n\nThe candidate has clearly stated that the most important factors are:\n1) Salary alignment\n2) Relevant skills\n3) Industry match\nOverall \"fit\" should prioritize those above everything else.\n\nRules:\n- Always return clean, valid JSON.\n- Base your evaluation ONLY on the data provided in the candidate profile and the job posting.\n- Be objective. No motivational or emotional language.\n\nScoring logic:\n- fit_score must be based on this weighted system:\n - 40% salary alignment\n - 40% skills match\n - 20% industry match\n- salary_score must reflect how close the offered salary is to the candidate's expectations.\n- skill_match_percentage must reflect the overlap between candidate skills and job-required skills.\n- industry_match_score must measure how close the job's industry is to the candidate's preferred industries.\n- seniority_match is OPTIONAL and purely informational. It MUST NOT affect fit_score.\n\nOther fields:\n- job_complexity_score must reflect how advanced or demanding the role appears based on the description.\n- final_recommendation must be one of: \"yes\", \"no\", or \"maybe\".\n- Explanations must be short, clear, and factual.\n- Never invent information not present in the job or profile.\n- If salary or industry are missing, say so explicitly and adjust scores accordingly, but do not make up numbers."
},
"promptType": "define"
},
"typeVersion": 3
},
{
"id": "2d7e990c-1f99-4446-b0fe-01fca99464c7",
"name": "Flatten AI scores into job fields",
"type": "n8n-nodes-base.code",
"position": [
224,
-208
],
"parameters": {
"jsCode": "// Safe version: no ?. no ?? no spread\nvar JOB_NODE_NAME = 'Create candidate-job pairs';\nvar jobs = $items(JOB_NODE_NAME) || [];\n\nfunction safeParse(s) {\n if (!s) return {};\n try { return JSON.parse(String(s)); } catch (e) { return {}; }\n}\n\nvar out = [];\n\nfor (var i = 0; i < items.length; i++) {\n var analysis = safeParse(items[i].json.output);\n var job = (jobs[i] && jobs[i].json) ? jobs[i].json : {};\n\n var merged = {};\n for (var k1 in job) merged[k1] = job[k1];\n for (var k2 in analysis) merged[k2] = analysis[k2];\n\n out.push({ json: merged });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "c0379c5d-fa0e-45c6-bed7-7e88e61f80ee",
"name": "Parse AI JSON output (safe)",
"type": "n8n-nodes-base.code",
"position": [
448,
-208
],
"parameters": {
"jsCode": "function parseModelOutput(raw) {\n if (!raw) return {};\n\n var s = String(raw).trim();\n\n // Remove ```json ... ``` without literal backticks\n // \\x60 is backtick\n s = s.replace(/(\\x60{3}json|\\x60{3})/g, '').trim();\n\n // If JSON-string wrapped in quotes, try decode\n if ((s.charAt(0) === '\"' && s.charAt(s.length - 1) === '\"') ||\n (s.charAt(0) === \"'\" && s.charAt(s.length - 1) === \"'\")) {\n try {\n s = JSON.parse(s);\n } catch (e) {\n s = s.substring(1, s.length - 1);\n }\n }\n\n try {\n return JSON.parse(s);\n } catch (e1) {\n try {\n var s2 = s.replace(/\\\\\\\"/g, '\"').replace(/\\\\n/g, '\\n');\n return JSON.parse(s2);\n } catch (e2) {\n return { _parse_error: 'Could not parse model output', _raw_preview: s.substring(0, 200) };\n }\n }\n}\n\nvar out = [];\nfor (var i = 0; i < items.length; i++) {\n var analysis = parseModelOutput(items[i].json.output);\n\n var merged = {};\n for (var k in items[i].json) merged[k] = items[i].json[k];\n merged.analysis = analysis;\n\n out.push({ json: merged });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "931f69da-6be3-4529-925e-49d71eca1863",
"name": "Normalize job data for output",
"type": "n8n-nodes-base.code",
"position": [
672,
-208
],
"parameters": {
"jsCode": "function parseOutput(raw) {\n if (!raw) return {};\n var s = String(raw).trim().replace(/(\\x60{3}json|\\x60{3})/g, '').trim();\n\n if ((s.charAt(0) === '\"' && s.charAt(s.length - 1) === '\"') ||\n (s.charAt(0) === \"'\" && s.charAt(s.length - 1) === \"'\")) {\n try { s = JSON.parse(s); } catch (e) {}\n }\n\n try { return JSON.parse(s); } catch (e1) {\n try { return JSON.parse(s.replace(/\\\\\\\"/g, '\"').replace(/\\\\n/g, '\\n')); } catch (e2) { return {}; }\n }\n}\n\nvar out = [];\n\nfor (var i = 0; i < items.length; i++) {\n var item = items[i].json;\n\n var j = item.job ? item.job : item;\n var parsed = parseOutput(item.output);\n\n function pick(v1, v2, v3, def) {\n if (v1 !== undefined && v1 !== null && v1 !== '') return v1;\n if (v2 !== undefined && v2 !== null && v2 !== '') return v2;\n if (v3 !== undefined && v3 !== null && v3 !== '') return v3;\n return def;\n }\n\n var skill = pick(item.skill_match_percentage, parsed.skill_match_percentage, null, 0);\n var complexity = pick(item.job_complexity_score, parsed.job_complexity_score, null, 0);\n var fit = pick(item.fit_score, parsed.fit_score, null, 0);\n var finalRec = pick(item.final_recommendation, parsed.final_recommendation, null, '');\n var summary = pick(item.summary, item.alignment_explanation, parsed.alignment_explanation, '');\n\n var locationsStr = '';\n if (j.locations && Array.isArray(j.locations)) locationsStr = j.locations.join(', ');\n else locationsStr = pick(j.locations, item.locations, null, '');\n\n out.push({\n json: {\n title: pick(j.title, item.title, null, ''),\n company: pick(j.company, item.company, null, ''),\n url: pick(j.url, item.url, null, ''),\n date_posted: pick(j.datePosted, item.date_posted, null, ''),\n currency: pick(j.currency, item.currency, null, ''),\n min_salary: pick(j.minSalary, item.minSalary, item.min_salary, ''),\n max_salary: pick(j.maxSalary, item.maxSalary, item.max_salary, ''),\n type: pick(j.employmentType, item.employmentType, item.type, ''),\n locations: locationsStr,\n\n skill_match_percentage: skill,\n job_complexity_score: complexity,\n fit_score: fit,\n final_recommendation: finalRec,\n summary: summary\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "77fb2694-9235-4ae5-9ccb-a40a0474e8de",
"name": "Build Top 5 email (HTML)",
"type": "n8n-nodes-base.code",
"position": [
1120,
-304
],
"parameters": {
"jsCode": "function esc(s) {\n return String(s || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nvar jobs = [];\nfor (var i = 0; i < items.length; i++) jobs.push(items[i].json);\njobs = jobs.slice(0, 5);\n\nvar rows = [];\n\nfor (var idx = 0; idx < jobs.length; idx++) {\n var j = jobs[idx];\n\n var title = esc(j.title);\n var company = esc(j.company);\n var url = esc(j.url);\n var locations = esc(j.locations);\n var currency = esc(j.currency);\n\n var min = j.min_salary || '';\n var max = j.max_salary || '';\n var fit = (j.fit_score !== undefined && j.fit_score !== null) ? j.fit_score : '';\n var match = (j.skill_match_percentage !== undefined && j.skill_match_percentage !== null) ? j.skill_match_percentage : '';\n\n var summaryRaw = j.summary || '';\n var summary = esc(summaryRaw.substring(0, 260)) + (summaryRaw.length > 260 ? '...' : '');\n\n var salaryPart = '';\n if (currency && (min || max)) salaryPart = ' \u00b7 ' + currency + ' ' + min + ((min && max) ? '-' : '') + max;\n\n var bar = Math.min(100, Math.max(0, Number(match) || 0));\n\n rows.push(\n '<div style=\"padding:14px 16px; border-top:' + (idx === 0 ? '0' : '1px solid #e5e7eb') + ';\">' +\n '<div style=\"display:flex; justify-content:space-between; gap:14px; align-items:flex-start;\">' +\n '<div style=\"flex:1;\">' +\n '<div style=\"font-size:16px; font-weight:800; margin:0 0 4px;\">' + (idx + 1) + '. ' + title + '</div>' +\n '<div style=\"color:#374151; margin:0 0 10px; font-size:13px;\">' +\n '<strong>' + company + '</strong>' +\n (locations ? ' \u00b7 <span>' + locations + '</span>' : '') +\n salaryPart +\n (fit !== '' ? ' \u00b7 <span>Fit: ' + esc(fit) + '</span>' : '') +\n '</div>' +\n (summary ? '<div style=\"color:#4b5563; font-size:13px; margin:0 0 10px;\">' + summary + '</div>' : '') +\n '<a href=\"' + url + '\" style=\"display:inline-block; padding:9px 12px; background:#111827; color:#ffffff; text-decoration:none; border-radius:10px; font-size:13px;\">Open job</a>' +\n '</div>' +\n '<div style=\"min-width:130px; text-align:right;\">' +\n '<div style=\"font-size:12px; color:#6b7280;\">Skill match</div>' +\n '<div style=\"font-size:22px; font-weight:900; margin-top:2px;\">' + esc(match) + '%</div>' +\n '<div style=\"margin-top:8px; height:8px; background:#e5e7eb; border-radius:999px; overflow:hidden;\">' +\n '<div style=\"height:8px; width:' + bar + '%; background:#10b981;\"></div>' +\n '</div>' +\n '</div>' +\n '</div>' +\n '</div>'\n );\n}\n\nvar html =\n '<div style=\"font-family: Arial, Helvetica, sans-serif; color:#111827; line-height:1.5;\">' +\n '<h2 style=\"margin:0 0 8px;\">Your Top Job Matches</h2>' +\n '<p style=\"margin:0 0 14px; color:#4b5563;\">Filter applied: <strong>Fit score >= 40</strong></p>' +\n '<div style=\"border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;\">' +\n (rows.length ? rows.join('') : '<div style=\"padding:14px 16px; color:#6b7280;\">No jobs matched the filter.</div>') +\n '</div>' +\n '<div style=\"margin-top:14px; font-size:12px; color:#6b7280;\">Sent by your n8n Job Matching workflow \u00b7 ' + new Date().toISOString() + '</div>' +\n '</div>';\n\nreturn [{ json: { html: html } }];\n"
},
"typeVersion": 2
},
{
"id": "2ad89074-f8a0-4de5-b5fa-f042718de305",
"name": "Send Top 5 email (Gmail)",
"type": "n8n-nodes-base.gmail",
"position": [
1344,
-304
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.html }}",
"options": {},
"subject": "Your Top 5 Job Matches"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "3b22a64e-db27-4c5c-9316-924d6ae546f5",
"name": "Save matches to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
896,
-112
],
"parameters": {
"columns": {
"value": {
"url": "={{ $json.job_url }}",
"name": "={{ $json.job_title }}",
"type": "={{ $json.job_employment_type }}",
"summary": "={{ $json.summary }}",
"currency": "={{ $json.job_currency }}",
"fit_score": "={{ $json.fit_score }}",
"max_salary": "={{ $json.job_max_salary }}",
"min_salary": "={{ $json.job_min_salary }}",
"date_posted": "={{ $json.job_date_posted }}",
"job_description": "={{ $json.job_description }}",
"job_complexity_score": "={{ $json.job_complexity_score }}",
"skill_match_percentage": "={{ $json.skill_match_percentage }}"
},
"schema": [
{
"id": "url",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "fit_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "fit_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "max_salary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "max_salary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "min_salary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "min_salary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "currency",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "currency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "job_description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "job_description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "type",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "skill_match_percentage",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "skill_match_percentage",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "job_complexity_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "job_complexity_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "summary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "date_posted",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "date_posted",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1TpKh77W5nLLwcLn8KU6LU58Y_GhEr9Cc2eJW1q9p4sk/edit#gid=1079400330",
"cachedResultName": "output"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1TpKh77W5nLLwcLn8KU6LU58Y_GhEr9Cc2eJW1q9p4sk/edit?usp=drivesdk",
"cachedResultName": "jobs_matching"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "99e7d532-6a36-4747-a41f-0f03914722e8",
"name": "Check whether the fit score is greater than X",
"type": "n8n-nodes-base.if",
"position": [
896,
-304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e0a1507f-da10-4e01-be10-d21334062b01",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.fit_score }}",
"rightValue": 40
}
]
}
},
"typeVersion": 2.3
},
{
"id": "4720e582-9855-4528-850e-6b161d224b36",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1728,
-320
],
"parameters": {
"color": 7,
"width": 688,
"height": 752,
"content": ""
},
"typeVersion": 1
},
{
"id": "2221b5c8-c29f-4272-9e05-b7da48099217",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1048,
-384
],
"parameters": {
"color": 7,
"width": 1840,
"height": 544,
"content": ""
},
"typeVersion": 1
},
{
"id": "aefd8868-5175-463a-b57e-644e1d1723c5",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
800,
-736
],
"parameters": {
"color": 7,
"width": 1024,
"height": 1200,
"content": ""
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "c8b0a8c8-792c-4cd4-8705-0c9d639f171d",
"connections": {
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Score fit with AI (salary + skills + industry)",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Extract JobPosting JSON": {
"main": [
[
{
"node": "Merge profile + job list",
"type": "main",
"index": 1
}
]
]
},
"Build Top 5 email (HTML)": {
"main": [
[
{
"node": "Send Top 5 email (Gmail)",
"type": "main",
"index": 0
}
]
]
},
"Daily trigger (job scan)": {
"main": [
[
{
"node": "Load candidate profile (Google Sheets)",
"type": "main",
"index": 0
}
]
]
},
"Merge profile + job list": {
"main": [
[
{
"node": "Create candidate-job pairs",
"type": "main",
"index": 0
}
]
]
},
"Create candidate-job pairs": {
"main": [
[
{
"node": "Score fit with AI (salary + skills + industry)",
"type": "main",
"index": 0
}
]
]
},
"Parse AI JSON output (safe)": {
"main": [
[
{
"node": "Normalize job data for output",
"type": "main",
"index": 0
}
]
]
},
"Fetch RemoteOK HTML (Decodo)": {
"main": [
[
{
"node": "Extract JobPosting JSON",
"type": "main",
"index": 0
}
]
]
},
"Normalize job data for output": {
"main": [
[
{
"node": "Save matches to Google Sheets",
"type": "main",
"index": 0
},
{
"node": "Check whether the fit score is greater than X",
"type": "main",
"index": 0
}
]
]
},
"Save matches to Google Sheets": {
"main": [
[]
]
},
"Flatten AI scores into job fields": {
"main": [
[
{
"node": "Parse AI JSON output (safe)",
"type": "main",
"index": 0
}
]
]
},
"Load candidate profile (Google Sheets)": {
"main": [
[
{
"node": "Fetch RemoteOK HTML (Decodo)",
"type": "main",
"index": 0
},
{
"node": "Merge profile + job list",
"type": "main",
"index": 0
}
]
]
},
"Check whether the fit score is greater than X": {
"main": [
[
{
"node": "Build Top 5 email (HTML)",
"type": "main",
"index": 0
}
]
]
},
"Score fit with AI (salary + skills + industry)": {
"main": [
[
{
"node": "Flatten AI scores into job fields",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
decodoApigmailOAuth2googleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates the discovery, evaluation, and notification of job opportunities based on a candidate’s professional profile.
Source: https://n8n.io/workflows/12304/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Stay ahead in your job search with this Automated Job Intelligence System! This workflow scans company career pages daily for new job listings, uses AI to analyze job relevance and seniority levels, a
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This n8n automation workflow automates the creation, scripting, production, and posting of YouTube videos. It leverages AI (OpenAI), image generation (PIAPI), video rendering (Shotstack), and platform
Created by: Peyton Leveillee Last updated: October 2025