Score Hiring Intent Signals from Prospect Career Boards
Turn a target account list into a ranked hiring intent score using postings from each company's own career board across Workday, Greenhouse, Lever and Ashby.

Hiring intent signals are buying triggers you can read directly off a prospect's own career board: which departments are opening requisitions, which teams appear for the first time, and which tools the job descriptions name. The Multi-ATS Jobs Scraper reads Workday, Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and BambooHR boards in a single run and returns one flat schema. Growth teams score accounts from that feed and hand SDRs a ranked list with the evidence attached.
Why score hiring intent from prospect career boards
A static ICP list tells you who could buy. A hiring signal tells you who is moving. When a mid-market fintech opens its first "Revenue Operations" req, someone just got a budget line and a mandate — and that is a materially better reason to call than "matches our firmographic filter." The same logic holds for a security team hiring its second detection engineer, or a support org posting three roles in a market you sell into.
Job openings are one of the most-tracked demand indicators in the economy for exactly this reason; the US Bureau of Labor Statistics has run its Job Openings and Labor Turnover Survey since 2000 precisely because open roles lead spending. What JOLTS gives you in aggregate, a career board gives you per account.
The catch is where you read it. Most GTM teams buy an intent feed that models signals from aggregator listings, and the modelling smooths away the parts that matter: the department label, the team name, the tooling stack listed in the description. Reading the employer's own board keeps the employer's own words. That is the difference between "they are hiring engineers" and "they are hiring a Platform Engineer whose description names three of your five integrations."
How does this compare to the alternatives?
Three practical options exist for turning career boards into an account score. Writing your own collectors means eight different pagination models, eight different JSON shapes, and a schema that changes whenever one vendor ships a redesign. A generic scraping API gets you HTML, which you then have to parse into a comparable structure yourself — the expensive half of the job. An intent-data subscription gives you a score but not the underlying posting text, so you cannot audit why an account ranked where it did or tune the weights to your category.
| Approach | Cost model | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| DIY collectors per ATS | Your engineering time | Breaks per-vendor; silent zero-row runs | 2-4 weeks for eight systems | Ongoing, per ATS |
| Generic scraping API | Subscription, priced on requests | Fetches pages; parsing is on you | Days, then weeks of normalisation | You own the parsers |
| Multi-ATS Jobs Scraper | Transparent pay-per-result | Unified schema across 8 systems, deduped | Under an hour | Handled for you |
How to score hiring intent from career boards in 4 steps
Which career boards do my target accounts actually use?
Start by resolving each account to a board URL, because that is the only mapping work in this whole pipeline. Almost every company career page redirects to, or embeds, one of eight systems, and the hostname tells you which: job-boards.greenhouse.io, jobs.lever.co, jobs.ashbyhq.com, apply.workable.com, jobs.smartrecruiters.com, *.recruitee.com, *.bamboohr.com/careers and *.myworkdayjobs.com. Paste the URL you land on and the actor identifies the system itself.
For accounts you already know, the platform:slug shorthand is faster to maintain in a spreadsheet than a full URL:
# accounts.py — one row per target account
TARGET_ACCOUNTS = {
"Stripe": "greenhouse:stripe",
"Spotify": "lever:spotify",
"Linear": "ashby:linear",
"Ramp": "ashby:ramp",
"bunq": "recruitee:bunq",
"NVIDIA": "workday:nvidia/wd5/NVIDIAExternalCareerSite",
}
board_urls = list(TARGET_ACCOUNTS.values())Workday needs three parts — tenant, shard and site name — because that is genuinely how Workday addresses a board. Pasting the full board URL avoids having to know that.
How do I pull every account's board without one enterprise swamping the run?
Cap the run per board, not just in total. A 2,000-role Workday board and a 40-role Ashby board are equally interesting as accounts, but wildly unequal as row counts, and without a per-board cap the enterprise account eats the entire budget before the startups are read. maxResultsPerBoard is the control that makes an account-level run comparable:
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("thirdwatch/multi-ats-jobs-scraper").call(run_input={
"boardUrls": board_urls,
"maxResults": 3000,
"maxResultsPerBoard": 150, # every account contributes at most 150 rows
"includeDescription": True, # description text drives the tooling signal
"locationFilter": "", # keep all geographies; filter at scoring time
"remoteOnly": False,
})
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(f"{len(items)} postings across {len({i['company_slug'] for i in items})} boards")Keep includeDescription on for intent work — the description is where tooling, team charter and scope live. Turn it off only when you want a fast listing-only count. Each row carries source_query, the exact input entry that produced it, so joining rows back to your CRM account ID is a dictionary lookup rather than a fuzzy name match.
How do I turn postings into a hiring intent score per account?
Score on three things you can defend to a sales leader: volume, composition and category proximity. Volume is how many roles are open. Composition is which departments and teams they sit in. Category proximity is whether the description mentions the problem you solve. Weight them explicitly so the score can be argued with:
from collections import defaultdict
CATEGORY_TERMS = ["data warehouse", "snowflake", "dbt", "reverse etl", "customer data"]
BUYER_DEPARTMENTS = {"revenue operations", "growth", "marketing", "data", "analytics"}
scores = defaultdict(lambda: {"roles": 0, "buyer_roles": 0, "mentions": 0, "evidence": []})
for job in items:
account = job["source_query"]
dept = (job.get("department") or "").lower()
team = (job.get("team") or "").lower()
text = (job.get("description") or "").lower()
s = scores[account]
s["roles"] += 1
if dept in BUYER_DEPARTMENTS or any(d in team for d in BUYER_DEPARTMENTS):
s["buyer_roles"] += 1
s["evidence"].append((job["title"], job["job_url"]))
if any(term in text for term in CATEGORY_TERMS):
s["mentions"] += 1
s["evidence"].append((job["title"], job["job_url"]))
ranked = sorted(
((a, 1 * v["roles"] + 5 * v["buyer_roles"] + 8 * v["mentions"], v) for a, v in scores.items()),
key=lambda r: r[1], reverse=True,
)The multipliers are the part you tune. A single posting that names your category is worth more than ten unrelated openings, so mentions outweigh raw volume. Use department, team and title for composition and reserve description for the category test — matching your terms against titles alone will miss most of the real signal.
How do I hand SDRs a ranked account list with the evidence attached?
Ship the reason, not just the number. A ranked CSV that carries the posting title and its canonical job_url gives a rep an opening line and a link they can read before dialling, which is what separates a used list from an ignored one:
import csv
with open("hiring_intent.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["account", "score", "open_roles", "buyer_roles", "top_evidence", "evidence_url"])
for account, score, v in ranked[:50]:
title, url = (v["evidence"][0] if v["evidence"] else ("", ""))
w.writerow([account, score, v["roles"], v["buyer_roles"], title, url])Re-run the same input weekly and diff the scores. An account whose buyer_roles count goes from zero to two in a week is a far stronger trigger than one sitting at a steady four — a rising score means something changed inside the org this week. For the mechanics of computing that delta cleanly, including which postings disappeared, see detecting closed postings with an incremental sync.
Sample output
Every posting from every system returns the same flat keys, so a Greenhouse row and an Ashby row concatenate without post-processing. These are the fields that matter for scoring: department and team for composition, description for category proximity, posted_at for recency weighting, and source_query for joining back to your CRM.
[
{
"platform": "ashby",
"company": "ramp",
"company_slug": "ramp",
"job_id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"title": "Security Engineer, Cloud",
"department": "Engineering",
"team": "Backend",
"employment_type": "FullTime",
"workplace_type": "hybrid",
"is_remote": true,
"location": "New York, NY (HQ), Remote (US)",
"city": "New York City",
"region": "NY",
"country": "USA",
"country_code": "",
"posted_at": "2026-04-07T17:12:35.753000Z",
"updated_at": "",
"salary_min": 211400,
"salary_max": 290600,
"salary_currency": "USD",
"salary_period": "year",
"requisition_id": "",
"job_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"apply_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
"description": "You will partner with platform teams to secure our cloud footprint...",
"board_url": "https://jobs.ashbyhq.com/ramp",
"source_query": "ashby:ramp",
"scraped_at": "2026-07-27T22:56:42.869947Z"
},
{
"platform": "greenhouse",
"company": "Stripe",
"company_slug": "stripe",
"job_id": "6421887",
"title": "Revenue Operations Manager",
"department": "Sales",
"team": "",
"employment_type": "",
"workplace_type": "onsite",
"is_remote": false,
"location": "San Francisco, CA",
"city": "",
"region": "",
"country": "",
"country_code": "",
"posted_at": "2026-07-14T09:02:11Z",
"updated_at": "",
"salary_min": null,
"salary_max": null,
"salary_currency": "",
"salary_period": "",
"requisition_id": "",
"job_url": "https://job-boards.greenhouse.io/stripe/jobs/6421887",
"apply_url": "https://job-boards.greenhouse.io/stripe/jobs/6421887#app",
"description": "Own forecasting, territory design and the reporting stack...",
"board_url": "https://job-boards.greenhouse.io/stripe",
"source_query": "greenhouse:stripe",
"scraped_at": "2026-07-27T22:56:44.114201Z"
}
]Common pitfalls
Field coverage varies by system, and it varies a lot. Every board publishes a title, location, ID and apply URL. Everything beyond that depends on how the employer configured their board. Across a mixed eight-board test run, department was populated on 88% of rows, description on 88%, posted_at on 100%, city on 48% and salary on 0% — none of those employers published ranges. Score on what is present rather than assuming a field is universal, and treat a missing department as unknown, not as zero.
Very large Workday boards under-report. Workday returns a display-capped total, commonly 2,000, and paging beyond it silently restarts at page one. The actor stops at the reported total, so a huge enterprise board may return fewer roles than the company actually has open. Score those accounts on composition rather than raw volume.
A wrong slug can look exactly like an empty board. SmartRecruiters answers an unknown company with a successful, empty response instead of a not-found, so a typo produces the same output as a company with nothing open. Validate new slugs on their first run before an account silently sits at score zero forever.
Keyword matching is not identical across systems. Workday and Workable search server-side; the other six are read in full and matched locally against title, department, team and description. For scoring, leave searchText empty and do your matching in your own code, where the rules are consistent.
Related use cases
- Track headcount changes at target accounts — pair posting-based intent with employee-count deltas for a two-source signal.
- Detect closed job postings with an incremental ATS sync — the delta mechanics behind a weekly score refresh.
- Audit live job postings against your ATS requisition data — the recruiting-ops view of the same board data.
- Build a pay-transparency salary dataset from ATS boards — turn published ranges into a benchmark.
- Find which companies hire remotely by country — segment your account list by where employers will actually hire.
- Guide to scraping job data — the category pillar, plus the rest of the Thirdwatch blog.
Frequently asked questions
What are hiring intent signals?
Hiring intent signals are buying triggers inferred from a company's open roles. A new team, a first hire in a function, or a tool named in a job description all imply budget and ownership for a category, which is why GTM teams score accounts on them.
Why read a company's own career board instead of an aggregator?
The employer's board carries the department, team and description text exactly as the employer wrote it, plus the canonical apply link and requisition ID. Aggregator copies are frequently truncated, re-titled or days stale, which corrupts scores that depend on wording.
How many accounts can I score in one run?
The actor accepts a list of career board URLs and caps both total postings and postings per board, so a 300-account run stays predictable. Set maxResultsPerBoard so a 2,000-role enterprise board cannot consume the budget meant for smaller accounts.
Do I get salary data for scoring seniority?
Only when the employer publishes a range. Pay bands appear most often on Ashby and Lever boards and almost never on Workday or BambooHR. There is no estimation, so treat salary as a bonus signal rather than a scoring input.
How often should I refresh hiring intent scores?
Weekly matches how most employers post. A weekly cadence gives you a clean delta between snapshots — new departments, first hires, sudden volume increases — without so much noise that every re-run looks like a change worth alerting on.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.