agentsclimarketplace

Create scraper

Skill chrisdothtml/ai-job-finder/.claude/skills/create-scraper

Let AI read the careers pages for you, so you can spend your time doing literally anything else

Install
npx -y skills add chrisdothtml/ai-job-finder --skill create-scraper

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Given a careers page URL, uses Playwright browser tools to explore the page, identify reliable scraping patterns (preferring hidden JSON APIs over DOM scraping), then writes a TypeScript Scraper subclass for that company

SKILL.md

7.5 KB, as published. Nobody here has run it

Your goal is to explore a company's careers page and write a permanent, reliable Scraper subclass that other code can use going forward. The argument is either:

  1. the name of a company, e.g. /create-scraper netflix
  2. a careers page URL, e.g. /create-scraper https://explore.jobs.netflix.net/careers, or

Step 1a - Identify the url for the careers page

If the careers page has been provided as the argument, move to the next step; otherwise search "[company-name] careers" on google and navigate until you find the careers page.

Step 1b — Identify the company name

Derive the class name and filename from the company's domain or brand name. E.g.:

  • explore.jobs.netflix.netNetflixScrapersrc/analysis/scraping/NetflixScraper.ts
  • jobs.stripe.comStripeScrapersrc/analysis/scraping/StripeScraper.ts

Step 2 — Navigate and capture network traffic

The goal is to find a JSON API the page calls internally — these are far more reliable than DOM scraping.

  1. Navigate to the URL with browser_navigate
  2. Immediately inject a network monitor via browser_evaluate:
window.__reqs = [];
const _f = window.fetch.bind(window);
window.fetch = function (input, init) {
  const url =
    typeof input === 'string'
      ? input
      : input instanceof URL
        ? input.href
        : input.url;
  window.__reqs.push({ url, method: (init?.method || 'GET').toUpperCase() });
  return _f(input, init);
};
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
  window.__reqs.push({ url: String(url), method: method.toUpperCase() });
  return _open.apply(this, arguments);
};
  1. Take a browser_snapshot to understand the page structure
  2. Interact with the page to trigger data loads: scroll to the bottom, wait a moment, then retrieve what was captured:
window.__reqs;
  1. Look for requests that look like job listing APIs — JSON endpoints with paths like /api/jobs, /search, /v1/positions, /careers/api, etc. Filter out analytics, fonts, images, and tracking pixels.

Step 3 — Probe the API (if found)

If you found a promising API endpoint:

  1. Use browser_evaluate to call it directly and inspect the response shape:
const res = await fetch('ENDPOINT_URL');
const data = await res.json();
JSON.stringify(data, null, 2).slice(0, 3000); // preview
  1. Understand the response structure: where are jobs listed? What fields are available (title, location, id, url)?
  2. Identify pagination: look for total, offset, limit, page, cursor, or similar fields. Test by modifying query params.
  3. Find the job detail endpoint: try appending a job ID to the base URL, or look for a detail URL in the job object.

If the site uses Greenhouse or Lever, stop — those are already handled by GreenhouseScraper and LeverScraper. Report this to the user instead of writing a new scraper.

Step 4 — Explore pagination

Click through to page 2 (or trigger a "Load more"), confirm the data pattern holds, and understand the full pagination mechanism you'll need to implement.

Step 5 — Inspect a job detail page

Navigate to 1–2 individual job listings:

  1. Either call the detail API endpoint directly (if found)
  2. Or click a job link and browser_snapshot the detail page

Understand what data is available: full description, requirements, salary, etc. This is what getJobContent will return.

Step 6 — Write the Scraper

Now write src/analysis/scraping/{Name}Scraper.ts. The approach depends on what you found above.

6a — API-based scraper (preferred)

If you found a reliable JSON API:

import { cachedFetch } from '../fetch.ts';
import { Scraper, type ListedJob } from './Scraper.ts';

export class {Name}Scraper extends Scraper {
  async getJobsList(testing = false): Promise<ListedJob[]> {
    // Fetch all pages via the API and return the full list
    // if `testing` is true, only fetch a single job (if possible) to speed up tests
  }

  async getJobContent(id: string): Promise<string> {
    // Call the job detail API endpoint and return JSON.stringify(data)
  }
}

6b — DOM-based scraper (fallback)

If no usable API exists, scrape the DOM with Playwright. Ask the user to confirm before adding the dependency (yarn add playwright), then write the scraper using the selectors and patterns you identified while exploring.

Use the accessibility snapshot you took in Step 2 to identify the most stable selectors — prefer aria-label, role, and data-* attributes over class names, which tend to change. If a job row has a consistent structure in the snapshot (e.g. a link with a heading inside it), reflect that in your selectors.

import { chromium } from 'playwright';
import { Scraper, type ListedJob } from './Scraper.ts';

export class {Name}Scraper extends Scraper {
  private async withPage<T>(fn: (page: import('playwright').Page) => Promise<T>): Promise<T> {
    const browser = await chromium.launch({ headless: true });
    try {
      return await fn(await browser.newPage());
    } finally {
      await browser.close();
    }
  }

  async getJobsList(testing = false): Promise<ListedJob[]> {
    return this.withPage(async (page) => {
      // Navigate, handle pagination, and extract jobs from the DOM
      // Return ListedJob[] = { title: string; location: string; id: string }
      // Use the job detail page URL (or a stable path segment) as `id`
    });
  }

  async getJobContent(id: string): Promise<string> {
    return this.withPage(async (page) => {
      // Navigate to the job detail page using `id`
      // Extract and return the meaningful text content of the posting
    });
  }
}

Pagination in DOM scrapers: look for a "Next" button or "Load more" and loop until it's gone or disabled. Confirm the selector works by watching the snapshot change between pages during exploration.

Shared conventions

  • Prefer cachedFetch over raw fetch for any HTTP calls — it's a drop-in and caches responses to disk
  • id in ListedJob must be stable and usable in getJobContent to retrieve details
  • getJobContent should return a rich string — either JSON.stringify(apiResponse) or the full text of the job posting — since it's passed to the Analyzer to assess fit
  • Handle pagination fully in getJobsList — return all jobs, not just page 1
  • The location field can be a comma-joined string if multiple locations exist

Step 7 — Register the company in companies.ts

Add the new company to the companies object in src/analysis/companies.ts, filling in every field of the Company interface. Use the WebSearch tool to research the company:

  • homepage: resolve the company's official homepage URL via web search. It must start with https:// and must not have a trailing slash (e.g. https://www.stripe.com, not http://stripe.com/)
  • summary: a very brief description of the company (2 sentences max) — what it does/makes and anything notably distinctive, based on your web search results

Step 8 — Verify

Run yarn typecheck to confirm the file compiles cleanly. Fix any type errors before reporting done.

Add the new company to scrapers.test.ts then run TEST_COMPANIES=[COMPANY_SLUG] yarn test ./src/analysis/scraping/scrapers.test.ts

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.