Devex Jobs Scraper — Complete Source Descriptions avatar

Devex Jobs Scraper — Complete Source Descriptions

Pricing

from $3.00 / 1,000 job results

Go to Apify Store
Devex Jobs Scraper — Complete Source Descriptions

Devex Jobs Scraper — Complete Source Descriptions

Discover public Devex jobs through Camoufox and Apify Residential. Emits exact canonical narrative text proven either by a top-level structured-metadata boundary or by the end of a selector node with no recognized metadata child or standalone metadata-label line.

Pricing

from $3.00 / 1,000 job results

Rating

0.0

(0)

Developer

Nomad Dev

Nomad Dev

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

5 days ago

Last modified

Categories

Share

Discover public Devex job postings through Camoufox and Apify Residential and return only records with the exact canonical source-page narrative. The Actor first uses public-search metadata only to find canonical candidate URLs; it never emits that metadata as a job. It opens the Residential Devex listing only if verified detail pages from that first tier cannot fill the requested count. No model or user-provided API key is used for discovery or extraction.

The Actor reads unauthenticated public pages. It does not use a Devex login, paywall access, CAPTCHA bypass, or an authenticated Devex session.

Where records come from

The Actor serves each run from the cheapest source that can prove a complete body, in this order:

  1. The durable Devex inventory. A private owner-only collector (devex-private-collector) discovers and reads Devex on a schedule and stores boundary-proven records. When that inventory is reachable and fresh, this Actor delivers from it: no browser launch, no Residential bandwidth, and a complete description on the first try. Cached rows carry deliveredFrom: "inventory".
  2. The live source. When the inventory is unconfigured, unreachable, stale, or has nothing matching your filters, the Actor falls back to the Camoufox + Residential path described below. Nothing about the filters or the body contract changes; only the transport does.

An alive check (jobUrls) always uses the live path. A cached record proves the page existed when it was collected, not that the posting is open now.

Cached rows carry descriptionContractVersion: "devex-complete-source-v4". v4 guarantees exactly what v3 does — a complete, untruncated body that ends where the source narrative ends — and adds one further proof mechanism: jsonld-description, where Devex itself declares the whole narrative in JSON-LD, so its own end is the boundary. On that boundary descriptionMetadataChildFound and descriptionMetadataLabelLineFound are null, because the DOM metadata-child question does not apply. Live rows keep emitting v3 and are still held to the v3 rules exactly.

Posted dates and their provenance

Devex publishes a posting date on some jobs and none at all on others. Every row now says which case it is, in postedAtProvenance:

ValuepostedAt means
source_metadataThe Devex detail page published this date.
listing_cardThe Devex listing card published it; the detail page did not.
inventory_admissionDevex published no date. This is the hour the durable inventory first collected the job — a collection timestamp, not a posting date.

No date is ever invented, and relative phrases such as "3 days ago" are never converted into one. Filter on postedWithin knowing that an inventory_admission value is a lower bound on the real posting date.

Complete-description contract

A discovery record is emitted only when all of these checks pass:

  • description is exact live [data-partial="job-description"] innerText under one of two source-owned proofs: when a recognized top-level metadata child exists, it is the exact prefix ending immediately before that rendered label; when none exists, it is the entire selector-node innerText.
  • A recognized metadata child that cannot be located as a rendered line rejects the page. It never falls back to selector-node-end.
  • The section contains at least 500 characters and begins Job Description.
  • descriptionContractVersion is devex-complete-source-v3.
  • descriptionSourceUrl normalizes to the same canonical URL as url.
  • descriptionComplete is exactly true.
  • descriptionBoundaryFound is exactly true and the proof fields agree: top-level-source-metadata uses a recognized label plus descriptionMetadataChildFound=true and descriptionMetadataLabelLineFound=true; selector-node-end uses the stable END_OF_SELECTOR_NODE label with both booleans false. A selector-end body containing a standalone recognized metadata label is independently rejected by the caller even if its evidence fields claim otherwise.
  • descriptionTail exactly matches the final 80 source characters.
  • descriptionHash is the SHA-256 of the exact description string.
  • The description is not the listing-card snippet, does not end in an isolated dangling word fragment, and has no ellipsis, truncated, read more, or equivalent clipping marker at its end.
  • HTTP status/redirect, challenge title/body, Career Account paywall, or a missing source selector rejects the detail page.

FindJobs independently repeats these checks before mapping a record into a job. A failed contract becomes a source error, not a healthy empty result.

Input

{
"keyword": "migration policy",
"location": "remote",
"postedWithin": "7d",
"maxItems": 3,
"proxyConfiguration": {
"useApifyProxy": true,
"apifyProxyGroups": ["RESIDENTIAL"]
}
}

postedWithin uses the shared job-search values 1h, 24h, 7d, 30d, and any; the default is 7d. A selected window excludes records without a source-proven posting date; any leaves unknown dates eligible. Devex currently exposes calendar-date precision, so 24h includes the current and previous UTC date and 1h is fail-closed instead of claiming hour-level freshness it cannot prove.

maxItems is caller-controlled from 1 to 10 (default: 3). Keyword and location are conservative filters over visible source listing-card text. The Actor first uses public search metadata to discover canonical candidate URLs without a Residential browser request, then reads every returned description directly from Devex. It falls back to Residential listing discovery only when that cheap path cannot fill the requested number of complete records. Supplying jobUrls switches to alive-check; only exact numeric-id detail URLs are accepted, while query and subpath URLs are rejected before navigation.

The first tier avoids a Residential listing navigation. It emits no job and takes no Actor start/result charge by itself, but it can issue up to three public-search requests. It is a lower-cost Actor path, not a guarantee about any third-party search service's pricing or latency.

Alive status is independent of description completeness. An explicit visible Devex closed/expired message yields false even if an apply-looking control is also present. An independently visible enabled apply control yields true. When neither signal exists, isActive is null rather than guessed from page existence.

Output

Every row follows nomad-agent-job-row-v1, the one shape shared by all of this fleet's job Actors. A row carries every field; null means the source did not publish that fact, and locations: [] means no usable location was parsed from the posting. Nothing is guessed.

{
"schemaVersion": "nomad-agent-job-row-v1",
"recordType": "posting",
"source": "devex",
"id": "a1b2c3",
"url": "https://example.com/devex/jobs/a1b2c3",
"title": "Senior Backend Engineer",
"company": "Example Company",
"locations": [
"Bilbao",
"Spain"
],
"postedAt": "2026-09-02T00:00:00Z",
"deadline": "2026-10-15",
"description": "The complete posting body as plain text, exactly as the source published it — never truncated.",
"descriptionHtml": "<p>The complete posting body as the source's own markup.</p>",
"workType": "remote",
"custom": {
"schemaId": "nomad-agent-job-custom-devex-v1",
"data": {
"externalId": "…",
"descriptionContractVersion": "…",
"descriptionSourceUrl": "…"
}
}
}
FieldMeaning
schemaVersionAlways "nomad-agent-job-row-v1".
recordType"posting" for a job, "diagnostic" for a row reporting something about the run itself.
sourceWhich job source the posting came from, from the collector registry's vocabulary — not the Actor name.
idStable identifier for the posting within source.
urlCanonical public URL of the posting on the source site.
titleJob title exactly as the source publishes it, untruncated.
companyEmployer name as published.
locationsPlaces the role is based, most specific first — e.g. ["Bilbao", "Spain"].
postedAtWhen the source published the posting, ISO-8601 UTC (YYYY-MM-DDTHH:MM:SSZ).
deadlineClosing date for applications as an ISO-8601 calendar date (YYYY-MM-DD).
descriptionThe complete posting body as plain text — never truncated, never summarised.
descriptionHtmlThe posting body as the source's own markup, preserving lists, headings and links.
workTypeWorking arrangement: "remote", "hybrid" or "onsite".
customFacts only this source publishes, as {"schemaId", "data"}.

A run also emits diagnostic rows — recordType: "diagnostic" with a warnings array — when it has something to report, such as a source returning nothing. They are never billed and are easy to filter out on recordType.

Diagnostics and charging

Unavailable Residential access exits before the Actor-start charge. Listing or detail transport failures emit a warning row with warning and docs. Job records are charged only after they pass the complete-description contract and are pushed. Callers must treat warning-only datasets as source failures rather than empty inventory.

Transport diagnostics are bounded and non-sensitive: they record whether an HTTP response existed, its status, the canonical/raw final-URL class, safe Devex host/path/query flags, and proxy scheme/hostname/port/username class plus password presence. Proxy passwords, session identifiers, and query contents are never logged or emitted.

No deployment, live canary, billing verification, or Store publication is implied by this repository version.