appscript.dev
Automation Intermediate Sheets

Build a job-listings aggregator

Collect Northwind-relevant postings via public job-board APIs into a sheet for the team.

Published Nov 20, 2025

Northwind keeps half an eye on the hiring market — partly to know what skills are in demand, partly because the studio sometimes refers contractors. Checking a job board by hand is the kind of task that gets skipped for weeks, then done in a frantic catch-up. A sheet that quietly fills itself is a far better habit.

This script queries a public job-board API for postings that match a search term and appends any it has not seen before to a Google Sheet. Because it tracks which job IDs are already logged, it can run on a schedule without ever duplicating a row — the sheet grows by exactly the new listings each time.

What you’ll need

  • A Google Sheet for the listings. The first sheet’s column A must hold job IDs — the script reads it to know what it has already seen.
  • A search term that matches the roles you care about — the example uses apps script developer.
  • Nothing else. The Remotive jobs API used here is public and needs no key.

The script

// The roles to search for on the job board.
const SEARCH = 'apps script developer';

// The spreadsheet that collects the listings.
const JOBS_SHEET_ID = '1abcJobsId';

// How many of the most recent results to consider per run.
const MAX_RESULTS = 30;

/**
 * Queries a public job-board API and appends any postings not already
 * in the sheet, keyed by the job's unique ID.
 */
function aggregateListings() {
  // 1. Ask the job board for postings matching the search term.
  const res = UrlFetchApp.fetch(
    'https://remotive.com/api/remote-jobs?search=' + encodeURIComponent(SEARCH),
    { muteHttpExceptions: true }
  );
  const data = JSON.parse(res.getContentText());

  if (!data.jobs || !data.jobs.length) {
    Logger.log('No jobs returned for search: ' + SEARCH);
    return;
  }

  // 2. Read the job IDs already logged so we never add a duplicate.
  const sheet = SpreadsheetApp.openById(JOBS_SHEET_ID).getSheets()[0];
  const known = new Set(sheet.getRange('A2:A').getValues().flat());

  // 3. Walk the newest results and append only the ones we have not seen.
  let added = 0;
  for (const j of data.jobs.slice(0, MAX_RESULTS)) {
    if (known.has(j.id)) continue;
    sheet.appendRow([
      j.id,
      j.title,
      j.company_name,
      j.candidate_required_location,
      j.url,
      new Date(),
    ]);
    added++;
  }
  Logger.log('Added ' + added + ' new listings.');
}

How it works

  1. aggregateListings calls the Remotive jobs API with the search term URL-encoded, and parses the JSON response.
  2. If the API returns no jobs it logs the search term and stops — useful when a term is too narrow or the board is having a quiet day.
  3. It reads column A of the sheet into a Set of job IDs already logged. A Set gives fast has lookups, which matters once the sheet is large.
  4. It takes the newest MAX_RESULTS postings and, for each one, skips it if the ID is already known and otherwise appends a row with the ID, title, company, location, link, and the date it was found.
  5. The found-date column lets you sort or filter the sheet by “what arrived this week”, and the ID in column A is what keeps the next run from duplicating it.

Example run

The sheet starts with two known listings. After a run, three new postings have been appended:

idtitlecompanylocationurlfound
9001Apps Script DeveloperAcmeWorldwideremotive.com/…(earlier)
9007Automation EngineerGlobexEU onlyremotive.com/…(earlier)
9015Google Workspace DevInitechUKremotive.com/…2026-05-25
9016Sheets Automation LeadUmbrellaWorldwideremotive.com/…2026-05-25
9018Apps Script ContractorHooliUS/EUremotive.com/…2026-05-25

Run it again an hour later and, if the board has nothing new, the log reads Added 0 new listings and the sheet is untouched.

Trigger it

Run this on a schedule so the sheet stays current without anyone remembering:

  1. In the Apps Script editor open Triggers (the clock icon).
  2. Add Trigger → choose aggregateListings, event source Time-driven, a Day timer (once a day is plenty for a job board).
  3. Save and approve the authorisation prompt the first time.

Watch out for

  • This depends on a third-party public API. If Remotive changes its response shape, rate-limits, or goes down, the script will fail — wrap it in a try/catch if a run failure should not break a daily report.
  • The API returns its own ordering. MAX_RESULTS caps how many results each run considers, so a burst of new postings beyond that cap is missed until the next run. Raise the cap if you search a busy term.
  • Postings are never removed. Once a job is logged it stays in the sheet even after it is filled or expires — add a cleanup step if staleness matters.
  • appendRow writes one row per call. Adding 30 rows means 30 writes, which is fine here but slow for hundreds — batch with setValues if you scale up.
  • UrlFetchApp has a daily quota. One search per day uses almost none of it; aggregating many search terms every few minutes can exhaust it.

Related