Scraper Workers

Jobs scraper

The Ultron Jobs scraper pulls LinkedIn job postings filtered by query, location, company, posted-within window, and experience level. It authenticates with the caller's li_at cookie, walks the LinkedIn Jobs result list, and emits one row per posting with the job text and, when LinkedIn exposes it, the hiring manager's name and profile link.

Updated today

Overview

At a glance
Worker
ultron-jobs
Runtime
Node 20, Crawlee PlaywrightCrawler
Source
apify-actors/jobs/
Auth
li_at cookie, required
Max per query
500 (default 50)
Webhook target
/api/apify-webhook?source=jobs
Downstream skill
hiring-manager-prospector when HM absent

Authentication

The scraper requires the caller's LinkedIn li_at cookie. There is no fallback.

apify-actors/jobs/src/main.jsjs
1if (!input.li_at_cookie) {
2 log.error("li_at_cookie missing - refusing to run")
3 await Actor.fail("li_at cookie required")
4 return
5}
Warning
A missing cookie is treated as a fatal error, not as a degraded run. LinkedIn Jobs returns a logged-out shell with no postings; silent fallback would push hundreds of empty rows into the dataset. The actor refuses to start.

Input schema

Queries, location, filters, cap, cookie.

apify-actors/jobs/.actor/actor.jsonjson
1{
2 "queries": ["sales engineer", "solutions architect"],
3 "location": "Berlin, Germany",
4 "company_filter": ["Acme", "Globex"],
5 "posted_within_days": 30,
6 "experience_levels": ["mid", "senior"],
7 "max_per_query": 50,
8 "li_at_cookie": "<required>",
9 "webhook_url": "https://app.51ultron.com/api/apify-webhook?source=jobs",
10 "webhook_secret": "<shared secret>",
11 "user_id": "<uuid>",
12 "conversation_id": "<uuid>"
13}

Crawl flow

Open the auth-injected URL, iterate the result list, click into each detail panel, push one row.

apify-actors/jobs/src/main.jsjs
1// For each (query, location) pair the worker:
2// 1. opens LinkedIn Jobs search with the user's li_at cookie attached
3// 2. iterates the result list (LinkedIn paginates 25 at a time)
4// 3. for each posting, clicks into the detail panel and extracts
5// { company, title, location, posted_at, jd_text, seniority,
6// hiring_manager_name?, hiring_manager_url? }
7// 4. dedupes by jd_url, pushes to the dataset, optionally webhooks Ultron

Output shape

One row per unique posting.

dataset-row.jsonjson
1{
2 "company_name": "Acme",
3 "company_domain": "acme.com",
4 "title": "Sales Engineer",
5 "seniority": "mid",
6 "location": "Berlin, Germany",
7 "posted_at": "2026-05-04",
8 "jd_url": "https://www.linkedin.com/jobs/view/...",
9 "jd_text_snippet": "Acme is looking for a Sales Engineer to ...",
10 "hiring_manager_name": "Maria Schultz", // ~30% of postings
11 "hiring_manager_url": "https://www.linkedin.com/in/...",
12 "scraped_at": "2026-05-11T18:42:00Z",
13 "user_id": "<uuid>",
14 "conversation_id": "<uuid>"
15}

Hiring manager metadata

Visible on roughly 30 percent of postings. A separate skill fills the gap.

LinkedIn shows the hiring manager only on postings where the company has linked the recruiter's profile to the job description. The other 70 percent of postings come back without the field. The platform's hiring-manager-prospector skill picks up from there: it takes the posting and the company name, runs a People search filtered by likely role keywords, and proposes a candidate to add to the lead set.

Tip
When a downstream skill needs HM data on every row, run jobs first to populate the easy 30 percent, then queue hiring-manager-prospector against the rows where the field is null. The cost split between scraping and prospecting balances out.

How to trigger it

Same pattern as the maps scraper. The platform abstracts the Apify run call.

invocationtext
1POST https://api.apify.com/v2/acts/ultron-jobs/runs?token=<APIFY_TOKEN>
2Content-Type: application/json
3
4{
5 "queries": ["sales engineer"],
6 "location": "Berlin, Germany",
7 "li_at_cookie": "<value from the connected LinkedIn integration>"
8}

File map

apify-actors/jobs
Dockerfile
package.json
src
main.jsinput parse + crawler loop
helpers.jsbuildJobsUrl + jitter
list.jsresult list iteration
detail.jsper-posting detail extraction
src/app/api/apify-webhook
route.tsingest jobs results into job_signals