Job Search Automation: How I'd Build It Today
Job hunting is a full-time job. Here's how to automate the boring parts so you can focus on what matters.
The Problem
Manual job search:
1. Visit 10 job boards every day
2. Search same keywords on each
3. Read 50 listings to find 5 relevant ones
4. Copy-paste info into spreadsheet
5. Write custom cover letter for each
6. Track applications in a mess of tabs and notes
7. Forget which companies you already applied to
8. Miss follow-up deadlines
Time spent: 4+ hours/day
Applications sent: 5-10/day (most are poor fits)
Mental energy: Drained before any real prep
The Solution: Automated Job Hunter
// job-hunter.js — Your personal job search assistant
const puppeteer = require('puppeteer');
const { GoogleSpreadsheet } = require('google-spreadsheet');
// Configuration
const config = {
keywords: ['Node.js developer', 'Full Stack Engineer', 'Backend Developer'],
locations: ['Remote', 'San Francisco', 'New York'],
excludeCompanies: ['Company I Hate', 'Spam Corp'],
minSalary: 120000,
mustHave: ['TypeScript', 'React', 'Node.js'],
niceToHave: ['PostgreSQL', 'Docker', 'AWS'],
maxAgeDays: 7, // Only jobs posted in last 7 days
};
// Step 1: Scrape Job Boards
class JobScraper {
constructor() {
this.browser = null;
this.results = [];
}
async init() {
this.browser = await puppeteer.launch({ headless: true });
}
async scrapeLinkedIn(keywords, location) {
const page = await this.browser.newPage();
// Note: LinkedIn has anti-scraping measures.
// For production: Use their API or a service like Indeed API.
// This is for demonstration:
await page.goto(`https://www.linkedin.com/jobs/search/?keywords=${encodeURIComponent(keywords)}&location=${encodeURIComponent(location)}`, {
waitUntil: 'networkidle2',
});
const jobs = await page.evaluate(() => {
const cards = document.querySelectorAll('.job-card-container');
return Array.from(cards).map(card => ({
title: card.querySelector('.base-search-card__title')?.textContent?.trim(),
company: card.querySelector('.job-card-container__company-name')?.textContent?.trim(),
location: card.querySelector('.job-card-container__location')?.textContent?.trim(),
postedTime: card.querySelector('time')?.getAttribute('datetime'),
url: card.querySelector('a.base-card__full-link')?.href,
}));
});
this.results.push(...jobs.filter(j => j.title && j.company));
await page.close();
return jobs;
}
// Similar methods for:
// - Indeed (easier to scrape)
// - Glassdoor
// - AngelList (Wellfound) — startup jobs
// - RemoteOK — remote-specific
// - Stack Overflow Jobs — tech-focused
}
// Step 2: Filter & Score Jobs
function scoreJob(job, config) {
let score = 0;
const reasons = [];
// Location match
if (config.locations.some(l =>
job.location?.toLowerCase().includes(l.toLowerCase()) || l === 'Remote'
)) {
score += 20;
reasons.push('Location match');
}
// Keyword match in title
for (const keyword of config.keywords) {
if (job.title?.toLowerCase().includes(keyword.toLowerCase())) {
score += 15;
reasons.push(`Keyword "${keyword}" in title`);
}
}
// Must-have skills check (requires fetching full description)
// This is done after initial scrape
// Company blacklist
if (config.excludeCompanies.includes(job.company)) {
score = -1;
reasons.push('Blacklisted company');
}
// Recency bonus
if (job.postedTime) {
const ageDays = (Date.now() - new Date(job.postedTime)) / 86400000;
if (ageDays < 1) score += 10; // Posted today
else if (ageDays < 3) score += 5; // This week
else if (ageDays > config.maxAgeDays) score = -1; // Too old
}
return { score, reasons, matched: score > 30 };
}
// Step 3: Track Applications
class ApplicationTracker {
constructor(sheetId) {
this.sheetId = sheetId;
}
async init() {
this.doc = new GoogleSpreadsheet(this.sheetId);
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_CLIENT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
this.sheet = doc.sheetsByIndex[0];
}
async addJob(job, score) {
await this.sheet.addRow({
date: new Date().toISOString().split('T')[0],
title: job.title,
company: job.company,
location: job.location,
url: job.url || '',
score: score.score,
status: 'New',
applied: 'No',
followUpDate: '', // Set to 7 days from now if applied
response: '',
notes: score.reasons.join('; '),
});
}
async getJobsToApply() {
const rows = await this.sheet.getRows();
return rows
.filter(row => row.status === 'New' && parseInt(row.score) >= 40)
.sort((a, b) => parseInt(b.score) - parseInt(a.score));
}
async markApplied(jobUrl, notes) {
const rows = await this.sheet.getRows();
const row = rows.find(r => r.url === jobUrl);
if (row) {
row.status = 'Applied';
row.applied = 'Yes';
row.appliedDate = new Date().toISOString().split('T')[0];
row.followUpDate = new Date(Date.now() + 7*86400000).toISOString().split('T')[0];
row.notes += ` | ${notes}`;
await row.save();
}
}
}
// Step 4: Generate Custom Cover Letter
async function generateCoverLetter(job, myInfo) {
// Use AI to generate personalized cover letter
const prompt = `
Write a concise, compelling cover letter for:
Position: ${job.title}
Company: ${job.company}
Job Description: ${job.description || '[fetch from URL]'}
My background:
- Name: ${myInfo.name}
- Experience: ${myInfo.experience} years as ${myInfo.currentRole}
- Key skills: ${myInfo.skills.join(', ')}
- Notable achievement: ${myInfo.achievement}
- Why interested: ${myInfo.whyInterested}
Requirements:
- Under 300 words
- Professional but not stiff
- Mention specific skills that match the job
- No generic fluff
- Include specific reference to the company
`;
// Call your preferred LLM API here
// const coverLetter = await callLLM(prompt);
// return coverLetter;
return prompt; // Placeholder
}
// Step 5: Follow-Up Manager
class FollowUpManager {
constructor(tracker) {
this.tracker = tracker;
}
async checkFollowUps() {
const rows = await this.tracker.sheet.getRows();
const today = new Date().toISOString().split('T')[0];
for (const row of rows) {
if (row.applied === 'Yes' && row.followUpDate === today) {
console.log(`⏰ Follow up needed: ${row.title} at ${row.company}`);
// Could send email notification, Slack message, etc.
}
}
}
}
// Orchestrate it all
async function main() {
const scraper = new JobScraper();
await scraper.init();
for (const keyword of config.keywords) {
for (const location of config.locations) {
console.log(`Searching: ${keyword} in ${location}`);
await scraper.scrapeLinkedIn(keyword, location);
// Add more job boards...
}
}
console.log(`Found ${scraper.results.length} raw results`);
// Score and filter
const scored = scraper.results
.map(job => ({ job, score: scoreJob(job, config) }))
.filter(({ score }) => score.matched)
.sort((a, b) => b.score.score - a.score.score);
console.log(`${scored.length} jobs scored above threshold`);
// Save to tracker
const tracker = new ApplicationTracker(process.env.SHEET_ID);
await tracker.init();
for (const { job, score } of scored.slice(0, 20)) {
await tracker.addJob(job, score);
}
console.log('Top 10 jobs:');
scored.slice(0, 10).forEach(({ job, score }, i) => {
console.log(` ${i + 1}. [${score.score}pts] ${job.title} at ${job.company}`);
console.log(` ${score.reasons.join(', ')}`);
});
}
// Run daily via cron:
// 0 8 * * * node job-hunter.js >> logs/job-hunter.log 2>&1
Bonus: Quick Apply Scripts
# Generate application template
generate_apply() {
local company=$1
local role=$2
cat << EOF
Subject: Application for $role - $company
Hi ${company} Hiring Team,
I'm writing to express my interest in the $role position.
[AI-generated custom paragraph about why this specific role excites me]
My relevant experience:
- [Auto-filled from my profile]
- [Auto-filled from my profile]
- [Auto-filled from my profile]
I've attached my resume and would love to discuss how I can contribute to ${company}'s team.
Best regards,
[My Name]
[My LinkedIn] | [My Portfolio] | [My GitHub]
EOF
}
# Usage: generate_apply "Stripe" "Senior Backend Engineer"
Tools That Help (No Code Required)
| Tool | What It Does | Cost |
|---|---|---|
| LinkedIn Easy Apply | One-click apply | Free |
| Huntr.io | Browser extension + tracker | $10-20/mo |
| JazzHR | Application tracking | Varies by employer |
| Google Alerts | Email you new job postings matching keywords | Free |
| IFTTT/Zapier | Auto-save jobs to spreadsheet | Free tier available |
What's your job hunting strategy? Manual or automated?
Follow @armorbreak for more career content.
Top comments (0)