cd /news/developer-tools/i-stopped-rewriting-my-resume-and-bu… · home topics developer-tools article
[ARTICLE · art-28284] src=pub.towardsai.net ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

I Stopped Rewriting my Resume and Built an Automated Tailoring Workflow in a Weekend

A data engineer built an automated resume tailoring workflow using n8n, Ollama, Google Docs, and Postgres after spending 45 minutes manually rewriting bullet points for a job application. The system reads a master resume, analyzes job descriptions, rewrites only relevant bullet points, saves tailored copies, and logs application history in about a minute at no cost.

read9 min publishedJun 15, 2026

Sometime last year I was applying for a Data Engineer role and spent almost 45 minutes rearranging the same three bullet points trying to make my experience sound more relevant. I submitted it. Never heard back. Then I opened a new job posting the next day and realized I had to do it all over again.

That’s when I started thinking I work with data pipelines for a living. Why am I doing this manually?

The tools that exist for this weren’t really cutting it for me. The AI ones are either locked behind a subscription or they rewrite your resume so aggressively it stops sounding like you. And none of them actually track what you applied to, what version you sent, how well it matched that data just disappears into the void.

So I built my own workflow using n8n, Ollama, Google Docs, and a Postgres database. It reads my resume, analyzes any job description I throw at it, rewrites only the bullet points that need changing, saves a tailored copy under the company name, and logs the whole thing so I can actually look back at my application history. Runs in about a minute. Costs nothing.

This post walks through exactly how I built it, and maybe more usefully the parts that completely broke on me and how I got past them.

Here’s the flow at a high level before we get into the weeds:

If n8n is new to you, the quickest way to get started is:

docker run -it --rm --name n8n -p 5678:5678 n8nio/n8n

Then go to http://localhost:5678 and you're in.

For official documentation, go here..

The first node is Read_Resume, a Google Docs node in get mode. You point it at your master resume with the document ID -that string of characters sitting in the middle of the URL when you have the doc open:

https://docs.google.com/document/d/THIS_PART_HERE/edit

Drop that into the node and every run will pull the freshest version of your resume automatically. One source of truth, nothing to sync manually.

One heads up here: you’ll need to set up Google OAuth2 credentials in Google Cloud Console before this node will work. It’s a bit of a setup process and there’s a good chance you’ll hit an error the first time I did, and I cover exactly what it is and how to fix it in the bugs section below.

Official documentation for OAuth setup here

The Job_Description node is just a Set node, basically a variable you update each time you run the workflow for a new role. Paste the full job description text into the Job Description field.

For my test run I used a real AI Data Engineer posting. It was heavy on Python, Parquet, Kubernetes, large-scale ML pipelines the kind of JD where if your resume doesn’t reflect that vocabulary back at them, it gets filtered out fast.

This node sends an HTTP POST to Ollama, which is running locally:

http://host.docker.internal:11434/api/generate

The prompt asks the model to return a structured JSON object containing the company name, a match percentage, missing skills, strong matches, and an array of rewritten bullet points each one with the original text, the new version, and a brief explanation of why the change helps.

The most important line in the whole prompt is this one: “Do not invent skills, metrics, or experiences.” I was pretty deliberate about including that because the whole point is to surface what’s already there, not fabricate things that aren’t.

Here’s a simplified version of the prompt and the JSON schema it targets:

You are an expert resume tailoring assistant.
Given the resume and job description below:1. Extract the hiring company name2. Calculate a semantic match percentage3. Identify strong matching skills4. Identify critical missing skills5. Rewrite ONLY the experience bullets that need improvement   → Target: overall match of AT LEAST 90%6. NEVER invent skills, metrics, or experiences
Return ONLY valid JSON:{  "company_name": "...",  "match_percentage": "72",  "missing_skills": ["Kubernetes", "Parquet"],  "strong_matches": ["Python", "ML pipelines"],  "rewritten_bullets": [    {      "section": "Work Experience",      "company": "Previous Employer",      "original": "Built data pipelines for reporting.",      "rewritten": "Designed and maintained production data pipelines processing terabyte-scale datasets for model training workflows.",      "reason": "Aligns with JD emphasis on large-scale pipeline engineering."    }  ]}

The model returns a raw string. This Code node turns it into something usable. It does three things:

const rawResponseString = $input.first().json.response;const parsedJson = JSON.parse(rawResponseString);
js
const googleDocRequests = [];
js
for (const r of parsedJson.rewritten_bullets) {  if (r.original && r.rewritten) {    googleDocRequests.push({      replaceAllText: {        containsText: {          text: r.original.trim(),          matchCase: true        },        replaceText: r.rewritten      }    });  }}

That array gets passed forward to the Google Docs step it’s literally the list of edits the doc will apply to itself.

The Replicate_Resume Google Drive node copies the master resume and names it after the company so for xAI it becomes Preethi_Kaluva_xAI. Your original never gets touched. Each application lives in its own file, clearly labeled.

This node calls the Google Docs Batch Update API on the new copy:

https://docs.googleapis.com/v1/documents/DOCUMENT_ID:batchUpdate

It passes the request array from Step 4 and the API does the find-and-replace directly in the document. No copy-paste, no manual editing.

A second, separate AI call reads the updated resume against the same JD and returns a single number, the new match score. This is the validation step. If the rewrites actually helped, the number should be higher than the original.

The prompt for this one is very intentionally stripped down: return only a raw integer between 0 and 100, nothing else. Getting clean output from a local model requires being pretty blunt about the format.

The Postgres node saves a record with everything

After a few weeks of running this, you start to see patterns. Which skills keep coming up as missing. Which roles you’re consistently a strong match for. It’s actually kind of useful data to have.

I want to be upfront that this didn’t go smoothly. These are the actual errors I hit, in roughly the order I hit them.

I wrote most of my initial prompt in Notes, then pasted it into n8n. The whole node immediately threw a syntax error.

Turns out Notes and most writing apps automatically replace straight quotes (") with the curly typographic ones (“ and ”). JavaScript inside n8n doesn't recognize those as string delimiters. It just sees invalid characters and stops.

The fix is tedious but simple: go through the expression block and manually replace every curly quote with a straight one. Also check that your newline characters are \n and not something weird that got introduced in the paste.

After setting up credentials in Google Cloud Console and clicking Connect in n8n, I got a screen telling me the app hadn’t completed Google’s verification process. Access denied.

What I didn’t know at the time is that new Google Cloud projects default to “Testing” mode, which meansonly accounts you’ve explicitly whitelisted can authenticateincluding your own. You’re locked out of your own app until you add yourself.

Fix: Google Cloud Console → APIs & Services → OAuth consent screen → Test users → add your email. Save, reconnect, done.

My Code node kept throwing “Code doesn’t return items properly” even when the logic was correct. This one stumped me for a bit.

n8n has a specific format it expects from Code nodes. You can’t return a plain value or a string it has to be an array of objects, each with a json key wrapping your data.

// this crashesreturn resume;
// this worksreturn [{ json: { updated_resume: resume } }];

Once you know it, it’s obvious. Before you know it, it’s maddening.

This one took the longest to diagnose and honestly frustrated me the most. The parse node kept throwing SyntaxError: Unexpected end of JSON input but only sometimes, not on every run.

What was happening: the model was running out of its output token budget in the middle of generating the JSON response. It would just stop mid-object, leaving me with something like:

{  "company_name": "xAI",  "match_percentage": "78",  "rewritten_bullets": [    {      "original": "Built data pipelines

…and nothing after that. The JSON parser sees that and gives up.

The tricky part is that Ollama doesn’t throw an error when this happens it just returns whatever it managed to generate before it hit the limit. So you only find out something went wrong when your parse step fails downstream.

Two things fixed it. First, I increased num_predict in the Ollama API call to give the model more room to finish. Second, I wrapped the parse in a try/catch so a truncated response doesn't bring the whole workflow down:

try {  const parsedJson = JSON.parse(rawResponseString);  // continue} catch (e) {  return [{ json: { error: "LLM response truncated", raw: rawResponseString } }];}

If you’re using a longer resume or a verbose JD, set num_predict generously. Better to give it too much room than to have it cut off halfway through.

It works. For a 7 billion parameter model running on a laptop, I was genuinely impressed at first. It understands the structure of a job description, picks up on the right keywords, and produces rewrites that are actually coherent.

That said I’d personally move to a bigger model pretty soon if I was doing this seriously. Sometimes it rewrites bullets that didn’t really need rewriting, which means I have to review the output before letting it auto-apply everything. And with very technical JDs it occasionally misses nuance in the terminology. Not deal-breaking, but noticeable.

For getting started and proving the concept, qwen2.5:7b is totally fine. Just know going in that you’ll want to sanity-check the rewrites rather than blindly trusting them.

My next natural step is to create web form trigger so I can paste a JD into a form instead of editing the Set node each time -that’s probably the most annoying part of the current setup. After that, a cover letter node using the same pattern, and eventually a small dashboard that visualizes match scores across all applications over time.

I built this mostly because the alternatives weren’t working for me. It’s not magic it won’t invent experience you don’t have or get you a job on its own. But it does take the 45-minute manual tailoring process down to about a minute, and it gives you an actual record of every application instead of just hoping you remember what you sent.

If you give this a try, I’d genuinely love to know what you change or run into. Do drop a comment.

Built with n8n · Ollama · qwen2.5:7b · Google Docs API · Postgres

I Stopped Rewriting my Resume and Built an Automated Tailoring Workflow in a Weekend was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.

── more in #developer-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-stopped-rewriting-…] indexed:0 read:9min 2026-06-15 ·