# Personio Jobs Lookup — Live Postings by Company (`accountable_eel/personio-jobs-lookup`) Actor

Look up any company's live Personio job board and get every open role — title, department, office, employment type, seniority, schedule and apply link. Reads Personio's own public XML feed directly, no scraping. Charged once per company, no matter how many roles are open — misses cost nothing.

- **URL**: https://apify.com/accountable\_eel/personio-jobs-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 successful lookups

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## Personio Jobs Lookup — Live Postings by Company

You give this actor a Personio subdomain or careers URL and it looks up that company's
live Personio job board and returns every open role — title, department, office,
employment type, seniority, schedule, posting date, and a direct apply link — as one row
per posting (or one row per company, your choice). It reads Personio's own public XML job
feed (`<subdomain>.jobs.personio.de/xml` or `.com/xml`) directly. No scraping a careers
page, no API key, no Personio account required.

### Who it's for

The accountable\_eel catalogue sells company intelligence columns for outbound. Each actor takes a
list of domains or company identifiers and returns one flat, stably-named row per input —
firmographics, registry IDs, tech stack, email route, hiring activity — the shape a Clay table, an
n8n workflow, or an AI agent can consume without post-processing. Pricing is pay-per-event and
per-domain: a few tenths of a cent for a row that was actually found, and nothing for a
miss, so a list that doesn't enrich costs you next to nothing. Where an official source exists —
VIES, GLEIF, SEC EDGAR, Brønnøysund, PRH, RDAP — it is queried directly instead of scraped. No
seat licence, no monthly minimum, no credit system to decode.

This one's column is hiring activity on Personio specifically — the ATS you'll find behind a large
share of DACH-region (Germany/Austria/Switzerland) small-and-mid company careers pages.

### Why this one

- **Reads Personio's own public XML feed directly** — structured, machine-readable data, not a
  scraped HTML careers page that breaks the moment the company redesigns its site.
- **Handles both Personio domains.** Some companies run on `jobs.personio.com` instead of the
  older `.de`, and Personio's own docs confirm there's no redirect between the two. Paste either
  form (or the bare subdomain) and the actor uses whichever domain your input actually names.
- **You pay per company, never per job.** A board with 40 open roles and a board with 2 both cost
  one charge.
- **Real job filters, not raw technical fields.** Title keyword, exclusion keyword, location,
  remote-only, and posted-after date — combined the same way every actor in this ATS family
  combines them, so switching between Greenhouse, Workday, and Personio lookups doesn't mean
  relearning a filter syntax.
- **A delta mode for scheduled watchlists.** Turn on `onlyNewSinceLastRun` and a scheduled run
  reports only the postings that appeared since the last one — a hiring alert instead of a full
  re-dump every time.

### What you get

| Field | Type | Description |
|---|---|---|
| `query` | text | The input exactly as you supplied it. |
| `found` | boolean | `true` if a Personio career page was found for this company, `false` for a miss. |
| `status` | text | `OK` on a hit, `NOT_FOUND` (no career page or nothing currently open), or `BAD_FORMAT` (input doesn't look like a Personio subdomain or URL). |
| `subdomain` | text | The Personio subdomain identified for this company. |
| `jobCount` | number | How many roles matched your filters (or are new, in delta mode) for this row/company. |
| `totalJobCount` | number | How many roles are on the board before any filtering — always the true total, so a filtered `jobCount` of 0 is never ambiguous. |
| `newJobs` | number | How many postings are new since your last run. `null` unless `onlyNewSinceLastRun` is on. |
| `firstRun` | boolean | Whether this was the baseline run of a delta watchlist. `null` unless `onlyNewSinceLastRun` is on. |
| `jobs` | array | The full list of matching postings for this company, each with the fields below nested inside. Present when "One row per job posting" is off; otherwise the per-job fields below are flattened onto the row instead. |
| `title` | text | Job title. |
| `department` | text | Department, as Personio labels it. |
| `office` | text | Office/location, Personio's own field name. |
| `location` | text | Alias of `office` — added so filters read the same way across every ATS actor in this family. |
| `employmentType` | text | e.g. `permanent`, `contract`, as Personio reports it. |
| `seniority` | text | e.g. `experienced`, `entry_level`, as Personio reports it. |
| `schedule` | text | e.g. `full-time`, `part-time`. |
| `createdAt` | date | ISO timestamp Personio recorded when the posting was created. |
| `postedAt` | date | Alias of `createdAt` — Personio's feed carries only one date, so there's no separate first-published vs. last-updated distinction to make. |
| `remote` | boolean | Best-effort read of whether the role is remote — matched off the title/location text (Personio has no dedicated remote flag). |
| `applyUrl` | link | Direct link to apply for this posting. |
| `scrapedAt` | date | When this actor fetched the data. |

A subdomain with no Personio career page, or a feed with zero open positions, is a `found: false`
row with `status`/`message` explaining why, and is never charged.

### Price

$6.32 per 1,000 companies, plus a $0.0001 start fee. Misses (`found:false`) are never charged.

You're charged once per company checked — a board with 40 open roles costs the same single
`item-lookup` charge as a board with 2, and a company with no Personio career page at all costs
nothing. 1,000 companies through this actor: **~$6.32** if every one is found on Personio, less if
some aren't. The same 1,000 rows through a credit-based enrichment platform: **$80–$400**.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `companies` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~personio-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"companies":["personio"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

Paste one company per line — the bare Personio subdomain (`personio`) or a full careers URL
(`https://personio.jobs.personio.de` or the `.com` equivalent). The subdomain, and domain if your
input named one, are extracted automatically.

By default you get every open role on each company's board. To narrow that down, open the
**🎯 Job filters** section of the Input tab:

| Input | What it does |
| --- | --- |
| `titleKeywords` | Keep only roles whose **title** contains at least one of these. Case-insensitive, partial words count — `"engineer"` matches *Engineering Manager*. Any-match: `["engineer","designer"]` keeps both kinds. |
| `excludeTitleKeywords` | Drop any role whose title contains one of these — `["intern","contract"]`. Applied after `titleKeywords`, so a role matching both is dropped. |
| `locations` | Keep only roles whose location contains one of these — `["Berlin","Munich"]`. Case-insensitive, partial matches count, any-match. |
| `remoteOnly` | Keep only roles that read as remote. Matches *remote*, *anywhere*, *work from home*, *wfh* and *distributed* in the location, and the word *remote* in the title. Personio has no dedicated "remote" flag, so this is best-effort off the office/title text. |
| `postedAfter` | Keep only roles Personio first created on or after this date — `"2026-06-01"`, or a full ISO timestamp. Inclusive of the date you give. |

Different filters combine with **AND** (title *and* location *and* remote); values inside one
filter combine with **OR**. A company whose roles all get filtered out still gets one
`found: true` row with `jobCount: 0` — you asked a question and got a real answer, so it's charged
like any other company. `totalJobCount` always shows how many were on the board before filtering,
so a zero row is never ambiguous with a broken run.

Turn off **"One row per job posting"** to get one row per company instead, with the full job list
nested inside `jobs`.

For a hiring alert instead of a full dump, turn on **`onlyNewSinceLastRun`** and put the actor on
an Apify schedule. The first run returns everything matching your filters and remembers it; every
run after that returns only postings it hasn't shown you before, via `newJobs` and `firstRun`. The
seen-list lives in a named key-value store (`personio-jobs-lookup-state`) in your own Apify
account — see the FAQ below for how it's scoped and how to reset it.

### Input

```json
{
  "companies": [
    "personio"
  ]
}
```

One company per line — paste the Personio subdomain or the full careers page URL. Accepted formats: personio, https://personio.jobs.personio.de, https://personio.jobs.personio.com.

### Sample output

By default you get **one row per open job posting** (turn off "One row per job posting"
in the Input tab to get one row per company instead, with the full job list nested
inside):

| query | found | status | subdomain | jobCount | totalJobCount | newJobs | firstRun | jobs | title | department | office | location | employmentType | seniority | schedule | createdAt | postedAt | remote | applyUrl | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| personio | true | OK | personio | 1 | 1 |  |  | \<all jobs (full list)> | Staff Software Engineer, Data Platform | Product and Tech | Munich | Munich | permanent | experienced | full-time | 2024-11-13T14:10:41+00:00 | 2024-11-13T14:10:41+00:00 | false | https://personio.jobs.personio.de/job/1834171 | 2026-08-24T06:01:17.215Z |

A real row looks like:

| query | found | status | subdomain | title | department | office | applyUrl |
| --- | --- | --- | --- | --- | --- | --- | --- |
| personio | true | OK | personio | Senior Backend Engineer | Engineering | Munich | https://personio.jobs.personio.de/job/482910 |

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~personio-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"companies":["personio"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~personio-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"companies":["personio"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~personio-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"companies":["{{company}}"]}`, mapping the row's company into the `companies` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Personio Jobs Lookup | Apify" — the agent will find and run this actor.

### Tips

- **Don't know the bare subdomain?** Paste the company's full careers page URL instead — either
  `<subdomain>.jobs.personio.de` or `<subdomain>.jobs.personio.com` works, and the subdomain (and
  domain) are extracted automatically.
- **A bare subdomain with no domain hint defaults to `.de`** — today's majority. If a company
  actually runs on `.com` and you only paste the bare subdomain, paste the full `.com` URL instead
  so the actor doesn't guess wrong.
- **Your filters are part of the identity of a delta watchlist.** Two schedules with different
  `titleKeywords` keep separate seen-lists and won't shadow each other. Editing a filter starts a
  fresh watchlist, so the next run re-baselines and returns the full matching set once.
- **Don't combine delta mode with "Only keep rows containing" (`includeKeywords`)** — that filter
  runs on the finished row and can drop the `0 new jobs` heartbeat row, which is the one row you
  want to keep seeing on a quiet week.
- **Adjust concurrency** in the Input tab if you're checking a long list and want to control how
  many requests run against Personio's feed in parallel.

### vs. alternatives

| | What it costs | What you get | Trade-off |
|---|---|---|---|
| **This actor** (`personio-jobs-lookup`) | $0.00632 per company found (less on paid tiers), $0.0001 actor start, nothing for a miss | Every open Personio posting for a company — title, department, office, employment type, seniority, schedule, apply URL — filtered by title keyword, location, remote, or posting date, with `onlyNewSinceLastRun` for deltas | Personio only. A bare subdomain with no domain hint defaults to `.de` rather than probing both domains. No salary or full job descriptions — Personio's public feed doesn't carry them. |
| **TheirStack** | $0.005–$0.03 per company | A job-postings dataset with history, normalised across many ATS platforms and job boards | Broader coverage than one platform, and it keeps history. This reads Personio's own feed live, at the bottom of that price range. |
| **Personio's own feed** | Free | The same XML | It is the same XML. This handles the `.de`/`.com` domain question, job-level filtering, delta tracking, and retries — and hands you a flat table. |
| **Clay** | $0.08–$0.40 per enriched row in credits, on top of a seat | A whole enrichment workspace — waterfalls across dozens of providers, plus the table and the sequencing around it | If you want one place that does everything and you're not counting rows, that's Clay. This is one column, priced per column, callable *from* Clay via its HTTP step. |

Prices for third-party tools are their published list prices as of August 2026 and are not tracked
here — check the vendor before relying on the comparison.

### FAQ

**What counts as a "company" input?**
Either the bare Personio subdomain (e.g. `personio`) or a full careers URL/hostname containing
`<subdomain>.jobs.personio.de` or `.com` — the subdomain (and domain) are extracted automatically
either way.

**Why is a row empty / why does `found` say false?**
Either the input doesn't look like a Personio subdomain or URL (`status: BAD_FORMAT`), or that
company doesn't have a Personio career page, or currently has zero open positions
(`status: NOT_FOUND`). Check the `message` column for the specific reason — either way, the row is
free.

**Why was I charged for a company that returned 0 jobs?**
Because its career page exists and has open roles — they just didn't match your filters, or
weren't new since your last run. `totalJobCount` shows how many were actually on the board. A
company with no Personio career page, or with nothing open at all, comes back `found: false` and
is never charged.

**Does the "new since last run" state cost anything or leak between users?**
It lives in a named key-value store (`personio-jobs-lookup-state`) inside your own Apify account,
so nobody else can see it and its storage cost is negligible (a list of numeric job IDs per
company). Delete that store to reset every watchlist, or change a filter to start a fresh
watchlist for just that combination.

**Are there rate limits I should worry about?**
The actor makes one request per company against Personio's own public feed — no API key or rate
limit tier applies. If you're checking a very long list, use the Input tab's concurrency setting to
control how many requests run in parallel.

**Does this touch any personal or candidate data — is it GDPR-safe?**
This actor reads only Personio's public job-postings XML feed: job title, department, office,
employment type, seniority, schedule, and a posting date. It doesn't read applications, candidate
records, or anything behind a login inside Personio's recruiting system — nothing here is personal
data about a candidate.

**Can I run this on a schedule?**
Yes — set it up as an Apify scheduled task like any other actor. Combine that with
`onlyNewSinceLastRun` for a genuine hiring alert instead of a full re-fetch every time.

**Can an AI agent call this directly?**
Yes — it's registered with the Apify MCP server, so an agent in Claude, Cursor, or any MCP client
can find and run it by name ("Personio Jobs Lookup"). See the integrations section above.

**Does this cover every company, or just ones in a particular region?**
Only companies you point it at that actually use Personio as their ATS. Personio is common among
small-and-mid-size companies in Germany, Austria, and Switzerland specifically — it isn't a global
ATS the way Greenhouse or Workday are, so if your list is mostly US or UK companies, expect a lower
hit rate. This actor doesn't discover which companies use Personio; you supply the list.

### Related actors

- [ATS Jobs Unified Lookup](https://apify.com/accountable_eel/ats-jobs-unified-lookup)
- [Workday Jobs Lookup](https://apify.com/accountable_eel/workday-jobs-lookup)
- [Greenhouse Jobs Lookup](https://apify.com/accountable_eel/greenhouse-jobs-lookup)

# Actor input Schema

## `companies` (type: `array`):

One company per line — paste the Personio subdomain or the full careers page URL. Accepted formats: personio, https://personio.jobs.personio.de, https://personio.jobs.personio.com. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `titleKeywords` (type: `array`):

Optional. Keep only roles whose title contains at least one of these words — e.g. "engineer", "account executive", "designer". Case doesn't matter, and partial words work ("engineer" matches "Engineering Manager"). Leave empty to keep every role.

## `excludeTitleKeywords` (type: `array`):

Optional. Drop any role whose title contains one of these words — e.g. "intern", "senior", "contract". Applied after the include list above, so a role matching both is dropped.

## `locations` (type: `array`):

Optional. Keep only roles whose location mentions one of these — e.g. "Berlin", "Munich", "Remote". Case doesn't matter and partial matches work. Leave empty to keep every location.

## `remoteOnly` (type: `boolean`):

Keep only roles whose location or title says remote (also matches "anywhere", "work from home", "distributed"). Companies label remote work inconsistently, so treat this as a strong filter, not a guarantee.

## `postedAfter` (type: `string`):

Optional. A date like 2026-06-01 (or a full timestamp). Keeps only roles Personio first created on or after this date. Leave empty for no date limit.

## `onlyNewSinceLastRun` (type: `boolean`):

Turn this on for a scheduled watchlist: the first run returns everything that matches and remembers it, and every run after that returns only postings it hasn't shown you before. A company with nothing new still gets a row (with 0 jobs), so a quiet schedule never looks like a broken one. Changing the filters above starts a fresh watchlist.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each job posting found gets its own row instead of being grouped under its company. You're still only charged once per company, no matter how many rows it produces.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## `proxyConfiguration` (type: `object`):

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "companies": [
    "personio"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locations": [],
  "remoteOnly": false,
  "postedAfter": "",
  "onlyNewSinceLastRun": false,
  "columns": [
    "subdomain",
    "jobCount",
    "totalJobCount",
    "newJobs",
    "firstRun",
    "jobs",
    "title",
    "department",
    "office",
    "location",
    "employmentType",
    "seniority",
    "schedule",
    "createdAt",
    "postedAt",
    "remote",
    "applyUrl"
  ],
  "expandRows": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (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 = {
    "companies": [
        "personio"
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
    "titleKeywords": [],
    "excludeTitleKeywords": [],
    "locations": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/personio-jobs-lookup").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 = {
    "companies": ["personio"],
    "includeKeywords": [],
    "excludeKeywords": [],
    "titleKeywords": [],
    "excludeTitleKeywords": [],
    "locations": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/personio-jobs-lookup").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "personio"
  ],
  "includeKeywords": [],
  "excludeKeywords": [],
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locations": []
}' |
apify call accountable_eel/personio-jobs-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/personio-jobs-lookup"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/UBxxFZU2nHAX5eCeJ/builds/F0Surh4QzI0EFAu4e/openapi.json
