Scripting the job search

The job hunt helper I built with a few lines of JavaScript

Refreshing job boards and reapplying filters every day is tedious.

When I reentered the job market after 8 years and began networking in earnest, I heard this gripe often. So, I built a JavaScript automation to simply the process.

Running in Google Apps Script, the script fetches and parses RSS feeds from LinkedIn, BuiltIn and Power to Fly. It extracts structured job data into a Google Sheet, filtering out duplicates and expired links—all without me lifting a finger!

To keep it flexible, I designed each parser as a modular component. If I, or anyone else, wants to add a new job board, it’s just a matter of plugging in a new parser class without touching the core logic.

The script uses regex and logic to accurately extract metadata, such as company name, job title, location, job board URL and (when available) an external job application URL. It can run at regular intervals (daily, hourly, etc.), and it filters out duplicate and expired postings.

The end result is not flashy, but it works. It shifts the focus from sifting through listings to applying for high-fit roles, saving effort and time.

I have included the source code below for anyone to explore the parser logic and the scheduler structure, and to extend it for other use cases.

If you're looking to simplify your job search, or just want a project to sharpen your JavaScript automation skills, this one’s easy to fire up and pays off quickly.

Feel free to reach out for more details or to share your automation success stories.


Fetch RSS code

<function fetchAllRSSJobs() {
  const feeds = [
    { url: 'https://rss.app/feeds/v1.1/PVhoP8nk6gvgKH97.json', sheetName: 'LinkedIn PM Jobs', parser: parseLinkedInRSS },
    { url: 'https://rss.app/feeds/v1.1/MDIHJNe31rKTxLyW.json', sheetName: 'BuiltIn BOS', parser: parseBuiltInBOSRSS },
    { url: 'https://rss.app/feeds/v1.1/QPSAF6S4BNU3Oqmv.json', sheetName: 'Power to fly', parser: parsePowerToFlyRSS }
  ];

  const spreadsheet = SpreadsheetApp.openByUrl('https://<destination Goolge Sheet URL here>');

  feeds.forEach(feed => {
    const sheet = spreadsheet.getSheetByName(feed.sheetName);

    if (sheet.getLastRow() === 0) {
      sheet.appendRow(['Company', 'Job Title', 'Job Location', 'Job Board URL']);
    }

    const response = UrlFetchApp.fetch(feed.url);
    const data = JSON.parse(response.getContentText());

    let existingLinks = [];
    if (sheet.getLastRow() > 1) {
      existingLinks = sheet.getRange(2, 4, sheet.getLastRow() - 1).getValues().flat();
    }

    const parsedItems = feed.parser(data, existingLinks);
    parsedItems.forEach(row => {
      sheet.appendRow(row);
    });
  });
}

LinkedIn parser code

}
function parseLinkedInRSS(data, existingLinks) {
  return data.items
    .filter(item => !existingLinks.includes(item.url || 'No URL provided'))
    .map(item => {
      const rawTitle = item.title || 'No title provided';
      const company = extractLinkedInCompany(rawTitle);
      const jobTitle = extractLinkedInJobTitle(rawTitle);
      const location = extractLinkedInLocation(rawTitle);
      const url = item.url || 'No LinkedIn URL provided';
      return [company, jobTitle, location, url];
    });
}
function extractLinkedInCompany(rawTitle) {
  const match = rawTitle.match(/^(.*?) hiring/i);
  return match ? match[1].trim() : 'No company provided';
}
function extractLinkedInJobTitle(rawTitle) {
  const match = rawTitle.match(/hiring (.*?) in/i);
  return match ? match[1].trim() : 'No job title provided';
}
function extractLinkedInLocation(rawTitle) {
  const match = rawTitle.match(/in (.*?)(,|$)/i);
  return match ? match[1].trim() : 'No location provided';
}

Key features

Regex

  • extractLinkedInCompany - extracts the company name using at as the keyword anchor

  • extractLinkedInJobTitle - extracts job title as the string before at

  • extractLinkedInLocation - corrects logic for location extraction, ensuring full city names appear

    • Adjusted patterns to match the LinkedIn job title format: Job Title at Company in Location

    • Specifically captures the Company after at and the Job Title before at.

    • Captures the Location after in.

Fallback defaults

  • Ensures that No company provided and No job title provided appear only when data is genuinely missing.

 

BuiltIn Boston parser code

}
function parseBuiltInBOSRSS(data, existingLinks) {
  return data.items
    .filter(item => !existingLinks.includes(item.url || 'No URL provided'))
    .map(item => {
      const rawTitle = item.title || 'No title provided';
      const content = item.content_text || '';
      const url = item.url || 'No URL provided';
      const company = extractBuiltInCompany(rawTitle, url);
      const jobTitle = extractBuiltInJobTitle(rawTitle, url);
      const location = extractBuiltInLocation(content);
      return [company, jobTitle, location, url];
    });
}
function extractBuiltInCompany(rawTitle, url) {
  const titleMatch = rawTitle.match(/-\s(.+?)\s(?:in|Remote|USA|$)/);
  if (titleMatch) return titleMatch[1].trim();

  // Extract from URL if rawTitle doesn't provide company
  const urlMatch = url.match(/company\/([^/]+)/);
  return urlMatch ? decodeURIComponent(urlMatch[1].replace(/-/g, ' ')) : 'No company provided';
}
function extractBuiltInJobTitle(rawTitle, url) {
  const titleMatch = rawTitle.match(/^(.+?)\s-/);
  if (titleMatch) return titleMatch[1].trim();

  // Extract from URL if rawTitle doesn't provide job title
  const urlMatch = url.match(/jobs\/product\/search\/([^/]+)/);
  return urlMatch ? decodeURIComponent(urlMatch[1].replace(/-/g, ' ')) : 'No job title provided';
}
function extractBuiltInLocation(content) {
  const locationMatch = content.match(/in\s(.+?),?\s(USA|MA|Remote)/i);
  return locationMatch ? locationMatch[1].trim() : 'No location provided';
}

Key features

  1. Company extraction

    • Fallback to extract the company name from URLs containing /company/.

    • Converts into spaces for a clean company name

  2. Job Title extraction

    • Enhanced to parse data from both title and URL, if available.

    • Extracts Job Title and Company from specific patterns, including fallback logic for URL-based parsing.

    • Fallback to extract job titles from URLs containing /jobs/product/search/.

  3. Location extraction

    • Left unchanged, as it relies on content or defaults to: "No location provided"

Expected behavior

  • For URLs like https://www.builtinboston.com/company/liberty-mutual-insurance, the company should now extract as Liberty Mutual Insurance.

  • For URLs like https://www.builtinboston.com/jobs/product/search/head-of-product, the job title should now extract as Head of Product.

 

Power to Fly parser code

}
function parsePowerToFlyRSS(data, existingLinks) {
  return data.items
    .filter(item => !existingLinks.includes(item.url || 'No URL provided'))
    .map(item => {
      const rawTitle = item.title || 'No title provided';
      const content = item.content_text || '';
      const url = item.url || 'No URL provided';
      const company = extractPowerToFlyCompany(rawTitle, content);
      const jobTitle = extractPowerToFlyJobTitle(rawTitle, content);
      const location = extractPowerToFlyLocation(rawTitle, content);
      return [company, jobTitle, location, url];
    });
}
function extractPowerToFlyCompany(rawTitle, content) {
  const match = rawTitle.match(/at\s(.+?)\sin/i);
  return match ? match[1].trim() : 'No company provided';
}
function extractPowerToFlyJobTitle(rawTitle, content) {
  const match = rawTitle.match(/^(.*?)\sat/i);
  return match ? match[1].trim() : 'No job title provided';
}
function extractPowerToFlyLocation(rawTitle, content) {
  const match = rawTitle.match(/in\s(.+?),/i);
  return match ? match[1].trim() : 'No location provided';
}

Key features

  • Adjusted to account for specific patterns from Power to Fly JSON data

  • Uses similar fallback logic and tailored patterns for extracting the required fields from title, content, and URL

  • Also has fallback logic for missing fields


Script testing

Follow these steps to test your script to ensure the RSS URL passes data to Google Sheets.

Step 1: Verify your script

Double-check it

  • Ensure the correct RSS JSON URL is set in the url variable:

    const url = 'YOUR_RSS_JSON_URL'; // Replace with your RSS JSON feed URL

  • Confirm the sheet name matches your actual Google Sheet tab name in this line:

    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');

Save it

  • Click File > Save and name your project (e.g., FetchRSSJobs).

Note: If you want to specify a specific Google Sheet (e.g., your "daily job search automation" sheet), you need to replace the getActiveSpreadsheet() call with openByUrl() and use the URL of your Google Sheet.

Step 2: Test your script

Run it

  • In the Apps Script editor, select the function fetchRSSJobs from the dropdown menu.

  • Click the Run button ▶️.

Authorize it

  • On the first run, you’ll need to authorize the script.

    • A prompt will appear asking for permission to access your spreadsheet and fetch data.

    • Click Review Permissions and log in with your Google account.

    • Choose Advanced Options > Go to Project (unsafe) if you see a warning.

    • Click Allow to authorize.

  1. Check for errors

    • If the script runs successfully, it will append the data from the RSS JSON to your Google Sheet.

    • If there’s an error, the Execution Log (found under View > Execution Log) will provide debugging information.

Step 3: Verify data in Sheets

  • Open your Google Sheet and check if the data from the RSS feed appears in the appropriate columns.

  • If the data doesn’t look right…

    • Double-check the structure of the RSS JSON feed or

    • modify the forEach loop in the script to correctly reference JSON keys.

Example of adjusting keys

data.items.forEach((item) => { sheet.appendRow([item.title, item.company, item.location, item.link]); // Update keys as needed });

Step 4: Common issues and resolutions *

  • "TypeError: Cannot read property 'items' of undefined"

    • The structure of the JSON feed might differ from the script's expectations. Use the Logger to inspect the JSON structure:

      Logger.log(data);

      • View the log under View > Logs to identify the correct keys.

  • "Authorization Required" error

    • Ensure you’ve authorized the script to access Google Sheets and external services.

Step 5: Deploy the script (optional)

You don’t need to deploy the script for testing, but if you want it to be accessible as a web app or trigger-able externally, then…

  1. Click Deploy > Test Deployments.

  2. Follow the prompts to set up a deployment method.


* Additional errors and resolutions

"Error 401: deleted_client"

This indicates that the OAuth client (your script project) was deleted or is misconfigured, likely preventing Google from authorizing access to services like Google Sheets or external APIs. Here’s how to fix it and reauthorize the script:

Step 1: Reset your script's OAuth credentials

If the OAuth client was deleted, you need to reset the script's authorization settings.

  1. Reauthorize the script

    • In the Apps Script editor, click Project Settings (gear icon on the left sidebar).

    • Scroll down to "Show" under Scopes and click it to see the script’s OAuth scopes.

    • Click Reset Authorization to reauthorize your script.

  2. Re-save your script

    • After saving the script, try running it to re-trigger the authorization flow.

Step 2: Verify script permissions

Ensure that your script uses the following scopes:

  1. Google Sheets API – For accessing your spreadsheet:

    • "https://www.googleapis.com/auth/spreadsheets"

  2. External API (UrlFetchApp) – For fetching the RSS feed:

    • "https://www.googleapis.com/auth/script.external_request"

If you still see the error…

  • Click "Deploy > Test Deployment" to reinitialize the OAuth client.

  • Test the deployment using the test version.

Step 3: (If necessary) Create a new script project

If the OAuth client is permanently deleted and cannot be reinitialized…

  1. Create a new Script Project

    • Open your Google Sheet.

    • Click Extensions > Apps Script to create a new script project.

  2. Copy-paste your code

    • Copy and paste the code from your current script into the new project.

  3. Reauthorize and run

    • Save and run the script as before.

    • Authorize the script again when prompted.

Step 4: Debugging

If you still encounter issues…

  1. Check View > Logs in the Apps Script editor for error messages.

  2. Verify that your Google account is authorized for both Google Sheets and external API calls.

"Google hasn’t verified this app"

This message appears because your Apps Script project hasn't been verified by Google, which is common for personal scripts. You can bypass this message for your own use without needing to verify the app. Here’s how.

Step 1: Proceed past the warning

  1. When the "Google hasn’t verified this app" screen appears…

    • Click Advanced at the bottom of the warning.

    • Click Go to [Project Name] (unsafe).

    • Grant the requested permissions to allow your script to access your Google Sheets and external services.

  2. Once permissions are granted, the script should run as expected.

Step 2: Confirm permissions granted

  • In the Apps Script editor, re-run your script.

  • If prompted again, authorize the permissions following the steps above.

  • Check your Execution Log (View > Execution Log) to ensure the script executes without errors.

Step 3: Debugging

If you see the warning in the Execution Log: "This project requires access to your Google Account to run. Please try again and allow it this time", this means authorization was not completed. Repeat Step 1 to bypass the verification warning and grant access.

Optional: Avoid future warnings

For smoother execution…

  1. Use Triggers for Automation:

    • Set up a time-driven trigger to run the script automatically.

    • Triggers typically bypass the manual authorization process after the first setup.

  2. Deploy as a Test Deployment:

    • Click Deploy > Test Deployments.

    • Test the deployment to ensure it works without further manual intervention.

 
Previous
Previous

Less but better

Next
Next

Signals from the deep