# Changelog of Personio Jobs Scraper (`redomic/personio-jobs`) Actor

- **URL**: https://apify.com/redomic/personio-jobs/changelog.md
- **Full Actor documentation**: https://apify.com/redomic/personio-jobs.md

## Changelog

### 0.1.0

- Initial release: scrape Personio company career pages from public XML feeds.
- Jobs and monitor modes (new/changed/removed with stateful diff).
- Filters for keyword, location, department, employment type, schedule, seniority, and recency.
- Per-company status rows and run summary.

### 0.1.1

- Extract salary from descriptions when present and add ageDays/isEvergreen flags.
- Flag placeholder/lorem ipsum descriptions as contentQuality/placeholder.
- Improve occupation/occupationCategory mapping for generic values.
- Surface total-open count vs capped results in status reason when maxItems limits output.
- Clarify monitor first-run baseline charging in status/reason and README.
- Derive `workplaceType` (remote/hybrid/onsite/unknown) per job and add `workplaceTypes` input filter.
- Extract compensation only near salary keywords (Gehalt/Vergütung/salary/compensation), normalize to `salary` + `salaryMin`/`salaryMax`/`salaryCurrency`/`salaryPeriod`, and reject revenue/GMV/million/billion context or implausible annual-equivalent amounts.

### 0.1.2

- Add `includeGeneralApplications` (default `false`) to exclude general/speculative applications (e.g. Initiativbewerbung/Spontanbewerbung) and return live role postings by default.
- Exclude placeholder/test rows and postings without a department by default (opt-in with `includeGeneralApplications=true`).
- Keep and document salary/compensation fields (`salary`, `salaryMin`/`salaryMax`, `salaryCurrency`, `salaryPeriod`) in README/output.
- Parse structured Personio `salaryInformation` (min/max/currency/period) to populate salary fields when present; drop rows without a department even in opt-in mode.

### 0.1.4

- Default first run is useful: `sort` (`newest` default, `oldest`) orders rows before the `maxItems` cap, and `excludeEvergreen` (default `true`) drops postings older than a year when a board has fresh roles.
- Add `daysOpen` to every row and a run-level `dataQuality` summary (`freshRows`, `evergreenRows`, `salaryCoverage`, `descriptionCoverage`, `failedRate`).
- README: real code-produced output table, real input example, reliability and value proof.

### 0.1.3

- Add a `language` input (default `en`, empty = board default) and request `?language=<code>` from the Personio feed.
- Two-pass description merge: when the requested language leaves descriptions blank, fetch the board-default feed and fill the gaps per position; the requested language wins and a failed second fetch never fails the run.
- Add `descriptionLanguage` (`en`, `default`, or null) to every row.

### 0.1.5

- Add a per-run `INPUT-REPORT` (and `INPUT-REPORT.json`) coverage report in the default key-value store for multi-company runs, with per-input status, reason, total/matched/delivered counts and mode.
- Add `coverageReport: "INPUT-REPORT"` to the dataset summary row; dataset status rows unchanged.

### 0.1.7

- Add `minSalary`/`maxSalary` (annual EUR, annualized year x1/month x12/hour x2080 via `src/text.py`) and `skillsInclude`/`skillsExclude` (keywords/occupation/description) filters in `src/main.py` (`Config`/`build_input`/`matches`/monitor key); null-salary rows are excluded when a salary bound is set.

### 0.1.8

- Add `includeRemoved` (default `true`) for monitor mode to suppress `changeType=removed` rows while still updating state, enabling new/changed-only alert runs. Includes input schema, docs and tests.

### 0.1.8

- Add structured `skills`, `responsibilities`, `benefits`, and `requirements` arrays extracted from job description sections (heading/keyword heuristics over `descriptionSections`/text, deduped, empty when absent) on every job row, and expose them in dataset output and README.

### 0.1.6

- Replace the demo-board default (`personio-gmbh`) with a live board (`solactive`) so empty `{}` runs return real postings; update README, input schema prefill/default, and fixtures README.

### 0.1.8

- Add `mode=search` (or omit `companies`) to fan out over the bundled 20 Personio boards in `src/discovery.py`, applying all filters across boards, deduping by `globalId`, ordering newest-first globally with a global `maxItems`, and writing per-board status to `INPUT-REPORT`.

### 0.1.9

- Add `.actor/store_task.json` showcase task for scraping a Personio company career page (Store quality / landing page).

### 0.1.10

- Expand `mode=search` discovery to a curated seed list of 115 verified public Personio boards (keeps dedupe/newest-first behavior) and update README/input schema seed-count references.
- Add tests for seed size/uniqueness and empty-`companies` search fan-out over the expanded list with per-board `INPUT-REPORT` entries and failed dead seeds without failing the run.

### 0.1.11

- Agent-ready defaults: `postedSinceDays`, `minSalary` and `maxSalary` now have `0` defaults (treated as unset) so agents sending `{}` can use them without omitting fields; Apify input schema now validates.

### 0.1.12

- Remove the demo `personio-gmbh` board from the bundled search seed list so search-mode fan-out uses live boards only; default, canary and battery inputs continue to target live boards (default run >= 25 real rows).
- Improve salary extraction in `src/text.py`: parse slash ranges, `ab/from/von`/`bis/to` ranges, currency-after-number forms, and `pro Jahr/Monat`/slash period markers.
- Reject revenue/average-compensation contexts in salary parsing; integration via `src/parser.py` with coverage and extraction tests.

### 0.1.13

- Add `postedAfter` (YYYY-MM-DD, inclusive) as an alternative to `postedSinceDays` for date-based pulls; invalid dates are rejected, and monitor state is keyed by the filter.

### 0.1.14

- Add structured location fields `city`, `country`, `location`, `locations` parsed from `office`/`additionalOffices` (including multi-office and `Remote - Germany` forms).
- Add `countries` and `cities` input filters (OR, case-insensitive) matched against parsed `country`/`city` in jobs, monitor and search modes.

### 0.1.15

- Add buyer-visible filters: `remoteOnly`, `titleKeywords`, `excludeTitleKeywords`, `excludeLocations`, `hasSalary`, `jobFunctions` (case-insensitive OR where applicable) in `build_input`/`matches` and monitor state keys; update input schema and README.

### 0.1.16

- Add `includeRawJson` (default `false`) to attach the original, unmodified Personio position payload to each job row under `raw` for full-fidelity integrations; omitted when false. Stable job fields unchanged.

### 2026-11-12

- Verify default `{}` input resolves to the live `solactive` board and returns at least 25 real rows; assert battery defaults to live boards (no `personio-gmbh` demo default).

### 0.1.17

- Add field-level change detection in monitor mode: `changed` rows include `changedFields` and scalar `changes` (`{field, from, to}`) for monitored fields; snapshots store per-field values; `removed` rows keep last-known `title`/`url`. README and dataset schema updated.

- 2026-11-12: search mode fans out over 25 boards that answer live (probed 12 Nov); 89 of the 114 bundled boards did not exist on jobs.personio.de or .com and only produced failed rows and slower runs.

- 2026-11-12: a search where some boards match nothing ends "ok"; "partial" now means a board could not be read (or, in jobs mode, a named company had no jobs).

### 0.1.8

- Add `discoverBoards` (and `discover_boards`) sourced from ATS seed registry slugs: probe each slug as `https://{slug}.jobs.personio.de/xml` and `.com`, keep only boards answering 200 with >=1 `<position>`.
- Add `load_registry_slugs` helper for reading `data/ats-seeds/*.txt` and record bundled seed probe date in `SEED_PROBE_DATE`.

### 0.1.18

- Add buyer-visible `firstSeenAt` (ISO-8601 UTC) on every job row, sourced from persisted monitor state/history: set to `scrapedAt` on first observation and preserved across runs, with backfill/null handling for unknown history; included in monitor change payloads, output/dataset schema, and README.

### 0.1.8

- Persist `firstSeenAt` across runs in all modes via named key-value store `personio-jobs-history` (`firstSeen` map keyed by `globalId`/id): jobs and search runs now preserve original first-seen dates instead of stamping every row with `scrapedAt`, while monitor mode keeps diff/state behavior unchanged. KV read/write failures never fail a run (fallback to current scrape time).

### Unreleased

- Write a per-input coverage report every run as `INPUT-REPORT` (JSON array) and `INPUT-REPORT.csv` in the default key-value store with `input`, `status`, `org`, `jobsFound`, `jobsEmitted`, `error`, preserving input order and including failed inputs without failing the run.
- Add `companyOpenRoles` (total open postings on the company board) to every job row in jobs, search and monitor modes.
- Derive `countryCode` (ISO 3166-1 alpha-2) from parsed country, office/city/location text, and board TLD.
- Improve salary parsing from description content (including description HTML/text fallback windows) to increase extracted compensation coverage.
- Write per-source outcomes and the run summary to the default key-value store as `STATUS` and `SUMMARY` records; the dataset contains only job rows.
- Drop postings whose title is only a test, dummy, placeholder or sample label.

### 0.1.8

- Ensure `countryCode` and `salaryMin`/`salaryMax`/`salaryCurrency`/`salaryPeriod` are parsed from structured `salaryInformation` and description content; verified non-null coverage on fixture-backed runs.

### 0.1.8

- Expose `contentHash` (stable 16-hex hash of tracked job fields) and `lastSeenAt` (ISO run timestamp) on every job/search/monitor row and on monitor `removed` rows.
- Persist both fields in monitor snapshots and first-seen history state for change-detection and repost dedupe across runs.

### Unreleased

- Add `includeUnchanged` (default false) for monitor mode: emit unchanged jobs with `changeType="unchanged"`, never charged; add input, diff, and tests.
- Derive `country` from `countryCode` when office/location parsing does not yield a country name, improving country coverage.
