DEV Community

Agents001-code
Agents001-code

Posted on

Use hiring velocity as a sales signal: track which companies are adding roles

When a company opens five finance roles in a month, it is probably about to buy finance software. When it starts hiring sales staff in a new city, it is probably about to need office space, payroll in a new state and recruiting help there too. Job postings are one of the few public signals that show where a company is putting its money, and they change week to week.

Sales and business development teams know this. The problem is the effort. Checking fifty careers pages by hand every week does not last long, and a single snapshot tells you little. What you want is the trend: which accounts are adding roles faster than usual, in which departments, and what changed since last week.

This tutorial shows how to build that trend from public job boards using the Job Postings Scraper: Company Hiring Signals Actor on the Apify Store.

Where the data comes from

Many fast-growing companies run recruiting on one of three applicant tracking systems: Greenhouse, Lever or Ashby. Each one hosts a public job board for its customers (pages like boards.greenhouse.io/<company>, jobs.lever.co/<company> and jobs.ashbyhq.com/<company>) and offers public endpoints so companies can list their open roles on their own careers sites.

The Actor reads those public endpoints for the companies you list. It uses no login and reads only jobs the company has published. It returns:

  • One record per open job: title, department, team, locations, remote flag, employment type, posted date, apply link and the salary range when the company publishes one.
  • One summary per company (optional): open roles, roles by department, roles posted in the last 7, 30 and 90 days, hiring velocity, median role age, and how many roles opened or closed since your last run.

It does not return job descriptions, recruiter names or contact details.

The Actor is independent and is not affiliated with, endorsed by or sponsored by Greenhouse, Lever, Ashby or any company whose jobs it returns.

What "hiring velocity" means here

In the company summary, hiringVelocityPerWeek is the number of roles posted per week, averaged over the last 30 days. A company with 36 roles posted in the last 30 days has a velocity of 8.4.

Velocity is more useful than the open role count on its own. A company with 150 open roles might have had most of them open for months. A company with 30 open roles that posted 20 of them this month is moving. medianRoleAgeDays helps you tell the two apart: a low median age means most roles are recent.

Step 1: Run it once in the Apify Console

Open the Store page (link at the end). With an empty company list the Actor runs a small example set, which is a good way to see the output.

Then fill in your own:

  • Companies: one entry per company. The most reliable format is greenhouse:<board>, lever:<site>, lever-eu:<site> or ashby:<name>. You can also paste a job board URL, a careers page URL, or just a name, and the Actor will try to find the board.
  • Department or team contains: for example Finance, Sales, Engineering.
  • Job title contains and Job title does not contain: for example include Account Executive, exclude Intern.
  • Location contains: for example London, Germany, Remote.
  • Remote jobs: any, remote only, or exclude remote.
  • Posted in the last N days: 0 means any age.
  • Add a hiring summary per company: on by default.
  • Maximum jobs per company and Maximum job records: caps on job records. The company summary still counts every open role.

To find a company's board, open its careers page, click any job and look at the link. jobs.lever.co/acme means lever:acme.

Step 2: Schedule it daily with onlyNew

  1. Switch on Only jobs not seen in previous runs (onlyNew). The Actor remembers which job IDs it has returned for this exact input, in a key-value store in your account (hiring-signals-feed-state by default). Later runs return only jobs that are new since then.
  2. Leave Add a hiring summary per company on. The summary's newRolesSinceLastRun, closedRolesSinceLastRun and netChangeSinceLastRun use the same history, so each run tells you what changed.
  3. Save the input as a task. In Schedules, create a schedule (for example cron 0 7 * * 1-5 in your timezone) and add the task.

The first run returns every matching job, and the "since last run" fields are empty. From the second run on, you get only new jobs and real change numbers. Changing the company list or filters starts a fresh history, so settle your filters before you rely on the trend.

Step 3: Rank accounts by velocity in Python

The official apify-client package runs the Actor and reads the results. Install it with pip install apify-client and copy your API token from the Integrations section of the Console.

This script runs the Actor over a list of target accounts, then ranks them by hiring velocity and flags any account with a jump in open roles since the last run.

import os

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run_input = {
    "companies": [
        "greenhouse:figma",
        "greenhouse:discord",
        "lever:spotify",
        "ashby:linear",
        "ashby:ramp",
    ],
    "onlyNew": True,
    "includeCompanySummary": True,
    "includeSalary": True,
}

run = client.actor("agents001/company-hiring-signals-feed").call(
    run_input=run_input,
    max_total_charge_usd=3,
)
if run is None:
    raise SystemExit("Run failed to start or finish")

summaries, new_jobs = [], []
for item in client.dataset(run.default_dataset_id).iterate_items():
    if item["recordType"] == "companySummary":
        summaries.append(item)
    else:
        new_jobs.append(item)

summaries.sort(key=lambda s: s["hiringVelocityPerWeek"] or 0, reverse=True)

print(f"{'Company':<12}{'Open':>6}{'7d':>5}{'30d':>6}{'Per wk':>8}  Top departments")
for s in summaries:
    print(
        f"{s['company']:<12}{s['openRoles']:>6}{s['postedLast7Days']:>5}"
        f"{s['postedLast30Days']:>6}{s['hiringVelocityPerWeek'] or 0:>8}  {s['topDepartments']}"
    )
    net = s.get("netChangeSinceLastRun")
    if net is not None and net >= 10:
        print(f"  -> {s['company']} added a net {net} roles since the last run")

print(f"\n{len(new_jobs)} new job records this run")
Enter fullscreen mode Exit fullscreen mode

On a sample run over those five companies, the ranking came out like this:

Company       Open   7d   30d  Per wk  Top departments
Ramp           168   15    51    11.9  Sales (67), Engineering (38), People & Talent (15), ...
Figma          142   11    36     8.4  Sales (44), Engineering (28), Early Career (11), ...
Spotify         81    5    34     7.9  Engineering (32), Markets and Subscriber Growth (9), ...
Discord         46    2    14     3.3  Product Engineering (12), Developers (7), ...
Linear          27    1     3     0.7  Product (13), GTM (12), Operations (2)
Enter fullscreen mode Exit fullscreen mode

Linear has 27 open roles but a median role age of 144 days and only three roles posted in the last month. Ramp is posting about twelve roles a week, most of them in sales. Those are two very different conversations for a seller.

What the output looks like

A job record, trimmed:

{
  "recordType": "job",
  "company": "Ramp",
  "companyKey": "ashby:ramp",
  "ats": "ashby",
  "title": "Senior Backend Engineer | Token Spend Management",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)"],
  "remote": true,
  "workplaceType": "hybrid",
  "employmentType": "full-time",
  "postedAt": "2026-10-20T13:40:20Z",
  "ageDays": 0,
  "salaryText": "USD 204,000 - 280,000 per year",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/b7514711-8a29-4bd2-a97a-967be14e336f",
  "isNew": true
}
Enter fullscreen mode Exit fullscreen mode

A company summary, trimmed:

{
  "recordType": "companySummary",
  "company": "Figma",
  "companyKey": "greenhouse:figma",
  "openRoles": 142,
  "topDepartments": "Sales (44), Engineering (28), Early Career (11), Business Operations (10), Design (10)",
  "postedLast7Days": 11,
  "postedLast30Days": 36,
  "postedLast90Days": 75,
  "hiringVelocityPerWeek": 8.4,
  "medianRoleAgeDays": 87,
  "remoteShare": 0.0,
  "rolesWithSalary": 93,
  "newRolesSinceLastRun": null,
  "closedRolesSinceLastRun": null,
  "netChangeSinceLastRun": null
}
Enter fullscreen mode Exit fullscreen mode

The null change fields are expected on a first run.

What it costs

The Actor uses pay-per-event pricing:

  • $3 per 1,000 job records ($0.003 each)
  • $0.005 per company summary

A daily onlyNew run over 50 target accounts costs $0.25 for the summaries plus $0.003 per new job. If those accounts post 60 new matching roles a day between them, that is $0.43 a day, or about $9 over 21 working days.

Watch the first run. It returns every matching open job, so 50 companies with 100 open roles each would be 5,000 records, or $15. Department, title and location filters cut that down, and Maximum jobs per company caps job records while the summary still counts every open role. You can also set a maximum cost per run. Check the Store page for current pricing.

What to do with the data

  • CRM: write hiringVelocityPerWeek, postedLast30Days and topDepartments onto the account record in your CRM through Zapier, Make or a webhook, and sort your territory by them.
  • Google Sheets: send the Company summaries view to a sheet each morning and chart velocity per account over time.
  • Alerts: post to Slack when an account's netChangeSinceLastRun crosses a threshold, or when a new role appears in the department you sell into.
  • Territory planning: use topLocations and new locations in job records to spot accounts expanding into your region.

Limitations

  • Only Greenhouse, Lever and Ashby boards are supported. Companies on Workday, SmartRecruiters, iCIMS and other systems are not covered yet.
  • You supply the company list. The Actor does not discover companies for you.
  • A posting is an intent to hire, which is different from a hire. Some roles stay open for months and some are evergreen listings.
  • Department names are whatever each company uses, so "GTM" at one company may be "Sales" at another.
  • Employment type and department are only filled when the company fills them. Salary appears only where the company publishes it, which is more common for US roles.
  • Job descriptions are not included.

Try it

https://apify.com/agents001/company-hiring-signals-feed

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.