P2503 · Karriere · F-280 · Spike-Artefakt

Personio-REST-Import — authentifizierte Spiegelung der Stellenanzeigen

SUPERSEDED P2503-106 Erstellt: 2026-05-15 · Verworfen: 2026-05-15

User Story

Als HR-Verantwortliche:r von SOLCOM möchte ich, dass alle in Personio gepflegten offenen Stellenanzeigen automatisch in den Karriere-Bereich von solcom.de gespiegelt werden — über eine authentifizierte Service-Identity, die compliance- und IT-Security-konform ist — damit die Website tagesaktuell den HR-Bestand widerspiegelt, ohne manuelle Doppelpflege in Drupal und ohne dass externe Datenquellen anonym angesprochen werden.

Technische Beschreibung

Drupal-Modul solcom_vacancy wird auf einen authentifizierten REST-API-basierten Sync gegen Personio REST API v1 umgebaut. Auth via JWT-Bearer-Token (Client-ID + Client-Secret aus dem drupal/key-Modul mit Env-Provider). Service-Layer PersonioApiClient + PersonioPositionMapper + PersonioVacancySyncer, getriggert von hook_cron, ausgeführt über Drupal Queue-API. Synchroner Hard-Delete-Sweep vor Queue-Befüllung. Datenmodell, Vocabularies und Canvas-Templates bleiben unverändert.

Narrativ-Hinweis

Diese Story ist als neuer authentifizierter Personio-Import formuliert (Outcome-orientiert). Der bisherige Backlog-Eintrag F-280 referenzierte einen anderen Datenkanal; mit dieser Story wird der authentifizierte REST-Pull gebaut. Begründung: Compliance- und IT-Security-seitige Anforderung, dass externe Datenquellen mit Service-Identity angesprochen werden.

Kontext aus docs/

  • PRD: 2026-05-15-personio-rest-import-prd.html — die vollständige Spezifikation (Problem, Solution, Goals, 12 Implementation Decisions, Sub-Task-Plan, FAQ).
  • ADR-0029: Solcom SF-Publication Hybrid-Architektur — wird im Zuge dieser Story in-place aktualisiert, weil das dort definierte SF-Pattern (direktes getenv()) auf das key-Modul-Pattern umgestellt wird.
  • ADR-0031 (TBD, neu): External API Credentials via key-Module — Pattern-Definition für künftige externe API-Integrationen.
  • ADR-0032 (TBD, neu): External REST Sync ohne Migrate-API — Service+Queue-Pattern für authentifizierte externe Pulls.
  • ADR-0026: Vacancy-Ausschluss aus Editorial-Workflow — bleibt unverändert in Kraft (Drupal-Editor:innen können Vacancy-Nodes nicht manuell anlegen oder ändern).
  • Bisheriges PRD: 2026-05-02-personio-vacancy-integration-prd.html — als superseded markiert; Inhalte bleiben als historischer Kontext erhalten.

Akzeptanzkriterien (Stakeholder-Sprache)

  1. Eine in Personio neu angelegte offene Stelle erscheint spätestens 60 Minuten später im Karriere-Bereich von solcom.de.
  2. Eine in Personio geschlossene Stelle verschwindet spätestens 60 Minuten später aus dem Karriere-Bereich.
  3. Eine in Personio geänderte Stelle (z. B. Titel, Beschreibung, Office) wird spätestens 60 Minuten später auf der Drupal-Site aktualisiert.
  4. Jeder Personio-Zugriff erfolgt authentifiziert über eine Service-Identity; Client-ID und Client-Secret liegen nicht im Git-Repository, sondern werden über das drupal/key-Modul aus Umgebungsvariablen bezogen.
  5. Bei einer leeren Personio-Antwort (z. B. wegen API-Outage) wird der Drupal-Bestand nicht gelöscht; eine Warnung wird im Watchdog-Log sichtbar.
  6. Ein:e System-Operator:in kann per Drush-Command einen Manual-Sync auslösen und den Sync-Zustand abfragen, ohne sich in Drupal-Admin einzuloggen.
  7. Drupal-Editor:innen können Vacancy-Nodes nicht manuell anlegen oder ändern (konsistent mit ADR-0026).
  8. Die Pattern-Entscheidungen (key-Modul für Credentials, Service+Queue für authentifizierte REST-Syncs ohne Migrate-API) sind als ADRs dokumentiert.
  9. Alle Tests laufen grün vor PR-Merge; Test-Pyramide deckt Mapper (Unit), Auth-Client, Syncer und Worker (Kernel) ab.
  10. Der Wechsel vom alten zum neuen Sync-Pfad erfolgt atomar (kein Zeitfenster mit parallel laufenden Pulls).

Abhängigkeiten

  • Personio Service-Identity-Credentials verfügbar (Client-ID papi-… liegt vor; Client-Secret muss zur Verfügung gestellt und in Env-Vars auf lokaler Workstation, Mittwald-Stage und Mittwald-Prod hinterlegt sein).
  • Mittwald-App-Config: Env-Vars PERSONIO_CLIENT_ID + PERSONIO_CLIENT_SECRET in Stage- und Prod-App.
  • ADR-0029: SF-Publication-Story (P2503-108) muss beim Bau das key-Modul-Pattern adoptieren — Cross-Story-Abhängigkeit, dort als Aufgabe einzuplanen.
  • Composer-Dependency: drupal/key wird hinzugefügt; CI-Pipeline muss composer install erfolgreich durchlaufen.

Sub-Task-Plan — 6 Vertical Slices

Wird beim Refinement als Jira-Sub-Tasks unter P2503-106 angelegt (Skill to-issues oder manuell via acli). Reihenfolge entspricht der empfohlenen Deployment-Sequence.

#SliceKurzbeschreibungStatus
1Spike + ADR-TrioLive-Spike gegen Personio REST API v1 (Token-Fetch, Positions-Call, Field-Mapping-Verifikation). ADR-0031 + ADR-0032 anlegen, ADR-0029 in-place Update.tbd
2PersonioApiClient + key-Moduldrupal/key + zwei Key-Entities (Env-Provider), PersonioApiClient-Service mit Token-Cache und Refresh-on-401. Unit/Kernel-Tests.tbd
3PersonioPositionMapperJSON-Position-Object → Drupal-Field-Array, inkl. Term-Auto-Create und Description-Aggregation. Unit-Tests gegen Fixture-Responses.tbd
4Syncer + QueueWorker + hook_cronSync-Orchestrierung mit synchronem Sweep, Queue-Befüllung, Worker-Verarbeitung. Kernel-Tests.tbd
5Cleanup Migrate-Legacymigrate_plus/migrate_tools entfernen, alte Migrate-Files löschen, alter Cron-Pfad ersetzen, CONTEXT.md-Update. Atomar mit Slice 4 mergen.tbd
6Operative ToolingDrush-Commands solcom_vacancy:sync + solcom_vacancy:status, Watchdog-Channel, State-Tracking.tbd

Offene Fragen (Pre-Implementation-Gates)

  • Client-Secret: Wird angefragt und vor Slice 2 in den drei Env-Layers hinterlegt sein? (Owner: SOLCOM-IT / Personio-Admin)
  • Spike-Output Slice 1: Welche Response-Struktur liefert /v1/recruiting/positions konkret (apply_url, description-Sections, paging)? Output fließt zurück ins PRD.
  • SF-Adoption: ADR-0029-Update setzt das key-Modul-Pattern auch für SF-Publication. Wer baut die Migration der SF-Story (P2503-108) — als Sub-Task hier oder im SF-Bau-Sprint? Vorschlag: SF-Sprint, weil die SF-Story noch nicht in Arbeit ist.

Spike-Findings (2026-05-15) — Plan-Annahme widerlegt

Stand: Plan/PRD basieren auf der Annahme „authentifizierte REST API liefert vollständigen Job-Content". Live-Spike gegen Personio (Bearer-Token papi-…, 24h gültig) hat diese Annahme erschöpfend widerlegt. Vor Implementation muss die Architektur-Richtung neu entschieden werden.

Quellen-Vergleich

QuelleAuthJob-SetFelder pro Job
POST api.personio.de/v1/auth +
GET api.personio.de/v2/recruiting/jobs (Beta-Header)
Bearer papi-…, Client-ID + Client-Secret via /v1/auth, opaque Token (kein JWT), expires_in: 86400s 429 distinct (alle Org-Jobs inkl. archiviert + Drafts) 10 Header-Felder: id, name, category{id,name}, company{id}, department{id,name}, hiring_team[], created_at, updated_at, requirements_parsed_data, total_requirements — die letzten zwei sind in 0/2345 Antworten je non-null
GET solcom-gmbh.jobs.personio.de/xml keine (public) 32 (nur Career-Site-live) 15 Content-Felder: id, subcompany, office, department, recruitingCategory, name, jobDescriptions, employmentType, seniority, schedule, yearsOfExperience, keywords, occupation, occupationCategory, salaryInformation

Beispiel: REST-Response für Job 2030483

{
  "id": "2030483",
  "name": " Sales Manager als Teamleiter:in (m/w/d) Vertrieb Karlsruhe",
  "category":   { "id": "524768",  "name": "Vertrieb" },
  "company":    { "id": "78231" },
  "department": { "id": "2277747", "name": "Vertrieb" },
  "hiring_team": [
    { "person": { "id": "25230789", "_meta": { "links": { "self": { "href": "https://api.personio.de/v2/persons/25230789" } } } },
      "role":   { "name": "Stellenverwalter" } },
    /* … 5 weitere hiring_team-Einträge … */
  ],
  "created_at": { "date-time": "2025-03-25T13:33:25Z", "timezone": "UTC" },
  "updated_at": { "date-time": "2025-07-31T10:10:26Z", "timezone": "UTC" },
  "requirements_parsed_data": null,
  "total_requirements": null,
  "_meta": { "links": { "self": { "href": "https://api.personio.de/v2/recruiting/jobs/2030483" } } }
}

Beispiel: XML-Response für Position 2519922 (verkürzt)

<position>
  <id>2519922</id>
  <subcompany>SOLCOM GmbH</subcompany>
  <office>Reutlingen</office>
  <department>Vertrieb</department>
  <recruitingCategory>Recruiting</recruitingCategory>
  <name>(Junior) Recruiter (m/w/d) – Vertrieb &amp; Talent Management</name>
  <jobDescriptions>
    <jobDescription>
      <name>Das erwartet Dich</name>
      <value><![CDATA[<span style="font-size:18px;">Seit 1994 … </span>…]]></value>
    </jobDescription>
    <jobDescription><name>Das bringst Du mit</name><value><![CDATA[<ul><li>…</li></ul>]]></value></jobDescription>
    <jobDescription><name>Deine Benefits</name><value><![CDATA[…]]></value></jobDescription>
    <jobDescription><name>Standort</name><value><![CDATA[…]]></value></jobDescription>
    <jobDescription><name>Team</name><value><![CDATA[…]]></value></jobDescription>
    <jobDescription><name>Startdatum</name><value>…</value></jobDescription>
    <jobDescription><name>Ansprechpartner/in</name><value>…</value></jobDescription>
  </jobDescriptions>
  <employmentType>permanent</employmentType>
  <seniority>entry-level</seniority>
  <schedule>full-time</schedule>
  <yearsOfExperience>lt-1</yearsOfExperience>
  <keywords>…</keywords>
  <occupation>…</occupation>
  <occupationCategory>…</occupationCategory>
  <salaryInformation>
    <min>45000</min><max>50000</max>
    <currencyCode>EUR</currencyCode><currencySymbol>€</currencySymbol><type>year</type>
  </salaryInformation>
</position>

Weitere verifizierte Befunde

  • Token-Format: opaque papi-…, kein JWT. Plan-AK „JWT-Format dokumentiert" ist nicht haltbar. TTL kommt aus expires_in-Feld der Auth-Response (24h).
  • Auth-Endpoint: POST /v1/auth mit application/x-www-form-urlencoded-Body (client_id + client_secret). Response: {success: true, data: {token, expires_in, scope}}.
  • Job-Endpoint: /v1/recruiting/positions (im Plan) existiert nicht. Korrekt ist GET /v2/recruiting/jobs mit Beta: true-Header.
  • Pagination: cursor-basiert (?limit=200&cursor=…). Termination muss über _data === [] laufen, nicht über fehlenden next-Link — Personio gibt auch nach erschöpfter Page-Folge weiter einen Cursor zurück (Loop). Über 21 Pages und 2345 Antworten gesammelt, 429 distinct IDs.
  • Schema-Audit über alle 2345 Antworten: 10 Top-Level-Keys, immer dieselben. Kein einziges Sample hatte je zusätzliche Felder. requirements_parsed_data und total_requirements sind 0/2345 non-null — die API setzt diese Felder im Recruiting-Resource derzeit nicht.
  • Set-Differenz: REST liefert 429 Jobs, XML 32 — Faktor 13,4×. Beispiel-Job 2030483 existiert in REST, aber NICHT im XML — REST enthält archivierte und Drafts, XML nur die live-veröffentlichten Career-Site-Positions.

Architektur-Optionen zur Stakeholder-Klärung

  1. Hybrid — REST für Lifecycle/Set-Wahrheit (welche Jobs aktuell, updated_at-Filter, archiviert-Erkennung), XML für Content (15 Felder). Service+Queue+key-Modul-Pattern aus Plan bleibt, aber Mapper liest XML, REST steuert die Sweep-Liste. PRD und Plan müssen umgeschrieben werden, Wording „authentifizierte Spiegelung" wird „authentifizierte Lifecycle-Steuerung + XML-Content". Brauchbarkeit des key-Moduls bleibt (audit, identity), wird aber kleiner.
  2. XML-only — Plan radikal vereinfachen: kein /v1/auth, kein drupal/key, kein REST. Service+Queue+hook_cron direkt auf dem public XML-Feed. Schnellster Weg zu lauffähigem MVP (2026-07-08). Kosten: der ursprüngliche Treiber „Compliance/Service-Identity" entfällt — externe Datenquelle bleibt anonym angesprochen, was IT-Security ggf. reframen muss.
  3. Pausieren / GA-warten — Personio-Support fragen, ob das v2-Job-Schema im GA-Release Content-Felder bekommt. Bis dahin Status quo (alter Migrate+XML) belassen. Risiko: MVP-Termin 2026-07-08 verfehlt.

Goal-Mode pausiert, bis Entscheidung gefallen ist.

Worklog

DatumPhaseEintrag
2026-05-15 Refinement Grilling-Session mit Skill grill-with-docs: 12 Architektur-Decisions herausgearbeitet (Treiber, API, Secret-Storage, Refactor-Scope, Sync-Stack, Token-Service-Lokation, Queue, Sweep, Tests, ADR-Stack, Doku-Form, Narrativ). PRD 2026-05-15-personio-rest-import-prd.html angelegt. Bisheriges PRD 2026-05-02-personio-vacancy-integration-prd auf superseded gesetzt.
2026-05-15 Spike (Slice 1) Live-Spike gegen Personio REST + XML mit Credentials, vollständige Pagination (429 distinct Jobs). Befund: REST liefert nur 10 Header-Felder, alle 11 Content-Felder leben ausschließlich im XML-Feed. Plan-Kern-Annahme „authentifizierte REST = vollständige Spiegelung" widerlegt. Details siehe Section Spike-Findings. Goal-Mode pausiert, drei Architektur-Optionen zur Stakeholder-Klärung dokumentiert.
2026-05-15 Architektur-Reframe Grilling-Session: Pfad (c) „Cancel + Tolerated-Risk-ADR" gewählt. Migrate+XML bleibt kanonisch — Drupal-Best-Practice für recurring strukturierte XML-Feeds; keine Service+Queue-Trigger erfüllt; Pattern-Geschwister F-290/F-329 (REST/auth) gehören zu ADR-0029. Kanonische Story zurück auf F-280-personio-xml-feed.html. Diese Story als historisches Spike-Artefakt erhalten. PRD und Plan werden auf superseded gesetzt. Architektur-Entscheidung in ADR-0035 festgehalten.