This workflow corresponds to n8n.io template #17215 — we link there as the canonical source.
This workflow follows the Agent → Form Trigger 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": "TRwXbaU8kVhhT2lX",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "Screen LinkedIn jobs with AI and email tailored applications after your approval",
"tags": [],
"nodes": [
{
"id": "f3ad54d7-7985-4679-92ab-a43bf173fff8",
"name": "Job preferences",
"type": "n8n-nodes-base.formTrigger",
"position": [
-16,
-112
],
"parameters": {
"options": {},
"formTitle": "Find and tailor jobs for me",
"formFields": {
"values": [
{
"fieldType": "email",
"fieldLabel": "Your email",
"requiredField": true
},
{
"fieldLabel": "Target role or keywords",
"requiredField": true
},
{
"fieldType": "dropdown",
"fieldLabel": "Seniority",
"fieldOptions": {
"values": [
{
"option": "Fresher / Entry"
},
{
"option": "Associate"
},
{
"option": "Mid"
},
{
"option": "Senior"
}
]
}
},
{
"fieldLabel": "Preferred locations"
},
{
"fieldType": "textarea",
"fieldLabel": "Your top skills"
},
{
"fieldLabel": "Deal-breakers (skip jobs needing these)"
},
{
"fieldType": "textarea",
"fieldLabel": "Your resume or profile",
"requiredField": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "aec89eda-4628-420f-93fa-07bd6e9d5651",
"name": "Configuration",
"type": "n8n-nodes-base.set",
"position": [
256,
-112
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "c1",
"name": "applicant_email",
"type": "string",
"value": "={{ $('Job preferences').first().json['Your email'] }}"
},
{
"id": "c2",
"name": "apify_actor",
"type": "string",
"value": "cheap_scraper~linkedin-job-scraper"
},
{
"id": "c3",
"name": "max_jobs",
"type": "number",
"value": 150
},
{
"id": "c4",
"name": "published_within",
"type": "string",
"value": "r604800"
},
{
"id": "c5",
"name": "top_matches",
"type": "number",
"value": 5
},
{
"id": "c6",
"name": "fit_threshold",
"type": "number",
"value": 40
},
{
"id": "c7",
"name": "screening_model",
"type": "string",
"value": "gpt-5-mini"
},
{
"id": "c8",
"name": "writing_model",
"type": "string",
"value": "gpt-5"
},
{
"id": "c9",
"name": "output_language",
"type": "string",
"value": "English"
},
{
"id": "c10",
"name": "approval_wait_hours",
"type": "number",
"value": 24
},
{
"id": "c11",
"name": "docraptor_test_mode",
"type": "boolean",
"value": true
}
]
}
},
"typeVersion": 3.4
},
{
"id": "d6a4167e-05b3-4901-adb7-7e092483c157",
"name": "Build Apify input",
"type": "n8n-nodes-base.code",
"position": [
448,
-112
],
"parameters": {
"jsCode": "const f = $('Job preferences').first().json;\nconst cfg = $('Configuration').first().json;\nconst keywords = String(f['Target role or keywords'] || '').split(',').map(s => s.trim()).filter(Boolean).slice(0, 3);\nconst location = (String(f['Preferred locations'] || '').split(',')[0] || '').trim() || 'Remote';\nconst senMap = { 'fresher / entry': 'entry-level', 'fresher': 'entry-level', 'entry': 'entry-level', 'associate': 'associate', 'mid': 'mid-senior', 'senior': 'mid-senior' };\nconst exp = senMap[String(f['Seniority'] || '').toLowerCase()];\nconst input = { keyword: keywords.length ? keywords : ['AI Engineer'], location: location, publishedAt: cfg.published_within || 'r604800', saveOnlyUniqueItems: true, enrichCompanyData: false, maxItems: Number(cfg.max_jobs) || 150 };\nif (exp) input.experienceLevel = [exp];\nreturn [{ json: input }];"
},
"typeVersion": 2
},
{
"id": "fc7a269c-84ca-4b19-9b51-37328ad3d6cc",
"name": "Scrape LinkedIn jobs",
"type": "n8n-nodes-base.httpRequest",
"position": [
624,
-112
],
"parameters": {
"url": "=https://api.apify.com/v2/acts/{{ $('Configuration').first().json.apify_actor }}/run-sync-get-dataset-items",
"method": "POST",
"options": {
"timeout": 300000
},
"jsonBody": "={{ JSON.stringify($json) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.2
},
{
"id": "1f80dced-68a3-453a-85ec-6eddbf65f5a8",
"name": "Pre-filter and dedupe",
"type": "n8n-nodes-base.code",
"position": [
816,
-112
],
"parameters": {
"jsCode": "const f = $('Job preferences').first().json;\nconst seniority = String(f['Seniority'] || '').toLowerCase();\nconst isJunior = /fresher|entry|associate/.test(seniority);\nconst wantLocs = String(f['Preferred locations'] || '').toLowerCase().split(/[,;/]| or /).map(s => s.trim()).filter(Boolean);\nconst seniorTitle = /(senior|sr\\.?|lead|principal|staff|architect|manager|director|head|vp|avp|\\biii\\b|sde-?3|sde-?ii)/i;\nconst seen = new Set();\nconst out = [];\nfor (const it of $input.all()) {\n const r = it.json;\n const title = String(r.jobTitle || '');\n const loc = String(r.location || '').toLowerCase();\n const key = (String(r.applyUrl || r.jobUrl || '') || (String(r.companyName || '') + '|' + title)).toLowerCase().trim();\n if (!key || seen.has(key)) continue;\n seen.add(key);\n if (isJunior && seniorTitle.test(title)) continue;\n if (/executive/i.test(String(r.experienceLevel || ''))) continue;\n let maxY = -1;\n for (let i = 0; i < 5; i++) {\n const m = String(r['yearsOfExperience/' + i + '/years'] || '').match(/(\\d+)/);\n if (m) maxY = Math.max(maxY, parseInt(m[1]));\n }\n const desc = String(r.jobDescription || '');\n const re = /(\\d+)\\s*(?:\\+|-\\s*\\d+)?\\s*(?:years|yrs)/gi;\n let tm;\n while ((tm = re.exec(desc)) !== null) {\n const n = parseInt(tm[1]);\n if (n <= 20) maxY = Math.max(maxY, n);\n }\n if (isJunior && maxY >= 4) continue;\n if (maxY >= 8) continue;\n if (wantLocs.length) {\n const ok = wantLocs.some(w => w && loc.includes(w)) || /remote|anywhere/.test(loc);\n if (!ok) continue;\n }\n out.push({ json: r });\n}\nreturn out;"
},
"typeVersion": 2
},
{
"id": "52bc5bfa-f474-4d42-919a-49ee70225942",
"name": "Build screening prompt",
"type": "n8n-nodes-base.code",
"position": [
1088,
-112
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const f = $('Job preferences').first().json;\nconst j = $json;\nconst jd = String(j.jobDescription || '').slice(0, 4500);\n$json.matchPrompt = 'You are a strict job screener for a job seeker. They want -> role/keywords: ' + (f['Target role or keywords'] || '') + '. seniority: ' + (f['Seniority'] || '') + '. preferred locations: ' + (f['Preferred locations'] || '') + '. their top skills: ' + (f['Your top skills'] || '') + '. deal-breakers (reject if the job requires any of these): ' + (f['Deal-breakers (skip jobs needing these)'] || '') + '. Infer the candidate experience from their seniority (Fresher/Entry = 0 to 1 years). Judge THIS job on relevance and eligibility. Return ONLY JSON: {\"relevant\": true or false, \"fit\": 0 to 100, \"verdict\": \"APPLY or STRETCH or REJECT\", \"reason\": \"one short sentence\"}. Rules: REJECT if the job needs clearly more years than the candidate has, is senior/lead/principal/manager level, requires a deal-breaker, or is a different field. APPLY if a genuine fit. STRETCH if close. JOB TITLE: ' + (j.jobTitle || '') + '. COMPANY: ' + (j.companyName || '') + '. LOCATION: ' + (j.location || '') + '. LEVEL: ' + (j.experienceLevel || '') + '. DESCRIPTION: ' + jd;\nreturn $json;"
},
"typeVersion": 2
},
{
"id": "8f8eaf05-3205-4f12-9ad5-4a179de78dee",
"name": "Screen job fit",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1248,
-112
],
"parameters": {
"text": "={{ $json.matchPrompt }}",
"options": {
"systemMessage": "You are a strict job screener. Reply with ONLY the JSON object asked for, nothing else."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "7d1eca0a-a23d-40d7-bfff-a52b6fac2edc",
"name": "Screening model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
1248,
64
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-5-mini",
"cachedResultName": "gpt-5-mini"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "59219b12-471d-41fb-9886-eed37424e0ed",
"name": "Rank and keep top matches",
"type": "n8n-nodes-base.code",
"position": [
1536,
-112
],
"parameters": {
"jsCode": "const prompts = $('Build screening prompt').all();\nconst inputs = $input.all();\nconst cfg = $('Configuration').first().json;\nconst threshold = Number(cfg.fit_threshold) || 40;\nconst topN = Number(cfg.top_matches) || 5;\nconst scored = [];\nfor (let i = 0; i < inputs.length; i++) {\n let m = { relevant: false, fit: 0, verdict: 'REJECT', reason: '' };\n try {\n let t = inputs[i].json.output;\n if (typeof t !== 'string') t = JSON.stringify(t || {});\n t = t.replace(/```json|```/g, '').trim();\n m = JSON.parse(t);\n } catch (e) {}\n const lead = (prompts[i] && prompts[i].json) || inputs[i].json;\n scored.push({ json: { ...lead, _fit: Number(m.fit) || 0, verdict: String(m.verdict || 'REJECT').toUpperCase(), reason: m.reason || '', _relevant: !!m.relevant } });\n}\nlet keep = scored.filter(r => r.json.verdict !== 'REJECT' && r.json._fit >= threshold);\nif (keep.length === 0) keep = scored.filter(r => r.json._fit >= Math.max(20, threshold - 10));\nkeep.sort((a, b) => b.json._fit - a.json._fit);\nreturn keep.slice(0, topN);"
},
"typeVersion": 2
},
{
"id": "8b6b7477-c658-4ee7-b45f-a9b8cd969ab5",
"name": "Approve shortlist",
"type": "n8n-nodes-base.gmail",
"position": [
1856,
-112
],
"parameters": {
"sendTo": "={{ $('Configuration').first().json.applicant_email }}",
"message": "={{ 'The AI shortlisted these roles for you:<br><br>' + $('Rank and keep top matches').all().map(x => '- <b>' + x.json.jobTitle + '</b> at ' + x.json.companyName + ' - fit ' + x.json._fit + ' (' + x.json.verdict + ')<br>' + (x.json.reason||'')).join('<br><br>') + '<br><br>Approve to generate tailored resumes and cover letters, or decline to stop.' }}",
"options": {
"limitWaitTime": {
"values": {
"resumeAmount": "={{ $('Configuration').first().json.approval_wait_hours }}"
}
}
},
"subject": "={{ 'Approve ' + $('Rank and keep top matches').all().length + ' job matches to tailor' }}",
"operation": "sendAndWait",
"approvalOptions": {
"values": {
"approvalType": "double",
"approveLabel": "Approve and tailor"
}
}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "100d4644-a4b4-4210-b5ad-8d2af069acc3",
"name": "Proceed if approved",
"type": "n8n-nodes-base.if",
"position": [
2032,
-112
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.data.approved }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "67b1e575-7900-4696-b8a9-d3fc8270894f",
"name": "Reload shortlist",
"type": "n8n-nodes-base.code",
"position": [
2208,
-128
],
"parameters": {
"jsCode": "return $('Rank and keep top matches').all();"
},
"typeVersion": 2
},
{
"id": "8d8e9feb-8e33-47f8-b35b-1bb75996d67c",
"name": "Build tailoring prompts",
"type": "n8n-nodes-base.code",
"position": [
2608,
-128
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const f = $('Job preferences').first().json;\nconst profile = f['Your resume or profile'] || '';\nconst rules = 'Rules: Do not use em-dashes, use commas colons or periods instead. Use ONLY facts present in the candidate profile text below, never invent skills, employers, dates, metrics or numbers. If the candidate lacks something the job wants, do not claim it, frame it honestly. Write in plain, human language.';\nconst j = $json;\nconst jd = String(j.jobDescription||'').slice(0,6000);\nconst role = j.jobTitle||'';\nconst company = (j.companyName||'').replace(/\"/g,'');\nconst PROFILE = 'CANDIDATE PROFILE (the only source of truth, do not invent beyond this): ' + profile;\n$json.resumePrompt = 'Write a tailored resume as JSON for this candidate and job. ' + rules + ' Output ONLY JSON with this shape: {\"_company\":\"' + company + '\",\"basics\":{\"name\":\"\",\"title\":\"\",\"email\":\"\",\"phone\":\"\",\"location\":\"\",\"links\":[{\"label\":\"\",\"url\":\"\"}]},\"summary\":\"\",\"experience\":[],\"projects\":[{\"name\":\"\",\"url\":\"\",\"tech\":[],\"bullets\":[]}],\"skills\":[],\"education\":[{\"degree\":\"\",\"school\":\"\",\"dates\":\"\"}]}. Extract the name, email, phone and links from the profile text. Keep it to 3 sections (summary, projects, skills). ' + PROFILE + ' ROLE: ' + role + ' at ' + company + '. JD: ' + jd;\n$json.coverPrompt = 'Write a humanized cover letter as JSON. ' + rules + ' 5 to 7 short paragraphs, open with the job and company, include one honest line if the candidate lacks the required years, and include one concrete achievement from the profile if one exists. Output ONLY JSON: {\"name\":\"\",\"contact\":\"\",\"links\":[{\"label\":\"\",\"url\":\"\"}],\"paragraphs\":[],\"filename\":\"\"}. Use the candidate name and contact from the profile. ' + PROFILE + ' ROLE: ' + role + ' at ' + company + '. JD: ' + jd;\nreturn $json;"
},
"typeVersion": 2
},
{
"id": "1449f3c3-76a6-4b51-a7a6-9647369aef10",
"name": "Write resume JSON",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2768,
-128
],
"parameters": {
"text": "={{ $json.resumePrompt }}",
"options": {
"systemMessage": "You are an expert resume writer. Return ONLY the JSON object requested. No preamble, no code fences."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "03bb5bfe-55a7-4891-b340-3cb85dec4758",
"name": "Resume model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
2768,
48
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-5",
"cachedResultName": "gpt-5"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "e26f5dea-9231-4af0-9a53-9df037870c21",
"name": "Render resume HTML",
"type": "n8n-nodes-base.code",
"position": [
3088,
-128
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "var esc=function(s){return String(s==null?'':s).replace(/[&<>]/g,function(c){return c==='&'?'&':c==='<'?'<':'>';});};\nvar r={};\ntry{var raw=(typeof $json.output==='string'?$json.output:JSON.stringify($json.output||{}));raw=raw.replace(/```json|```/g,'').trim();r=JSON.parse(raw);}catch(e){throw new Error('resume JSON parse failed');}\nvar b=r.basics||{};\nvar links=(b.links||[]).map(function(l){return '<a href=\"'+esc(l.url)+'\">'+esc(l.label)+'</a>';}).join('<span class=\"dot\">.</span>');\nvar skills=(r.skills||[]).map(function(s){return '<span class=\"skill\">'+esc(s)+'</span>';}).join('');\nvar projects=(r.projects||[]).map(function(p){var pu=p.url?'<span class=\"dates\"><a href=\"'+esc(p.url)+'\">'+esc(String(p.url).replace(/^https?:\\/\\//,''))+'</a></span>':'';var pt=(p.tech&&p.tech.length)?'<div class=\"tech\">'+esc(p.tech.join(' . '))+'</div>':'';var pb=(p.bullets||[]).map(function(x){return '<li>'+esc(x)+'</li>';}).join('');return '<div class=\"entry\"><div class=\"entry-head\"><span class=\"role\">'+esc(p.name)+'</span>'+pu+'</div>'+pt+'<ul>'+pb+'</ul></div>';}).join('');\nvar edu=(r.education||[]).map(function(e){return '<div class=\"edu-row\"><span>'+esc(e.degree)+', '+esc(e.school)+'</span><span class=\"dates\">'+esc(e.dates)+'</span></div>';}).join('');\nvar css='*{box-sizing:border-box}@page{margin:14mm 16mm}body{font-family:Georgia,serif;color:#1a1a1a;font-size:10.5pt;line-height:1.4;margin:0}h1{font-family:Arial,sans-serif;font-size:20pt;margin:0}.title{font-family:Arial,sans-serif;color:#444;font-size:11pt;margin:2px 0 6px}.contact{font-family:Arial,sans-serif;font-size:9pt;color:#333}.contact a{color:#1a4f8b;text-decoration:none}.dot{margin:0 6px;color:#aaa}h2{font-family:Arial,sans-serif;font-size:10.5pt;text-transform:uppercase;color:#1a4f8b;border-bottom:1.5px solid #1a4f8b;padding-bottom:2px;margin:16px 0 8px}.summary{margin:8px 0 4px}.skills{display:flex;flex-wrap:wrap;gap:5px}.skill{font-family:Arial,sans-serif;font-size:9pt;background:#eef2f7;border-radius:4px;padding:2px 7px}.entry{margin-bottom:10px}.entry-head{display:flex;align-items:baseline;gap:4px}.role{font-weight:bold}.dates{font-family:Arial,sans-serif;font-size:9pt;color:#666;margin-left:auto}.dates a{color:#666;text-decoration:none}.tech{font-family:Arial,sans-serif;font-size:9pt;color:#555;font-style:italic;margin:1px 0}ul{margin:4px 0 0;padding-left:16px}li{margin-bottom:2px}.edu-row{display:flex;justify-content:space-between}';\nvar contactExtra=links?'<span class=\"dot\">.</span>'+links:'';\nvar header='<header><h1>'+esc(b.name)+'</h1><div class=\"title\">'+esc(b.title)+'</div><div class=\"contact\">'+esc(b.email)+'<span class=\"dot\">.</span>'+esc(b.phone)+'<span class=\"dot\">.</span>'+esc(b.location)+contactExtra+'</div></header>';\nvar secSummary=r.summary?'<section><h2>Summary</h2><div class=\"summary\">'+esc(r.summary)+'</div></section>':'';\nvar secProjects=projects?'<section><h2>Projects</h2>'+projects+'</section>':'';\nvar secSkills=skills?'<section><h2>Skills</h2><div class=\"skills\">'+skills+'</div></section>':'';\nvar secEdu=edu?'<section><h2>Education</h2>'+edu+'</section>':'';\nvar html='<!doctype html><html><head><meta charset=\"utf-8\"><style>'+css+'</style></head><body>'+header+secSummary+secProjects+secSkills+secEdu+'</body></html>';\nreturn { ...$('Build tailoring prompts').item.json, resumeHtml: html, resumeName: (r._company||'resume')+'-resume' };"
},
"typeVersion": 2
},
{
"id": "b6cf48af-f199-47f1-abd0-985b68e86243",
"name": "Resume PDF (DocRaptor)",
"type": "n8n-nodes-base.httpRequest",
"position": [
3264,
-128
],
"parameters": {
"url": "https://api.docraptor.com/docs",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "resumePdf"
}
}
},
"jsonBody": "={ \"type\": \"pdf\", \"test\": {{ $(\"Configuration\").first().json.docraptor_test_mode }}, \"name\": {{ JSON.stringify($json.resumeName + \".pdf\") }}, \"document_content\": {{ JSON.stringify($json.resumeHtml) }} }",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"typeVersion": 4.2
},
{
"id": "a0093796-7f57-461a-a557-babfcb7976ea",
"name": "Write cover letter JSON",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
3424,
-128
],
"parameters": {
"text": "={{ $('Build tailoring prompts').item.json.coverPrompt }}",
"options": {
"systemMessage": "You are an expert cover-letter writer. Return ONLY the JSON object requested. No preamble, no code fences."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "00c6c254-4362-44b9-8b2e-bda27998577f",
"name": "Cover model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
3424,
48
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-5",
"cachedResultName": "gpt-5"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "d35bfc88-64fb-46f9-9db6-9e2b278b9c60",
"name": "Render cover HTML",
"type": "n8n-nodes-base.code",
"position": [
3696,
-128
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "var esc=function(s){return String(s==null?'':s).replace(/[&<>]/g,function(c){return c==='&'?'&':c==='<'?'<':'>';});};\nvar d={};\ntry{var raw=(typeof $json.output==='string'?$json.output:JSON.stringify($json.output||{}));raw=raw.replace(/```json|```/g,'').trim();d=JSON.parse(raw);}catch(e){throw new Error('cover JSON parse failed');}\nvar links=(d.links||[]).map(function(l){return '<a href=\"'+esc(l.url)+'\">'+esc(l.label)+'</a>';}).join('<span class=\"dot\"> . </span>');\nvar paras=(d.paragraphs||[]).map(function(p){return '<p>'+esc(p)+'</p>';}).join('');\nvar css='@page{margin:22mm 20mm}body{font-family:Georgia,serif;color:#1a1a1a;font-size:11pt;line-height:1.65;margin:0}h1{font-family:Arial,sans-serif;font-size:18pt;margin:0}.contact{font-family:Arial,sans-serif;font-size:9.5pt;color:#333;margin:5px 0 0}.contact a{color:#1a4f8b;text-decoration:none}.dot{color:#aaa}hr{border:none;border-top:1.5px solid #1a4f8b;margin:11px 0 18px}p{margin:0 0 12px}';\nvar contactExtra=links?'<span class=\"dot\"> . </span>'+links:'';\nvar html='<!doctype html><html><head><meta charset=\"utf-8\"><style>'+css+'</style></head><body><h1>'+esc(d.name)+'</h1><div class=\"contact\">'+esc(d.contact)+contactExtra+'</div><hr>'+paras+'</body></html>';\nreturn { ...$('Render resume HTML').item.json, coverHtml: html, coverName: (d.filename||'cover-letter') };"
},
"typeVersion": 2
},
{
"id": "eb5bb504-d3a2-4008-850e-11188fc75cd4",
"name": "Cover PDF (DocRaptor)",
"type": "n8n-nodes-base.httpRequest",
"position": [
3888,
-128
],
"parameters": {
"url": "https://api.docraptor.com/docs",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "coverPdf"
}
}
},
"jsonBody": "={ \"type\": \"pdf\", \"test\": {{ $(\"Configuration\").first().json.docraptor_test_mode }}, \"name\": {{ JSON.stringify($json.coverName + \".pdf\") }}, \"document_content\": {{ JSON.stringify($json.coverHtml) }} }",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"typeVersion": 4.2
},
{
"id": "7a86ce93-f8fc-412f-b938-8a104b0e0f0e",
"name": "Write interview prep",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
4048,
-128
],
"parameters": {
"text": "={{ 'You are an interview coach. For the role of ' + $('Build tailoring prompts').item.json.jobTitle + ' at ' + $('Build tailoring prompts').item.json.companyName + ', using this job description: ' + ($('Build tailoring prompts').item.json.jobDescription||'').slice(0,3000) + ' and this candidate profile: ' + ($('Job preferences').first().json['Your resume or profile']||'').slice(0,2000) + '. Return clean HTML only: an <h3>Likely interview questions</h3> heading, then an ordered list <ol> of 8 <li> items. In each <li> put the question in <b> tags, then a <br>, then a one line prep tip. Mix behavioral and technical questions relevant to this exact job. No preamble, no code fences.' }}",
"options": {
"systemMessage": "You are an expert interview coach. Return ONLY clean HTML as instructed. No preamble, no code fences."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "b6e3b09c-2d5d-41bc-974a-c53709f99c11",
"name": "Interview model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
4048,
48
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-5-mini",
"cachedResultName": "gpt-5-mini"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "95fdc61d-ba42-40f0-b027-5bdeb52d0ed8",
"name": "Assemble email",
"type": "n8n-nodes-base.code",
"position": [
4400,
-128
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const lead = $('Build tailoring prompts').item.json;\nlet interview = '';\ntry { let o = $('Write interview prep').item.json.output; interview = (typeof o === 'string') ? o : JSON.stringify(o); } catch (e) {}\nconst out = { json: { ...lead, interviewHtml: interview }, binary: {} };\ntry { out.binary.resumePdf = $('Resume PDF (DocRaptor)').item.binary.resumePdf; } catch (e) {}\ntry { out.binary.coverPdf = $('Cover PDF (DocRaptor)').item.binary.coverPdf; } catch (e) {}\nreturn out;"
},
"typeVersion": 2
},
{
"id": "489146aa-f6b2-49ec-8950-ce15630c2e3c",
"name": "Stopped (declined)",
"type": "n8n-nodes-base.noOp",
"position": [
2384,
-96
],
"parameters": {},
"typeVersion": 1
},
{
"id": "8ec5f1e2-ff4c-4537-a11b-2e3e2adb181f",
"name": "Email tailored application",
"type": "n8n-nodes-base.gmail",
"position": [
4624,
-192
],
"parameters": {
"sendTo": "={{ $('Configuration').first().json.applicant_email }}",
"message": "={{ '<h3>' + $json.jobTitle + ' at ' + $json.companyName + '</h3><p><b>' + $json.verdict + '</b> - fit ' + $json._fit + '</p><p>' + ($json.reason||'') + '</p>' + ($json.interviewHtml||'') + '<p><a href=\"https://www.google.com/search?q=' + encodeURIComponent($json.companyName + ' careers ' + $json.jobTitle) + '\">Find the official application page</a></p><p>Your tailored resume and cover letter are attached.</p>' }}",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{
"property": "resumePdf"
},
{
"property": "coverPdf"
}
]
}
},
"subject": "={{ 'Job match: ' + $json.jobTitle + ' at ' + $json.companyName + ' (' + $json.verdict + ')' }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "ee6342da-4195-4bd8-acf5-5e55e926f185",
"name": "Log match to tracker",
"type": "n8n-nodes-base.googleSheets",
"position": [
4624,
-32
],
"parameters": {
"columns": {
"value": {
"Fit": "={{ $json._fit }}",
"Why": "={{ $json.reason }}",
"Date": "={{ $now.format('yyyy-LL-dd') }}",
"Role": "={{ $json.jobTitle }}",
"Status": "To Apply",
"Company": "={{ $json.companyName }}",
"Verdict": "={{ $json.verdict }}",
"ApplyURL": "={{ $json.applyUrl }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Tracker"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": ""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "da57c0ed-3221-4e64-94d1-fee37ab06ce3",
"name": "Sticky Note 43d69fea",
"type": "n8n-nodes-base.stickyNote",
"position": [
-576,
-512
],
"parameters": {
"color": 4,
"width": 508,
"height": 2088,
"content": "# Screen LinkedIn jobs with AI and email tailored applications after your approval\n\n# [Open full documentation on Notion](https://automatisation.notion.site/Course-Screen-LinkedIn-jobs-with-AI-and-email-tailored-applications-after-your-approval-39d3d6550fd981428828f2fa8cb5b582)\n\nThis workflow collects your job preferences and resume from a form, scrapes fresh LinkedIn jobs with Apify, screens and ranks the matches with OpenAI, asks YOU to approve the shortlist by email, then writes a tailored resume and cover letter plus likely interview questions for each approved role, emails everything to you as PDFs, and logs each match to Google Sheets.\n\n## How it works\n1. You submit a short form: email, target role, seniority, locations, top skills, deal-breakers, and your resume text.\n2. Apify scrapes fresh LinkedIn jobs; a free pre-filter removes duplicates and obvious mismatches.\n3. OpenAI scores each remaining job for relevance and eligibility and returns a JSON verdict.\n4. The workflow keeps the best matches, then sends you ONE approval email listing the shortlist.\n5. You Approve or Decline. Nothing is generated until you approve (the AI proposes, you decide).\n6. For each approved job, OpenAI drafts a tailored resume and cover letter using only facts from your profile, plus 8 likely interview questions with prep tips.\n7. DocRaptor renders clean PDFs; Gmail emails them to you; Google Sheets logs the match.\n\n## Setup\n1. Add credentials: OpenAi account, Gmail account, Google Sheets account, Apify API (httpHeaderAuth, Authorization Bearer token), DocRaptor account (httpBasicAuth, API key as username, blank password).\n2. Open the Configuration node and set your defaults (models, number of matches, fit threshold, Apify actor, approval wait time, PDF test mode).\n3. Create a Google Sheet with a Tracker tab (Date, Company, Role, Fit, Verdict, Why, ApplyURL, Status) and select it in the Log match to tracker node.\n4. Keep DocRaptor test mode ON for free watermarked PDFs while testing; set it to false in Configuration for clean PDFs.\n5. Open the form URL and submit.\n\n## Requirements\n- n8n (self-hosted or Cloud).\n- OpenAI API key.\n- Apify account with access to a LinkedIn Jobs Scraper actor.\n- DocRaptor account for HTML to PDF (free test mode available).\n- Gmail and Google Sheets accounts.\n\n## Customization\n- Change models, shortlist size and fit threshold in the Configuration node (single editing point).\n- Broaden the target keywords for more matches, or tighten deal-breakers to filter harder.\n- Turn the approval into a single-click gate, or extend the wait time, in the Approve shortlist node.\n\nNeed help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/doctor-firass/)\n\n# MY NEW YOUTUBE CHANNEL\n[Subscribe to my new YouTube channel](https://www.youtube.com/@DrFiras_AI). Here I'll share videos and Shorts with practical tutorials and FREE templates for n8n.\n\n[](https://www.youtube.com/@DrFiras_AI)"
},
"typeVersion": 1
},
{
"id": "1da8dacd-dc77-4c0e-a798-da2aa7ea1c99",
"name": "Sticky Note 3cc256c6",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
-320
],
"parameters": {
"color": 7,
"width": 820,
"height": 344,
"content": "## 1. Collect preferences and scrape\nThe form captures the user's preferences and resume. Apify scrapes fresh LinkedIn jobs, then a free pre-filter removes duplicates and clear mismatches."
},
"typeVersion": 1
},
{
"id": "fc6f59fa-fff1-4aa2-b47c-417f16a4bd68",
"name": "Sticky Note e39ac1bf",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
-320
],
"parameters": {
"color": 7,
"width": 760,
"height": 344,
"content": "## 2. Screen and rank with AI\nOpenAI scores each job for relevance and eligibility. Only the best matches above your fit threshold are kept."
},
"typeVersion": 1
},
{
"id": "475d0365-7538-4e43-b875-a66a8bf2430a",
"name": "Sticky Note 2db61c18",
"type": "n8n-nodes-base.stickyNote",
"position": [
1808,
-320
],
"parameters": {
"color": 7,
"width": 760,
"height": 344,
"content": "## 3. You approve the shortlist\nOne approval email lists the shortlist. Nothing is generated until you Approve. Decline stops the run. The AI proposes, you decide."
},
"typeVersion": 1
},
{
"id": "04f1aa76-bfbc-4b52-8e9b-fc829dae7b9c",
"name": "Sticky Note 75f90598",
"type": "n8n-nodes-base.stickyNote",
"position": [
2576,
-320
],
"parameters": {
"color": 7,
"width": 1782,
"height": 344,
"content": "## 4. Tailor resume, cover and interview prep\nFor each approved job OpenAI writes a resume and cover letter from your profile, plus likely interview questions. DocRaptor renders the PDFs."
},
"typeVersion": 1
},
{
"id": "c0a7dd21-ce9f-4a90-a4bd-686e776dd4d3",
"name": "Sticky Note 5f12b77f",
"type": "n8n-nodes-base.stickyNote",
"position": [
4368,
-320
],
"parameters": {
"color": 7,
"width": 530,
"height": 352,
"content": "## 5. Deliver and log\nEmail the PDFs and interview prep to you, and append the match to the Google Sheet tracker."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "693d5b2f-f2d7-4dd4-9fec-36ef58ff7e7c",
"nodeGroups": [],
"connections": {
"Cover model": {
"ai_languageModel": [
[
{
"node": "Write cover letter JSON",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Resume model": {
"ai_languageModel": [
[
{
"node": "Write resume JSON",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Configuration": {
"main": [
[
{
"node": "Build Apify input",
"type": "main",
"index": 0
}
]
]
},
"Assemble email": {
"main": [
[
{
"node": "Email tailored application",
"type": "main",
"index": 0
},
{
"node": "Log match to tracker",
"type": "main",
"index": 0
}
]
]
},
"Screen job fit": {
"main": [
[
{
"node": "Rank and keep top matches",
"type": "main",
"index": 0
}
]
]
},
"Interview model": {
"ai_languageModel": [
[
{
"node": "Write interview prep",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Job preferences": {
"main": [
[
{
"node": "Configuration",
"type": "main",
"index": 0
}
]
]
},
"Screening model": {
"ai_languageModel": [
[
{
"node": "Screen job fit",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Reload shortlist": {
"main": [
[
{
"node": "Build tailoring prompts",
"type": "main",
"index": 0
}
]
]
},
"Approve shortlist": {
"main": [
[
{
"node": "Proceed if approved",
"type": "main",
"index": 0
}
]
]
},
"Build Apify input": {
"main": [
[
{
"node": "Scrape LinkedIn jobs",
"type": "main",
"index": 0
}
]
]
},
"Render cover HTML": {
"main": [
[
{
"node": "Cover PDF (DocRaptor)",
"type": "main",
"index": 0
}
]
]
},
"Write resume JSON": {
"main": [
[
{
"node": "Render resume HTML",
"type": "main",
"index": 0
}
]
]
},
"Render resume HTML": {
"main": [
[
{
"node": "Resume PDF (DocRaptor)",
"type": "main",
"index": 0
}
]
]
},
"Proceed if approved": {
"main": [
[
{
"node": "Reload shortlist",
"type": "main",
"index": 0
}
],
[
{
"node": "Stopped (declined)",
"type": "main",
"index": 0
}
]
]
},
"Scrape LinkedIn jobs": {
"main": [
[
{
"node": "Pre-filter and dedupe",
"type": "main",
"index": 0
}
]
]
},
"Write interview prep": {
"main": [
[
{
"node": "Assemble email",
"type": "main",
"index": 0
}
]
]
},
"Cover PDF (DocRaptor)": {
"main": [
[
{
"node": "Write interview prep",
"type": "main",
"index": 0
}
]
]
},
"Pre-filter and dedupe": {
"main": [
[
{
"node": "Build screening prompt",
"type": "main",
"index": 0
}
]
]
},
"Build screening prompt": {
"main": [
[
{
"node": "Screen job fit",
"type": "main",
"index": 0
}
]
]
},
"Resume PDF (DocRaptor)": {
"main": [
[
{
"node": "Write cover letter JSON",
"type": "main",
"index": 0
}
]
]
},
"Build tailoring prompts": {
"main": [
[
{
"node": "Write resume JSON",
"type": "main",
"index": 0
}
]
]
},
"Write cover letter JSON": {
"main": [
[
{
"node": "Render cover HTML",
"type": "main",
"index": 0
}
]
]
},
"Rank and keep top matches": {
"main": [
[
{
"node": "Approve shortlist",
"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.
gmailOAuth2googleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow collects your job preferences via an n8n form, uses Apify to scrape LinkedIn job listings, and has OpenAI screen and rank them. After you approve a shortlist by Gmail, it generates tailored resume and cover-letter PDFs with DocRaptor, emails them to you, and logs…
Source: https://n8n.io/workflows/17215/ — 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.
The workflow runs every hour with a randomized delay of 5–20 minutes to help distribute load. It records the exact date and time a lead is emailed so you can track outreach. Follow-ups are automatical
This n8n workflow automates turning short user ideas into production-ready real-estate marketing assets (photorealistic images and optional 360° videos). A form submission seeds a prompt board → an LL
Transform your manual hiring process into an intelligent evaluation system that saves 15-20 minutes per candidate! This workflow automates the entire candidate assessment pipeline - from CSV/XLSX uplo
This workflow automates the entire Calendly onboarding and offboarding process for company users. It relies on form submissions, Google Sheets as a source of truth, AI-generated HR emails, man-in-the-
This n8n workflow is designed for e-commerce businesses, digital marketers, and content creators who want to automatically generate professional 3D product videos from product images. It's perfect for