DEV Community

Paopao Studio
Paopao Studio

Posted on

Track hiring changes at target companies on Greenhouse, Lever and Ashby (with a hiring signal score)

If you sell to companies or place candidates with them, one of the most useful public signals is the company's own job board. A team that opens five Account Executive roles this month is building a sales org. A board that suddenly closes forty roles is telling you something too.

Most of that data is public. Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee all publish job-board endpoints so that companies can show their openings on their own careers pages. The hard parts are:

  1. normalizing six different formats,
  2. remembering what you saw last time, so you only look at what changed, and
  3. turning a pile of rows into "which account should I call first".

This post walks through doing that with an Apify Actor I maintain, ATS Jobs Monitor (disclosure: I built it). Everything below is copied from two real runs on 2026-10-31 (JST). Nothing is edited except trimming fields.

The input

Three target accounts on three different ATS, only sales roles, and both optional features switched on:

{
  "companies": ["greenhouse:anthropic", "lever:palantir", "ashby:ramp"],
  "keywords": ["account executive", "sales"],
  "changeTracking": true,
  "stateKey": "devto-demo",
  "signalScoring": true,
  "maxJobsPerCompany": 3,
  "outputCompanySummary": true
}
Enter fullscreen mode Exit fullscreen mode
  • greenhouse:anthropic style inputs are exact. You can also paste a board URL (jobs.lever.co/palantir) or a careers page, or a bare slug (ramp), in which case the Actor tries all six ATS and marks the row match_confidence: "guessed".
  • changeTracking + stateKey stores what was seen under that key, so the next run with the same key only outputs new, changed and closed jobs.
  • signalScoring adds signal_score, signal_tier and plain-English score_reasons to each company summary.
  • maxJobsPerCompany: 3 only limits how many job rows this run outputs. I used it to keep the demo short. With change tracking on, every scanned job is still saved as seen, so if you want the full list as your baseline, set it to 0 on the first run.

Run 1: the baseline

The run took a few seconds and pushed 7 job rows plus 3 company summaries (one per company). Here are two job rows (trimmed):

{"type": "job", "change_type": "new", "company_name": "ramp",
 "title": "Senior Manager, Account Executive | Mid-Market", "seniority": "manager",
 "location": "New York, NY (HQ)", "work_model": "hybrid",
 "posted_at": "2026-09-10T00:03:38.552000Z", "days_open": 51, "is_long_open": true,
 "ats": "ashby", "url": "https://jobs.ashbyhq.com/ramp/60ed4fac-3833-4495-9b0f-75e6dfe4b36c"}

{"type": "job", "change_type": "new", "company_name": "palantir",
 "title": "Sales", "seniority": "mid", "location": "New York, NY", "work_model": "hybrid",
 "posted_at": "2026-01-09T15:50:00.613000Z", "days_open": 294, "is_long_open": true,
 "ats": "lever", "url": "https://jobs.lever.co/palantir/24060586-7ba8-47c0-bfc1-f1dd5b82d8bd"}
Enter fullscreen mode Exit fullscreen mode

seniority is a rule on the job title (not AI), and is_long_open flags roles open for at least longOpenDays days (30 by default). For recruiter BD, long-open roles are often the easiest conversation: the company is already struggling to fill them.

The more interesting part is the company summary. This is Ramp's, trimmed:

{"type": "company_summary", "company_slug": "ramp", "total_open_jobs": 171,
 "posted_last_7_days": 13, "posted_last_30_days": 48, "remote_share": 0.89,
 "top_departments": ["Sales", "Engineering", "People & Talent", "Marketing", "Compliance"],
 "is_baseline_run": true,
 "signal_score": 87, "signal_tier": "hot",
 "score_reasons": [
   "+25 new demand: 48 roles posted in the last 30 days (13 in the last 7)",
   "+15 seniority: 50 manager/director/executive roles (29%), 47% senior-or-above",
   "+15 department: biggest build-out is sales (77 of 171 roles)",
   "+10 hard-to-fill: 125 roles open 30+ days (73%)",
   "+9 velocity: 13 postings this week vs 11.2/week 30-day average (1.2x pace)",
   "+8 volume: 171 open roles",
   "+5 remote: 89% of roles are remote"]}
Enter fullscreen mode Exit fullscreen mode

The same run scored Anthropic 81 (hot) and Palantir 57 (warm). Palantir's reasons show why it ranked lower:

+19 new demand: 17 roles posted in the last 30 days (2 in the last 7)
+0 velocity: 2 postings this week vs 4.0/week 30-day average (0.5x pace)
Enter fullscreen mode Exit fullscreen mode

The score is the sum of seven capped parts: recent new roles (25), velocity (20), seniority (15), function (15), long-open roles (10), volume (10) and remote share (5). Every point comes with a reason, so you can disagree with it. It is a way to sort a list, not a prediction. If you sell to engineering teams rather than sales teams, pass scoreFunctions so the function points go to the departments you care about.

Run 2: only what changed

I ran the exact same input again right after the first run. The dataset came back with 3 rows, one company summary per company, and no job rows. Ramp's, trimmed:

{"type": "company_summary", "company_slug": "ramp", "total_open_jobs": 171,
 "new_jobs_since_last_run": 0, "changed_jobs_since_last_run": 0,
 "closed_jobs_since_last_run": 0, "net_change_since_last_run": 0,
 "is_baseline_run": false, "signal_score": 87, "signal_tier": "hot"}
Enter fullscreen mode Exit fullscreen mode

That's the point: nothing changed, so there were no job rows to read and none to pay for. On a daily schedule, a later run outputs:

  • change_type: "new" for roles that weren't there before,
  • change_type: "changed" when a tracked field such as title or location changed,
  • job_closed rows for roles that disappeared (filled or pulled). Closed rows are free.

When a scan is incomplete (for example, a board was truncated), the Actor merges into the old state and does not report closures, so you don't get fake "closed" alerts.

Using it for sales or recruiter BD

A setup that works:

  1. Put 50 to 500 target accounts in companies (use ats:slug or the board URL for accounts that matter, so a bare slug can't match the wrong company).
  2. Run once with maxJobsPerCompany: 0 as the baseline.
  3. Schedule it daily or weekly with the same stateKey.
  4. Send the dataset to Slack, Google Sheets, n8n, Make or Zapier through Apify integrations.
  5. Each morning, sort company summaries by signal_score and read score_reasons. Use the new job rows (titles and departments) as the opening line for outreach.

Things it does not do: it has no contact data or email addresses, it only covers companies on those six ATS (for Workday sites there is a separate Workday Jobs Monitor), and it reads only public job postings.

Pricing

Pay per event on Apify:

  • $0.001 per job row ($1 per 1,000 jobs)
  • $0.005 per company summary
  • a $0.00005 Actor start event
  • closed-job rows and error rows are free

The two demo runs above would have been 7 job rows + 6 summaries ≈ $0.04 in total. A daily run over 100 accounts where nothing changed costs mostly the summaries (100 × $0.005 = $0.50); switch outputCompanySummary off if you only want changed jobs.

Try it on the Apify Store: ATS Jobs Monitor. The source is on GitHub: paopao-studio-ai/ats-hiring-signals. Questions and bug reports are welcome there.

Top comments (0)