DEV Community

Alex Chen
Alex Chen

Posted on

Job Search Automation: How I'd Build It Today

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)