This workflow corresponds to n8n.io template #17300 — we link there as the canonical source.
This workflow follows the Gmail → HTTP Request 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": "resumepdf16live",
"name": "Tailor your Markdown resume to a pasted job description and email a PDF",
"tags": [],
"nodes": [
{
"id": "overview",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1072,
0
],
"parameters": {
"width": 748,
"height": 844,
"content": "## Resume tailor: JD + resume in, tailored PDF out\n\nPut a job description and your Markdown resume in the config node and click Test workflow. Get back a resume rewritten to match the role as a PDF, plus a match score, what changed, the gaps and how to close them, emailed to you.\n\n### How it works\nThe config node holds the job description and your resume in Markdown (both prefilled with dummy demo values). A direct Anthropic call, keyed from the config node, rewrites the resume to fit the job into structured fields: it reframes and reorders your real experience, mirrors the job's terminology for applicant tracking systems, scores the fit, and lists what changed, matched keywords, honest gaps and suggestions to close them. The resume renders through one fixed template, converts to PDF, and is emailed to you with the details.\n\n### Setup\n- Config node: paste the job description and your resume, and add your Anthropic key, PDFShift key and delivery email.\n- Add Gmail on the send node.\n- Click Test workflow to run it.\n\n### Truthful by design\nThe model only reframes what is in your pasted resume and flags gaps rather than inventing employers, titles, metrics or skills. Always review the PDF before sending.\n\n### Customization\nEdit the CSS in the render node to match your style, save the PDF to Google Drive, or accept the resume from a file instead of paste.\n\nBuilt by **nocode.expert** \u2014 done-for-you automation and AI workflows. https://nocode.expert"
},
"typeVersion": 1
},
{
"id": "section-input",
"name": "Section: Input",
"type": "n8n-nodes-base.stickyNote",
"position": [
-192,
336
],
"parameters": {
"color": 7,
"width": 568,
"height": 560,
"content": "## 1. JD + resume in config\nPut the job description and your Markdown resume in the config node, then click Test workflow. Dummy values are prefilled so it runs out of the box."
},
"typeVersion": 1
},
{
"id": "section-tailor",
"name": "Section: Tailor",
"type": "n8n-nodes-base.stickyNote",
"position": [
432,
336
],
"parameters": {
"color": 7,
"width": 464,
"height": 544,
"content": "## 2. Tailor with AI\nA direct Anthropic call (key in config, no credential) rewrites your real experience to match the JD, scores the fit, mirrors ATS keywords and lists gaps + suggestions."
},
"typeVersion": 1
},
{
"id": "section-pdf-send",
"name": "Section: PDF + send",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
336
],
"parameters": {
"color": 7,
"width": 816,
"height": 544,
"content": "## 3. Render PDF + email\nRender one fixed template, convert to PDF via PDFShift (key in config), and email it with the match score, changes, gaps and suggestions."
},
"typeVersion": 1
},
{
"id": "when-clicking-test-workflow",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-128,
624
],
"parameters": {},
"typeVersion": 1
},
{
"id": "set-config-jd-resume-keys-rules",
"name": "Set config: JD, resume, keys & rules",
"type": "n8n-nodes-base.set",
"position": [
208,
624
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a1",
"name": "jd",
"type": "string",
"value": "Senior Product Manager, Fintech Payments. You will own activation and retention for our merchant dashboard. Must have: 5+ years B2B SaaS product management, hypothesis-driven experimentation, SQL, roadmap ownership, strong cross-functional leadership. Nice to have: payments or fintech domain, compliance awareness."
},
{
"id": "a2",
"name": "resume",
"type": "string",
"value": "# Jane Doe\n## Senior Product Manager\nuser@example.com \u00b7 linkedin.com/in/janedoe \u00b7 Remote (India/US/UK)\n\n**Summary:** Senior PM with 8 years shipping B2B SaaS products end to end at Series A/B startups. Grew activation 40% through experimentation. Strong SQL and analytics.\n\n**Experience**\n- Acme (2021\u2013Present) \u2014 Senior PM. Led discovery-to-delivery, grew activation 40%, owned the roadmap.\n- Beta (2018\u20132021) \u2014 Product Manager. Owned the analytics suite, ran A/B experiments.\n\n**Skills:** roadmap, discovery, experimentation, SQL, analytics, stakeholder management\n**Education:** B.Tech, 2016"
},
{
"id": "a3",
"name": "target",
"type": "string",
"value": "Senior PM, PayCo"
},
{
"id": "a4",
"name": "rules",
"type": "string",
"value": "Only reframe, reorder and emphasise experience already present in the resume. Never invent employers, titles, dates, metrics or skills. Mirror the job's terminology where it genuinely applies (ATS). Concise, professional, reverse-chronological."
},
{
"id": "a5",
"name": "accent",
"type": "string",
"value": "#1a56db"
},
{
"id": "a6",
"name": "email",
"type": "string",
"value": "you@example.com"
},
{
"id": "a7",
"name": "anthropicKey",
"type": "string",
"value": ""
},
{
"id": "a8",
"name": "pdfApiKey",
"type": "string",
"value": ""
},
{
"id": "a9",
"name": "model",
"type": "string",
"value": "claude-haiku-4-5"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "build-tailoring-prompt",
"name": "Build tailoring prompt",
"type": "n8n-nodes-base.code",
"position": [
512,
624
],
"parameters": {
"jsCode": "const cfg = $('Set config: JD, resume, keys & rules').first().json;\nconst jd = cfg.jd || '';\nconst resumeText = cfg.resume || '';\nconst target = cfg.target || '';\nconst schema = { type: 'object', additionalProperties: false, required: ['name','title','contact','summary','experience','skills','education','match_score','key_changes','keywords_matched','gaps','suggestions'], properties: {\n name: { type: 'string' }, title: { type: 'string' }, contact: { type: 'string' }, summary: { type: 'string' },\n experience: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['role','company','dates','bullets'], properties: {\n role: { type: 'string' }, company: { type: 'string' }, dates: { type: 'string' }, bullets: { type: 'array', items: { type: 'string' } } } } },\n skills: { type: 'array', items: { type: 'string' } }, education: { type: 'array', items: { type: 'string' } },\n match_score: { type: 'integer' },\n key_changes: { type: 'array', items: { type: 'string' } }, keywords_matched: { type: 'array', items: { type: 'string' } },\n gaps: { type: 'array', items: { type: 'string' } }, suggestions: { type: 'array', items: { type: 'string' } } } };\nconst system = 'You are an expert resume writer and ATS specialist. Rewrite the candidate resume to fit the target job. HARD RULES: ' + cfg.rules;\nconst user = 'Return the structured fields. match_score = 0-100 honest fit of the candidate to this job BEFORE tailoring. key_changes = what you emphasised/reordered and why. keywords_matched = JD terms now genuinely reflected. gaps = JD requirements the candidate does not clearly meet (be honest). suggestions = concrete things the candidate could do or add (courses, projects, framing) to close the gaps.'\n + '\\n\\nTARGET ROLE/COMPANY: ' + (target || '(not specified)')\n + '\\n\\n=== TARGET JOB DESCRIPTION ===\\n' + jd\n + '\\n\\n=== CANDIDATE RESUME (Markdown, source of truth - add nothing not supported here) ===\\n' + resumeText;\nreturn [{ json: { body: {\n model: cfg.model || 'claude-haiku-4-5', max_tokens: 2500, system,\n messages: [{ role: 'user', content: user }],\n output_config: { format: { type: 'json_schema', schema } },\n} } }];"
},
"typeVersion": 2
},
{
"id": "tailor-resume-with-claude",
"name": "Tailor resume with Claude",
"type": "n8n-nodes-base.httpRequest",
"position": [
720,
624
],
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify($json.body) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $('Set config: JD, resume, keys & rules').first().json.anthropicKey }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "render-resume-html",
"name": "Render resume HTML",
"type": "n8n-nodes-base.code",
"position": [
1152,
624
],
"parameters": {
"jsCode": "const cfg = $('Set config: JD, resume, keys & rules').first().json;\nconst accent = cfg.accent || '#1a56db';\nconst blocks = $json.content || [];\nlet out = {};\ntry { out = JSON.parse((blocks.find((b) => b.type === 'text') || {}).text || '{}'); } catch (e) {}\nconst esc = (s) => String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\nconst exp = (out.experience || []).map((e) =>\n '<div class=\"job\"><div class=\"jobhead\"><span class=\"role\">' + esc(e.role) + '</span><span class=\"dates\">' + esc(e.dates) + '</span></div>' +\n '<div class=\"company\">' + esc(e.company) + '</div><ul>' + (e.bullets || []).map((b) => '<li>' + esc(b) + '</li>').join('') + '</ul></div>').join('');\nconst html =\n '<!doctype html><html><head><meta charset=\"utf-8\"><style>' +\n '*{box-sizing:border-box} body{font-family:Georgia,\\'Times New Roman\\',serif;color:#222;margin:0;padding:40px 48px;font-size:11pt;line-height:1.4}' +\n 'h1{font-size:22pt;margin:0;color:' + accent + '} .title{font-size:12pt;color:#555;margin:2px 0 6px} .contact{font-size:9.5pt;color:#666;margin-bottom:14px}' +\n 'h2{font-size:11pt;text-transform:uppercase;letter-spacing:.06em;color:' + accent + ';border-bottom:1.5px solid ' + accent + ';padding-bottom:2px;margin:16px 0 8px}' +\n '.job{margin-bottom:10px} .jobhead{display:flex;justify-content:space-between} .role{font-weight:bold} .dates{color:#777;font-size:9.5pt} .company{font-style:italic;color:#444;margin-bottom:3px}' +\n 'ul{margin:3px 0 0 18px;padding:0} li{margin:2px 0} .skills{font-size:10pt}</style></head><body>' +\n '<h1>' + esc(out.name) + '</h1><div class=\"title\">' + esc(out.title) + '</div><div class=\"contact\">' + esc(out.contact) + '</div>' +\n (out.summary ? '<h2>Summary</h2><div>' + esc(out.summary) + '</div>' : '') +\n '<h2>Experience</h2>' + exp +\n (out.skills && out.skills.length ? '<h2>Skills</h2><div class=\"skills\">' + out.skills.map(esc).join(' \u00b7 ') + '</div>' : '') +\n (out.education && out.education.length ? '<h2>Education</h2><ul>' + out.education.map((e) => '<li>' + esc(e) + '</li>').join('') + '</ul>' : '') +\n '</body></html>';\nconst listp = (a) => Array.isArray(a) && a.length ? '<ul>' + a.map((x) => '<li>' + esc(x) + '</li>').join('') + '</ul>' : '<p>none</p>';\nconst score = typeof out.match_score === 'number' ? out.match_score : null;\nconst detailsHtml =\n (score !== null ? '<p style=\"font-size:16px\"><strong>Match score:</strong> <span style=\"color:' + (score >= 75 ? '#0a0' : score >= 50 ? '#c80' : '#c00') + '\">' + score + '/100</span> (before tailoring)</p>' : '') +\n '<h3>What changed</h3>' + listp(out.key_changes) +\n '<h3>ATS keywords covered</h3>' + listp(out.keywords_matched) +\n '<h3 style=\"color:#a00\">Gaps vs the JD</h3>' + listp(out.gaps) +\n '<h3>Suggestions to close the gaps</h3>' + listp(out.suggestions);\nconst authHeader = 'Basic ' + Buffer.from('api:' + (cfg.pdfApiKey || '')).toString('base64');\nreturn [{ json: { html, detailsHtml, authHeader, deliverTo: cfg.email } }];"
},
"typeVersion": 2
},
{
"id": "convert-html-to-pdf",
"name": "Convert HTML to PDF",
"type": "n8n-nodes-base.httpRequest",
"position": [
1408,
624
],
"parameters": {
"url": "https://api.pdfshift.io/v3/convert/pdf",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"jsonBody": "={{ JSON.stringify({ source: $json.html, format: 'A4', margin: '0' }) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ $json.authHeader }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "email-the-tailored-pdf",
"name": "Email the tailored PDF",
"type": "n8n-nodes-base.gmail",
"position": [
1616,
624
],
"parameters": {
"sendTo": "={{ $('Render resume HTML').first().json.deliverTo }}",
"message": "=Attached is your resume tailored to the job.<br><br>{{ $('Render resume HTML').first().json.detailsHtml }}<br><br>Review before sending. Generated with nocode.expert.",
"options": {},
"subject": "Your tailored resume + match report"
},
"typeVersion": 2.1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "b98bb4a5-aa4b-4a3a-ac9f-43f7571b63bd",
"nodeGroups": [],
"connections": {
"Render resume HTML": {
"main": [
[
{
"node": "Convert HTML to PDF",
"type": "main",
"index": 0
}
]
]
},
"Convert HTML to PDF": {
"main": [
[
{
"node": "Email the tailored PDF",
"type": "main",
"index": 0
}
]
]
},
"Build tailoring prompt": {
"main": [
[
{
"node": "Tailor resume with Claude",
"type": "main",
"index": 0
}
]
]
},
"Tailor resume with Claude": {
"main": [
[
{
"node": "Render resume HTML",
"type": "main",
"index": 0
}
]
]
},
"When clicking Test workflow": {
"main": [
[
{
"node": "Set config: JD, resume, keys & rules",
"type": "main",
"index": 0
}
]
]
},
"Set config: JD, resume, keys & rules": {
"main": [
[
{
"node": "Build tailoring prompt",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs manually to tailor a Markdown resume to a pasted job description using Anthropic Claude, converts the tailored resume HTML to a PDF with PDFShift, and emails the PDF along with a match score and change/gap report via Gmail. Runs when you click Test workflow in…
Source: https://n8n.io/workflows/17300/ — 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.
Gmail Workflow. Uses gmail, googleCalendar, executeWorkflowTrigger, whatsApp. Event-driven trigger; 61 nodes.
This workflow ingests proof-of-delivery and completion documents from Gmail or a webhook, extracts key fields with an OpenRouter vision model, reconciles them against Google Sheets dispatch data, arch
Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.
Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.
Echo Brand Voice Analysis (Processor) - TASK-074 Dec 10 Fix. Uses formTrigger, httpRequest, executeWorkflowTrigger, moveBinaryData. Event-driven trigger; 40 nodes.