# IT Hiring Signals: Tech Stack from Job Postings (`fecklesspleb/it-hiring-signals`) Actor

Technographics from public Greenhouse, Lever and Ashby job postings: each job tagged with the technologies it mentions (Intune, Okta, AWS, ServiceNow, Kubernetes and 320+ more). Filter by title, location, date or tech category, get only new jobs, add per-company tech summaries as hiring signals.

- **URL**: https://apify.com/fecklesspleb/it-hiring-signals.md
- **Developed by:** [Scott Williams](https://apify.com/fecklesspleb) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## IT Hiring Signals: Tech Stack from Job Postings

**Technographics and hiring signals from public job postings.** This Actor reads public Greenhouse, Lever and Ashby job boards and tags each job posting with the technologies it mentions, such as Microsoft Intune, Okta, AWS, ServiceNow, Kubernetes and 320+ more (328 in the built-in dictionary). You can filter by title, location, date or tech category, return only jobs that are new since the last run, and add a summary row per company with its top technologies.

**At a glance:** $1.50 per 1,000 tagged jobs, with company summaries $3.00 per 1,000 (optional). Pay per result, with no subscription. You can set a maximum cost per run, export to JSON, CSV or Excel, or call it from the Apify API, Make, Zapier or n8n.

### Who it is for

- **MSPs and IT-services sales teams:** find companies hiring a "Microsoft Intune administrator" or an "Okta engineer". These are hiring-intent signals that a team runs, or is moving to, that stack.
- **SaaS sales and partnerships:** build account lists of companies whose job postings mention your product, a competitor or an integration partner.
- **Recruiters and staffing firms:** track which companies are hiring for a skill, and get only new postings each day.
- **Market research:** count how often technologies appear across a set of companies' job postings over time.

### Input example

```json
{
  "companies": ["ashby:ashby", "greenhouse:gitlab", "lever:palantir", "https://jobs.lever.co/somecompany"],
  "titleFilter": "engineer|admin|it support|sysadmin",
  "techCategories": ["endpoint-mgmt", "identity", "security"],
  "onlyTaggedJobs": true,
  "postedSinceDays": 30,
  "onlyNew": false,
  "includeCompanySummaries": true,
  "maxTotalJobs": 1000
}
```

- **companies**: board slugs (`gitlab`), `ats:slug` (`greenhouse:gitlab`) or board URLs. A bare slug is tried on Greenhouse, then Lever, then Ashby. You can also give a careers-page URL, and the Actor looks for an embedded Greenhouse, Lever or Ashby board on it. Entries that cannot be resolved produce a free `unresolved` row, and the run continues.
- **Empty input** (no companies, searchDirectory off) runs a small demo on 3 public example boards (`ashby:ashby`, `greenhouse:gitlab`, `lever:palantir`), at most 4 jobs, with a warning in the log and `demoMode: true` in `RUN_SUMMARY`. Demo jobs are real live jobs and are charged like any other job: 4 x $0.0015 = $0.006 at most. The Console form is prefilled with the same demo (those 3 boards, `maxTotalJobs` 4); raise `maxTotalJobs` for real runs.
- **searchDirectory** / **maxDirectoryCompanies**: adds up to 200 boards from the built-in directory of known Greenhouse, Lever and Ashby companies (default 50; 0 adds none).
- **techKeywords**: your own terms, as `Tag` or `Tag=alias1|alias2` (up to 200). **useDefaultDictionary** turns the built-in dictionary of 328 technologies on or off.
- **techCategories**, **requiredTechTags**, **onlyTaggedJobs**: keep only jobs that mention a category or specific tags, or any tag at all.
- **titleFilter** (a case-insensitive regular expression; use `\bit\b` rather than `it` so it does not match "Benefits"), **keywords**, **excludeKeywords**, **locations**, **departments**, **remoteOnly**, **postedSinceDays**: the usual job filters.
- **onlyNew**: return only jobs that earlier runs have not seen. The "first seen" state is kept in a named key-value store (default `it-hiring-signals-state`).
- **includeDescription** (default off): adds the plain-text description. Personal e-mail addresses, phone numbers, "your recruiter" lines and LinkedIn profile links are masked.
- Limits: **maxJobsPerCompany** (0 means no limit, max 5,000), **maxTotalJobs** (default 1,000, max 20,000; the form is prefilled with 4 for the demo), **maxConcurrency** (default 4, max 10). When `maxTotalJobs` is 100 or less, 2 or more boards are read and `maxJobsPerCompany` is not set, the cap is shared round-robin: a board takes its next job only when no other board still being read has fewer, so one fast board cannot fill the whole cap (the 4-job demo returns jobs from all 3 boards). `capSharedRoundRobin` in `RUN_SUMMARY` says whether this applied. Larger caps keep first-come order.

Categories: ai-ml, backup-storage, business-apps, cloud, collaboration, compliance, data, database, devops, endpoint-mgmt, frameworks, identity, itsm, languages, networking, observability, os, rmm-msp, security, virtualization.

### Output example

A job row (from a live test run on 2026-10-28; description omitted):

```json
{
  "id": "8771512002",
  "ats": "greenhouse",
  "companySlug": "gitlab",
  "companyName": "GitLab",
  "title": "Backend Engineer, Knowledge Graph",
  "department": "Data Engineering",
  "team": null,
  "location": "Remote, Canada; Remote, United States",
  "locations": [
    "Remote, Canada; Remote, United States",
    "North America"
  ],
  "country": null,
  "isRemote": true,
  "workplaceType": null,
  "employmentType": null,
  "workplaceTypeRaw": null,
  "employmentTypeRaw": null,
  "postedAt": "2026-10-23T22:47:07+00:00",
  "updatedAt": "2026-10-28T16:23:39+00:00",
  "url": "https://job-boards.greenhouse.io/gitlab/jobs/8771512002",
  "applyUrl": "https://job-boards.greenhouse.io/gitlab/jobs/8771512002",
  "salaryMin": 115200,
  "salaryMax": 162000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "salaryText": "$115,200-$162,000 USD",
  "salarySource": "description",
  "hiringEntity": null,
  "requisitionId": "6918",
  "uid": "greenhouse:gitlab:8771512002",
  "expiresAt": null,
  "postedAtPrecision": "exact",
  "salaryAnnualMin": 115200,
  "salaryAnnualMax": 162000,
  "workArrangement": "remote",
  "seniority": null,
  "dedupeKey": "7b15d4aedcb0c49b81df",
  "firstSeenAt": "2026-10-29T02:05:28.496152+00:00",
  "lastSeenAt": "2026-10-29T02:05:28.496152+00:00",
  "scrapedAt": "2026-10-29T02:05:28.496152+00:00",
  "techTags": [
    "Rust",
    "Ruby",
    "ClickHouse",
    "Apache Kafka",
    "TypeScript",
    "Vue.js"
  ],
  "techCategories": [
    "data",
    "database",
    "frameworks",
    "languages"
  ],
  "isNew": true,
  "recordType": "job"
}
```

A company summary row (`includeCompanySummaries`), shortened to the top 5 tags:

```json
{
  "recordType": "companySummary",
  "ats": "ashby",
  "companySlug": "ashby",
  "companyName": "Ashby",
  "jobsRead": 15,
  "jobsMatched": 10,
  "jobsReturned": 10,
  "taggedJobsCount": 10,
  "topTechTags": [
    {
      "tag": "GraphQL",
      "count": 10
    },
    {
      "tag": "Node.js",
      "count": 10
    },
    {
      "tag": "PostgreSQL",
      "count": 10
    },
    {
      "tag": "React",
      "count": 10
    },
    {
      "tag": "Redis",
      "count": 10
    }
  ],
  "boardFullyRead": false,
  "scrapedAt": "2026-10-29T02:05:44.062121+00:00"
}
```

- `techTags` are sorted by how often each one is mentioned. Turn on `includeTechMentions` to get the counts in `techMentions`, plus `techContext`, which marks each tag `requirement` (named in the title, a requirements, qualifications or tech-stack section, or next to a phrase such as "experience with") or `mention` (named elsewhere in the post).
- `isNew` is true the first time a job is seen (it is null when `trackFirstSeen` is off).
- `jobsRead` is the number of postings read from the board. Reading stops early once `maxJobsPerCompany` or `maxTotalJobs` is reached; `boardFullyRead` is false when that happened.
- `taggedJobsCount` is the number of returned jobs with at least one tag.
- Field names follow the shared ATS engine (`id`, `companyName`, `isRemote`), not `jobId`/`company`/`remote`. The company domain is not included, because job boards do not reliably publish it.

### Price per 1,000 jobs

Pay per event, with no start fee:

| Event | Price | Per 1,000 |
|---|---|---|
| Tagged job (`job`) | $0.0015 | $1.50 |
| Company summary (`company-summary`, optional) | $0.003 | $3.00 |
| Unresolved-entry row | free | free |
| Status row (only when a run returns no jobs) | free | free |

Platform usage is included: you pay only for the events above. The run stops cleanly when your maximum cost per run is reached.

A run never ends with an empty dataset. If no board can be read (network errors, 404s, robots.txt, private addresses), the run still succeeds and saves one free status row (`recordType: "status"`, `statusRow: true`, `status: "error"`, `reason: "allBoardsFailed"`) with the number of failed boards (`boardsFailed`, split into `boardsBlocked`, `boardsNetworkError`, `boardsNotFound`, `boardsSkippedRobots`) and the first errors in `failedBoards`. If boards were read but no job matched (filters, `onlyNew`), the row has `status: "warning"` and `reason: "noJobs"`. Its `message` starts with "Not charged:".

### FAQ and limits

- **Can it read pages on private or internal addresses?** No. On the Apify platform a careers-page URL whose host resolves to a private, loopback or link-local address (also after a redirect) is not fetched and costs nothing. The entry gets a free `unresolved` row with `reason` `"not fetched: private, loopback and link-local addresses are not allowed"`.
- **Which sources?** Only the public, keyless job-board APIs of Greenhouse (`boards-api.greenhouse.io`), Lever (`api.lever.co`, `api.eu.lever.co`) and Ashby (`api.ashbyhq.com/posting-api`). No logins, no proxies and no anti-bot workarounds. robots.txt is checked (RFC 9309) before each host is fetched, and its Crawl-delay is honoured. If a host's robots.txt cannot be read (a server error, a page that is not a robots file, or a network failure), that host is skipped and the run log says so. A missing robots.txt (4xx) means no rules.
- **How is tagging done?** By whole-word matching against a curated dictionary. Ambiguous words only match in safe forms: "Palo Alto" only as "Palo Alto Networks", "Autopilot" only as "Windows Autopilot", and short acronyms such as ZIA or JSM only in capitals. Mentions of the employer's own products are dropped. So are sentences that list the employer's customers or investors ("customers like Notion and Snowflake").
- **What about comparisons and chat tools?** Text that names a product only to compare with it, to say people switched away from it, or to describe a competitor is skipped: "engineers switched from languages like Swift and Kotlin" and "the playbook Workday used to win the HCM market" add no tags, while "experience with Swift" still does. Slack, Zoom, Microsoft Teams, Webex and Google Meet count only in a requirements, qualifications or tech-stack section, or next to a phrase such as "experience with", "administer", "manage" or "support", so "our company Slack" and "coffee over Zoom" are not tagged.
- **Is it perfect?** No. A tag means the posting *mentions* the technology, not that the company runs it, and the context rules are wording-based, so some odd phrasings will slip through or be dropped. Check the description (`includeDescription`) before you act on a single tag.
- **Personal data?** Only job-posting fields are returned. Descriptions are off by default; when they are on, contact details and profile links are masked.
- **What about other ATSs?** Workday, Workable, SmartRecruiters and others are not supported in this Actor. Such entries return a free `unresolved` row.
- **Speed?** About one request per board for Greenhouse and Ashby, and one per 100 jobs for Lever (with a 1-second crawl delay). In tests, 3 boards took about 17 seconds, and 1,000 jobs used under 110 MB of memory.

# Changelog

This Actor's version history is a separate document: https://apify.com/fecklesspleb/it-hiring-signals/changelog.md

# Actor input Schema

## `companies` (type: `array`):

One entry per company: a board URL (https://boards.greenhouse.io/<token>, https://jobs.lever.co/<slug>, https://jobs.ashbyhq.com/<slug>), an explicit 'greenhouse:<token>', 'lever:<slug>' or 'ashby:<slug>', a bare slug or company name (the Actor tries Greenhouse, Lever and Ashby), or a careers page that embeds one of these boards. Other ATSs are skipped with a free 'unresolved' record. Up to 500 entries. Leave empty (with 'searchDirectory' off) for a demo run on 3 public example boards (ashby:ashby, greenhouse:gitlab, lever:palantir), at most 4 jobs (about $0.006).

## `ats` (type: `string`):

How to read bare slugs such as 'stripe'. 'Auto-detect' probes Greenhouse, Lever and Ashby and keeps the board with the most jobs. URLs and 'ats:slug' entries ignore this setting.

## `searchDirectory` (type: `boolean`):

Add boards from the bundled directory of verified public Greenhouse, Lever and Ashby boards (largest first). Use with filters and 'Max directory companies'.

## `maxDirectoryCompanies` (type: `integer`):

How many directory boards to add when the directory is on (largest first). 0 adds none.

## `useDefaultDictionary` (type: `boolean`):

Tag jobs with the bundled dictionary (328 technologies in 20 categories). Matching is whole-word; see the README for how ambiguous words are handled.

## `techKeywords` (type: `array`):

Your own terms, tagged with category 'custom'. Use 'Tag=alias1|alias2' to give one tag several spellings, e.g. 'Microsoft Graph=Graph API|MS Graph'. Up to 200 terms.

## `techCategories` (type: `array`):

Keep only jobs that mention at least one technology in one of these categories. Leave empty to keep all.

## `requiredTechTags` (type: `array`):

Keep only jobs tagged with at least one of these tags (exact tag names as they appear in techTags, case-insensitive), e.g. 'Microsoft Intune', 'Okta'.

## `onlyTaggedJobs` (type: `boolean`):

Drop jobs that mention no technology from the dictionary (and are therefore not charged).

## `includeTechMentions` (type: `boolean`):

Add techMentions (how many times each technology appears in the title and description) and techContext (requirement or mention, for each tag).

## `titleFilter` (type: `string`):

Keep jobs whose title matches this regular expression (case-insensitive), e.g. '(IT|systems|endpoint|security|devops|cloud) (engineer|administrator|analyst|manager)'.

## `keywords` (type: `array`):

Keep jobs whose title contains at least one of these words (case-insensitive).

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these words, e.g. 'intern'.

## `searchInDescription` (type: `boolean`):

Also match 'Title keywords' against the description.

## `locations` (type: `array`):

Keep jobs whose location contains one of these strings, e.g. 'London', 'United States', 'Remote'.

## `departments` (type: `array`):

Keep jobs whose department or team contains one of these strings, e.g. 'IT', 'Security'.

## `remoteOnly` (type: `boolean`):

Keep only jobs marked or described as remote.

## `postedSinceDays` (type: `integer`):

Keep jobs first published within this many days. Leave empty for all.

## `includeUndatedJobs` (type: `boolean`):

When 'Posted within' is set, keep jobs whose board gives no publish date.

## `onlyNew` (type: `boolean`):

Return only jobs not returned by an earlier run that used the same state store. The first run returns everything. Seen jobs are remembered for 120 days.

## `trackFirstSeen` (type: `boolean`):

Remember when each job was first seen, so isNew and firstSeenAt work across runs. Turn off to keep no state (isNew is then null).

## `stateStoreName` (type: `string`):

Named key-value store that holds seen-job history. Use different names for separate monitors.

## `includeCompanySummaries` (type: `boolean`):

After the jobs, add one row per board read: jobs read and returned, the number of jobs with at least one tag, and the top 15 technologies. Counts cover the jobs returned in this run. Charged as 'company-summary'.

## `includeDescription` (type: `boolean`):

Keep the plain-text description in each row (contact e-mails and phone numbers are masked). Off by default to keep rows small; tagging always reads the description.

## `maxJobsPerCompany` (type: `integer`):

Stop after this many matching jobs per board. 0 means no per-board limit.

## `maxTotalJobs` (type: `integer`):

Stop the run after this many jobs (summary rows not counted). Default 1,000, hard cap 20,000. The form is prefilled with 4 for a cheap demo run; raise it for real runs.

## `maxConcurrency` (type: `integer`):

How many boards are read at the same time. Requests to each API host are always spaced at least 0.25 s apart and follow robots.txt crawl-delay.

## Actor input object example

```json
{
  "companies": [
    "ashby:ashby",
    "greenhouse:gitlab",
    "lever:palantir"
  ],
  "ats": "auto",
  "searchDirectory": false,
  "maxDirectoryCompanies": 50,
  "useDefaultDictionary": true,
  "onlyTaggedJobs": false,
  "includeTechMentions": false,
  "searchInDescription": false,
  "remoteOnly": false,
  "includeUndatedJobs": true,
  "onlyNew": false,
  "trackFirstSeen": true,
  "stateStoreName": "it-hiring-signals-state",
  "includeCompanySummaries": false,
  "includeDescription": false,
  "maxJobsPerCompany": 0,
  "maxTotalJobs": 4,
  "maxConcurrency": 4
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "ashby:ashby",
        "greenhouse:gitlab",
        "lever:palantir"
    ],
    "maxTotalJobs": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("fecklesspleb/it-hiring-signals").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "ashby:ashby",
        "greenhouse:gitlab",
        "lever:palantir",
    ],
    "maxTotalJobs": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("fecklesspleb/it-hiring-signals").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "ashby:ashby",
    "greenhouse:gitlab",
    "lever:palantir"
  ],
  "maxTotalJobs": 4
}' |
apify call fecklesspleb/it-hiring-signals --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fecklesspleb/it-hiring-signals"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ABshJkX3axiG0PT6I/builds/MhmXvrh3CEg4OnQZR/openapi.json
