How to Automate New Grad Job Applications with GitHub Actions and Simplify API in 2025

How to Automate New Grad Job Applications with GitHub Actions and Simplify API in 2025

If you're a new grad hunting for software engineering roles, you've probably discovered the SimplifyJobs/New-Grad-Positions repository—a continuously updated list of 300+ entry-level positions. But manually checking this repo daily and filling out applications is tedious. This guide shows you how to build a GitHub Actions automation pipeline that monitors new job postings and streamlines your application process.

Why Automate Your Job Search Pipeline

The SimplifyJobs repository updates multiple times per day with new roles from companies like FAANG, startups, and enterprise organizations. The repo currently tracks:

  • 194 Software Engineering positions
  • 45 Data Science/ML roles
  • 53 Hardware Engineering openings
  • 5 Product Management positions
  • 5 Quantitative Finance roles

Manually checking this repo wastes 15-30 minutes daily. More critically, you might miss time-sensitive applications that close within hours. By automating the monitoring and application preparation process, you can respond to new postings within minutes instead of days.

Prerequisites and Setup

Before building the automation, ensure you have:

  • A GitHub account with Actions enabled (free tier works)
  • Basic knowledge of YAML for GitHub Actions workflows
  • Node.js 18+ installed locally for testing
  • A Simplify account (free tier available at simplify.jobs)
  • Your resume and cover letter templates in markdown format

Step 1: Create the Repository Monitor Script

First, create a Node.js script that fetches and parses the SimplifyJobs repository data. Create a new file monitor-jobs.js:

const https = require('https');
const fs = require('fs');

const REPO_URL = 'https://raw.githubusercontent.com/SimplifyJobs/New-Grad-Positions/dev/README.md';
const CACHE_FILE = './job-cache.json';

function fetchRepoContent() {
  return new Promise((resolve, reject) => {
    https.get(REPO_URL, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => resolve(data));
    }).on('error', reject);
  });
}

function parseJobListings(markdown) {
  const jobs = [];
  const tableRegex = /<tr>\s*<td><strong><a[^>]*>([^<]+)<\/a><\/strong><\/td>\s*<td>([^<]+)<\/td>\s*<td>([^<]+)<\/td>\s*<td[^>]*>.*?href="([^"]+)"/gs;
  
  let match;
  while ((match = tableRegex.exec(markdown)) !== null) {
    const [, company, role, location, applicationUrl] = match;
    
    // Skip closed positions (marked with 🔒)
    if (role.includes('🔒')) continue;
    
    jobs.push({
      company: company.trim(),
      role: role.trim(),
      location: location.trim(),
      applicationUrl: applicationUrl.trim(),
      foundAt: new Date().toISOString()
    });
  }
  
  return jobs;
}

async function getNewJobs() {
  const content = await fetchRepoContent();
  const currentJobs = parseJobListings(content);
  
  let cachedJobs = [];
  if (fs.existsSync(CACHE_FILE)) {
    cachedJobs = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
  }
  
  const cachedUrls = new Set(cachedJobs.map(j => j.applicationUrl));
  const newJobs = currentJobs.filter(j => !cachedUrls.has(j.applicationUrl));
  
  // Update cache
  fs.writeFileSync(CACHE_FILE, JSON.stringify(currentJobs, null, 2));
  
  return newJobs;
}

getNewJobs().then(newJobs => {
  console.log(`Found ${newJobs.length} new job postings`);
  console.log(JSON.stringify(newJobs, null, 2));
  
  // Write to output file for GitHub Actions
  fs.writeFileSync('./new-jobs.json', JSON.stringify(newJobs, null, 2));
}).catch(console.error);

This script:

  • Fetches the latest README from the SimplifyJobs repository
  • Parses HTML table rows to extract job details
  • Filters out closed positions (marked with 🔒 emoji)
  • Compares against cached results to identify new listings
  • Outputs new jobs to a JSON file for processing

Step 2: Configure GitHub Actions Workflow

Create .github/workflows/monitor-jobs.yml to run the script on a schedule:

name: Monitor New Grad Jobs

on:
  schedule:
    - cron: '0 */4 * * *'  # Run every 4 hours
  workflow_dispatch:  # Allow manual triggers

jobs:
  check-new-jobs:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          
      - name: Restore job cache
        uses: actions/cache@v3
        with:
          path: job-cache.json
          key: job-cache-${{ github.run_id }}
          restore-keys: job-cache-
          
      - name: Run job monitor
        run: node monitor-jobs.js
        
      - name: Check for new jobs
        id: check
        run: |
          if [ -f new-jobs.json ]; then
            COUNT=$(jq length new-jobs.json)
            echo "count=$COUNT" >> $GITHUB_OUTPUT
            if [ $COUNT -gt 0 ]; then
              echo "has_new_jobs=true" >> $GITHUB_OUTPUT
            fi
          fi
          
      - name: Send notification
        if: steps.check.outputs.has_new_jobs == 'true'
        uses: dawidd6/action-send-mail@v3
        with:
          server_address: smtp.gmail.com
          server_port: 465
          username: ${{ secrets.EMAIL_USERNAME }}
          password: ${{ secrets.EMAIL_PASSWORD }}
          subject: '${{ steps.check.outputs.count }} New Grad Jobs Found'
          body: file://new-jobs.json
          to: your-email@example.com
          from: Job Monitor
          
      - name: Save cache
        uses: actions/cache/save@v3
        if: always()
        with:
          path: job-cache.json
          key: job-cache-${{ github.run_id }}

Step 3: Filter Jobs by Your Criteria

Enhance the monitoring script to filter jobs based on your preferences. Add this filtering function:

function filterJobs(jobs, criteria) {
  return jobs.filter(job => {
    // Location filtering
    if (criteria.locations && criteria.locations.length > 0) {
      const matchesLocation = criteria.locations.some(loc => 
        job.location.toLowerCase().includes(loc.toLowerCase()) ||
        job.location.toLowerCase().includes('remote')
      );
      if (!matchesLocation) return false;
    }
    
    // Exclude roles requiring citizenship if you don't have it
    if (criteria.excludeCitizenship && job.role.includes('🇺🇸')) {
      return false;
    }
    
    // Exclude roles without sponsorship if you need it
    if (criteria.requiresSponsorship && job.role.includes('🛂')) {
      return false;
    }
    
    // Company filtering
    if (criteria.excludeCompanies && criteria.excludeCompanies.length > 0) {
      if (criteria.excludeCompanies.some(c => 
        job.company.toLowerCase().includes(c.toLowerCase())
      )) {
        return false;
      }
    }
    
    return true;
  });
}

// Usage example
const criteria = {
  locations: ['San Francisco', 'New York', 'Remote'],
  requiresSponsorship: true,
  excludeCitizenship: false,
  excludeCompanies: ['Defense', 'Military']
};

const filteredJobs = filterJobs(newJobs, criteria);

Step 4: Integrate with Simplify's Autofill Extension

The SimplifyJobs repository promotes Simplify's browser extension for one-click applications. You can programmatically prepare application data for Simplify:

function generateSimplifyData(job, resumeData) {
  return {
    company: job.company,
    position: job.role,
    applicationUrl: job.applicationUrl,
    applicantData: {
      fullName: resumeData.fullName,
      email: resumeData.email,
      phone: resumeData.phone,
      location: resumeData.location,
      resumeUrl: resumeData.resumeUrl,
      coverLetterUrl: resumeData.coverLetterUrl,
      linkedIn: resumeData.linkedIn,
      github: resumeData.github,
      portfolio: resumeData.portfolio
    },
    timestamp: new Date().toISOString()
  };
}

Step 5: Set Up Notification System

Beyond email, you can integrate with multiple notification services. Here's a Slack webhook integration:

const https = require('https');

function sendSlackNotification(jobs, webhookUrl) {
  const message = {
    text: `🚨 ${jobs.length} New Grad Positions Posted`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${jobs.length} new positions* found in SimplifyJobs repository`
        }
      },
      ...jobs.slice(0, 5).map(job => ({
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${job.company}* - ${job.role}\n📍 ${job.location}\n<${job.applicationUrl}|Apply Now>`
        }
      }))
    ]
  };
  
  const data = JSON.stringify(message);
  const url = new URL(webhookUrl);
  
  const options = {
    hostname: url.hostname,
    path: url.pathname,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': data.length
    }
  };
  
  return new Promise((resolve, reject) => {
    const req = https.request(options, resolve);
    req.on('error', reject);
    req.write(data);
    req.end();
  });
}

Comparison: Manual vs Automated Job Tracking

| Aspect | Manual Checking | Automated Pipeline | |--------|----------------|--------------------| | Time per day | 15-30 minutes | 2 minutes (reviewing notifications) | | Response speed | Hours to days | Minutes | | Coverage | 50-100 jobs/week | All 300+ jobs | | Filtering | Manual scanning | Automatic by location/criteria | | Missed opportunities | High (time zones, sleep) | Minimal | | Application prep | Manual data entry | Pre-filled with Simplify | | Cost | Free | Free (GitHub Actions free tier) |

Advanced: Add Application Tracking

Extend your automation to track application status:

function trackApplication(job, status) {
  const tracking = {
    jobId: Buffer.from(job.applicationUrl).toString('base64'),
    company: job.company,
    role: job.role,
    appliedAt: new Date().toISOString(),
    status: status, // 'applied', 'interviewing', 'offer', 'rejected'
    notes: ''
  };
  
  let applications = [];
  if (fs.existsSync('./applications.json')) {
    applications = JSON.parse(fs.readFileSync('./applications.json'));
  }
  
  applications.push(tracking);
  fs.writeFileSync('./applications.json', JSON.stringify(applications, null, 2));
}

Troubleshooting Common Issues

Issue: GitHub Actions not triggering

Solution: Ensure your repository has Actions enabled. Go to Settings → Actions → General and verify "Allow all actions" is selected. The workflow_dispatch trigger allows manual testing.

Issue: Parsing fails after repo format changes

Solution: The SimplifyJobs repo updates its format occasionally. Add error handling and fallback parsing:

function parseJobListings(markdown) {
  try {
    // Primary parsing logic
    return parseTableFormat(markdown);
  } catch (error) {
    console.error('Primary parser failed:', error);
    // Fallback to regex-based parsing
    return parseFallback(markdown);
  }
}

Issue: Rate limiting on repository fetches

Solution: Use GitHub's API with authentication instead of raw content fetching:

const options = {
  hostname: 'api.github.com',
  path: '/repos/SimplifyJobs/New-Grad-Positions/contents/README.md',
  headers: {
    'User-Agent': 'Job-Monitor',
    'Authorization': `token ${process.env.GITHUB_TOKEN}`
  }
};

Deployment to Production

  1. Fork the SimplifyJobs repository: This allows you to customize the list and add personal notes
  2. Set up GitHub Secrets: Store email credentials and API keys in repository secrets
  3. Configure cron schedule: Adjust based on how frequently the repo updates (every 4 hours is optimal)
  4. Enable branch protection: Prevent accidental workflow deletions
  5. Monitor GitHub Actions usage: Free tier includes 2,000 minutes/month, sufficient for this use case

Integrating with Application Tracking Systems

For serious job seekers, connect your automation to tools like Notion, Airtable, or Trello:

async function addToNotion(job, notionApiKey, databaseId) {
  const response = await fetch('https://api.notion.com/v1/pages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${notionApiKey}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2022-06-28'
    },
    body: JSON.stringify({
      parent: { database_id: databaseId },
      properties: {
        'Company': { title: [{ text: { content: job.company } }] },
        'Role': { rich_text: [{ text: { content: job.role } }] },
        'Location': { rich_text: [{ text: { content: job.location } }] },
        'Status': { select: { name: 'To Apply' } },
        'URL': { url: job.applicationUrl }
      }
    })
  });
  
  return response.json();
}

Next Steps and Optimization

Once your basic automation works, consider these enhancements:

  • AI-powered cover letter generation: Use OpenAI's API to customize cover letters per job
  • Company research automation: Fetch company data from Crunchbase or LinkedIn
  • Application deadline tracking: Parse and alert on upcoming deadlines
  • Success metrics: Track application-to-interview conversion rates by company type
  • Network integration: Cross-reference jobs with your LinkedIn connections at those companies

By automating the tedious parts of job hunting, you free up time for what actually matters: preparing for interviews, building projects, and networking. This GitHub Actions pipeline ensures you never miss an opportunity while maintaining sanity during the stressful new grad job search.

The SimplifyJobs repository updates continuously, and with this automation in place, you'll be among the first to know—and apply—when your dream role appears.

Recommended Tools