Build an AI Autofill Extension: Playwright + Gemini Reads Your Resume
What We're Building
We're building a browser extension that reads a resume PDF, parses any job application form in the browser, and uses a free LLM to intelligently fill in the fields. The core loop: you click "Autofill", the extension extracts all form fields from the active tab, sends them alongside your parsed resume to Google Gemini, receives a mapping, and fills everything in. No copy-pasting, no manual data entry.
Feature List
- Drag-and-drop resume PDF upload into the extension popup
- Resume parsing (text extraction) via a local Node.js companion service
- Dynamic form-field extraction from any web page (inputs, selects, textareas)
- Semantic field matching using Gemini’s free tier
- One-click autofill directly into the live page
- Lightweight Chrome Extension Manifest V3 architecture
Architecture Overview
We have three moving parts: the browser extension (popup + content script), a local Node.js service that parses PDFs, and the Gemini API. The extension popup handles user interaction. When you drop a resume, it sends the file to the local service, which returns raw text. The content script is injected into the job application tab to scrape all form fields. The popup sends both the resume text and the field list to Gemini, gets back a key-value mapping, and then messages the content script to populate the DOM.
Prerequisites & Free Tools
Everything here is free-tier or open-source. No credit card required to start.
- Node.js 20+ – Download
- Google Gemini API Key – Free tier gives you 60 requests per minute. Sign up at Google AI Studio.
- Playwright – We'll use
playwrightto launch a headless browser for scraping forms if needed, but the extension itself handles live pages. - pdf-parse – npm package that extracts text from PDFs (free, open-source).
- Chrome/Chromium – For loading the unpacked extension.
Step 1: Bootstrapping the Extension
Create a new directory autofill-extension. Inside, create a manifest.json for Manifest V3:
{
"manifest_version": 3,
"name": "Job App Autofill",
"version": "1.0.0",
"description": "Autofill job applications using your resume and Gemini.",
"permissions": ["activeTab", "storage", "scripting"],
"host_permissions": ["http://localhost:3000/*"],
"action": {
"default_popup": "popup.html",
"default_title": "Autofill"
}
}
We need host_permissions for localhost because the extension will talk to our local resume parser service. Create popup.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { width: 320px; padding: 16px; font-family: system-ui; }
#dropzone { border: 2px dashed #ccc; padding: 20px; text-align: center; margin-bottom: 12px; }
button { width: 100%; padding: 10px; background: #1a73e8; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:disabled { opacity: 0.5; }
#status { margin-top: 8px; font-size: 13px; color: #555; }
</style>
</head>
<body>
<div id="dropzone">Drop resume PDF here</div>
<button id="autofill-btn" disabled>Autofill Current Page</button>
<div id="status"></div>
<script src="popup.js"></script>
</body>
</html>
Now popup.js – this handles file drop, stores resume text, and triggers autofill:
const dropzone = document.getElementById('dropzone');
const autofillBtn = document.getElementById('autofill-btn');
const status = document.getElementById('status');
let resumeText = '';
dropzone.addEventListener('dragover', (e) => { e.preventDefault(); });
dropzone.addEventListener('drop', async (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (!file || file.type !== 'application/pdf') {
status.textContent = 'Please drop a PDF file.';
return;
}
status.textContent = 'Parsing resume...';
const formData = new FormData();
formData.append('resume', file);
try {
const res = await fetch('http://localhost:3000/parse', { method: 'POST', body: formData });
const data = await res.json();
resumeText = data.text;
status.textContent = 'Resume parsed successfully.';
autofillBtn.disabled = false;
} catch (err) {
status.textContent = 'Error parsing resume. Is the local service running?';
}
});
autofillBtn.addEventListener('click', async () => {
status.textContent = 'Extracting form fields...';
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, { action: 'extractFields' }, async (fields) => {
if (chrome.runtime.lastError || !fields) {
status.textContent = 'Could not access page. Refresh and try again.';
return;
}
status.textContent = 'Matching fields with Gemini...';
const mapping = await getGeminiMapping(resumeText, fields);
chrome.tabs.sendMessage(tab.id, { action: 'fillFields', mapping });
status.textContent = 'Form filled!';
});
});
Step 2: The Resume Parser Service (Node.js)
In a separate directory resume-parser, initialize a Node project:
npm init -y
npm install express multer pdf-parse cors
Create server.js:
const express = require('express');
const multer = require('multer');
const pdfParse = require('pdf-parse');
const cors = require('cors');
const app = express();
const upload = multer();
app.use(cors());
app.post('/parse', upload.single('resume'), async (req, res) => {
try {
const data = await pdfParse(req.file.buffer);
res.json({ text: data.text });
} catch (err) {
res.status(500).json({ error: 'PDF parsing failed' });
}
});
app.listen(3000, () => console.log('Resume parser running on port 3000'));
Run it: node server.js. This is our local bridge between the binary PDF and the extension.
Step 3: Injecting the Content Script
Create content.js in the extension directory. This script extracts all form fields and fills them on command:
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'extractFields') {
const fields = [];
const inputs = document.querySelectorAll('input:not([type="hidden"]), select, textarea');
inputs.forEach((el, idx) => {
const label = findLabel(el);
fields.push({
id: el.id || el.name || `field-${idx}`,
tag: el.tagName.toLowerCase(),
type: el.type || '',
label: label,
placeholder: el.placeholder || '',
options: el.tagName === 'SELECT' ? Array.from(el.options).map(o => o.text) : []
});
});
sendResponse(fields);
} else if (request.action === 'fillFields') {
const mapping = request.mapping;
const inputs = document.querySelectorAll('input:not([type="hidden"]), select, textarea');
inputs.forEach((el, idx) => {
const key = el.id || el.name || `field-${idx}`;
if (mapping[key]) {
if (el.tagName === 'SELECT') {
const option = Array.from(el.options).find(o => o.text.toLowerCase() === mapping[key].toLowerCase());
if (option) el.value = option.value;
} else {
el.value = mapping[key];
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
}
});
function findLabel(el) {
if (el.labels && el.labels[0]) return el.labels[0].textContent.trim();
const id = el.id;
if (id) {
const label = document.querySelector(`label[for="${id}"]`);
if (label) return label.textContent.trim();
}
const parent = el.closest('label');
if (parent) return parent.textContent.trim();
return '';
}
Add this to manifest.json under "content_scripts":
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
Step 4: Orchestrating with Playwright
Wait – why Playwright? The extension works on live tabs, but what if you want to automate the entire pipeline headlessly for testing or batch applications? We’ll add a Playwright script that launches a browser with the extension loaded, navigates to a job posting, and triggers autofill programmatically. This is optional but powerful.
Create automate.js in the extension root:
npm init -y
npm install playwright
const { chromium } = require('playwright');
const path = require('path');
(async () => {
const extensionPath = path.join(__dirname);
const browser = await chromium.launch({
headless: false,
args: [`--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`]
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://boards.greenhouse.io/example/job/12345');
// Wait for extension to inject content script, then programmatically trigger autofill
// In a real scenario, you'd connect to the extension's popup logic via chrome.debugger or evaluate
await page.waitForTimeout(5000);
await browser.close();
})();
This script demonstrates how an FDE would embed the extension into an automated pipeline. For a deeper dive into shipping LLM features fast inside enterprise environments, see our case study on deploying an LLM feature in 5 days.
Step 5: The Gemini Integration
The magic happens in popup.js. We send the resume text and the extracted fields to Gemini and ask it to return a JSON mapping. Add this function to popup.js:
async function getGeminiMapping(resumeText, fields) {
const API_KEY = 'YOUR_GEMINI_API_KEY'; // Store securely in practice
const prompt = `
You are an expert form-filling assistant. Given a resume and a list of form fields from a job application, return a JSON object mapping field IDs to the correct value from the resume. If a value isn't in the resume, use "N/A". Preserve exact field IDs.
Resume:
${resumeText}
Fields:
${JSON.stringify(fields, null, 2)}
Return ONLY valid JSON, no markdown fences.
`;
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
})
});
const data = await res.json();
const raw = data.candidates[0].content.parts[0].text;
// Clean up in case Gemini wraps it in backticks
const jsonStr = raw.replace(/```json|```/g, '').trim();
return JSON.parse(jsonStr);
}
Important: In production, never hardcode API keys. Use Chrome storage or environment variables. The free Gemini tier is generous but rate-limited; implement exponential backoff if you hit limits.
How to Run It
- Start the resume parser:
cd resume-parser && node server.js - Load the extension: Go to
chrome://extensions, enable Developer mode, click "Load unpacked", select theautofill-extensionfolder. - Navigate to any job application page (Lever, Greenhouse, Workday).
- Click the extension icon, drop your resume PDF, wait for parsing confirmation, then click "Autofill Current Page".
- Watch the fields populate. For select dropdowns, the script matches option text case-insensitively.
The mental model here mirrors what an FDE does daily—gluing APIs, handling edge cases, and shipping a working prototype fast. If you're curious about that rhythm, our breakdown of an FDE’s weekly routine shows exactly how these patterns compound.
Extensions & Improvements
- Local LLM fallback: Swap Gemini for Ollama running Gemma 2 locally. Change the endpoint to
http://localhost:11434/api/generateand adjust the prompt format. This keeps everything offline. - Field confidence scoring: Have Gemini return a confidence score per field. Highlight fields in yellow where confidence < 0.8 so the user reviews them.
- Multi-resume support: Store multiple parsed resumes in Chrome storage and let users switch between them for different roles.
- Autosubmit after review: Add a final "Submit" button that clicks the form’s submit button only after user confirmation.
- Playwright CI pipeline: Run the Playwright script in GitHub Actions to test the extension against known job board forms daily, catching DOM changes early.
Building these extensions is exactly the kind of high-leverage work we explore in our piece on the highest-leverage FDE skills in the AI era. Prompt engineering, data modeling, and system integration are the new power trio.
Common Pitfalls
- CORS with localhost: The extension’s
host_permissionsmust includehttp://localhost:3000/*. Without it,fetchfrom the popup will fail silently. - Dynamic forms (React/Vue): Many modern job boards render inputs after hydration. Your content script may run before the fields exist. Use a
MutationObserverin the content script to detect when the form container appears, then extract fields. - Gemini output formatting: Occasionally Gemini wraps JSON in markdown fences despite the prompt. The cleanup regex handles most cases, but add a try/catch with a fallback prompt if parsing fails.
- Select dropdowns with non-matching text: Some dropdowns use internal codes. If the option text doesn’t match, fall back to fuzzy matching (e.g., Levenshtein distance) or skip.
- Large PDFs:
pdf-parseloads the entire file into memory. For multi-megabyte resumes, stream the upload and consider a size limit in multer.
FAQ
Q: Does this work on any job board? A: Yes, any page with standard HTML form elements. Single-page apps (React, Angular) may require a slight delay or MutationObserver to catch late-rendered fields.
Q: Is my resume data sent to Google? A: Yes, when using Gemini. If privacy is critical, swap in Ollama with a local model (Gemma 2, Llama 3) and keep everything on your machine.
Q: Why not use the built-in Chrome autofill? A: Chrome autofill matches on field name heuristics and stored profiles. It doesn’t parse your resume PDF or semantically understand that “most recent company” maps to “current employer.”
Q: Can I use this for applications that require login? A: Yes. The extension works on any tab you’re authenticated in. The content script runs in the page context and can interact with fields behind auth.
Q: What if Gemini returns the wrong value? A: Always review before submitting. The extension is an accelerator, not a final decision-maker. For complex fields, the confidence-scoring extension mentioned above is a solid safety net.
Q: How do I debug the content script?
A: Right-click the page → Inspect → Sources tab → Content scripts. Set breakpoints in content.js. The popup’s console is separate (right-click extension icon → Inspect popup).
Q: Can this be packaged as a full product? A: Absolutely. Add OAuth for multi-user resume storage, a cloud parser instead of localhost, and a settings page for API key management. The core architecture scales cleanly.
For those looking to push this further into agentic territory—where the extension not only fills forms but navigates multi-step applications—the patterns in building advanced agentic harnesses are directly applicable.
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