Bewerbungs-Radar
Experte
Dies ist ein HR, Multimodal AI-Bereich Automatisierungsworkflow mit 20 Nodes. Hauptsächlich werden If, Code, Merge, Airtable, GoogleDrive und andere Nodes verwendet. Automatisierung der Resume-Sichtung mit Gmail, Gemini AI und Airtable zu Google Sheets
Voraussetzungen
- •Airtable API Key
- •Google Drive API-Anmeldedaten
- •Möglicherweise sind Ziel-API-Anmeldedaten erforderlich
- •Google-Konto + Gmail API-Anmeldedaten
- •Google Sheets API-Anmeldedaten
Verwendete Nodes (20)
Kategorie
Workflow-Vorschau
Visualisierung der Node-Verbindungen, mit Zoom und Pan
Workflow exportieren
Kopieren Sie die folgende JSON-Konfiguration und importieren Sie sie in n8n
{
"id": "eRS0KpRKSwAZs7rW",
"meta": {
"instanceId": "241556d3cf444495f115cc4ed051381e5c047247800e2729fa805d67681495ea",
"templateCredsSetupCompleted": true
},
"name": "ResumeRadar",
"tags": [],
"nodes": [
{
"id": "049242f2-4300-462a-9aa0-4b02dfde99c1",
"name": "Gmail-Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
220,
30
],
"parameters": {
"simple": false,
"filters": {
"q": "has:attachment"
},
"options": {
"downloadAttachments": true
},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"credentials": {
"gmailOAuth2": {
"id": "u8SdDakudI0DsN99",
"name": "Gmail account"
}
},
"typeVersion": 1.2
},
{
"id": "c610f1ed-df34-4bec-a308-ffa68b1eda74",
"name": "Airtable",
"type": "n8n-nodes-base.airtable",
"position": [
440,
155
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appzbBhAyKeU2danJ",
"cachedResultUrl": "https://airtable.com/appzbBhAyKeU2danJ",
"cachedResultName": "ResumeRadar"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblnk2uJSYm7R5ABB",
"cachedResultUrl": "https://airtable.com/appzbBhAyKeU2danJ/tblnk2uJSYm7R5ABB",
"cachedResultName": "Job Descriptions"
},
"options": {},
"operation": "search"
},
"credentials": {
"airtableTokenApi": {
"id": "O4DgdwO5EDlt3Eft",
"name": "Airtable Personal Access Token account"
}
},
"typeVersion": 2.1
},
{
"id": "2873ae38-0c44-4493-a604-1088e768ec63",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
880,
155
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "66b8762f-c4ee-46c4-b2e5-a9e78ffccebd",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.matchFound }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "424f557a-1bab-4116-ad34-2f03251f44d9",
"name": "E-MailMatcher",
"type": "n8n-nodes-base.code",
"position": [
660,
155
],
"parameters": {
"jsCode": "// ---------- config ----------\nconst MIN_SCORE = 0.60; // raise/lower to be stricter/looser\n// ----------------------------\n\n// Build job list from Airtable items\nconst jobListings = [];\nif (Array.isArray(items)) {\n for (const item of items) {\n if (item?.json) {\n jobListings.push({\n id: item.json.id || '',\n title: item.json[\"Job Title\"] || '',\n jobCode: item.json[\"Job Code\"] || '',\n description: item.json[\"Description\"] || '',\n skills: item.json[\"Skills Required\"] || '',\n experience: item.json[\"Experience Required\"] || item.json[\"Experience Required (Years)\"] || '',\n location: item.json[\"Location\"] || ''\n });\n }\n }\n}\n\n// Email subject from Gmail Trigger (Option A: Simplify OFF)\nconst emailNode = $node[\"Gmail Trigger\"];\nconst email = emailNode?.json || {};\nlet subject = email.subject || email.Subject || '';\n\nif (!subject && items?.[0]?.json) {\n subject = items[0].json.subject || items[0].json.Subject || '';\n}\n\nfunction extractJobTitle(subj) {\n const lower = (subj || '').toLowerCase();\n const patterns = [\n /application (?:for|to) (.*?)(?:position|role|job)?$/i,\n /(?:applying|apply) (?:for|to) (?:the )?(.*?)(?:position|role|job)?$/i,\n /interested in (?:the )?(.*?)(?:position|role|job)/i,\n /regarding (?:the )?(.*?)(?:position|role|job)/i,\n /^(.*?)(?:position|job|role|application)$/i,\n ];\n for (const p of patterns) {\n const m = lower.match(p);\n if (m?.[1]?.trim()?.length > 3) return m[1].trim();\n }\n const capRe = /([A-Z][a-z]+(?: [A-Z][a-z]+)+)/g;\n let best = '', m;\n while ((m = capRe.exec(subj || '')) !== null) if (m[0].length > best.length) best = m[0];\n if (best.length > 5) return best.toLowerCase();\n return lower\n .replace(/application for|applying for|regarding|re:|job application|position|role|job|application|interest in/g, '')\n .replace(/^for the |for a |for |the |a /g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n// ---------- NEW similarity ----------\nconst STOP = new Set(['the','a','an','and','for','to','of','role','position','job','senior','jr','junior','lead','manager','developer','engineer']);\nconst PHRASE_REPL = [\n ['machine learning','ai'],\n ['artificial intelligence','ai'],\n ['quality assurance','qa'],\n ['full stack','fullstack'],\n ['front end','frontend'],\n ['back end','backend'],\n];\nconst TOKEN_MAP = { // single-token synonyms\n ml: 'ai',\n sdet: 'qa',\n testing: 'qa',\n test: 'qa',\n sre: 'devops',\n};\n\nfunction normalizeTitle(s = '') {\n s = s.toLowerCase();\n for (const [a,b] of PHRASE_REPL) s = s.replace(new RegExp(a, 'g'), b);\n s = s.replace(/[\\/,()\\-]/g, ' ');\n s = s.replace(/\\s+/g, ' ').trim();\n return s;\n}\n\nfunction canonicalTokens(s = '') {\n const t = normalizeTitle(s).split(' ').filter(Boolean);\n const out = [];\n for (const tok of t) {\n if (STOP.has(tok)) continue;\n out.push(TOKEN_MAP[tok] || tok);\n }\n // dedupe, preserve order\n return Array.from(new Set(out));\n}\n\nfunction jaccard(a, b) {\n const A = new Set(a), B = new Set(b);\n if (A.size === 0 && B.size === 0) return 1;\n const inter = new Set([...A].filter(x => B.has(x))).size;\n const uni = new Set([...A, ...B]).size;\n return uni ? inter / uni : 0;\n}\n\nfunction lev(a, b) {\n a = normalizeTitle(a); b = normalizeTitle(b);\n if (!a && !b) return 1;\n const T = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n for (let i = 0; i <= a.length; i++) T[0][i] = i;\n for (let j = 0; j <= b.length; j++) T[j][0] = j;\n for (let j = 1; j <= b.length; j++) {\n for (let i = 1; i <= a.length; i++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n T[j][i] = Math.min(T[j][i - 1] + 1, T[j - 1][i] + 1, T[j - 1][i - 1] + cost);\n }\n }\n const d = T[b.length][a.length];\n const maxLen = Math.max(a.length, b.length) || 1;\n return 1 - d / maxLen;\n}\n\n// final score = 70% token match + 30% string similarity\nfunction smartSim(s1, s2) {\n const jac = jaccard(canonicalTokens(s1), canonicalTokens(s2));\n const levSim = lev(s1, s2);\n return 0.7 * jac + 0.3 * levSim;\n}\n// ------------------------------------\n\nconst jobKeywords = extractJobTitle(subject);\n\n// guard: no keywords\nif (!jobKeywords) {\n return [{\n json: {\n subject,\n extractedJobTitle: jobKeywords,\n matchFound: false,\n reason: \"No extractable job title from subject\"\n },\n }];\n}\n\n// find best match using smartSim\nlet best = null, bestScore = 0;\nfor (const j of jobListings) {\n const s = smartSim(jobKeywords, j.title);\n if (s > bestScore) { best = j; bestScore = s; }\n}\n\n// Return the match\nif (best && bestScore >= MIN_SCORE) {\n return [{\n json: {\n subject,\n extractedJobTitle: jobKeywords,\n matchFound: true,\n jobMatch: {\n id: best.id,\n title: best.title,\n code: best.jobCode,\n description: best.description,\n skills: best.skills,\n experience: best.experience,\n location: best.location,\n confidence: bestScore\n }\n },\n }];\n}\n\nreturn [{\n json: {\n subject,\n extractedJobTitle: jobKeywords,\n matchFound: false,\n reason: best\n ? `Low confidence (${bestScore.toFixed(2)}) vs threshold ${MIN_SCORE}`\n : 'No jobs in list'\n },\n}];"
},
"typeVersion": 2
},
{
"id": "a528ce65-eeb4-49c4-8c8d-bf152ea6f52a",
"name": "PromptBuilder",
"type": "n8n-nodes-base.code",
"position": [
1300,
140
],
"parameters": {
"jsCode": "// PromptBuilder — builds the LLM prompt + carries email body & job meta\n\nconst m = items[0]?.json ?? {};\nconst job = m.jobMatch ?? {};\n\n// helpers\nfunction stripHtml(s = '') {\n return String(s)\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction firstStr(...vals) {\n for (const v of vals) if (typeof v === 'string' && v.trim()) return v.trim();\n return '';\n}\n\n// get Gmail payload (paired item first, then pinned/previous run as fallback)\nconst g1 = $item(0, 'Gmail Trigger')?.$json ?? {};\nconst g2 = (($items && $items('Gmail Trigger', 0)) || [])[0]?.json ?? {};\n\n// build email body: prefer any body already carried on the item, else read from Gmail\nlet emailBody = firstStr(\n m.emailBody,\n g1.text, g1.textPlain,\n g1.html ? stripHtml(g1.html) : '',\n g1.textHtml ? stripHtml(g1.textHtml) : '',\n g1.textAsHtml ? stripHtml(g1.textAsHtml) : '',\n g1.snippet,\n g2.text, g2.textPlain,\n g2.html ? stripHtml(g2.html) : '',\n g2.textHtml ? stripHtml(g2.textHtml) : '',\n g2.textAsHtml ? stripHtml(g2.textAsHtml) : '',\n g2.snippet\n);\nemailBody = emailBody.slice(0, 5000); // keep prompt compact\n\n// subject: prefer the one already on the item (from EmailMatcher), else Gmail\nconst subject = firstStr(m.subject, g1.subject, g2.subject);\n\n// prompt\nconst prompt = `\nYou are a senior technical recruiter.\n\nTASKS\n1) Read the ATTACHED resume (primary source of truth).\n2) Consider the email subject & body for extra context.\n3) Evaluate fit against the JOB description.\n\nExtract ONLY from the resume (do NOT infer from email headers):\n- candidateName: full legal name as printed in the resume. If missing, return \"\".\n- candidatePhone: best reachable phone (normalize to E.164 if possible; else raw). If missing, return \"\".\n\nScoring:\n- score: integer 1–10 (10=excellent, 7–9=strong, 4–6=partial, 1–3=poor).\n- explanation: one concise sentence under 40 words.\n\nEMAIL\n- Subject: ${subject}\n- Body: ${emailBody}\n\nJOB\n- Title: ${job.title || \"\"}\n- Code: ${job.code || \"\"}\n- Description: ${job.description || \"\"}\n- Skills Required: ${job.skills || \"\"}\n- Experience Required: ${job.experience || \"\"}\n- Location: ${job.location || \"\"}\n\nReturn STRICT JSON only (no prose, no markdown):\n{\n \"score\": <1-10>,\n \"explanation\": \"<under 40 words>\",\n \"candidateName\": \"<from resume or empty string>\",\n \"candidatePhone\": \"<E.164 if possible else raw or empty string>\"\n}\n`.trim();\n\nreturn [{ json: { prompt, jobMeta: job } }];\n"
},
"typeVersion": 2
},
{
"id": "39fc01a4-5a9b-438e-9932-e4b82f47bb4c",
"name": "ReadyAttachment",
"type": "n8n-nodes-base.code",
"position": [
1220,
-160
],
"parameters": {
"jsCode": "const matched = items.some(i => i.json?.matchFound === true || i.json?.jobMatch);\nif (!matched) return [];\n\n\n// Keep only real resume-ish attachments\nconst ALLOWED = new Set([\n 'application/pdf',\n]);\n\n \n// --- helpers ---\nfunction headerPick(g, key) {\n const v = g.headers?.[key] || g.headers?.[key.toLowerCase()];\n return v ? String(v).replace(/^[A-Za-z-]+:\\s*/, '') : '';\n}\nfunction pickEmail(str) {\n const m = String(str || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n return m ? m[0].toLowerCase() : '';\n}\nfunction pickName(str) {\n // \"Display Name\" <email@domain>\n const m = String(str || '').match(/^\"?([^\"<]+?)\"?\\s*<[^>]+>/);\n return m ? m[1].trim() : '';\n}\nfunction extractSender(g) {\n // 1) Preferred: rich shape\n const v0 = g.from?.value?.[0];\n const addr = v0?.address || '';\n const nm = v0?.name || '';\n\n // 2) Text forms (rich shape has these too)\n const fromText = typeof g.from?.text === 'string' ? g.from.text : '';\n const replyTo = headerPick(g, 'reply-to');\n const fromHdr = headerPick(g, 'from') || fromText;\n const returnPath = headerPick(g, 'return-path');\n\n // Decide email\n const email = addr || pickEmail(fromHdr) || pickEmail(replyTo) || pickEmail(returnPath) || '';\n // Decide name\n let name = nm || pickName(fromHdr);\n if (!name && email) name = email.split('@')[0];\n\n return { email, name };\n}\n\nconst out = [];\nfor (const item of items) {\n const g = item.json || {};\n const { email: fromEmail, name: fromName } = extractSender(g);\n\n // Prefer internalDate (epoch); else parse ISO \"date\"; else now\n const when =\n (g.internalDate ? Number(g.internalDate) : 0) ||\n (g.date ? Date.parse(g.date) : 0) ||\n Date.now();\n\n if (!item.binary) continue;\n for (const key of Object.keys(item.binary)) {\n const bin = item.binary[key];\n if (!bin?.data) continue;\n if (ALLOWED.size && !ALLOWED.has(bin.mimeType)) continue;\n\n out.push({\n json: {\n fileName: bin.fileName || 'resume',\n mimeType: bin.mimeType || 'application/octet-stream',\n fromEmail,\n fromName,\n internalDate: when,\n subject: g.subject || headerPick(g, 'subject') || ''\n },\n binary: { data: bin } // Downstream nodes use Binary Property: \"data\"\n });\n }\n}\nreturn out;\n"
},
"typeVersion": 2,
"alwaysOutputData": false
},
{
"id": "ee2d7f60-58be-475c-8d54-502f0bbf0aea",
"name": "Zusammenführen",
"type": "n8n-nodes-base.merge",
"position": [
1640,
-20
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "08d15341-fad1-41b4-8dd1-45c19ef66e19",
"name": "PayloadBuilder",
"type": "n8n-nodes-base.code",
"position": [
1860,
-20
],
"parameters": {
"jsCode": "const inItems = items.map(i => i.json);\n\n// 1) file object from FileUpload\nconst fileBlock = inItems.find(o => o.file?.uri)?.file;\nif (!fileBlock?.uri) throw new Error('Missing Gemini file URI');\n\n// 2) prompt/meta from PromptBuilder\nconst metaBlock = inItems.find(o => typeof o.prompt === 'string') || {};\nconst jm = metaBlock.jobMeta || {};\n\n// 3) prefer the ORIGINAL attachment mimeType (from ReadyAttachment);\n// fallback to the FileUpload's mimeType; final fallback to octet-stream.\nconst realMime =\n (inItems.find(o => o.mimeType)?.mimeType) || // ReadyAttachment\n fileBlock.mimeType || // FileUpload (if correct)\n 'application/octet-stream';\n\nconst parts = [];\nif (metaBlock.prompt) parts.push({ text: metaBlock.prompt });\nif (metaBlock.emailBody) parts.push({ text: `\\nEMAIL BODY:\\n${metaBlock.emailBody}` });\n\nparts.push({\n text:\n`JOB META:\nTitle: ${jm.title ?? ''}\nCode: ${jm.code ?? ''}\nDescription: ${jm.description ?? ''}\nSkills: ${jm.skills ?? ''}\nExperience: ${jm.experience ?? ''}\nLocation: ${jm.location ?? ''}`\n});\n\n// ✅ pass the real mime type here\nparts.push({\n fileData: {\n fileUri: fileBlock.uri,\n mimeType: realMime\n }\n});\n\nconst body = {\n contents: [{ role: 'user', parts }],\n generationConfig: {\n temperature: 0.1,\n responseMimeType: 'application/json',\n responseSchema: {\n type: 'object',\n properties: {\n score: { type: 'integer', minimum: 1, maximum: 10 },\n explanation: { type: 'string' },\n candidateName: { type: 'string' },\n candidatePhone: { type: 'string' }\n },\n required: ['score','explanation']\n },\n },\n};\n\nreturn [{ json: { body } }];\n"
},
"typeVersion": 2
},
{
"id": "c18f2954-dced-4342-a0aa-66a81545f5ff",
"name": "FileUpload",
"type": "n8n-nodes-base.httpRequest",
"position": [
1420,
-20
],
"parameters": {
"url": "https://generativelanguage.googleapis.com/upload/v1beta/files?uploadType=media",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "binaryData",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-goog-api-key",
"value": "={{$env.GEMINI_API_KEY}}"
},
{
"name": "Content-Type",
"value": "={{ $json.mimeType || $binary.data.mimeType || 'application/octet-stream' }}"
}
]
},
"inputDataFieldName": "data"
},
"typeVersion": 4.2
},
{
"id": "14be41de-3ef7-4258-952d-affe1c439341",
"name": "ResponseGenerator",
"type": "n8n-nodes-base.httpRequest",
"position": [
2080,
-20
],
"parameters": {
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent",
"method": "POST",
"options": {},
"jsonBody": "={{$json.body}}",
"sendBody": true,
"sendQuery": true,
"sendHeaders": true,
"specifyBody": "json",
"queryParameters": {
"parameters": [
{
"name": "key",
"value": "={{$env.GEMINI_API_KEY}}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "e0dd0699-059a-49ba-942d-58f10aaec4b7",
"name": "ParseResponse",
"type": "n8n-nodes-base.code",
"position": [
2400,
-100
],
"parameters": {
"jsCode": "// Input is the ONE merged item\nconst j = items[0].json;\n\n// Parse model JSON\nconst text = j?.candidates?.[0]?.content?.parts?.[0]?.text ?? '{}';\nlet data = {};\ntry { data = JSON.parse(text); } catch {}\n\nconst normPhone = s => s ? s.toString().trim().replace(/[^\\d+]/g,'') : '';\n\n// Build the exact columns we want for Sheets\nreturn [{\n json: {\n JobTitle: j.jobMeta?.title || j.jobMatch?.title || j.subject || '',\n CandidateName: (data.candidateName || '').trim() || (j.fromName || ''),\n CandidateEmail: j.fromEmail || '',\n CandidatePhoneNumber: normPhone(data.candidatePhone || ''),\n // prefer open link; fallback to direct or ID\n ResumeLink: j.webViewLink || j.webContentLink || (j.id ? `https://drive.google.com/file/d/${j.id}/view?usp=drivesdk` : ''),\n AIScore: data.score ?? null,\n AIExplanation: data.explanation ?? ''\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "f0c3079a-8cda-4a2c-bfde-0fc3d61037bd",
"name": "Zusammenführen1",
"type": "n8n-nodes-base.merge",
"position": [
1020,
-160
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "03a33b24-68a2-4f5f-afc2-36ef1cbdb0f7",
"name": "Google Drive",
"type": "n8n-nodes-base.googleDrive",
"position": [
1900,
-240
],
"parameters": {
"name": "={{\n (function () {\n const email = ($json.fromEmail || 'unknown').toLowerCase();\n const user = email.split('@')[0].replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'');\n const d = new Date(Number($json.internalDate) || Date.now());\n const yyyy=d.getUTCFullYear(), mm=String(d.getUTCMonth()+1).padStart(2,'0'),\n dd=String(d.getUTCDate()).padStart(2,'0'),\n hh=String(d.getUTCHours()).padStart(2,'0'),\n mi=String(d.getUTCMinutes()).padStart(2,'0'),\n ss=String(d.getUTCSeconds()).padStart(2,'0');\n const ext = (($json.fileName || '').split('.').pop()\n || ($json.mimeType==='application/pdf'?'pdf':'bin')).toLowerCase();\n return `${user || 'unknown'}__${yyyy}-${mm}-${dd}-${hh}${mi}${ss}.${ext}`;\n })()\n}}\n",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "list",
"value": "1nI3oQ_hfa8eEDJ2aThNT45T7SfJ5jGzd",
"cachedResultUrl": "https://drive.google.com/drive/folders/1nI3oQ_hfa8eEDJ2aThNT45T7SfJ5jGzd",
"cachedResultName": "ResumeRadar"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "fFPZO0wX94Keroqy",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "2ef593bc-bd85-451b-91a4-e93c32739659",
"name": "CombineData",
"type": "n8n-nodes-base.merge",
"position": [
2240,
-200
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition",
"numberInputs": 4
},
"typeVersion": 3.2
},
{
"id": "3cf0a49e-df9a-4cca-b245-a46838748e69",
"name": "Google Tabellen",
"type": "n8n-nodes-base.googleSheets",
"position": [
2580,
-100
],
"parameters": {
"columns": {
"value": {
"Email": "={{ $json.CandidateEmail }}",
"Resume": "={{ '=HYPERLINK(\"' + $json.ResumeLink + '\",\"Download\")' }}",
"Contact": "={{ $json.CandidatePhoneNumber }}",
"AI Score": "={{ $json.AIScore }}",
"Job Title": "={{ $json.JobTitle }}",
"AI Explanation": "={{ $json.AIExplanation }}",
"Candidate Name": "={{ $json.CandidateName }}"
},
"schema": [
{
"id": "Job Title",
"type": "string",
"display": true,
"required": false,
"displayName": "Job Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Candidate Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Candidate Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email",
"type": "string",
"display": true,
"required": false,
"displayName": "Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Contact",
"type": "string",
"display": true,
"required": false,
"displayName": "Contact",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Resume",
"type": "string",
"display": true,
"required": false,
"displayName": "Resume",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Score",
"type": "string",
"display": true,
"required": false,
"displayName": "AI Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Explanation",
"type": "string",
"display": true,
"required": false,
"displayName": "AI Explanation",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OGWk5UJdb2XrTS-5k9R0Q44d8HpvhdQW3AtzqkdWffg/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1OGWk5UJdb2XrTS-5k9R0Q44d8HpvhdQW3AtzqkdWffg",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OGWk5UJdb2XrTS-5k9R0Q44d8HpvhdQW3AtzqkdWffg/edit?usp=drivesdk",
"cachedResultName": "ResumeRadar - Applicants"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "G25jStA6MmWt8RXy",
"name": "Google Sheets account"
}
},
"typeVersion": 4.6
},
{
"id": "da71f4e7-21d7-403b-8e0b-d0ce2e9ee7b1",
"name": "Haftnotiz",
"type": "n8n-nodes-base.stickyNote",
"position": [
240,
-520
],
"parameters": {
"width": 500,
"height": 380,
"content": "# ResumeRadar\n\nThis workflow automates the resume screening process:\n1. Monitors Gmail for emails with PDF attachments\n2. Extracts job titles from email subjects\n3. Matches against job listings in Airtable\n4. Uses Gemini AI to analyze the resume against job requirements\n5. Saves data to Google Sheets and archives resumes in Google Drive\n\n## Setup Requirements:\n* Gmail, Google Drive, Google Sheets credentials\n* Airtable with job listings\n* Gemini API key in environment variables\n* Google Sheet for results"
},
"typeVersion": 1
},
{
"id": "1e73497d-9df7-403c-a89a-4555a76681f6",
"name": "Haftnotiz1",
"type": "n8n-nodes-base.stickyNote",
"position": [
540,
300
],
"parameters": {
"width": 380,
"height": 240,
"content": "## Job Matching Logic\n\nThis code extracts job titles from email subjects and matches against Airtable listings using:\n- Regular expressions for common application phrases\n- Smart similarity matching with synonyms handling\n\nAdjust MIN_SCORE (currently 0.60) to make matching stricter or more lenient."
},
"typeVersion": 1
},
{
"id": "37c8a308-2fdd-42f4-8b97-d9a25c8437dd",
"name": "Haftnotiz2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
-480
],
"parameters": {
"width": 420,
"height": 280,
"content": "## Resume Processing\n\nCurrently configured to accept PDF resumes only.\n\nTo support other formats:\n1. Edit the ALLOWED set in this node\n2. Add formats like 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n\nThis node also extracts sender information from email headers."
},
"typeVersion": 1
},
{
"id": "2528ce2a-089a-49be-9674-393d73d28c7f",
"name": "Haftnotiz3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1180,
280
],
"parameters": {
"width": 420,
"height": 220,
"content": "## AI Prompt\n\nThis node creates a structured prompt for Gemini to:\n- Extract candidate name and phone from resume\n- Score candidate fit on a scale of 1-10\n- Provide a concise explanation\n\nCustomize the prompt to change evaluation criteria or output format."
},
"typeVersion": 1
},
{
"id": "2161802f-3bd9-4366-926c-717b38ec5257",
"name": "Haftnotiz4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1720,
180
],
"parameters": {
"width": 420,
"height": 260,
"content": "## Gemini API Request Builder\n\nThis node prepares the payload for Gemini AI by:\n1. Combining the resume file with job context\n2. Structuring the prompt for consistent analysis\n3. Setting response schema for structured JSON output\n\nThe temperature setting (0.1) ensures consistent evaluations.\nAdjust the schema properties if you need different data points."
},
"typeVersion": 1
}
],
"active": true,
"pinData": {},
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1",
"saveExecutionProgress": true
},
"versionId": "62965170-c2de-4fc3-bffd-d2596db843ec",
"connections": {
"2873ae38-0c44-4493-a604-1088e768ec63": {
"main": [
[
{
"node": "Merge1",
"type": "main",
"index": 1
},
{
"node": "a528ce65-eeb4-49c4-8c8d-bf152ea6f52a",
"type": "main",
"index": 0
}
],
[]
]
},
"Merge": {
"main": [
[
{
"node": "08d15341-fad1-41b4-8dd1-45c19ef66e19",
"type": "main",
"index": 0
}
]
]
},
"Merge1": {
"main": [
[
{
"node": "39fc01a4-5a9b-438e-9932-e4b82f47bb4c",
"type": "main",
"index": 0
}
]
]
},
"c610f1ed-df34-4bec-a308-ffa68b1eda74": {
"main": [
[
{
"node": "EmailMatcher",
"type": "main",
"index": 0
}
]
]
},
"c18f2954-dced-4342-a0aa-66a81545f5ff": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"2ef593bc-bd85-451b-91a4-e93c32739659": {
"main": [
[
{
"node": "e0dd0699-059a-49ba-942d-58f10aaec4b7",
"type": "main",
"index": 0
}
]
]
},
"EmailMatcher": {
"main": [
[
{
"node": "2873ae38-0c44-4493-a604-1088e768ec63",
"type": "main",
"index": 0
}
]
]
},
"03a33b24-68a2-4f5f-afc2-36ef1cbdb0f7": {
"main": [
[
{
"node": "2ef593bc-bd85-451b-91a4-e93c32739659",
"type": "main",
"index": 0
}
]
]
},
"Gmail Trigger": {
"main": [
[
{
"node": "c610f1ed-df34-4bec-a308-ffa68b1eda74",
"type": "main",
"index": 0
},
{
"node": "Merge1",
"type": "main",
"index": 0
}
]
]
},
"e0dd0699-059a-49ba-942d-58f10aaec4b7": {
"main": [
[
{
"node": "Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"a528ce65-eeb4-49c4-8c8d-bf152ea6f52a": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
},
{
"node": "2ef593bc-bd85-451b-91a4-e93c32739659",
"type": "main",
"index": 3
}
]
]
},
"08d15341-fad1-41b4-8dd1-45c19ef66e19": {
"main": [
[
{
"node": "14be41de-3ef7-4258-952d-affe1c439341",
"type": "main",
"index": 0
}
]
]
},
"39fc01a4-5a9b-438e-9932-e4b82f47bb4c": {
"main": [
[
{
"node": "c18f2954-dced-4342-a0aa-66a81545f5ff",
"type": "main",
"index": 0
},
{
"node": "03a33b24-68a2-4f5f-afc2-36ef1cbdb0f7",
"type": "main",
"index": 0
},
{
"node": "2ef593bc-bd85-451b-91a4-e93c32739659",
"type": "main",
"index": 1
}
]
]
},
"14be41de-3ef7-4258-952d-affe1c439341": {
"main": [
[
{
"node": "2ef593bc-bd85-451b-91a4-e93c32739659",
"type": "main",
"index": 2
}
]
]
}
}
}Häufig gestellte Fragen
Wie verwende ich diesen Workflow?
Kopieren Sie den obigen JSON-Code, erstellen Sie einen neuen Workflow in Ihrer n8n-Instanz und wählen Sie "Aus JSON importieren". Fügen Sie die Konfiguration ein und passen Sie die Anmeldedaten nach Bedarf an.
Für welche Szenarien ist dieser Workflow geeignet?
Experte - Personalwesen, Multimodales KI
Ist es kostenpflichtig?
Dieser Workflow ist völlig kostenlos. Beachten Sie jedoch, dass Drittanbieterdienste (wie OpenAI API), die im Workflow verwendet werden, möglicherweise kostenpflichtig sind.
Verwandte Workflows
Mein Workflow 3
Automatisierter Lebenslauf-Screening und -Bewertung mit AI, Gmail, GoogleDrive und Airtable
Set
Code
Merge
+
Set
Code
Merge
23 Nodesusamaahmed
Personalwesen
Automatisiertes Kandidatenmanagement und Feedback-System mit DocuSign und Trello
Automatisierter Rekrutierungsprozess mithilfe von Slack-, DocuSign-, Trello- und Gmail-Benachrichtigungen
If
Code
Wait
+
If
Code
Wait
29 NodesMarth
Personalwesen
AI Candidate Screening Pipeline_ LinkedIn to Telegram with Gemini & Apify
If
Set
Code
+
If
Set
Code
55 NodesDean Pike
Personalwesen
AI Image Generation & Editing with Google Gemini and Telegram Bot
KI-Bildgenerierung und -bearbeitung basierend auf Google Gemini sowie Telegram-Bot
If
Set
Code
+
If
Set
Code
28 NodesSanthej Kallada
Content-Erstellung
KI-gesteuerte Videowerkzeugung und Upload zu Instagram, TikTok und YouTube
AI-gestützte Videoerstellung aus Google Drive und hochladen zu Instagram, TikTok und YouTube
If
Set
Code
+
If
Set
Code
53 NodesDevCode Journey
Content-Erstellung
OnboardAIgen - Slack x Jira x Drive Onboarding-Automatisierungs-Workflow
Automatisierung der Mitarbeiter-Einbindung über die Integration von Slack, Jira und Google Workspace
If
Set
Code
+
If
Set
Code
77 NodesVelebit from Innovatio
Personalwesen
Workflow-Informationen
Schwierigkeitsgrad
Experte
Anzahl der Nodes20
Kategorie2
Node-Typen9
Autor
Sameed
@sameedhusaynExterne Links
Auf n8n.io ansehen →
Diesen Workflow teilen