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 →
{
"nodes": [
{
"parameters": {
"formTitle": "Resume Generator",
"formDescription": "Paste the job description to generate a tailored resume",
"formFields": {
"values": [
{
"fieldLabel": "Job Description",
"fieldType": "textarea",
"placeholder": "Paste the full job description here...",
"requiredField": true
}
]
},
"options": {}
},
"id": "6fc16644-add8-44b3-abf9-636df3ce6470",
"name": "Form Trigger",
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2.2,
"position": [
3968,
8112
]
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-5-20250929",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"maxTokensToSample": 2000,
"temperature": 0.3
}
},
"id": "69106628-7cf7-4d7a-b560-e519fb20758c",
"name": "Claude - JD Analysis",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
4272,
8336
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"promptType": "define",
"text": "=Extract TOP 10 skills from this job description, ranked by importance.\n\n<job_description>\n{{ $json['Job Description'] }}\n</job_description>\n\n<ranking_criteria>\n- Higher rank: mentioned multiple times, marked required/must-have, appears in title/summary\n- Lower rank: mentioned once, listed as nice-to-have/preferred\n</ranking_criteria>\n\n<rules>\n1. Extract EXACT keywords verbatim from JD (preserve original phrasing)\n2. Categorize: technical | soft | domain\n3. If fewer than 10 skills exist, return only what's found\n</rules>\n\nReturn ONLY valid JSON:\n{\"top_10_skills\": [{\"rank\": 1, \"skill\": \"Python\", \"category\": \"technical\", \"jd_keywords\": [\"Python\", \"scripting\"]}], \"job_title\": \"Data Analyst\", \"company_focus\": \"Business intelligence\"}",
"options": {
"returnIntermediateSteps": false
}
},
"id": "1d84bcb2-59ce-4b97-a794-49428b751646",
"name": "AI Agent - Analyze JD",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [
4192,
8112
]
},
{
"parameters": {
"jsCode": "const response = $input.first().json;\nlet jdAnalysis;\n\ntry {\n const content = response.output || response.text || '';\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n jdAnalysis = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found');\n }\n} catch (e) {\n jdAnalysis = {\n top_10_skills: [],\n job_title: 'Unknown',\n company_focus: 'Unknown',\n error: e.message\n };\n}\n\n// Get original JD from Form Trigger\nconst formData = $('Form Trigger').first().json;\nconst originalJD = formData['Job Description'] || '';\n\nreturn [{\n json: {\n jobDescription: originalJD,\n jdAnalysis: jdAnalysis,\n top3Skills: jdAnalysis.top_10_skills?.slice(0, 3) || []\n }\n}];"
},
"id": "a066b7c1-e424-4bdf-91db-bea991d47007",
"name": "Parse JD Analysis",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4544,
8112
]
},
{
"parameters": {
"fileSelector": "/home/node/.n8n-files/data/edu_master_course.md",
"options": {}
},
"id": "d21cc72c-15a6-4d07-b00e-690ccfaff413",
"name": "Read Master Courses File",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
4768,
7584
]
},
{
"parameters": {
"jsCode": "const previousData = $('Parse JD Analysis').first().json;\n\nlet masterCoursesContent = '';\ntry {\n // Use n8n's built-in helper to read binary data (handles filesystem-v2 mode)\n const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n masterCoursesContent = binaryDataBuffer.toString('utf-8');\n} catch (e) {\n masterCoursesContent = '';\n}\n\nreturn [{\n json: {\n ...previousData,\n masterCoursesContent: masterCoursesContent,\n branch: 'master_courses'\n }\n}];"
},
"id": "d513d769-e20c-43a6-a1cf-61074a8dcf28",
"name": "Process Master Courses",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4992,
7584
]
},
{
"parameters": {
"promptType": "define",
"text": "=Select courses most relevant to the target job skills.\n\n<target_skills>\n{{ JSON.stringify($json.jdAnalysis.top_10_skills?.slice(0, 5).map(s => s.skill) || []) }}\n</target_skills>\n\n<available_courses>\n{{ $json.masterCoursesContent }}\n</available_courses>\n\n<rules>\n1. Select 4-6 courses with strongest skill alignment\n2. Prioritize courses matching top-ranked skills\n3. Return EXACT course names as listed\n4. If no strong matches, return empty array\n</rules>\n\nReturn ONLY valid JSON: {\"selected_courses\": [\"Machine Learning\", \"Data Mining\"]}",
"options": {
"returnIntermediateSteps": false
}
},
"id": "530bedb7-cd08-41d3-8110-3f4359e0d0ea",
"name": "AI - Extract Master Courses",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [
5216,
7472
]
},
{
"parameters": {
"jsCode": "const response = $input.first().json;\nlet coursesData;\n\ntry {\n const content = response.output || response.text || '';\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n coursesData = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found');\n }\n} catch (e) {\n coursesData = { selected_courses: [] };\n}\n\nreturn [{\n json: {\n masterCourses: coursesData.selected_courses || [],\n branch: 'master_courses'\n }\n}];"
},
"id": "eaa66e8a-d399-4ebe-8107-f5b69439085d",
"name": "Parse Master Courses",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5568,
7584
]
},
{
"parameters": {
"fileSelector": "/home/node/.n8n-files/data/edu_bech_course.md",
"options": {}
},
"id": "c190a601-e76b-4322-8cf7-1a1312544415",
"name": "Read Bachelor Courses File",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
4768,
7984
]
},
{
"parameters": {
"jsCode": "const previousData = $('Parse JD Analysis').first().json;\n\nlet bachelorCoursesContent = '';\ntry {\n // Use n8n's built-in helper to read binary data (handles filesystem-v2 mode)\n const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n bachelorCoursesContent = binaryDataBuffer.toString('utf-8');\n} catch (e) {\n bachelorCoursesContent = '';\n}\n\nreturn [{\n json: {\n ...previousData,\n bachelorCoursesContent: bachelorCoursesContent,\n branch: 'bachelor_courses'\n }\n}];"
},
"id": "b8d8e8aa-5b7f-4a04-812b-9e5c25a992fe",
"name": "Process Bachelor Courses",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4992,
7984
]
},
{
"parameters": {
"promptType": "define",
"text": "=Select courses most relevant to the target job skills.\n\n<target_skills>\n{{ JSON.stringify($json.jdAnalysis.top_10_skills?.slice(0, 5).map(s => s.skill) || []) }}\n</target_skills>\n\n<available_courses>\n{{ $json.bachelorCoursesContent }}\n</available_courses>\n\n<rules>\n1. Select 4-6 courses with strongest skill alignment\n2. Prioritize courses matching top-ranked skills\n3. Return EXACT course names as listed\n4. If no strong matches, return empty array\n</rules>\n\nReturn ONLY valid JSON: {\"selected_courses\": [\"Statistics\", \"Linear Algebra\"]}",
"options": {
"returnIntermediateSteps": false
}
},
"id": "07a08cf5-b002-440b-86b3-050bc8cdb5b3",
"name": "AI - Extract Bachelor Courses",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [
5216,
7984
]
},
{
"parameters": {
"jsCode": "const response = $input.first().json;\nlet coursesData;\n\ntry {\n const content = response.output || response.text || '';\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n coursesData = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found');\n }\n} catch (e) {\n coursesData = { selected_courses: [] };\n}\n\nreturn [{\n json: {\n bachelorCourses: coursesData.selected_courses || [],\n branch: 'bachelor_courses'\n }\n}];"
},
"id": "1a3c916f-0803-455e-9448-c76923a9fe8e",
"name": "Parse Bachelor Courses",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5568,
7984
]
},
{
"parameters": {
"fileSelector": "/home/node/.n8n-files/data/relevant_skills.md",
"options": {}
},
"id": "cebe41ad-90b9-4ac9-9a5a-976ae4be0ffc",
"name": "Read Skills File",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
5568,
8224
]
},
{
"parameters": {
"jsCode": "const previousData = $('Parse JD Analysis').first().json;\n\nlet skillsContent = '';\ntry {\n // Use n8n's built-in helper to read binary data (handles filesystem-v2 mode)\n const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n skillsContent = binaryDataBuffer.toString('utf-8');\n} catch (e) {\n skillsContent = '';\n}\n\nreturn [{\n json: {\n ...previousData,\n skillsContent: skillsContent,\n branch: 'skills'\n }\n}];"
},
"id": "213660a3-0ef4-4cde-804e-55f8c9ceec53",
"name": "Process Skills",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5856,
8224
]
},
{
"parameters": {
"promptType": "define",
"text": "=Match user's skills to JD requirements and organize by category.\n\n<jd_required_skills>\n{{ JSON.stringify($json.jdAnalysis.top_10_skills, null, 2) }}\n</jd_required_skills>\n\n<user_skills>\n{{ $json.skillsContent }}\n</user_skills>\n\n<inflation_rules>\nALLOWED to add (low learning curve, commonly assumed):\n- Version control: Git, GitHub\n- Collaboration: Jira, Confluence, Slack\n- Basic tools: VS Code, CLI, basic SQL\n\nNEVER add (requires significant learning):\n- Advanced: ML/AI, System Design, Kubernetes, Terraform\n- Certifications: AWS Solutions Architect, CKA\n- Languages/frameworks user hasn't shown evidence of\n</inflation_rules>\n\n<rules>\n1. Prioritize skills matching JD keywords exactly\n2. Group into logical categories (max 4 categories)\n3. Order skills within category by JD relevance\n4. Use JD terminology when user skill matches\n5. CRITICAL: Include ONLY 4-6 skills per category maximum\n6. Focus on most relevant skills - quality over quantity\n7. Total skills across all categories should not exceed 20\n8. CRITICAL: Each complete line (category label + colon + skills) MUST be under 86 characters\n9. If a line exceeds 86 characters, reduce the number of skills in that category\n10. Use short category names to leave more space for skills\n</rules>\n\nReturn ONLY valid JSON: {\"skills_by_category\": {\"Programming Languages\": [\"Python\", \"SQL\"], \"Tools\": [\"Git\", \"Docker\"]}}",
"options": {
"returnIntermediateSteps": false
}
},
"id": "dbbb10c1-95a2-4427-8216-e5a9fa4beb34",
"name": "AI - Extract Skills",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [
6144,
8128
]
},
{
"parameters": {
"jsCode": "const response = $input.first().json;\nlet skillsData;\n\ntry {\n const content = response.output || response.text || '';\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n skillsData = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found');\n }\n} catch (e) {\n skillsData = { skills_by_category: {} };\n}\n\nreturn [{\n json: {\n skillsByCategory: skillsData.skills_by_category || {},\n branch: 'skills'\n }\n}];"
},
"id": "c4fa4c45-14ac-4a4c-abbe-1d8b1468d5b6",
"name": "Parse Skills",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6496,
8224
]
},
{
"parameters": {
"fileSelector": "/home/node/.n8n-files/data/relevant_project/*.md",
"options": {}
},
"id": "a4a323aa-02ea-4e02-8ea2-73597c1a6a00",
"name": "Read Project Files",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
4768,
8528
]
},
{
"parameters": {
"jsCode": "const previousData = $('Parse JD Analysis').first().json;\nconst items = $input.all();\n\nlet projectsContent = '';\nconst projectsMap = {}; // file_name -> content mapping\nconst debugInfo = []; // DEBUG: track file processing\n\ntry {\n // Process each file item using n8n's built-in helper (handles filesystem-v2 mode)\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n try {\n const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, 'data');\n const content = binaryDataBuffer.toString('utf-8');\n \n // Extract file name from binary metadata\n const fileName = item.binary?.data?.fileName || '';\n // Remove .md extension and path to get clean file name\n const cleanName = fileName.replace(/\\.md$/i, '').replace(/^.*[\\\\\\/]/, '');\n \n // Add file name marker before content\n projectsContent += `[FILE: ${cleanName}]\\n${content}\\n\\n`;\n \n // DEBUG: Log file processing\n debugInfo.push({\n originalFileName: fileName,\n cleanName: cleanName,\n contentLength: content.length,\n firstLine: content.split('\\n')[0],\n secondLine: content.split('\\n')[1] || 'NO SECOND LINE'\n });\n \n if (cleanName) {\n projectsMap[cleanName] = content;\n }\n } catch (e) {\n debugInfo.push({ error: e.message, index: i });\n }\n }\n} catch (error) {\n projectsContent = '';\n}\n\nreturn [{\n json: {\n ...previousData,\n projectsContent: projectsContent,\n projectsMap: projectsMap,\n branch: 'projects',\n debugProcessProjects: {\n filesProcessed: debugInfo,\n mapKeys: Object.keys(projectsMap),\n totalFiles: items.length\n }\n }\n}];"
},
"id": "0c955399-2fcc-4fe3-88fa-f33637ea3287",
"name": "Process Projects",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4992,
8528
]
},
{
"parameters": {
"promptType": "define",
"text": "=Select exactly 3 projects that best demonstrate the target job skills.\n\n<top_jd_skills>\n{{ JSON.stringify($json.top3Skills, null, 2) }}\n</top_jd_skills>\n\n<available_projects>\n{{ $json.projectsContent }}\n</available_projects>\n\n<file_format>\nEach project starts with [FILE: filename] marker.\nExample: [FILE: car-price-regression]\nYou MUST extract the exact filename from this marker.\n</file_format>\n\n<selection_criteria>\n1. CRITICAL: Only select from available_projects - NEVER invent projects\n2. Match each project to one of the top 3 JD skills\n3. Prefer projects with: quantifiable results, relevant tech stack, recent dates\n4. Each project should demonstrate a DIFFERENT skill if possible\n</selection_criteria>\n\n<output_rules>\n1. CRITICAL: Extract file_name from [FILE: xxx] marker EXACTLY as written\n2. DO NOT modify, translate, or generate file names based on project titles\n3. Example: If you see [FILE: car-price-regression], return \"car-price-regression\"\n4. Rank by relevance (best match = index 0)\n5. Keep relevance_reason under 15 words\n</output_rules>\n\nReturn ONLY valid JSON: {\"selected_projects\": [{\"file_name\": \"car-price-regression\", \"matched_skill\": \"Python\", \"relevance_reason\": \"Built ETL pipeline processing 1M+ records\"}]}",
"options": {
"returnIntermediateSteps": false
}
},
"id": "ef2d06cf-813f-44a1-bc7d-f6b608362cdf",
"name": "AI - Select Top 3 Projects",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [
5216,
8528
]
},
{
"parameters": {
"jsCode": "const response = $input.first().json;\nconst previousData = $('Process Projects').first().json;\nlet projectsData;\n\ntry {\n const content = response.output || response.text || '';\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n projectsData = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found');\n }\n} catch (e) {\n projectsData = { selected_projects: [] };\n}\n\n// Get projects map from Process Projects node\nconst projectsMap = previousData.projectsMap || {};\n\n// DEBUG: Track available keys\nconst availableKeys = Object.keys(projectsMap);\n\n// Enrich selected projects with their content from the map\nconst enrichedProjects = (projectsData.selected_projects || []).map(project => {\n const fileName = project.file_name || '';\n let matchedKey = '';\n \n // Try exact match first, then case-insensitive match\n let projectContent = projectsMap[fileName] || '';\n if (projectContent) {\n matchedKey = fileName;\n }\n \n // If no exact match, try case-insensitive and normalized matching\n if (!projectContent) {\n const normalizedFileName = fileName.toLowerCase().replace(/[\\s-]/g, '_').replace(/_+/g, '_');\n for (const [key, value] of Object.entries(projectsMap)) {\n const normalizedKey = key.toLowerCase().replace(/[\\s-]/g, '_').replace(/_+/g, '_');\n if (normalizedKey === normalizedFileName || key.toLowerCase() === fileName.toLowerCase()) {\n projectContent = value;\n matchedKey = key;\n break;\n }\n }\n }\n \n return {\n ...project,\n projectContent: projectContent,\n matchedKey: matchedKey\n };\n});\n\n// Return as individual items for parallel AI processing\nreturn enrichedProjects.map(project => ({\n json: {\n jdAnalysis: previousData.jdAnalysis,\n top3Skills: previousData.top3Skills,\n currentProject: project,\n projectContent: project.projectContent,\n projectFileName: project.file_name,\n branch: 'projects',\n debug: {\n availableKeys: availableKeys,\n requestedFileName: project.file_name,\n matchedKey: project.matchedKey,\n contentFound: !!project.projectContent,\n contentLength: project.projectContent?.length || 0,\n contentPreview: project.projectContent?.substring(0, 200) || 'EMPTY'\n }\n }\n}));"
},
"id": "55d615c9-7a11-4c47-9812-f4e0e8f060ce",
"name": "Parse Selected Projects",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5568,
8528
]
},
{
"parameters": {
"promptType": "define",
"text": "=Write resume bullet points for this project.\n\n<context>\nTarget skill: {{ $json.currentProject.matched_skill }}\nRelevance: {{ $json.currentProject.relevance_reason }}\nJD keywords: {{ JSON.stringify($json.jdAnalysis.top_10_skills?.map(s => s.jd_keywords).flat() || []) }}\n</context>\n\n<project>\n{{ $json.projectContent }}\n</project>\n\n<sentence_format>\nAction Verb + What You Did + Measurable Result\nExamples:\n- Developed <b>REST API</b> using Python Flask, reducing latency by 40%\n- Automated data pipeline processing 500K daily records with 99.9% accuracy\n</sentence_format>\n\n<rules>\n1. Use JD keywords VERBATIM when applicable\n2. Use <b> tags for max 3 key terms per project\n3. Use numerals: 3, 50%, 100K (not three, fifty percent)\n4. NO bullet/dash prefix - sentences only (bullet points will be added later)\n5. Integrate tech naturally (no separate \"Technologies:\" line)\n6. Generate 3-5 sentences per project\n7. CRITICAL: Each sentence MUST be under 86 characters (including spaces and <b> tags)\n8. CRITICAL: Project title MUST be under 60 characters\n</rules>\n\n<date_extraction>\nCRITICAL: Extract the project date from LINE 2 of the project content.\n\nThe date is ALWAYS on the second line, immediately after the title, in format:\n\"Month YYYY - Month YYYY\" (e.g., \"September 2024 - December 2024\")\nor \"Month YYYY - Present\" (e.g., \"December 2025 - Present\")\n\nSimply copy the date string from line 2 exactly as written.\nDo NOT return \"Recent\" - the date is always available on line 2.\n</date_extraction>\n\nReturn ONLY valid JSON: {\"project_title\": \"Real-time Data Pipeline\", \"project_date\": \"Jan 2024 - Mar 2024\", \"original_name\": \"{{ $json.projectFileName }}\", \"matched_skill\": \"{{ $json.currentProject.matched_skill }}\", \"impact_sentences\": [\"Built <b>ETL pipeline</b> processing 1M+ records daily\"]}",
"options": {
"returnIntermediateSteps": false
}
},
"id": "a41794c7-3237-41a8-a8e2-f40baed5a060",
"name": "AI - Write Impact for Project",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [
5792,
8528
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const response = $input.item.json;\nlet impactData;\n\ntry {\n const content = response.output || response.text || '';\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n impactData = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found');\n }\n} catch (e) {\n impactData = {\n project_title: 'Unknown Project',\n original_name: '',\n matched_skill: '',\n impact_sentences: [],\n technologies_used: []\n };\n}\n\nreturn {\n json: {\n projectWithImpact: impactData,\n branch: 'projects'\n }\n};"
},
"id": "4f30c00b-872f-41a2-8086-a76f386c7d76",
"name": "Parse Impact Sentence",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6208,
8528
]
},
{
"parameters": {
"jsCode": "const items = $input.all();\n\n// Collect all project impacts\nconst projectsWithImpact = items\n .map(item => item.json.projectWithImpact)\n .filter(p => p && p.impact_sentences && p.impact_sentences.length > 0);\n\nreturn [{\n json: {\n projectsWithImpact: projectsWithImpact,\n branch: 'projects'\n }\n}];"
},
"id": "326cbf1b-ca59-407a-ae2e-834c2afd8468",
"name": "Aggregate Project Results",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6496,
8528
]
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"id": "3795abc0-05b6-48c9-9584-3e630e9daeb1",
"name": "Merge Courses",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
5856,
7776
]
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"id": "6efd7d80-ed8d-4b15-8a7a-4deb3d13015c",
"name": "Merge Skills & Projects",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
6720,
8384
]
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"id": "bf2f6652-b2b6-410d-953a-8fa658fd16ed",
"name": "Merge All Results",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
6944,
8048
]
},
{
"parameters": {
"jsCode": "const items = $input.all();\n\n// Fixed personal info\nconst PERSONAL_INFO = {\n name: 'Your Name',\n email: 'sam.guan@example.com',\n phone: '(123) 456-7890',\n linkedin: 'https://linkedin.com/in/samguan',\n github: 'https://github.com/samguan',\n location: 'San Francisco, CA'\n};\n\n// Extract data from each branch\nlet masterCourses = [];\nlet bachelorCourses = [];\nlet skillsByCategory = {};\nlet projectsWithImpact = [];\nlet unmatchedSkills = [];\n\nfor (const item of items) {\n const data = item.json;\n if (data.masterCourses) masterCourses = data.masterCourses;\n if (data.bachelorCourses) bachelorCourses = data.bachelorCourses;\n if (data.skillsByCategory) skillsByCategory = data.skillsByCategory;\n if (data.projectsWithImpact) projectsWithImpact = data.projectsWithImpact;\n if (data.unmatchedSkills) unmatchedSkills = data.unmatchedSkills;\n}\n\nreturn [{\n json: {\n personalInfo: PERSONAL_INFO,\n masterCourses: masterCourses,\n bachelorCourses: bachelorCourses,\n skillsByCategory: skillsByCategory,\n matchedProjects: projectsWithImpact,\n unmatchedSkills: unmatchedSkills\n }\n}];"
},
"id": "e519697e-cc96-459b-873c-24e77a2bb291",
"name": "Combine All Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7168,
8048
]
},
{
"parameters": {
"fileSelector": "/home/node/.n8n-files/data/templates/resume_template.html",
"options": {}
},
"id": "48a8ae46-64d7-4262-ab05-f1bee59fb5ba",
"name": "Read Template File",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
7392,
8048
]
},
{
"parameters": {
"jsCode": "// This code uses async to properly read binary data from n8n filesystem mode\nconst data = $('Combine All Data').first().json;\nconst templateFile = $input.first();\n\n// Helper: Generate contact line\nfunction generateContactLine(info) {\n if (!info) return '';\n const parts = [];\n if (info.location) parts.push(info.location);\n if (info.phone) parts.push(info.phone);\n if (info.email) parts.push(`<a href=\"mailto:${info.email}\">${info.email}</a>`);\n if (info.linkedin) parts.push(`<a href=\"${info.linkedin}\">LinkedIn</a>`);\n if (info.github) parts.push(`<a href=\"${info.github}\">GitHub</a>`);\n return parts.join(' | ');\n}\n\n// Helper: Generate skills HTML\nfunction generateSkillsSection(skillsByCategory) {\n if (!skillsByCategory || typeof skillsByCategory !== 'object') {\n return '<p>No skills data available.</p>';\n }\n let html = '';\n for (const [category, skills] of Object.entries(skillsByCategory)) {\n if (Array.isArray(skills) && skills.length > 0) {\n html += `<p class=\"skills-line\"><span class=\"skills-label\">${category}:</span> ${skills.join(', ')}</p>\\n`;\n }\n }\n return html || '<p>No skills data available.</p>';\n}\n\n// Helper: Generate projects HTML\nfunction generateProjectsSection(projects) {\n if (!Array.isArray(projects) || projects.length === 0) {\n return '<p>No matching projects found.</p>';\n }\n return projects.map(project => {\n if (!project) return '';\n const title = project.project_title || project.original_name || 'Untitled Project';\n const date = project.project_date || '';\n const bullets = Array.isArray(project.impact_sentences) \n ? project.impact_sentences.map(s => `<li>${s}</li>`).join('\\n')\n : '';\n const dateSpan = date ? `<span class=\"entry-date\">${date}</span>` : '';\n return `<div class=\"entry\">\\n<div class=\"entry-header\">\\n<span class=\"entry-title\">${title}</span>\\n${dateSpan}\\n</div>\\n<ul>\\n${bullets}\\n</ul>\\n</div>`;\n }).filter(Boolean).join('\\n');\n}\n\n// Read template using n8n's built-in binary helper (handles filesystem-v2 mode)\nlet template = '';\ntry {\n // Use getBinaryDataBuffer to properly read binary data from filesystem\n const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n template = binaryDataBuffer.toString('utf-8');\n} catch (e) {\n throw new Error('Failed to read template file: ' + e.message);\n}\n\nif (!template || template.trim().length === 0) {\n throw new Error('Template file is empty');\n}\n\n// Prepare replacement values with null checks\nconst replacements = {\n '{{NAME}}': data.personalInfo?.name || 'Your Name',\n '{{CONTACT_LINE}}': generateContactLine(data.personalInfo),\n '{{MASTER_COURSES}}': Array.isArray(data.masterCourses) && data.masterCourses.length > 0 ? data.masterCourses.join('; ') : 'N/A',\n '{{BACHELOR_COURSES}}': Array.isArray(data.bachelorCourses) && data.bachelorCourses.length > 0 ? data.bachelorCourses.join('; ') : 'N/A',\n '{{PROJECTS_SECTION}}': generateProjectsSection(data.matchedProjects),\n '{{SKILLS_SECTION}}': generateSkillsSection(data.skillsByCategory)\n};\n\n// Replace all placeholders in template\nlet html = template;\nfor (const [placeholder, value] of Object.entries(replacements)) {\n html = html.replace(new RegExp(placeholder.replace(/[{}]/g, '\\\\$&'), 'g'), value);\n}\n\nconst fileName = (data.personalInfo?.name || 'Resume').replace(/[^a-zA-Z0-9]/g, '_');\n\nreturn [{\n json: {\n ...data,\n resumeHtml: html,\n fileName: fileName\n },\n binary: {\n resume: await this.helpers.prepareBinaryData(\n Buffer.from(html, 'utf-8'),\n `${fileName}_Resume.html`,\n 'text/html; charset=utf-8'\n )\n }\n}];"
},
"id": "e0269d88-381e-4a02-91b2-06cc1690e6fc",
"name": "Generate Resume HTML",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7616,
8048
]
},
{
"parameters": {
"jsCode": "const input = $input.first();\n\nlet htmlContent = '';\n\n// Option 1: Get HTML from upstream Generate Resume HTML node (json.resumeHtml)\nif (input.json.resumeHtml) {\n htmlContent = input.json.resumeHtml;\n}\n// Option 2: Get HTML from binary data (binary.resume)\nelse if (input.binary && input.binary.resume) {\n htmlContent = Buffer.from(input.binary.resume.data, 'base64').toString('utf-8');\n}\nelse {\n throw new Error('No HTML content found from upstream node');\n}\n\n// Fixed page settings for resume\nconst pageSize = 'Letter';\nconst orientation = 'portrait';\n\n// Use fileName from upstream or generate default\nconst fileName = input.json.fileName || `resume_${Date.now()}`;\n\nreturn [{\n json: {\n htmlContent: htmlContent,\n pageSize: pageSize,\n orientation: orientation,\n fileName: fileName\n }\n}];"
},
"id": "52a27bd2-3dbb-4e8a-ace8-1d562dd34d81",
"name": "Process Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7840,
8048
]
},
{
"parameters": {
"jsCode": "// SIMPLIFIED: Page spacing is now controlled by template file\n// This node only prepares HTML for Gotenberg (requires file named 'index.html')\n\nconst data = $input.first().json;\nconst htmlContent = data.htmlContent;\n\n// No need to inject any styles - template already has @page and .page padding\n// Just pass through the HTML content as-is\nconst finalHtml = htmlContent;\n\nreturn [{\n json: {\n htmlContent: finalHtml,\n pageSize: data.pageSize,\n orientation: data.orientation,\n fileName: data.fileName,\n info: 'Page spacing controlled by template file'\n },\n binary: {\n htmlFile: {\n data: Buffer.from(finalHtml).toString('base64'),\n mimeType: 'text/html',\n fileName: 'index.html'\n }\n }\n}];"
},
"id": "98862e9e-458d-48cd-b842-a7d4cac9e4cb",
"name": "Convert to PDF",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8064,
8048
]
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:3000/forms/chromium/convert/html",
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "files",
"inputDataFieldName": "htmlFile"
},
{
"name": "paperWidth",
"value": "8.5"
},
{
"name": "paperHeight",
"value": "11"
},
{
"name": "landscape",
"value": "false"
},
{
"name": "marginTop",
"value": "0"
},
{
"name": "marginBottom",
"value": "0"
},
{
"name": "marginLeft",
"value": "0"
},
{
"name": "marginRight",
"value": "0"
},
{
"name": "printBackground",
"value": "true"
},
{
"name": "scale",
"value": "1"
},
{
"name": "preferCssPageSize",
"value": "true"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"id": "d888e745-b8c4-4ebd-a94c-3ea7772ab151",
"name": "Gotenberg PDF",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
8288,
8048
],
"notesInFlow": true,
"notes": "Using Gotenberg (Docker: gotenberg/gotenberg:8). Run: docker run -p 3000:3000 gotenberg/gotenberg:8"
},
{
"parameters": {
"jsCode": "const input = $input.first();\nconst previousData = $('Convert to PDF').first().json;\n\nif (!input.binary) {\n throw new Error('No binary data received from conversion service');\n}\n\n// Gotenberg returns PDF in 'data' key, not 'htmlFile'\nconst pdfBinary = input.binary.data;\nif (!pdfBinary) {\n throw new Error('No PDF data found. Available keys: ' + Object.keys(input.binary).join(', '));\n}\n\nconst fileName = previousData.fileName || 'converted';\n\nreturn [{\n json: {\n success: true,\n fileName: `${fileName}.pdf`,\n pageSize: previousData.pageSize,\n orientation: previousData.orientation\n },\n binary: {\n pdf: {\n ...pdfBinary,\n fileName: `${fileName}.pdf`,\n mimeType: 'application/pdf'\n }\n }\n}];"
},
"id": "95008bfd-3a60-4570-99a5-93ca50995904",
"name": "Rename PDF File",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8512,
8048
]
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-5-20250929",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"maxTokensToSample": 2000,
"temperature": 0.3
}
},
"id": "c22a9064-67de-479f-b1d3-28efa15a3d76",
"name": "Claude - JD Analysis6",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
5296,
8208
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-5-20250929",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"maxTokensToSample": 2000,
"temperature": 0.3
}
},
"id": "b3fc50ac-f030-4d8c-b9be-0e33f1795c12",
"name": "Claude - JD Analysis1",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
5872,
8752
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-5-20250929",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"maxTokensToSample": 2000,
"temperature": 0.3
}
},
"id": "c34c7389-0d27-4e39-a27a-7f4a08113059",
"name": "Claude - JD Analysis2",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
5296,
8752
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-5-20250929",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"maxTokensToSample": 2000,
"temperature": 0.3
}
},
"id": "11dce675-44f2-41d7-92ee-81b7af342857",
"name": "Claude - JD Analysis3",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
6224,
8352
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-5-20250929",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"maxTokensToSample": 2000,
"temperature": 0.3
}
},
"id": "a4f9fa59-f93f-489a-a448-1dc622f8fdd3",
"name": "Claude - JD Analysis4",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
5296,
7696
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Form Trigger": {
"main": [
[
{
"node": "AI Agent - Analyze JD",
"type": "main",
"index": 0
}
]
]
},
"Claude - JD Analysis": {
"ai_languageModel": [
[
{
"node": "AI Agent - Analyze JD",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"AI Agent - Analyze JD": {
"main": [
[
{
"node": "Parse JD Analysis",
"type": "main",
"index": 0
}
]
]
},
"Parse JD Analysis": {
"main": [
[
{
"node": "Read Master Courses File",
"type": "main",
"index": 0
},
{
"node": "Read Bachelor Courses File",
"type": "main",
"index": 0
},
{
"node": "Read Skills File",
"type": "main",
"index": 0
},
{
"node": "Read Project Files",
"type": "main",
"index": 0
}
]
]
},
"Read Master Courses File": {
"main": [
[
{
"node": "Process Master Courses",
"type": "main",
"index": 0
}
]
]
},
"Process Master Courses": {
"main": [
[
{
"node": "AI - Extract Master Courses",
"type": "main",
"index": 0
}
]
]
},
"AI - Extract Master Courses": {
"main": [
[
{
"node": "Parse Master Courses",
"type": "main",
"index": 0
}
]
]
},
"Parse Master Courses": {
"main": [
[
{
"node": "Merge Courses",
"type": "main",
"index": 0
}
]
]
},
"Read Bachelor Courses File": {
"main": [
[
{
"node": "Process Bachelor Courses",
"type": "main",
"index": 0
}
]
]
},
"Process Bachelor Courses": {
"main": [
[
{
"node": "AI - Extract Bachelor Courses",
"type": "main",
"index": 0
}
]
]
},
"AI - Extract Bachelor Courses": {
"main": [
[
{
"node": "Parse Bachelor Courses",
"type": "main",
"index": 0
}
]
]
},
"Parse Bachelor Courses": {
"main": [
[
{
"node": "Merge Courses",
"type": "main",
"index": 1
}
]
]
},
"Read Skills File": {
"main": [
[
{
"node": "Process Skills",
"type": "main",
"index": 0
}
]
]
},
"Process Skills": {
"main": [
[
{
"node": "AI - Extract Skills",
"type": "main",
"index": 0
}
]
]
},
"AI - Extract Skills": {
"main": [
[
{
"node": "Parse Skills",
"type": "main",
"index": 0
}
]
]
},
"Parse Skills": {
"main": [
[
{
"node": "Merge Skills & Projects",
"type": "main",
"index": 0
}
]
]
},
"Read Project Files": {
"main": [
[
{
"node": "Process Projects",
"type": "main",
"index": 0
}
]
]
},
"Process Projects": {
"main": [
[
{
"node": "AI - Select Top 3 Projects",
"type": "main",
"index": 0
}
]
]
},
"AI - Select Top 3 Projects": {
"main": [
[
{
"node": "Parse Selected Projects",
"type": "main",
"index": 0
}
]
]
},
"Parse Selected Projects": {
"main": [
[
{
"node": "AI - Write Impact for Project",
"type": "main",
"index": 0
}
]
]
},
"AI - Write Impact for Project": {
"main": [
[
{
"node": "Parse Impact Sentence",
"type": "main",
"index": 0
}
]
]
},
"Parse Impact Sentence": {
"main": [
[
{
"node": "Aggregate Project Results",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Project Results": {
"main": [
[
{
"node": "Merge Skills & Projects",
"type": "main",
"index": 1
}
]
]
},
"Merge Courses": {
"main": [
[
{
"node": "Merge All Results",
"type": "main",
"index": 0
}
]
]
},
"Merge Skills & Projects": {
"main": [
[
{
"node": "Merge All Results",
"type": "main",
"index": 1
}
]
]
},
"Merge All Results": {
"main": [
[
{
"node": "Combine All Data",
"type": "main",
"index": 0
}
]
]
},
"Combine All Data": {
"main": [
[
{
"node": "Read Template File",
"type": "main",
"index": 0
}
]
]
},
"Read Template File": {
"main": [
[
{
"node": "Generate Resume HTML",
"type": "main",
"index": 0
}
]
]
},
"Generate Resume HTML": {
"main": [
[
{
"node": "Process Input",
"type": "main",
"index": 0
}
]
]
},
"Process Input": {
"main": [
[
{
"node": "Convert to PDF",
"type": "main",
"index": 0
}
]
]
},
"Convert to PDF": {
"main": [
[
{
"node": "Gotenberg PDF",
"type": "main",
"index": 0
}
]
]
},
"Gotenberg PDF": {
"main": [
[
{
"node": "Rename PDF File",
"type": "main",
"index": 0
}
]
]
},
"Claude - JD Analysis6": {
"ai_languageModel": [
[
{
"node": "AI - Extract Bachelor Courses",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Claude - JD Analysis1": {
"ai_languageModel": [
[
{
"node": "AI - Write Impact for Project",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Claude - JD Analysis2": {
"ai_languageModel": [
[
{
"node": "AI - Select Top 3 Projects",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Claude - JD Analysis3": {
"ai_languageModel": [
[
{
"node": "AI - Extract Skills",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Claude - JD Analysis4": {
"ai_languageModel": [
[
{
"node": "AI - Extract Master Courses",
"type": "ai_languageModel",
"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.
anthropicApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Jd-Resume-Generator. Uses formTrigger, lmChatAnthropic, agent, readWriteFile. Event-driven trigger; 38 nodes.
Source: https://github.com/sguan119/job-automation-toolkit/blob/96b26a0403ad778d4ce144571fa5f4f0fd3db0d4/n8n-workflows/jd-resume-generator.json — 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.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This workflow is designed for marketers, founders, agencies, and product teams who want to understand how real customers talk about a product category, market, or problem space.
This workflow streamlines academic paper development through a multi-agent AI architecture that collects references, drafts individual sections autonomously, compiles the manuscript, and exports a pro
This n8n workflow provides a secure, enterprise-grade response system for AWS IAM access key compromises with built-in form submission and human approval mechanisms. When an AWS access key is suspecte
This workflow is designed for marketers, founders, agencies, and content teams who want to generate static ad creatives faster from minimal brand input.