{
  "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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\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(' &nbsp;\u00b7&nbsp; ') + '</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
          }
        ]
      ]
    }
  }
}