# PhD, Postdoc & Research Jobs Scraper — 12 Sources in 1 (`nomad-agent/researcher-bundle`) Actor

Every PhD, postdoc and research job source behind one endpoint: EURAXESS, jobs.ac.uk, UN Careers, ReliefWeb, Impactpool, Devex, Ikerbasque, EURES, LinkedIn, AcademicPositions + 2 university boards. One run returns a merged, deduped dataset - billed once. Incremental mode surfaces only new postings.

- **URL**: https://apify.com/nomad-agent/researcher-bundle.md
- **Developed by:** [Nomad.Dev](https://apify.com/nomad-agent) (community)
- **Categories:** Jobs
- **Stats:** 9 total users, 3 monthly users, 80.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PhD, Postdoc & Research Jobs Scraper — 12 Sources in 1

> **Claude / Codex skill to describe and setup this actor: [SKILL.md](https://github.com/Exdenta/OinkAIJobSearch/blob/main/skill/researcher-bundle/SKILL.md)**

Every PhD, postdoc, academic, research, policy and NGO job board behind **one endpoint, one run, one bill**.

Most academic job scrapers cover a single portal. This one merges **12** of them, dedupes across all of them, and hands you a single flat dataset — so you stop running (and paying for) a dozen scrapers and stitching their output together yourself.

```
EURAXESS · jobs.ac.uk · UN Careers · ReliefWeb · Impactpool · Devex
Ikerbasque · U. of Copenhagen · Universitat de Barcelona
AcademicPositions · EURES · LinkedIn
```

### Why this one

| | This Actor | Typical single-portal scraper | Other academic aggregators |
|---|---|---|---|
| Sources | **12** | 1 | 9 (EU only) |
| Coverage | EU + UK + global academia **and** UN / NGO / policy | one board | EU academia only |
| Cross-source dedupe | ✅ | n/a | ✅ |
| Only-new-since-last-run | ✅ | rarely | ✗ |
| Price per 1,000 jobs | **$3.00 all-in** | $0.50 – $4.90 | $4.00 |

No other Actor in the store combines academic boards **and** the UN / NGO / development-career sources (UN Careers, ReliefWeb, Impactpool, Devex). If you track research careers that cross between a university and an international organisation, this is the only single feed that sees both.

### Pricing

Pay-per-event. **You are billed by this Actor and nothing else** — the sources run *inside* this Actor, so there are no hidden child-Actor fees stacked on top.

| Event | Price |
|---|---|
| Actor start | $0.01 per run |
| Job result | $0.003 per **unique** job returned |

**$3.00 per 1,000 unique jobs.** A default run (all boards, 20 jobs each) returns ~170 deduped postings and costs about **$0.52**.

Two details that keep the bill honest:

- **You pay per *unique* job.** Cross-source duplicates are dropped *before* billing, not after. A posting that shows up on both EURAXESS and jobs.ac.uk is one charge, not two.
- **`incrementalMode` doesn't re-bill.** On a schedule, the Actor remembers every URL it already delivered and skips — and does not charge for — anything you've already received. A daily run typically bills only the handful of genuinely new postings.

> **One exception:** `AcademicPositions` needs a real browser to clear Cloudflare, so it runs as a separate child Actor and adds *that* Actor's own fees on top of this one's. It is **off by default** — tick it only if you want it.

### Input

Everything is optional. Run it with `{}` and you get a sensible default sweep.

| Field | Default | What it does |
|---|---|---|
| `sources` | all 11 in-process boards | Which boards to include. |
| `keyword` | — | Free-text filter, forwarded to the boards that support it (`machine learning`, `climate`, `postdoc`…). |
| `incrementalMode` | `false` | Return **only** postings not delivered on a previous run. Turn this on for scheduled runs. |
| `maxItemsPerSource` | `20` | Cap per board before merging. |
| `maxItems` | `240` | Hard cap on the merged output — and therefore on your bill. `0` = no cap. |
| `mistralApiKey` / `anthropicApiKey` | — | Unlocks `devex` and `ub_doctoral`, which use an LLM to extract listings. Billed by Mistral/Anthropic, not by us. |

```json
{
  "keyword": "machine learning",
  "sources": ["euraxess", "jobs_ac_uk", "un_careers", "eures"],
  "maxItemsPerSource": 25,
  "incrementalMode": true
}
```

#### BYOK sources (Devex & UB doctoral)

These two read listings that need extra rendering and use an AI provider (Mistral or Anthropic) to extract structured data, so they need `mistralApiKey` or `anthropicApiKey` — either one is enough; Mistral is used when both are set. Without a key:

- Both are dropped from the run and **you are not charged for them**.
- One warning row per skipped source is added to the dataset: `{"source": "devex", "warning": "skipped: requires anthropicApiKey or mistralApiKey", ...}`.
- The run's status message says which sources were skipped and why.
- The other 9 boards are completely unaffected.

Your Mistral/Anthropic usage is billed directly by that provider — it is not part of this Actor's Apify pricing.

### Output

One flat, deduped row per posting:

```json
{
  "source": "euraxess",
  "id": "123456",
  "title": "Postdoctoral Researcher in Machine Learning",
  "company": "KU Leuven",
  "location": "Leuven, Belgium",
  "url": "https://euraxess.ec.europa.eu/jobs/123456",
  "postedAt": "2026-07-10",
  "deadline": "2026-08-31",
  "snippet": "The Department of Computer Science invites applications…",
  "salary": ""
}
```

| Field | Meaning |
|---|---|
| `source` | Which board the record came from, e.g. `"euraxess"` |
| `id` | Stable source-side identifier (`""` when the board has none) |
| `title` | Job title as posted |
| `company` | Hiring organisation |
| `location` | Location / duty station (may include remote hints) |
| `url` | Direct link to the posting — also the cross-source dedupe key |
| `postedAt` | Posting date where the board provides it, else `""` |
| `deadline` | Application deadline — populated by `euraxess`, `jobs_ac_uk`, `un_careers`, `reliefweb`, `devex`, `math_ku_phd`, `ub_doctoral`; the others return `""` |
| `snippet` | Short description excerpt |
| `salary` | Salary text — only `jobs_ac_uk` populates this today; the others return `""` |
| `hiringContactName` | LinkedIn rows: person named as the job poster (null elsewhere) |
| `hiringContactTitle` | That person's LinkedIn headline |
| `hiringContactUrl` | Their LinkedIn profile URL |

Fields a board doesn't provide come back as `""`, never missing — so the schema stays stable across all 12 boards. Warning rows carry an extra `warning` field, with every other field `null`.

### Run it from your own code

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("nomad-agent/researcher-bundle").call(
    run_input={"keyword": "postdoc", "maxItems": 50}
)
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], "—", job["company"], "—", job["url"])
```

### Typical uses

- **Job-alert products** — schedule it daily with `incrementalMode: true` and pipe the new rows into email / Telegram / Slack.
- **Academic labour-market research** — one dataset spanning EU, UK and international-organisation research posts.
- **Recruiting and talent mapping** — see who is hiring researchers, where, and on what deadline cadence.

### Reliability

Every board fails **open**: if one is down or slow, the run still returns everything the others found, and the failing board is named in the log. A run that hits an unexpected error still finishes `SUCCEEDED` carrying a diagnostic row, rather than failing and losing the results you already paid for.

***

Found a bug, or want a board added? Open an issue on the Actor page — we read them. If this saved you time, a review helps other researchers find it.

**Is this Actor useful to you?**
A quick ⭐ review on the Actor's **Reviews** tab helps other researchers and PhD applicants find it — and tells us what to build next.

***

**From the maker of [Oink](https://github.com/Exdenta/OinkAIJobSearch)** — an open-source, AI-powered job-search bot for Telegram that runs on these Actors. [Try the free bot](https://t.me/job_search_everyday_bot), get a managed instance at [oinkjobsearch.com](https://oinkjobsearch.com), or browse the [full catalog of 50+ Actors](https://apify.com/nomad-agent).

# Actor input Schema

## `sources` (type: `array`):

Which boards to include. Leave empty to run the default set (all 11 boards that run inside this Actor — one flat bill, no extra fees). 'AcademicPositions' is the one exception: it needs a real browser to clear Cloudflare, so it runs as a separate child Actor and adds that Actor's own start + per-result fees on top of this bundle's. It is off by default; tick it only if you want it. 'Devex' and 'UB doctoral' additionally need the Anthropic, Mistral or OpenAI API key below (any one); without a key they are skipped, you are not charged for them, and a warning row explaining why is added to the results.

## `keyword` (type: `string`):

Optional free-text filter forwarded to the boards that support it (the others ignore it). E.g. 'machine learning', 'climate', 'postdoc'.

## `incrementalMode` (type: `boolean`):

Return only postings this bundle has NOT already delivered on a previous run. The bundle remembers every listing URL it returned (in a private key-value store on your own Apify account) and, on later runs, skips — and does not re-charge its per-result fee for — anything you already received. The first run always returns everything; leave this off for a full snapshot each run, turn it on for a scheduled (e.g. daily/weekly) job that should surface only fresh postings.

## `anthropicApiKey` (type: `string`):

Optional fallback for 'Devex' and 'UB doctoral' when no Mistral key is supplied — they use an AI provider to extract listings. Without any key those two sources are skipped (you are not charged for them) and a warning row is added to the output explaining why; every other source runs normally either way. Usage is billed separately by Anthropic, not by this Actor.

## `mistralApiKey` (type: `string`):

Preferred key for 'Devex' and 'UB doctoral'. If more than one of Mistral, Anthropic and OpenAI keys are supplied, the bundle uses Mistral for those sources. Without any key those two sources are skipped and a warning row is added explaining why. Usage is billed separately by Mistral, not by this Actor.

## `mistralModel` (type: `string`):

Mistral model used for 'Devex' and 'UB doctoral' when mistralApiKey is supplied. Small is the default — it matches larger Mistral models on this task at a fraction of the cost.

## `openaiApiKey` (type: `string`):

Alternative to anthropicApiKey/mistralApiKey for 'Devex' and 'UB doctoral' — pass this instead if you'd rather bring an OpenAI key (Mistral, then Anthropic, are preferred when more than one key is set). Without any key those two sources are skipped and a warning row is added explaining why. Usage is billed separately by OpenAI, not by this Actor.

## `openaiModel` (type: `string`):

OpenAI model used for 'Devex' and 'UB doctoral' when openaiApiKey is the key in effect. Defaults to gpt-4.1-mini — cheap and fast for this task. Any chat-completions model works, including the gpt-5 family.

## `maxItemsPerSource` (type: `integer`):

Cap on items fetched from EACH board before merging. Raise it for a deeper sweep of every board; the merged total is still bounded by 'Max items (total)' below.

## `maxItems` (type: `integer`):

Hard cap on the merged, deduped output — and therefore on what you pay, since this Actor charges per unique job returned. Default (240) is sources × 'Max items per source'. Set 0 to remove the cap; cost then scales with every board's raw output.

## `cacheTtlSeconds` (type: `integer`):

How long to reuse results already fetched from a board instead of fetching again. Set 0 to always fetch fresh data.

## `concurrency` (type: `integer`):

How many boards to fetch at the same time. The default fetches them all at once, so a run takes as long as the slowest board rather than the sum.

## `runTimeoutSecs` (type: `integer`):

How long to wait for each board before giving up on it and merging what the others returned. Boards run in parallel, so this is a per-board ceiling, not a budget shared across them.

## `apifyToken` (type: `string`):

Leave empty — injected automatically when this Actor runs on the Apify platform. Only set this when running the code outside the platform, e.g. on your own machine.

## `actorOwner` (type: `string`):

Only used to address the 'AcademicPositions' child Actor. Leave empty to use the published one — set it only if you run your own fork under a different Apify account.

## Actor input object example

```json
{
  "sources": [
    "euraxess",
    "jobs_ac_uk",
    "ikerbasque",
    "math_ku_phd",
    "ub_doctoral",
    "un_careers",
    "reliefweb",
    "impactpool",
    "devex",
    "linkedin",
    "eures"
  ],
  "incrementalMode": false,
  "mistralModel": "mistral-small-latest",
  "openaiModel": "gpt-4.1-mini",
  "maxItemsPerSource": 20,
  "maxItems": 240,
  "cacheTtlSeconds": 1800,
  "concurrency": 12,
  "runTimeoutSecs": 240,
  "actorOwner": ""
}
```

# Actor output Schema

## `dataset` (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("nomad-agent/researcher-bundle").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("nomad-agent/researcher-bundle").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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 nomad-agent/researcher-bundle --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=nomad-agent/researcher-bundle",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/vW8dfaG0Dybl5oVQT/builds/sYUK7uPxrKoo6tlm4/openapi.json
