LinkedIn job records include an employer URL. You can use that URL to attach a company's public industry, website and size range to a job feed, keeping the source identity through both steps. This is useful when the employer's displayed name is abbreviated, shared by several organizations or belongs to a recruiting agency.
I maintain the LinkedIn Jobs and LinkedIn Company Actors used here. This example joins their exports with Python and SQLite. It saves the jobs before attempting company lookups, so a company failure leaves a recoverable job export.
What the live sample produced
On September 10, 2026, a bounded search for Python jobs in Berlin returned three postings. Their employer URLs led to three verified company records:
| Posted employer | Company ID | Public company-size range | LinkedIn-associated members |
|---|---|---|---|
| Flix | 2726149 | 1,001–5,000 employees | 2,631 |
| Nelly Solutions | 74522753 | 51–200 employees | 206 |
| Berliner Verlag | 1178870 | 201–500 employees | 100 |
These are dated source observations. Nelly's size range and associated-member count disagree because they are different LinkedIn fields; the workflow keeps both. Neither establishes an independently verified workforce size. The employer on a job advertisement can also be an agency, so this join doesn't identify an undisclosed end client.
The first capture stored three jobs, including their complete public descriptions. Enrichment attached three company profiles. A fresh job run returned the same IDs; the database added zero jobs and the enrichment step made zero API calls. The resulting JSONL file matched byte for byte.
The Jobs run IDs were vM96cCZQDC8v24ltV and 4AyEwm8YDWjMvrQ2v; the Company run was uNaVz3GdaNyn4gXUP. All three used build 1.0.4 of their respective Actor and ended successfully. This sample covers three returned postings, not every Python vacancy in Berlin.
Run the two steps
Download linkedin_hiring_feed.py, test_linkedin_hiring_feed.py and linkedin-hiring-input.json from the public workflow files. Keep the Python files together. They require Python 3.11 or newer and use its standard library.
python3.11 -m venv venv
source venv/bin/activate
python test_linkedin_hiring_feed.py
The checks run without credentials or network access. They exercise repeated imports, exact employer matching, invalid rows, a failed company run and recovery from the saved jobs. Set APIFY_TOKEN through your environment or secret manager before the live commands; keep it out of the input JSON and downloaded scripts.
The supplied search is small:
{
"keywords": "python",
"location": "Berlin, Germany",
"maxResults": 3,
"maxPages": 2,
"timeRange": "week",
"sortBy": "date",
"strictKeywords": true,
"onlyNew": false,
"proxyConfiguration": {"useApifyProxy": false}
}
Capture the jobs, then enrich the pending employer URLs:
python linkedin_hiring_feed.py capture linkedin-hiring-input.json berlin-hiring
python linkedin_hiring_feed.py enrich berlin-hiring
Each command that starts an Actor fixes memory at 512 MB, timeout at 300 seconds and the maximum Actor charge at $0.05. The two starts have separate caps. The example accepts at most ten jobs and five search pages per capture; each enrichment command requests at most ten pending companies. Inspect the printed counts before increasing your search within those bounds.
The folder contains hiring.sqlite3, the persistent database; hiring-feed.jsonl, the joined export; and runs/, the run receipts and successfully downloaded source rows. Keep the input file outside this folder. A changed input requires a different folder, which prevents accidentally mixing two search definitions.
Run the same two commands sequentially on later days. capture stores each job ID once. enrich requests only eligible employer URLs without a saved company snapshot, so several jobs from the same employer share one lookup. The second command exits without contacting Apify when nothing is pending.
Read the joined record
Every JSONL line has an outer job_id, the original job, a company_profile and an enrichment_status. These selected fields come from the saved Nelly record; the full file also retains the job description and the remaining company fields:
{
"job_id": "4434332752",
"job": {
"title": "Senior Software Engineer, Python",
"company": "Nelly Solutions",
"company_url": "https://de.linkedin.com/company/nelly-solutions"
},
"company_profile": {
"company_id": "74522753",
"url": "https://www.linkedin.com/company/nelly-solutions",
"industry": "Software Development",
"company_size": "51-200 employees",
"linkedin_employee_count": 206,
"identity_verified": true
},
"enrichment_status": "verified"
}
The join normalizes country-specific LinkedIn hostnames, case, trailing slashes and tracking parameters. It then requires the Company Actor's verified requested URL to equal that normalized employer URL. Company names never serve as a fallback key. Related organizations or a profile returned for a different URL fail validation.
Missing or unsupported employer links keep their jobs in the export with missing_company_url or unsupported_company_url. A supported URL awaiting a successful lookup has pending. Only a matched, verified company row receives verified; the other states have a null company_profile. The Company Actor currently accepts named company slugs containing letters, digits, underscores and hyphens. Numeric redirect URLs and other forms need separate source review; this example doesn't rewrite them into guessed company pages.
Use the outer job_id as the destination key when importing into a sheet or database. Retain job.scraped_at and company_profile.scraped_at so readers can see when each observation was collected. The saved profile also has the source-published website, when available.
Recover without collecting the jobs again
The first live capture stopped because a separate dataset-statistics read disagreed with the item response. A follow-up read confirmed all three jobs. The released script validates the total from the same paginated response as the items, removing that separate statistics dependency. I resumed the existing successful run:
python linkedin_hiring_feed.py capture linkedin-hiring-input.json berlin-hiring --run-id vM96cCZQDC8v24ltV
For your own recovery, substitute your own run ID. The script checks the Actor identity, successful terminal status, input and complete bounded export before importing it. A failed run cannot pass as a successful empty feed. If a start request times out before returning an ID, inspect your Apify Runs page before starting another; the server may already have accepted it. The script never retries a start automatically.
When a company batch fails, the captured jobs remain saved and their supported employer URLs remain pending. Inspect that run and the source issue before retrying enrich; no new Jobs run is needed. A successful Company run whose input still matches the pending batch can also be imported with enrich berlin-hiring --run-id YOUR_COMPANY_RUN_ID. Failed batches are not partially imported. A successful spending-limited batch can leave companies pending, and the printed count makes that visible.
SQLite rolls back an import if a later row has a duplicate or mismatched identity. The JSONL file replaces its predecessor only after writing finishes. If export is interrupted, regenerate it locally:
python linkedin_hiring_feed.py export berlin-hiring
That command makes no network requests. Avoid overlapping commands on one folder, and retain the database as the source of truth.
Costs and observation limits
At the verified Free-tier rates, Jobs costs $0.002 per exported posting and Company costs $0.004 per exported company. Each run at this memory setting adds a $0.00005 start event.
| Step | Calculated Actor charge |
|---|---|
| Initial three-job capture | $0.00605 |
| Three company profiles | $0.01205 |
| Fresh three-job repeat | $0.00605 |
| Repeat enrichment with no pending companies | $0.00000 |
| Total for the demonstrated sequence | $0.02415 |
These are customer-price equivalents calculated from the recorded events, not revenue from my owner tests. Apify's current pricing panel is authoritative, and plan discounts may apply. Repeated job captures still export and charge for the snapshot; local ID deduplication prevents duplicate destination records. An existing-run import makes API reads but starts no new Actor run.
This example keeps the first observation of each job and the first successful company snapshot per URL. Later captures won't update their descriptions or company metrics. It doesn't detect job closure, refresh company headcount, send alerts or activate a schedule. The dates inside the saved records remain meaningful precisely because they're preserved. For a fresh research snapshot, use a new folder; that starts a separate history and incurs fresh lookups.
The source search and text filtering bound which jobs enter this feed. An absent result does not establish that a vacancy closed, and a public profile may become unavailable. Exports load the local feed into memory; use batched or streaming exports if the accumulated archive becomes large. The workflow contains no personal-profile collection or employee enumeration. Follow source terms and your organization's data-use requirements.
You can inspect the Berlin Jobs example and the separate verified Company example before running the workflow in your own account. If you need an account, this Apify signup link is a referral link; I may earn a commission at no extra cost to you.

Top comments (0)