Job Posting Buying Signal Extractor
Pricing
from $8.40 / 1,000 hiring signals
Job Posting Buying Signal Extractor
Turn job posting datasets into company hiring signals with matched text and deduplicated job evidence. Identify recurring role demand using transparent rules for sales and staffing research.
Job Posting Buying Signal Extractor — turn an existing job-postings dataset into company hiring-need signals
Feed it an existing job-postings dataset and get back one company-plus-need hiring signal per match, with the matched text, the deduplicated job count, and a next action — built from explicit keyword rules you can read in full below, never a semantic AI guess.

This Actor does not scrape LinkedIn, Himalayas, or any job board. It reads job-posting rows you already have — from an upstream job-scraper Actor's Dataset, from an Integrations payload, or pasted in directly — and turns them into a structured view of which companies are showing recurring, evidence-linked hiring activity in specific functional areas (sales, marketing, data, security, cloud, software, operations). Every signal names the exact keyword that matched, the exact source row it came from, and whether the underlying job identity was actually confirmed (a real jobId/URL) or only inferred from posting content. It is a research and prioritization tool for sales and staffing teams working from a job-postings batch they already collected — it is not a purchase-intent score, a budget estimate, a live vacancy checker, or a way to contact anyone.
What you get
For each distinct company and matched hiring-need category found in your source batch, you get one signal record containing:
companyName— the company as reported by the source (a string field, orcompany.name/company.companyNameon an object), andneed— which of seven fixed categories matched (sales,marketing,data,security,cloud,software,operations).jobCount,confirmedJobCount, andinferredJobCount— how many distinct job postings support this signal in total, how many of those had a real, verifiable identity (jobIdor a canonicalized job URL), and how many were only distinguishable by their title/description/date content because the source gave nojobId/URL at all. These three numbers are never conflated: a company with five postings but no job IDs showsjobCount: 5, confirmedJobCount: 0, inferredJobCount: 5, and the recommendedactionreacts to that distinction (see Decision routing).matchedJobs— up to 25 of the actual distinct postings behind the count, each with its ownjobId/jobUrl(when confirmed),title,postedDate, andcategories— so you can see exactly which postings drove the signal, not just a number.evidence— the literal matched keyword, which need-category it belongs to, whether it matched in thetitleor thedescription, and the exact source row index it came from — up to 12 evidence entries per signal, with the truncation itself disclosed when there are more.action— one of three plain labels (prioritize_outreach,monitor_recurring_demand,log_single_signal) based strictly onconfirmedJobCount, andconfidence—title_matchwhen at least one matched job had the keyword in its title,description_match_onlyotherwise.- Honest diagnostics for everything that isn't a signal — a row with neither a title nor a description, a row with no identifiable company, a row whose text matched none of the seven need categories, or a row the source itself reported as failed — are free business diagnostics. The initial list also includes source-read notices and writes at most 200 entries; later result-size/cap notices can raise total free rows above 200.
What this Actor explicitly is not: it does not claim proven purchase intent, an open budget, confirmed headcount growth, or that a posting is a currently-open vacancy (a postedDate only tells you when the source observed the posting, never that it's still live today). It does not send outreach messages, does not look up or expose personal contact details, and does not infer meaning semantically — every match is a literal, documented keyword hit against a fixed, disclosed dictionary (see Field dictionary), not an AI judgment call.
Who uses it
Sales researchers, recruiters and category analysts use these records to prioritize review of companies with observed hiring needs. The output supplies literal source evidence and confirmed-versus-inferred identity counts. It does not verify purchasing intent, budget, urgency or a current vacancy beyond the supplied observation date. Use it with your existing account research process and refresh the upstream dataset when freshness matters.
How to run
- Console (fastest for a one-off run). Open the Actor, go to Input, and either paste your job rows into Source records (inline) or fill in Source Dataset with the Dataset ID from your job-postings scrape. Leave everything else at its default and hit Start.
- From an existing job-postings Dataset. If you already have a Dataset ID from a job-scraper run, put it in
datasetId. The Dataset is read with your own run's permissions — no separate authorization step, and this Actor never elevates access beyond what your token already has. - Chained after an upstream job-scraper Actor via Integrations. Add this Actor as an "Actor/task run" integration on your upstream job-scraper Actor. Apify's Integrations UI auto-populates the payload with
{{resource.defaultDatasetId}}; map that value into this Actor'sdatasetIdfield explicitly (see Integration recipes) to requestREADaccess underLIMITED_PERMISSIONS, conditional on resource-picker or integration authorization — a payload reference alone does not grant access to a private Dataset. - Via the API. Call
POST /v2/acts/Uzd4yKIybbufQ9oV4/runs(orrun-sync-get-dataset-itemsfor a synchronous single call) with a JSON body containingdatasetId(oritems) and, if you want a hard spending ceiling, the run optionmaxTotalChargeUsd. - Read the results. One row per matched company+need pair lands in the run's default Dataset. A matching run-level
OUTPUT/SUMMARYrecord in the Key-value store tells you how many signals were delivered, how many rows were free diagnostics, and whether a row or spending cap ended the run early.
Every run is independent processing — see Operating guide for exactly what that means for re-running against the same batch.
Pricing
Pay-per-event, one event type: result-found — one delivered hiring signal (one company+need pair). There is no start fee, and every free diagnostic (an unmatched row, a missing company, a budget stop, an empty batch) requests no charge event at all.
| Tier | Price per signal | Discount |
|---|---|---|
| Free (default) | $0.012 | — |
| Bronze | $0.0108 | 10% |
| Silver | $0.0096 | 20% |
| Gold | $0.0084 | 30% |
| Platinum | $0.0084 | 30% |
| Diamond | $0.0084 | 30% |
These are Apify's platform-wide account pricing tiers — not a volume schedule this Actor invents itself — assigned automatically based on your account's own usage; you don't select one. One "signal" is one company+need assessment, which can represent multiple deduplicated job postings behind it (see jobCount in What you get) — it is never billed per individual input job row. A company showing hiring signals in three different need categories in the same run produces (and is billed for) three separate signal rows, one per category, because each is a distinct, independently-evidenced finding. Upstream job-scraping costs (running the source Actor that produced your batch) are entirely separate from this Actor's own price.
Set the platform's own Max total charge (USD) run option to cap total spend for a single run. See Evidence and boundaries for a documented case of a maxTotalChargeUsd value that does not behave as its name suggests when set via a raw API call.
Input contract
| Field | Type | Default | Notes |
|---|---|---|---|
items | array (JSON editor), max 1000 entries | prefilled real example | Inline source records. Ignored if datasetId, runId, or a resolvable payload.resource is also given. |
datasetId | string (Dataset picker, READ) | — | Dataset ID from a job-postings scrape. Read with your own run's permissions. Overrides inline items. |
runId | string, max 64 chars | — | An Actor run ID to read instead of a Dataset ID directly; its default Dataset is resolved automatically. |
payload | object (JSON editor) | — | Optional ACTOR.RUN.SUCCEEDED-shaped payload carrying resource.defaultDatasetId/resource.id from an upstream run. The resource picker on datasetId is the recommended way to grant private-Dataset access; a bare payload reference does not itself grant access. |
maxInputRows | integer | 100 | Hard cap on source records examined before grouping (1–1000). Bounded to 4 MiB total and 64 KiB per row regardless of this value. |
maxResults | integer | 10 | Maximum number of useful hiring-signal results this run can deliver (1–1000), independent of the platform's Max total charge run option. |
Source precedence, exactly: an explicit datasetId or payload.resource.defaultDatasetId (they must agree if both are present) beats a runId or payload.resource.id, which beats inline items. A dataset/run source is never merged with inline rows. A malformed or conflicting reference is rejected as invalid input, never silently downgraded to the inline example.
Accepted row fields (any subset present is used; nothing here is strictly required except enough to identify a company and some text — see Evidence and boundaries for what happens with less):
| Field | Accepted as | Role |
|---|---|---|
title | string; display capped at 200 characters, including the final ellipsis | Matched against the need-keyword dictionary; also read for confidence: title_match. |
description | string | Matched against the dictionary when title alone doesn't cover a need. |
company (string), companyName (string), company.name / company.companyName (object) | string | Required for a row to become a signal — no fuzzy/first-result company joining is ever performed. |
jobId, guid, id | string (≤200 chars, no control characters) or a non-negative safe integer | Confirmed job identity, in that priority order. |
jobURL, applicationLink, url | string, http(s) only, ≤2048 chars | Confirmed job identity via a canonicalized URL (fragment dropped, only recognized tracking parameters like utm_*/gclid/fbclid stripped — a functional query parameter like ?id=123 is preserved, so it never falsely equates two different postings). |
postedDate, publishedAt | string; display capped at 64 characters, including the final ellipsis | The posting's own reported date. A separate crawl/fetchedAt timestamp is never read as if it were the posting date. |
categories | array of strings | Up to 10 entries, each up to 120 characters; excess is truncated and disclosed. |
found, error, errors, failed, status | various | found:false, failed:true, or a failed/error/timed-out/aborted status always yields a free diagnostic, even when contradictory found:true, partial:true flags coexist. A non-empty error/errors warning may survive only with explicit found:true, partial:true and no hard failure; the result preserves source incompleteness. |
Title/date shortening alone does not set partial. Matching uses the original title/description. Inferred identity hashes the title, description and shortened date; dates differing only beyond that bound can merge. Dates are not validated as calendar values.
Hard read bounds, always enforced regardless of maxInputRows: at most 1000 source rows, at most 4 MiB of total source JSON per run, at most 64 KiB per individual row (an oversized row is dropped and disclosed). This Actor reads only the fixed https://api.apify.com origin with your run's own token — it never fetches an arbitrary URL and never starts an upstream Actor.
Output examples
These four outputs reproduce actual September 23 canaries from source-observation build ROYBvPErVmNhfrCbI, with values, IDs and timestamps unchanged. Example 1 reads the real Dataset; example 2 projects its identity fields; examples 3–4 are budget and empty-source controls. Underlying job observations remain September 13. Run IDs: BHQS5Fey5nGTnrrwB, Ujkam31i4TSGsn80g, lMafcR2hpNxcSLIUP, xm3B8j1gVqPf7a0Bw.
1. First useful signal — a full, confirmed dataset row
This is not a fresh LinkedIn scrape. The five underlying job postings were observed by the upstream job-scraper on 2026-09-13 (observedAt inside each source row) and processed by this Actor on 2026-09-23 — a 10-day-old saved batch, not today's hiring status.
Actor input:
{"datasetId": "ErVyHjUdtLa1Dftkn","maxResults": 10,"maxInputRows": 100}
First Dataset output row (of four delivered from this run):
{"entityId": "company:amazon web services (aws)|need:cloud","found": true,"status": "observed_hiring_signal","action": "monitor_recurring_demand","confidence": "title_match","sourceRowCount": 2,"companyName": "Amazon Web Services (AWS)","need": "cloud","jobCount": 2,"confirmedJobCount": 2,"inferredJobCount": 0,"matchedJobs": [{"jobId": "4455931376","jobUrl": "https://www.linkedin.com/jobs/view/software-development-engineer-aws-iam-distributed-database-system-at-amazon-web-services-aws-4455931376","title": "Software Development Engineer, AWS IAM Distributed Database System","postedDate": "2026-09-09","categories": []},{"jobId": "4455914852","jobUrl": "https://www.linkedin.com/jobs/view/software-dev-engineer-ii-aws-sts-at-amazon-web-services-aws-4455914852","title": "Software Dev Engineer II, AWS STS","postedDate": "2026-09-09","categories": []}],"observedAt": "2026-09-13T15:50:54.556Z","sourceObservationWindow": {"earliest": "2026-09-13T15:50:54.556Z","latest": "2026-09-13T15:50:54.556Z","validCount": 2,"totalRowCount": 2},"evidence": [{"field": "title","value": "matched \"aws\" in \"Software Development Engineer, AWS IAM Distributed Database System\"","rule": "keyword_dictionary:cloud","sourceRowIndex": 1},{"field": "title","value": "matched \"aws\" in \"Software Dev Engineer II, AWS STS\"","rule": "keyword_dictionary:cloud","sourceRowIndex": 3}],"businessPartial": false,"sourceRowIndexes": [1,3],"partial": false,"sourceCoverage": {"complete": true,"reason": null,"droppedOversized": 0},"product": "job-posting-buying-signal-extractor","schemaVersion": "1.0","sourceDatasetId": "ErVyHjUdtLa1Dftkn","sourceRunId": null,"suppliedSourceRunId": null,"sourceRunIdVerified": null,"processedAt": "2026-09-23T05:10:39.182Z"}
This run's Dataset ErVyHjUdtLa1Dftkn (the public example named in the Input contract) is a curated real copy of an upstream zinin/linkedin-public-jobs-scraper-shaped run (_exampleProvenance.sourceActorId: JQgY5E6jpd3bvtCmU, sourceRunId: JrRgbvNEFop2XoObT, sourceDatasetId: sagPWb8mF8Q9TyChd). Both AWS postings had real jobId/jobUrl values, so confirmedJobCount equals jobCount, both matched "aws" in their title (hence confidence: title_match), and two confirmed jobs at one company produce action: monitor_recurring_demand — not yet the three-job prioritize_outreach threshold. This run delivered 4 signals total across 3 companies from the 5-row batch; the remaining 3 rows are in the accepted Dataset alongside this one.
2. Same batch with jobId/jobURL removed — a controlled projection, not a new scrape
This is the identical 5-row 2026-09-13 batch above, with jobId and jobURL deliberately stripped from every row before submission — a controlled demonstration of confirmed-vs-inferred identity handling, not a fresh measurement of anything.
Actor input (first record of five submitted inline; company/title/postedDate otherwise unchanged from example 1):
{"items": [{"schemaVersion": "1.0","recordType": "linkedin_public_job","title": "Senior Software Engineer – Go (Golang)","company": "General Motors","location": "Warren, MI","postedTimeText": "1 day ago","postedDate": "2026-09-12","keywords": "software engineer","searchLocation": "United States","pageStart": 0,"sourceURL": "https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=software+engineer&location=United+States&start=0","observedAt": "2026-09-13T15:50:54.556Z","source": { "htmlSha256": "7394931a2e2a538b365c1d5f422aa4e6e38b2e6d2a5c12e6204e7db8ae880606" },"_exampleProvenance": {"sourceActorId": "JQgY5E6jpd3bvtCmU","sourceRunId": "JrRgbvNEFop2XoObT","sourceDatasetId": "sagPWb8mF8Q9TyChd","curatedPublicSnapshot": true,"redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."}}]}
Resulting Dataset output row for the AWS/cloud pair (partial: true):
{"entityId": "company:amazon web services (aws)|need:cloud","found": true,"status": "observed_hiring_signal","action": "log_single_signal","confidence": "title_match","sourceRowCount": 2,"companyName": "Amazon Web Services (AWS)","need": "cloud","jobCount": 2,"confirmedJobCount": 0,"inferredJobCount": 2,"matchedJobs": [{"jobId": null,"jobUrl": null,"title": "Software Development Engineer, AWS IAM Distributed Database System","postedDate": "2026-09-09","categories": []},{"jobId": null,"jobUrl": null,"title": "Software Dev Engineer II, AWS STS","postedDate": "2026-09-09","categories": []}],"observedAt": "2026-09-13T15:50:54.556Z","sourceObservationWindow": {"earliest": "2026-09-13T15:50:54.556Z","latest": "2026-09-13T15:50:54.556Z","validCount": 2,"totalRowCount": 2},"evidence": [{"field": "title","value": "matched \"aws\" in \"Software Development Engineer, AWS IAM Distributed Database System\"","rule": "keyword_dictionary:cloud","sourceRowIndex": 1},{"field": "title","value": "matched \"aws\" in \"Software Dev Engineer II, AWS STS\"","rule": "keyword_dictionary:cloud","sourceRowIndex": 3}],"businessPartial": true,"sourceRowIndexes": [1,3],"partial": true,"sourceCoverage": {"complete": true,"reason": null,"droppedOversized": 0},"product": "job-posting-buying-signal-extractor","schemaVersion": "1.0","sourceDatasetId": null,"sourceRunId": null,"suppliedSourceRunId": null,"sourceRunIdVerified": null,"processedAt": "2026-09-23T05:11:04.289Z"}
Same two postings, same jobCount: 2 — but with no jobId/URL in the source, these two are only distinguishable by title/description/company/date content (the module's content:-hash fallback identity), so confirmedJobCount drops to 0 and inferredJobCount rises to 2. Because action is driven by confirmedJobCount, not jobCount, this pair now reads log_single_signal instead of example 1's monitor_recurring_demand — the same underlying postings, correctly downgraded because the source no longer proves they're actually two distinct, trackable jobs rather than duplicate or malformed records that happen to differ in text.
3. Below-price run — free budget_stopped diagnostic, no partial charge
[{"product": "job-posting-buying-signal-extractor","schemaVersion": "1.0","status": "budget_stopped","reason": "Run charge limit cannot cover any result; no source was read.","found": false,"processedAt": "2026-09-23T05:11:16.569Z"}]
This run was started with the platform run option maxTotalChargeUsd: 0.005 — below every pricing tier's per-signal price (even Diamond's $0.0084). The Actor never reads the source batch, delivers zero paid signals, and writes this one free diagnostic. See Evidence and boundaries for what the literal value 0 does instead, which is different from this.
4. Empty batch — free no_results diagnostic
Actor input:
{ "items": [] }
Dataset output:
[{"product": "job-posting-buying-signal-extractor","schemaVersion": "1.0","status": "no_results","found": false,"reason": "no_usable_rows_in_batch","sourceCoverage": {"complete": true,"reason": null,"droppedOversized": 0},"processedAt": "2026-09-23T05:11:26.037Z"}]
An empty or entirely-unusable batch is a free diagnostic, never a silent zero-row success and never a charge.
Field dictionary
| Field | Type | Meaning |
|---|---|---|
entityId | string | `"company: |
found | boolean | true on every billed signal; false on every free diagnostic. |
status | string | "observed_hiring_signal" on a signal; a diagnostic reason (invalid_input, source_unavailable, invalid_row, source_reported_failure, missing_title_and_description, missing_company, no_keyword_match, budget_stopped, no_results, max_results_reached, result_dropped_oversized, source_rows_dropped, partial_source) on a free row. |
companyName | string | The company exactly as extracted from the source (original casing preserved for display; matching/grouping is case-insensitive). |
need | string | One of sales, marketing, data, security, cloud, software, operations — see the full keyword dictionary below. |
jobCount | number | Distinct job identities (confirmed + inferred) behind this signal, uncapped — this is the true count even when matchedJobs is display-truncated. |
confirmedJobCount | number | Of jobCount, how many had a real jobId or canonicalized job URL. |
inferredJobCount | number | jobCount − confirmedJobCount — postings distinguished only by title/description/company/date content, because the source gave no identity field at all. |
matchedJobs | array | Up to 25 of the actual distinct postings, each { jobId, jobUrl, title, postedDate, categories } (jobId/jobUrl are null for an inferred-only posting). |
evidence | array | Up to 12 entries, each { field: "title"|"description", value, rule: "keyword_dictionary:<need>", sourceRowIndex }, plus a reserved slot each for a job_list_truncated or identity_bridge_conflict note when those apply. |
action | string | prioritize_outreach (confirmedJobCount ≥ 3), monitor_recurring_demand (confirmedJobCount = 2), or log_single_signal (0–1) — driven by confirmed, not total, job count. |
confidence | string | title_match if any displayed matched job hit the keyword in its title; description_match_only otherwise. |
partial / businessPartial | boolean | businessPartial is this module's own signal (a truncated job list, an identity conflict, a truncated category list, a source row marked partial: true, confirmedJobCount < jobCount, or more than 50 contributing source rows). partial also folds in run-level source-read incompleteness — check partial if you only check one flag. |
sourceRowCount / sourceRowIndexes | number / array | True count and (capped at 50) list of original source rows contributing to this signal. |
sourceCoverage | object | Run-level: { complete, reason, droppedOversized } — whether the source read itself hit a cap. |
sourceDatasetId, sourceRunId, suppliedSourceRunId, sourceRunIdVerified | string/null, boolean/null | Where the batch came from; sourceRunId is populated only when independently verified against the Dataset. |
observedAt | string/null | Latest valid source observation across the signal’s complete contributing identity clusters. Uses only source observedAt, never postedDate or processing time. |
sourceObservationWindow | object | {earliest, latest, validCount, totalRowCount} over contributing accepted source occurrences before display caps. Missing/invalid timestamps increase only totalRowCount; endpoints are null without valid observations. Keyword-free identity-bridging rows contribute to their cluster window. |
processedAt | string (ISO 8601) | When this Actor processed the row. Never a claim about when the underlying posting was made. |
reason / error | string | Present on free diagnostic rows only. |
The full need-keyword dictionary (case-insensitive, word-boundary matched — "aws" matches "AWS-based platform" but not "awsome" or "claws"):
| Need | Keywords |
|---|---|
sales | sales, account executive, business development, sales representative, sdr, bdr |
marketing | marketing, seo, content marketing, brand manager, digital marketing, growth marketer |
data | data engineer, data scientist, data analyst, machine learning, data engineering, analytics engineer |
security | security engineer, cybersecurity, infosec, application security, security analyst, penetration tester |
cloud | cloud engineer, cloud architect, aws, azure, kubernetes, devops engineer |
software | software engineer, software developer, backend engineer, frontend engineer, full stack, programmer |
operations | operations manager, supply chain, logistics, operations analyst, process improvement, ops manager |
A posting can match more than one need (e.g. "DevOps Engineer" matches cloud; "Senior Software Engineer – Go" matches software) and will appear as a separate signal row per matched need. This dictionary is fixed and disclosed in full above — it is not adjustable per run and not an AI classifier, so the same posting text always produces the same match, every time.
Evidence and boundaries
Company identity is never fuzzy-joined. A row becomes part of a company's signal only via an explicit company/companyName string or company.name/company.companyName object field. There is no first-result matching, no "closest name" heuristic, and no cross-company merging by similar spelling — "Amazon Web Services (AWS)" and "Amazon" are two different companyName values and are never silently unified.
Job identity resolution happens company-wide, before need-grouping, and includes keyword-free rows. Every accepted row for a company — including a row whose own title/description matched no configured need keyword — participates in identity resolution: if that keyword-free row shares a jobId or job URL with a row that did match a need, it correctly "bridges" into that job's identity (e.g. confirming the same posting was seen twice, once with enough text to match and once without). A keyword-free row never creates a signal on its own, but it can still legitimately affect jobCount/confirmedJobCount for a need another row already matched. This was independently verified against a 999-row batch where 998 keyword-free bridge rows correctly collapsed into a single confirmed job under one real signal, while a separate 1,000-genuinely-distinct-ID batch correctly produced 1,000 counted jobs (with matchedJobs display capped at 25, jobCount itself never capped).
Identity conflicts are resolved conservatively, never by fabricating separate demand. If a later row's shared identity keys bridge two previously-separate job clusters for the same company (a genuine ambiguity in the source data), they are merged into one job — favoring undercounting over inventing two postings that might actually be duplicates — and the signal is marked partial: true with an identity_bridge_conflict evidence entry.
Confirmed vs. inferred identity is not a detail — it drives action. An inferredJobCount posting was only ever distinguished from another by matching company + title + description + posted date (hashed together) because the source gave no jobId/URL. This is a real, disclosed signal that the underlying source data is thinner than ideal — it is never silently treated the same as a confirmed, independently-trackable job. Compare examples 1 and 2 above: identical postings, different action, because identity confirmation genuinely changed.
Bounded, disclosed caps, applied only to display, never to the underlying count: matchedJobs is capped at 25 postings per signal (jobCount/confirmedJobCount/inferredJobCount remain the true totals); evidence is capped at 12 entries per signal, with truncation/conflict notes given their own reserved slot so they can never be crowded out; sourceRowIndexes is capped at 50 per signal (sourceRowCount is the true total); categories is capped at 10 entries of up to 120 characters each, with truncation disclosed. The initial business-diagnostic plus source-notice list writes at most 200 entries. businessSummary.diagnosticsCount counts business diagnostics only; diagnosticsTruncated flags the combined-list cap. Later result-size/cap notices add free rows.
Keyword matching is literal and deterministic, never semantic. A match requires the exact configured phrase (case-insensitive, word-boundary bounded) to appear in title or description. This Actor does not use an LLM, does not infer synonyms not in the dictionary above, and does not weigh context — "we need more sales" and "Sales Representative — Enterprise" both match sales identically, by the literal word. This is a deliberate trade-off for full auditability: every match is traceable to one documented rule, never an opaque model decision.
No proof of purchase intent, budget, live vacancy, or outreach permission. A hiring-need signal means the source data contained a job posting whose text matched a configured keyword — nothing more. It is not evidence the company has an approved budget, is actually growing headcount, or that the posting is still open today (postedDate is the source's own reported date, and a separate fetchedAt-style crawl timestamp is deliberately never substituted for it). This Actor never sends a message to anyone and never looks up or exposes a personal contact — it only classifies job-posting text you already have.
Budget behavior — read this before relying on a specific maxTotalChargeUsd value. The platform's SDK-level budget check is real and independently verified: a positive maxTotalChargeUsd below the price of one signal correctly delivers zero paid signals and one free budget_stopped diagnostic (example 3 above). However, a raw API request setting maxTotalChargeUsd to exactly 0 has been independently observed to be normalized by the platform to an account-level default remaining allowance rather than passed through as a literal zero-spend instruction, after which a run could bill normally under that restored default. Do not rely on maxTotalChargeUsd: 0 as a guaranteed no-spend switch when calling the API directly — always inspect the run's actual, effective options.maxTotalChargeUsd after it starts, and prefer a small positive value below one signal's price if your intent is to exercise the free-limit-stop path. This is a platform-level API behavior, not specific to this Actor.
Delivery is not transactional. Dataset storage precedes event charging. A lost acknowledgment can leave an unconfirmed row; the run stops without retry and can fail after some deliveries succeeded. Inspect the Dataset, platform event counts and available summary before retrying. A new run can bill again. If summary persistence fails, a complete OUTPUT/SUMMARY pair may be unavailable.
Reading the run summary
OUTPUT and SUMMARY contain the same run record when both writes succeed. These counters describe acknowledged processing, not every storage side effect that might have happened during a failed request:
| Field | Meaning |
|---|---|
schemaVersion, product, outcome, processedAt | Contract version, product identifier, outcome and processing time (not a posting date). |
requested | Number of source rows encountered by the source reader. It is zero if the budget precheck stopped before reading. |
usefulResults | Useful result rows remaining after oversized-result filtering, before delivery caps. |
delivered | Useful Dataset writes for which the complete delivery and charge check succeeded. |
paid | Confirmed named result-found events in this platform run. |
free | Acknowledged diagnostic Dataset writes without a named result event. |
failed | Oversized useful result rows dropped before delivery; this is not the count of fatal exceptions. |
fatalFailure, errorCode | Whether the run failed and its fixed diagnostic code. Input-policy diagnostics can succeed with a non-null errorCode, so read both fields. |
deliveryUncertain | A Dataset write or event-charge acknowledgment could not be confirmed. Actual Dataset rows may exceed confirmed counters. |
replaySafety | Text reminder that a new run can bill again. |
replayBlocked | The same run/storage already had a processing claim or named result events, so automatic replay was refused. |
budgetStopped, resultsCapped | Delivery stopped because of the effective charge allowance or maxResults. |
sourcePartial, sourcePartialReason, sourceDroppedOversized | Source-read coverage limits and rows discarded for size. |
businessPartialResults | Useful transform results carrying incomplete business evidence, counted before delivery filtering. |
oversizedDiagnosticsDropped, diagnosticsTruncated | Diagnostics omitted because of size or the 200-entry combined business/source-notice limit; later result-size/cap notices are outside it. |
partialOverall | At least one source, output, diagnostic or business-coverage limit was recorded. It does not replace fatalFailure. |
budgetMode, maxTotalChargeUsd | Verified finite/unlimited limit mode; the limit is null when no finite verified limit is available. |
businessSummary | Module counts can exceed delivered counts. sourceObservationWindow spans all accepted source rows using earliest, latest and validCount (no summary totalRowCount). Legacy observedAt remains null; use the explicit source window. |
Decision routing
action: prioritize_outreach— at least 3 confirmed distinct postings for this company+need. The strongest available signal in this dataset: real, independently-identified job records, not inferred duplicates.action: monitor_recurring_demand— exactly 2 confirmed postings. Worth tracking across your next batch to see if it becomes 3, but not yet the strongest tier.action: log_single_signal— 0 or 1 confirmed postings, regardless ofjobCount. This includes both a genuinely single posting and a case (like example 2 above) where multiple postings exist but none could be confirmed as distinct — checkinferredJobCountbefore treating this as "weak" outright; it may just mean your source data lacks job IDs/URLs for otherwise-real postings.confidence: description_match_only— the keyword only appeared in body text, not the job title. Still a real, literal match, but worth a quick manual glance at the matched evidence before treating it with the same weight as a title match.partial: truewithidentity_bridge_conflictin evidence — the source data itself was ambiguous about whether two postings were the same job. The count was resolved conservatively (merged, undercounted), but the underlying ambiguity is worth understanding if this account matters.- Free diagnostic rows (
found: false) — never a signal about the company.missing_title_and_description/missing_companymean the row couldn't be interpreted at all;no_keyword_matchmeans real content existed but matched none of the seven configured needs (it may still have bridged into another row's identity — see Evidence and boundaries);budget_stopped/max_results_reachedmean a run-level cap was hit — re-run with a higher cap to see the rest of the batch.
Commercial playbooks
Filter by need and confirmed job count, then verify matched jobs, source dates and partial flags before using a signal in account research. Hiring activity is an observation, not proof of purchasing authority or budget.
Compare repeated batches using the same company+need keys and preserve every capture window. Replaying a saved batch supplies no fresh observation and can charge again. A high inferred-job share suggests missing upstream IDs or URLs; improve that capture before interpreting apparent growth. This Actor does not contact prospects.
Integration recipes

This Actor's ID is Uzd4yKIybbufQ9oV4. A natural upstream source is LinkedIn Jobs Scraper (Actor ID JQgY5E6jpd3bvtCmU); any Actor or manual process producing the row shape in Input contract works identically. The recipes below describe exactly how to configure each mechanism — they are not a claim that any specific trigger is currently installed on your account. A saved public example is available in the Tasks tab.
1. Console-native chaining (no code). On your job-scraper Actor's run page, open the Integrations tab, add an "Actor/task run" action targeting this Actor, and let Apify's Integrations UI populate the payload with {{resource.defaultDatasetId}} from the finished upstream run. Map that value explicitly into this Actor's datasetId field — not the generic payload pass-through alone — to request READ access under LIMITED_PERMISSIONS, conditional on resource-picker or integration authorization. This is the pattern documented at docs.apify.com/integrations/actors.
2. Direct webhook (POST /v2/webhooks), full recipe. Configure a webhook on your upstream job-scraper Actor for the ACTOR.RUN.SUCCEEDED event, scoped to that specific Actor via its condition.actorId, targeting this Actor's run-creation endpoint:
{"isApifyIntegration": true,"shouldInterpolateStrings": true,"eventTypes": ["ACTOR.RUN.SUCCEEDED"],"condition": { "actorId": "JQgY5E6jpd3bvtCmU" },"requestUrl": "https://api.apify.com/v2/acts/Uzd4yKIybbufQ9oV4/runs?maxTotalChargeUsd=0.50","doNotRetry": true,"headersTemplate": "{\"Authorization\": \"Bearer <APIFY_TOKEN>\"}","payloadTemplate": "{\"datasetId\": \"{{resource.defaultDatasetId}}\", \"payload\": {\"resource\": {\"defaultDatasetId\": \"{{resource.defaultDatasetId}}\", \"id\": \"{{resource.id}}\"}}, \"maxResults\": 25}"}
Submit this to POST https://api.apify.com/v2/webhooks with your own Authorization: Bearer <APIFY_TOKEN> header — the <APIFY_TOKEN> placeholder in headersTemplate above is filled in by whichever secrets-management step provisions the webhook, never hardcoded into the template itself or placed in a URL query string. doNotRetry: true avoids a duplicate-charge risk from an automatic webhook retry. Note datasetId is set explicitly alongside payload.resource.defaultDatasetId, pointing at the same Dataset — an authorized resource-picker selection or integration can request scoped READ access. The raw datasetId string and payload.resource alone do not grant access to a private Dataset; the run still needs platform-authorized permission. A positive maxTotalChargeUsd on the request URL caps this chained run's spend; per Evidence and boundaries, avoid the literal value 0 here.
3. API-level integration outside webhooks. For a programmatic caller that isn't a webhook — per docs.apify.com/integrations/actors/integrating-actors-via-api — the same isApifyIntegration: true / shouldInterpolateStrings: true / string-encoded payloadTemplate shape from recipe 2 applies to any integration definition your own tooling submits against this Actor's ID.
4. Export / CRM-adjacent mapping. Every run's Dataset supports the platform's standard export formats (CSV, Excel, JSON) from the Console or via GET /v2/datasets/{datasetId}/items?format=csv. Map companyName, need, jobCount, confirmedJobCount, action, and matchedJobs into a CRM import or spreadsheet — those fields alone are enough to drive an account-prioritization view.
5. Generic automation tools (n8n, Zapier, Make). Use each tool's generic Apify/HTTP node to call run-sync-get-dataset-items on this Actor's ID with a JSON body containing datasetId (from the upstream job-scraper run) and your chosen maxResults/maxInputRows. This is a plain authenticated HTTPS POST and works without native Apify-integration support in the automation tool.
This Actor being publicly searchable as an Apify integration target (once public) does not imply a recommended-placement partnership with any specific upstream Actor — per docs.apify.com/integrations/actors/integration-ready-actors, that requires a separate request this Actor has not made. No outreach to any third party was performed to produce this README.
Operating guide
- Confirm your source batch shape first. Spot-check a few rows against Input contract — specifically that each has a
titleordescription, and an explicitcompany/companyName. A batch missing company data entirely will produce mostlymissing_companydiagnostics, which is worth knowing before a full run. - Start with a small
maxResultson a new source. For a source you haven't run through this Actor before, setmaxResultslow (e.g. 5) to confirm the output matches your expectations before committing to a full batch. - Set
maxTotalChargeUsddeliberately, and check the effective value after the run starts. Use the platform run option, not an Actor input field. If calling the API directly and you want zero paid signals guaranteed, use a small positive value below your applicable tier price rather than literal0— see Evidence and boundaries. - Read the
OUTPUT/SUMMARYKey-value store record, not just the Dataset row count. It reportsrequested/delivered/paid/free/failed, whetherbudgetStopped/resultsCappedended the run early,diagnosticsTruncatedwhen the initial combined diagnostic list exceeded 200, andsourcePartialcompleteness. - If a run finalizes
FAILED, inspect before retrying. Check that run's Dataset andOUTPUTrecord for what was actually delivered and charged up to the failure point before deciding whether to re-run — a blind retry can duplicate already-successful paid signals. - Re-running is always new billing. There is no dedup key across runs. Treat "run again" as "process (and pay for) this batch again," reserved for when you actually have a new or updated batch, not as a routine safety retry.
- Use
confirmedJobCountas your primary trust signal, notjobCount. When deciding how much weight to put on a signal, check whether it's backed by confirmed job identities or mostly inferred ones (see example 2) — the same headline count can mean very different data quality underneath.
FAQ
Does this Actor scrape LinkedIn or any job board itself? No. It only reads an existing job-postings batch you or an upstream Actor already collected. It never fetches a page from the open web and never starts another Actor.
Can I use a source other than LinkedIn Jobs Scraper? Yes — any source producing rows with the fields in Input contract (title/description, a company field, optionally jobId/URL/postedDate/categories) works identically.
Does a hiring signal mean the company has budget or is actively hiring right now? No. It means a job posting in your source batch, observed on the date the source recorded (postedDate), contained matching text. It is not evidence of an approved budget, confirmed headcount growth, or that the position is still open today.
Can this Actor send outreach messages or find someone's email/phone? No. It only classifies job-posting text you already have. It has no messaging capability and does not look up, infer, or expose personal contact information.
Why did the same two postings get a weaker action when I removed jobId/jobURL? Because action is driven by confirmedJobCount, not raw jobCount. Without an identity field, the module can only distinguish postings by content, which is a genuinely weaker guarantee that they're actually two distinct, trackable jobs — see example 2 in Output examples.
What happens if a posting doesn't match any of the seven need categories? It produces a free no_keyword_match diagnostic — unless it shares a jobId/URL with a row that did match a need, in which case it still contributes to that need's job/identity counts as a "bridge" row, without creating a signal of its own.
Is matchedJobs the complete list of postings behind a signal? Only up to 25 — jobCount/confirmedJobCount/inferredJobCount are always the true, uncapped totals even when matchedJobs is truncated (disclosed via a reserved job_list_truncated evidence entry).
Can I set a hard spending cap for a run? Yes, via the platform's own Max total charge (USD) run option. See Pricing and Evidence and boundaries for how a cap below one signal's price behaves, and an important caveat about the literal value 0 via a raw API call.
Does re-running the same batch avoid double-billing? No. Every run is independent processing with no cross-run deduplication — re-running the identical batch is billed again for every signal it produces.
What does a FAILED run mean for what was actually delivered? It means the run's own delivery/billing state could not be confirmed at some point — not necessarily that zero signals were delivered. Inspect that run's Dataset and OUTPUT record before deciding whether to retry.
Is the public example Dataset (ErVyHjUdtLa1Dftkn) something I should point production traffic at? It's a small, curated real example for trying the Actor without your own source — usable to see real output shape, not a general-purpose data source.
Why do I see fewer than 200 free diagnostic rows even though my batch had many more unmatched rows? The initial business-diagnostic plus source-notice list has a 200-entry cap. businessSummary.diagnosticsCount counts business diagnostics only; diagnosticsTruncated covers that combined list. Later result-size/cap notices are additional.
Sources and rights
This Actor reads only Datasets and runs your own Apify account token already has permission to read; it never uses another user's credentials and never starts an upstream Actor on your behalf. The public prefilled example (Dataset ErVyHjUdtLa1Dftkn) is a curated, non-sensitive copy of real postings originally captured by an upstream zinin/linkedin-public-jobs-scraper-shaped run (sourceActorId JQgY5E6jpd3bvtCmU, sourceRunId JrRgbvNEFop2XoObT, sourceDatasetId sagPWb8mF8Q9TyChd) observed on 2026-09-13, reused here with its original observation timestamps and measured field values unchanged — it is a saved example, not a live re-scrape, and contains only allowlisted public job-posting fields with no reviewer/personal identities. All output values shown in this README are copied verbatim from real runs of this Actor's accepted build; none are illustrative or invented.
Related tools
Related tools for adjacent workflows in B2B lead generation and data enrichment, jobs and hiring.
| Actor | What it does |
|---|---|
| Company Hiring Radar | Pair it in the jobs and hiring workflow: Turn buyer-supplied Greenhouse, Lever, or Ashby board tokens into evidence-backed public hiring... |
| LinkedIn Jobs Scraper | Pair it in the jobs and hiring workflow: Scrape current public LinkedIn job search cards with job IDs, titles, companies, locations, posted dates,... |
| Himalayas Remote Jobs API | Pair it in the jobs and hiring workflow: Return a bounded, attributed set of current remote jobs from the official Himalayas public JSON API |