Job boards are a tricky source for job data: they're slow to scrape, rate-limited, and full of re-posts. In October 2026 I ran a LinkedIn jobs search for "software engineer, US, past week" and matched the results to the employers' own career pages: 46% of the jobs I could match had been open there for 14+ days. The real source is the employer's applicant tracking system (ATS). Greenhouse, Lever, Ashby, Workday and friends all publish public JSON feeds for their career pages.

The catch is that you'd need to know about 20,000 board URLs and crawl them every hour. This tutorial skips that part. We'll query an index that already does it: the Job Postings API on Apify. (Disclosure: I built it and run the crawler behind it.) It covers about 1.18M open jobs from 17,500+ companies, re-checked hourly (Workday daily), and rows include the full job description as plain text (most sources today; see the limits at the end). Once you have an account, a CSV of jobs posted in the last 24 hours takes one API call that returns in about 10 seconds.

Want to see the data before signing up? Two public samples (CSV, no login), saved from real runs on Oct 21, 2026:

In those samples 39 of 50 and 46 of 50 rows have a description. The rest are Workday and Rippling jobs (see the limits at the end).

⏱️ About 10 minutes, including signup. Cost: on Apify's free plan rows cost $2.50 per 1,000, so the 50-200-row examples below cost $0.13-$0.50 and the 1,000-row description example at most $2.50. The free plan includes $5 of credit every month. Paid Apify plans pay less per row (down to $1.00 per 1,000).

1. Run it once from the browser (no code)

  1. Open https://apify.com/cleanrowdata/fresh-jobs-index and click Try for free. You can sign up with email, Google or GitHub.
  2. The input is prefilled with software engineer, United States / Remote and last 7 days, 50 results. Click Start.
  3. About 5-10 seconds later you have 50 rows. Each one has the title, company, locations, remote flag, ATS, posted_at, first_seen, salary (when published), the direct apply link and the full description. Export to CSV, Excel or JSON.

2. Call it from the command line

Get your API token from Console → Settings → API & Integrations, then:

curl -s -X POST \ "https://api.apify.com/v2/acts/cleanrowdata~fresh-jobs-index/run-sync-get-dataset-items?format=csv" \ -H "Authorization: Bearer $APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"timeRange":"24h","titleSearch":["data engineer"],"locationSearch":["United States","Remote"],"limit":100}' \ > data-engineer-jobs.csv 

run-sync-get-dataset-items starts the run, waits for it and returns the rows in one call. Add "includeDescription": false to the input if you only want the structured fields (smaller files).

3. Python: a daily "new jobs" feed

# pip install apify-client import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("cleanrowdata/fresh-jobs-index").call(run_input={ "timeRange": "24h", # 1h, 24h, 7d, 30d or all  "titleSearch": ["account executive", "sales development"], "locationSearch": ["United States", "Remote"], "ats": ["greenhouse", "lever", "ashby"], # optional: startups mostly use these  "limit": 200, }) for job in client.dataset(run["defaultDatasetId"]).iterate_items(): print(job["posted_at"][:16], job["company"], "|", job["title"], "|", job["apply_url"]) 

Schedule it in Apify (Schedules → Create) with "timeRange": "24h" daily, or "1h" hourly, and you get each new job exactly once. Re-posted old jobs are excluded by default.

4. Use the descriptions

The input filters (titleSearch, keywords) match titles, departments and company names. The description field is where the requirements, tech stack, benefits and visa notes live, so filter or enrich on it in your own code. For example, find data engineering jobs that mention dbt and say how many ask for Snowflake:

# continues from the snippet above (same `client`) import re run = client.actor("cleanrowdata/fresh-jobs-index").call(run_input={ "timeRange": "7d", "titleSearch": ["data engineer", "analytics engineer"], "limit": 1000, }) jobs = list(client.dataset(run["defaultDatasetId"]).iterate_items()) with_text = [j for j in jobs if j.get("description")] dbt = [j for j in with_text if re.search(r"\bdbt\b", j["description"], re.I)] snow = [j for j in dbt if "snowflake" in j["description"].lower()] print(f"{len(dbt)} of {len(with_text)} jobs mention dbt; {len(snow)} of those also mention Snowflake") 

The same text works well as LLM input: extract skills, years of experience, salary ranges written only in the text, or a one-line summary per job.

5. Useful recipes

Goal Input
Recruiters: new roles to fill this week {"timeRange":"7d","titleSearch":["data engineer","analytics engineer"],"locationSearch":["United States","Remote"],"titleExclusionSearch":["intern"],"limit":500}
Sales: companies hiring your buyer use the sister Actor Hiring Signals API: {"timeRange":"7d","jobFunctions":["security"],"minNewJobs":3,"limit":100} gives one row per company
HR / comp analysts: posted salary ranges {"timeRange":"30d","titleSearch":["machine learning engineer"],"hasSalary":true,"limit":2000}
Clean up a job board: what closed? {"jobStatus":"closed","timeRange":"24h","limit":1000}
Remote jobs you can take from Europe use Remote Jobs API: {"timeRange":"7d","eligibleFrom":["europe"],"limit":100} (descriptions included there too)

6. What's in a row

{ "title": "Principal Software Engineer, Full Stack — Codex", "company": "OpenAI", "ats": "ashby", "location": "San Francisco", "remote": false, "posted_at": "2026-10-21T17:32:33Z", "first_seen": "2026-10-21T17:37:01Z", "salary_text": "$441K - $515K", "description": "About the Role We’re hiring a Principal Software Engineer to help define and build the next generation of … (full text)", "job_url": "https://jobs.ashbyhq.com/…", "apply_url": "https://jobs.ashbyhq.com/…/application", "status": "active" } 

posted_at is the ATS publish time and first_seen is when the index first saw the job; for that OpenAI job the gap was under 5 minutes, and for Greenhouse, Ashby and Lever the median gap is about 1.1 hours. description is plain text, up to 10,000 characters.

Limits, honestly

  • Only companies that use one of the 9 supported ATS platforms are covered. That's 17,500+ companies, but not every employer.
  • Workday boards refresh daily, not hourly.
  • Descriptions: Greenhouse, Lever, Ashby and Workable jobs have them today (over 99% of their roughly 394,000 open jobs). Workday (about 728,000 jobs) and SmartRecruiters descriptions are backfilling over the next few days, newest jobs first, so many Workday rows still have an empty description for now. Recruitee, Personio and Rippling jobs don't have descriptions yet. A brand-new job's description can be empty for its first hour or two.

If you build something with it, I'd love to hear what you're tracking. Drop a comment 💬