All articles
Build Guides

Build a Job-Application Autofill Browser Extension with Local LLM

FDE Coach EditorialAugust 27, 202611 min read

What We’re Building

We’re building a Chrome extension that reads a job description directly from the active tab, cross-references it against your resume (stored locally as structured JSON), and uses a local Llama 3 model via Ollama to map your experience to the application form fields. No API keys, no cloud costs, no data leaving your machine.

Feature list:

  • One-click job description parsing from any Greenhouse, Lever, or Workday page.
  • Structured resume ingestion that converts your PDF/JSON resume into a vector-friendly schema.
  • Local LLM reasoning that decides which bullet point maps to “Years of Python experience” or “Describe a time you led a project.”
  • Autofill injection that populates text inputs, textareas, and select dropdowns directly in the DOM.
  • Confidence highlighting that color-codes fields green (high confidence), yellow (needs review), and red (manual input required).

Architecture Overview

The extension’s popup triggers a content script that scrapes the current page’s job description. That text, along with your pre-processed resume JSON, is sent to a local Ollama endpoint. The LLM returns a structured mapping of form field labels to your most relevant experience. The content script then walks the DOM, matches labels, and fills the inputs.

Prerequisites

Everything here is free and runs locally.

  • Chrome or any Chromium browserDownload Chrome
  • Ollamaollama.com/download. We’ll use Llama 3 8B: ollama pull llama3
  • Node.js 20+ – only needed if you want to pre-process a PDF resume with a script. nodejs.org
  • A text editor – VS Code, Cursor, whatever you have.

Verify Ollama is running:

ollama serve
# In another terminal:
ollama run llama3 "Hello, confirm you are running locally."

Step 1: Bootstrap the Chrome Extension

Create a project folder:

mkdir autofill-extension && cd autofill-extension
mkdir icons popup scripts

manifest.json (Manifest V3):

{
  "manifest_version": 3,
  "name": "Job Autofill with Local LLM",
  "version": "1.0.0",
  "description": "Parses job descriptions and autofills forms using your resume and a local Llama 3 model.",
  "permissions": ["activeTab", "scripting", "storage"],
  "host_permissions": ["http://localhost:11434/*"],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["scripts/content.js"],
      "run_at": "document_idle"
    }
  ]
}

Drop any PNG icons into icons/ or generate solid-color placeholders.

Step 2: Set Up Local LLM with Ollama

Pull the model:

ollama pull llama3

Test the API endpoint the extension will call:

curl http://localhost:11434/api/generate -d '{
  "model": "llama3",
  "prompt": "Map the following form field to a resume bullet: Field: Years of Python experience. Resume: Built Django APIs for 4 years.",
  "stream": false
}'

We’ll use this exact endpoint from the extension’s background logic. No streaming needed—we want a single JSON response.

Step 3: Build the Resume Ingestion Pipeline

We need your resume in a structured format the LLM can reason over. If you have a PDF, use a free local script to extract text and chunk it into labeled sections.

Create scripts/ingest-resume.js:

// Run once: node scripts/ingest-resume.js > resume.json
const fs = require('fs');
// For PDF extraction, use pdf-parse (npm install pdf-parse)
const pdfParse = require('pdf-parse');

async function ingest(pdfPath) {
  const dataBuffer = fs.readFileSync(pdfPath);
  const data = await pdfParse(dataBuffer);
  const text = data.text;

  // Simple section splitter—customize to your resume headings
  const sections = text.split(/\n(?=[A-Z][A-Za-z\s]+\n)/);
  const structured = {
    full_text: text,
    sections: sections.map(s => s.trim()).filter(s => s.length > 20)
  };

  console.log(JSON.stringify(structured, null, 2));
}

ingest('./my-resume.pdf');

Run it:

npm init -y && npm install pdf-parse
node scripts/ingest-resume.js > resume.json

Load resume.json into Chrome storage. In scripts/background.js (we’ll create this next), add a one-time import:

chrome.runtime.onInstalled.addListener(async () => {
  const resume = await fetch(chrome.runtime.getURL('resume.json')).then(r => r.json());
  await chrome.storage.local.set({ resume });
});

Update manifest.json to include "background": { "service_worker": "scripts/background.js" } and add resume.json to web_accessible_resources.

Step 4: Implement the Content Script for DOM Scraping

The content script needs to extract the job description from the page. Most ATS platforms have consistent selectors.

scripts/content.js (partial):

function extractJobDescription() {
  // Try common selectors for Greenhouse, Lever, Workday, etc.
  const selectors = [
    '[data-testid="job-description"]',
    '.job-description',
    '#job-description',
    '[class*="description"]',
    'section[class*="description"]'
  ];

  for (const sel of selectors) {
    const el = document.querySelector(sel);
    if (el && el.textContent.trim().length > 100) {
      return el.textContent.trim();
    }
  }

  // Fallback: grab the largest text block on the page
  const bodyText = document.body.innerText;
  const chunks = bodyText.split('\n').filter(l => l.trim().length > 50);
  return chunks.join('\n');
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'scrape') {
    const jd = extractJobDescription();
    sendResponse({ jobDescription: jd });
  }
  if (request.action === 'autofill') {
    applyMappings(request.mappings);
    sendResponse({ success: true });
  }
  return true; // keep channel open for async
});

Step 5: Build the Autofill Mapping Engine

This is the core. We send the job description + resume to Ollama and ask it to return a JSON mapping of field labels to values.

scripts/llm.js (loaded by the popup or background):

async function getFieldMappings(jobDescription, resume) {
  const prompt = `You are a precise job application assistant. Given the job description and my resume below, identify every form field likely present on the application page. Return ONLY a valid JSON object where keys are the exact field labels (e.g., "Years of Python experience", "LinkedIn URL", "Describe a time you led a project") and values are the best answer extracted or inferred from the resume. For dropdowns, return the option text exactly as it should appear. If you are unsure, set the value to "__MANUAL__".

Resume:
${resume.full_text}

Job Description:
${jobDescription}

JSON:`;

  const response = await fetch('http://localhost:11434/api/generate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'llama3',
      prompt,
      stream: false,
      format: 'json'
    })
  });

  const data = await response.json();
  // Ollama returns the generated text; parse the JSON from it
  const jsonStr = data.response.trim();
  return JSON.parse(jsonStr);
}

Llama 3’s JSON mode (format: 'json') ensures we get parseable output. The prompt explicitly constrains the response shape.

Step 6: Wire Up the Popup UI

popup/popup.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { width: 300px; padding: 16px; font-family: system-ui; }
    button { width: 100%; padding: 10px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
    button:disabled { opacity: 0.5; cursor: not-allowed; }
    #status { margin-top: 12px; font-size: 13px; }
    .confidence-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; }
    .green { background: #22c55e; }
    .yellow { background: #eab308; }
    .red { background: #ef4444; }
  </style>
</head>
<body>
  <button id="autofill-btn">Autofill This Application</button>
  <div id="status"></div>
  <script src="popup.js"></script>
</body>
</html>

popup/popup.js:

document.getElementById('autofill-btn').addEventListener('click', async () => {
  const btn = document.getElementById('autofill-btn');
  const status = document.getElementById('status');
  btn.disabled = true;
  status.innerHTML = 'Scraping job description...';

  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  // Step 1: Scrape JD
  const scrapeResult = await chrome.tabs.sendMessage(tab.id, { action: 'scrape' });
  if (!scrapeResult || !scrapeResult.jobDescription) {
    status.innerHTML = '❌ Could not find job description on this page.';
    btn.disabled = false;
    return;
  }

  status.innerHTML = 'Loading resume and calling local LLM...';

  // Step 2: Get resume from storage
  const { resume } = await chrome.storage.local.get('resume');
  if (!resume) {
    status.innerHTML = '❌ No resume found. Load resume.json first.';
    btn.disabled = false;
    return;
  }

  // Step 3: Get mappings from Ollama
  try {
    const mappings = await getFieldMappings(scrapeResult.jobDescription, resume);
    status.innerHTML = 'LLM mapping complete. Injecting into form...';

    // Step 4: Send mappings to content script for injection
    await chrome.tabs.sendMessage(tab.id, { action: 'autofill', mappings });

    const manualCount = Object.values(mappings).filter(v => v === '__MANUAL__').length;
    const total = Object.keys(mappings).length;
    status.innerHTML = `✅ Autofilled ${total - manualCount}/${total} fields. <br><span class="confidence-dot green"></span> High: ${total - manualCount} <br><span class="confidence-dot red"></span> Manual: ${manualCount}`;
  } catch (e) {
    status.innerHTML = '❌ LLM call failed. Is Ollama running on port 11434?';
  }

  btn.disabled = false;
});

Step 7: Testing the Full Loop

  1. Load the extension: Go to chrome://extensions, enable “Developer mode,” click “Load unpacked,” and select your autofill-extension folder.
  2. Ensure Ollama is running: ollama serve in a terminal.
  3. Navigate to a job posting on Greenhouse or Lever. Click the extension icon and hit “Autofill This Application.”
  4. Watch the console (right-click > Inspect > Console on the job page) for any content script errors.

If the LLM mapping returns __MANUAL__ for many fields, your resume JSON likely lacks the relevant sections. Re-run the ingestion script with a more detailed PDF.

Extensions and Next Steps

  • Multi-page form handling: Some applications span 3-5 pages. Extend the content script to detect “Next” buttons and inject mappings progressively.
  • Resume vector store: Instead of passing the full resume text every time, chunk it and store embeddings locally with Transformers.js for semantic retrieval—reducing prompt size and improving accuracy.
  • Playwright integration: For one-click application submission, use Playwright to control a headless browser. See our guide on building a multi-agent research assistant with OpenRouter and Playwright for patterns on orchestrating browser automation with LLM decision-making.
  • ATS-specific parsers: Write custom extractors for Greenhouse, Lever, and Workday that pull structured fields (location, seniority, required skills) directly rather than relying on the LLM to infer them from raw text.
  • Confidence scoring: Have the LLM return a confidence score (0-1) per mapping and use it to drive the green/yellow/red UI.
  • Profile switching: Store multiple resumes (e.g., “Engineering Manager,” “Senior IC”) and let the popup choose which to use.

If you’re interested in deeper LLM pipeline patterns, check out our OCR document extraction pipeline guide for handling job descriptions locked behind images or copy-paste blockers.

Common Pitfalls

  • Ollama CORS errors: Chrome extensions can call localhost directly, but ensure host_permissions includes http://localhost:11434/* in manifest.json.
  • Content script not injected: The matches: ["<all_urls>"] pattern covers most pages, but some ATS portals use iframes. You may need "all_frames": true in the content script declaration.
  • LLM returns invalid JSON: Even with format: 'json', Llama 3 occasionally wraps the JSON in markdown fences. Strip them: jsonStr.replace(/```json|```/g, '').trim().
  • Resume too large: Llama 3’s context window is 8K tokens. If your resume + JD exceed this, the LLM will truncate. Chunk the resume and use only the most relevant sections (see Extensions above).
  • Field label mismatch: The LLM guesses field labels from the JD, but the actual DOM labels may differ. Implement fuzzy matching (Levenshtein distance) in the content script’s injection logic.
  • Dropdown options: The LLM may return a value not present in the <select> options. Always validate against the actual option list before setting select.value.

FAQ

Q: Does this work on any job board? A: It works best on structured ATS platforms (Greenhouse, Lever, Workday). For custom forms, the LLM’s field-label guessing is less reliable. You can improve hit rate by fine-tuning the extraction prompt with examples from the target site.

Q: Can I use a different local model? A: Absolutely. Swap llama3 for mistral, phi3, or any model Ollama supports. Smaller models (3B-7B) are faster but may produce less accurate mappings. Test with your resume to find the sweet spot.

Q: Is my resume data safe? A: Yes. Everything runs locally—the Chrome extension, Ollama, and your resume file. No data is sent to external servers.

Q: How do I handle file upload fields? A: The extension can’t programmatically set file inputs for security reasons. Mark those fields as __MANUAL__ and handle them yourself. You can pre-populate a file picker prompt, but the user must confirm.

Q: What if the job description is behind a login? A: The extension runs in the context of your already-authenticated browser session. If you can see the job description, the content script can scrape it.

Q: How do I become proficient at building these LLM-integrated workflows professionally? A: This pattern—local inference, DOM manipulation, structured extraction—is exactly the kind of work Forward Deployed Engineers ship daily. If you want to master the full stack from browser APIs to LLM orchestration, FDE Coach offers project-based training that mirrors real enterprise deployments.

Q: Can I automate the entire application submission? A: You can extend this with Playwright to click “Submit,” but be thoughtful about automation ethics. Many companies view fully automated applications as spam. Use the autofill as a productivity tool, not a bot.

Q: What if Ollama is slow on my machine? A: Llama 3 8B runs comfortably on any machine with 16GB RAM and a modern CPU. If you’re on lower specs, try llama3:latest (the 3B variant) or use quantization: ollama pull llama3:8b-instruct-q4_K_M.

#browser-extension#local-llm#automation#privacy

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now