# Job Posting Tech Signals — ATS Evidence (`afkdigital/job-posting-tech-signals`) Actor

Find explicit technology terms in public Greenhouse and Lever job descriptions for user-supplied company boards. Short sourced evidence; no purchase-intent claims or global company discovery.

- **URL**: https://apify.com/afkdigital/job-posting-tech-signals.md
- **Developed by:** [AFK Digital](https://apify.com/afkdigital) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 evidence-backed technology mentions

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

## Job Posting Tech Signals — Greenhouse & Lever

**Job-text technology evidence for research, not a generic job aggregator or purchasing-intent classifier.**

Get source-linked signals when **your selected companies' current public job descriptions expressly mention a technology**. Search AWS, Kubernetes, Terraform, Postgres, Snowflake, React, and your own exact terms. Each output is one distinct posting/technology pair with a concise source excerpt and the original posting URL.

### What this Actor does

- Reads the officially documented public **Greenhouse Job Board API** and **Lever Postings API v0**, for company/board identifiers you provide.
- Scans only description/body/requirements text; it does **not** infer technology from the job title, URLs, metadata tags, company marketing language, or related websites.
- Returns the company label, ATS provider, board, job title, job ID, publication date if explicitly supplied, matching technology, short snippet, posting URL, and company website only when the supplied origin is corroborated by the posting URL returned by the official ATS API.
- Deduplicates identical provider/job ID/technology combinations and limits results, requests, fetch sizes and retries.
- **Does not** provide worldwide company search, board discovery, applicant details, private postings, inferred technology adoption, vendor relationships, customer lists or purchase intent.
- **Does not** enable proxies or bypass blocked endpoints. All reads use public HTTPS GETs. No applicant forms are submitted.

### Input

Enter your terms, identify one or more public boards, and optionally restrict publication dates or results. A Greenhouse board identifier is the final path segment in the public board URL, such as discord from job-boards.greenhouse.io/discord. A Lever site identifier is the final path segment in jobs.lever.co/field-ai. Lever EU users can add region: eu.

Example input (also at examples/input.json):

```
{
  "technologyTerms": ["AWS", "Kubernetes", "Terraform", "Python"],
  "boards": [
    { "provider": "greenhouse", "board": "discord", "company": "Discord" },
    { "provider": "lever", "board": "field-ai", "company": "FieldAI" }
  ],
  "maxResults": 12,
  "maxJobsPerBoard": 12,
  "requestDelayMs": 1100
}
```

**No ATS board list is bundled.** The sample input covers only two known public ATS boards, not arbitrary companies.

Use dateFrom / dateTo formatted YYYY-MM-DD to restrict **publication dates**; undated posts are excluded when this filter is used. Greenhouse's first_published is preferred; it can be obtained from the per-job GET when not present in the list. Lever v0 typically omits first-published timestamps: those results report null instead of falsely labeling createdAt or updated_at as publication date.

The company value is a user-supplied label or board identifier, **not a verified legal entity name**. For optional website verification, provide a company's HTTPS homepage as website in the board object. The supplied origin must exactly match the non-ATS origin of the job URL returned by the official ATS API. Arbitrary homepage backlinks are NOT proof of company ownership, and no arbitrary websites are fetched. If the job is hosted on Greenhouse or Lever, or corroboration is unavailable, this field stays null. We do not guess domains from company names.

### Output and evidence

Default Dataset has one object per unique detected posting/technology pair. The run summary is in default Key-value Store at RUN_SUMMARY and reports request count, downloaded bytes, duration, inspected jobs, duplicate suppression, actual SDK charge counts, budget termination and failures. Each row has an observedAt timestamp; this is separate from the publication date.

Example output (illustrative, not a claim about any named company):

```
{
  "company": "Example Ltd",
  "provider": "greenhouse",
  "boardIdentifier": "example",
  "jobTitle": "Platform Engineer",
  "jobId": "123456",
  "publishedAt": "2026-09-24T09:00:00.000Z",
  "matchedTechnology": "AWS",
  "evidenceSnippet": "Experience deploying services on AWS and Kubernetes.",
  "jobUrl": "https://job-boards.greenhouse.io/example/jobs/123456",
  "verifiedCompanyWebsite": null,
  "signalType": "explicit_job_description_mention",
  "interpretation": "Job text mentions this technology; not verified usage, vendor-customer status or purchase intent."
}
```

A posting may mention multiple technologies, generating one evidence-backed result per technology. **An AWS mention does not establish that the company is an AWS customer, intends to purchase AWS, or has approved a budget.**

Keyword matching is conservative: whole-token boundaries, punctuation-aware matches, HTML stripping, URLs removed, and case-sensitive treatment for ambiguity-prone short terms such as Go, R, AI and React. This is evidence-based term matching, not a semantic inference engine. A company could list future interests, former technologies or "nice-to-have" skills, so human review of the linked posting is essential.

### Pricing and resource use

**Price: USD 0.002 per unique output row — USD 2 per 1,000 technology signals.**

The sole paid event is `tech-signal`. No startup fee and no additional synthetic dataset-item event are configured. A minimum run spending limit of USD 0.002 is a budget setting, not an upfront fee. A zero-match run produces zero charge events.
One matched job with AWS + Kubernetes creates **two** billable events, not one. No result means no tech-signal event. Duplicate posts or failed/unmatched records are not charged as tech-signal events. The event price shown in the Store pricing panel is authoritative. The run stops when the SDK reports that the user spending limit is reached.

Indicative event charges at USD 0.002 each: 10 results = USD 0.02; 100 results = USD 0.20. These are **not** a platform usage cost quote. Public API response sizes, selected board size and memory/time affect underlying platform compute/data cost. Greenhouse returns the full board response even when you ask to inspect only a small number of jobs. No proxy, LLM or paid data supplier is used.

Only successful pushes of unique signals are passed to the `tech-signal` event. `RUN_SUMMARY.billableSignals` counts delivered rows; `RUN_SUMMARY.chargedEvents` counts SDK charge receipts. Local callback counts are not represented as paid events. Owner validation runs are not customer revenue.

### Sources, rights and boundaries

- Greenhouse official Job Board API: https://docs.greenhouse.io/job-board.html
- Lever official Postings API: https://github.com/lever/postings-api/blob/master/README.md
- Compliance review: docs/SOURCE_RIGHTS.md

The vendors document public GET access for published postings, primarily to display companies' own career pages. Their API documentation is **not a blanket license for commercial redistribution** or an assurance that every hiring organization's own conditions allow such use. This Actor never supplies full job descriptions, does not bulk export their raw feeds and always provides source attribution; **you remain responsible for rights and permitted downstream use**. This is a paid processing tool for boards you are authorized to analyze, not a pre-licensed company database. Use company-owned or expressly permitted sources; do not treat the technical sample as a content redistribution license. Do not use it for unlicensed resale or mass republishing of job content. Review site and provider terms for your intended board list; when rights are unclear, obtain appropriate permission. Not affiliated with or endorsed by Greenhouse or Lever.

### Limits, failures and privacy

- 1–20 explicitly specified boards, 1–30 terms, max 200 signals, 300 postings inspected per board.
- 1-second minimum delay between GET attempts, 18-second per-request timeout, at most one retry for HTTP 429/503, honoring Retry-After; long cooldowns stop the board instead of retrying early. Response sizes are bounded.
- Missing/blocked boards are reported in RUN_SUMMARY without fake matches. If all boards fail, the run fails.
- No crawling of search engines, candidate profiles or application records; no anti-blocking proxy; no bulk storage of raw job descriptions.
- Individual results can change as employers edit or close postings. This is a current, best-effort snapshot, not historical archival coverage.

### Local development and verification

Within the project folder:

```
npm install --omit=dev
npm test
npm run live
```

Unit tests cover false positives, HTML decoding, duplicates, dates, safe inputs, isolated failures, correct FAILED status on invalid input, spending-limit termination, and company-domain false positives. The live sample exercises two public ATS APIs with 12 posts inspected per board; details are recorded in reports/live-validation.json.

Apify local run, after installing the Apify CLI, reads input via the default Apify storage INPUT record. For cloud deployment, only deploy from this API07 folder and use the existing authenticated Apify CLI. API04 and other projects must remain untouched.

**Support:** Report any issue in the Actor's Apify Issues section with a board identifier, job URL, a redacted input, and an Apify run ID. Do not include any Apify API tokens or applicant data.

# Actor input Schema

## `technologyTerms` (type: `array`):

Example: AWS, Kubernetes, Terraform, PostgreSQL. Ambiguous short terms (Go, R, AI, React) are matched case-sensitively.

## `boards` (type: `array`):

Explicit company ATS identifiers only. Use boards whose access and downstream-use terms permit your intended analysis. Optional website is kept only if its exact origin matches the non-ATS posting URL returned by the ATS; otherwise null.

## `dateFrom` (type: `string`):

Not an updated-date filter. Undated postings are excluded when either date filter is active.

## `dateTo` (type: `string`):

Published through (YYYY-MM-DD, optional). See README for limits and provenance.

## `maxResults` (type: `integer`):

Maximum unique technology signals. See README for limits and provenance.

## `maxJobsPerBoard` (type: `integer`):

Greenhouse returns all listings in one API response; this limit applies after retrieval. Lever is paginated.

## `requestDelayMs` (type: `integer`):

Delay between ATS requests (ms). See README for limits and provenance.

## Actor input object example

```json
{
  "technologyTerms": [
    "AWS",
    "Kubernetes"
  ],
  "boards": [
    {
      "provider": "greenhouse",
      "board": "discord",
      "company": "Discord"
    },
    {
      "provider": "lever",
      "board": "field-ai",
      "company": "FieldAI"
    }
  ],
  "maxResults": 50,
  "maxJobsPerBoard": 100,
  "requestDelayMs": 1100
}
```

# Actor output Schema

## `signals` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("afkdigital/job-posting-tech-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("afkdigital/job-posting-tech-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 '{}' |
apify call afkdigital/job-posting-tech-signals --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,afkdigital/job-posting-tech-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/W4o4hFnstR4n1HSWR/builds/diRbnjOSeSUPMbbQd/openapi.json
