From 16a3b9e2ee92938999f57459d8c7ffbaf422f573 Mon Sep 17 00:00:00 2001 From: SuperUser Date: Thu, 5 Mar 2026 13:56:00 +0000 Subject: [PATCH] Add personio-headcount skill (migrated from plugin) --- personio-headcount/SKILL.md | 302 ++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 personio-headcount/SKILL.md diff --git a/personio-headcount/SKILL.md b/personio-headcount/SKILL.md new file mode 100644 index 0000000..98edc03 --- /dev/null +++ b/personio-headcount/SKILL.md @@ -0,0 +1,302 @@ +--- +name: personio-headcount +description: > + Retrieves and reports HelloPrint's current headcount from Personio via the N8N Personio MCP tool. + Maintains a local anonymized snapshot (no PII — no names or personal details) so that repeat + queries are instant and only delta/incremental fetches are needed. Can generate professional + PDF workforce reports with team/department breakdowns, joiner/leaver analysis, and net movement. + Use this skill whenever someone asks about headcount, employee count, active employees, FTE count, + team size, staffing numbers, Personio data, workforce metrics, workforce report, HR report, + or anything related to how many people work at HelloPrint. Also trigger when someone mentions + "personio headcount", "HR numbers", "who's active", "onboarding count", "leavers", "attrition", + "joiners", "departures", "workforce insights", "headcount report", "team size report", + or wants a breakdown by status, department, team, office, or start date. Even casual questions + like "how many people do we have?" should trigger this skill. +--- + +# Personio Headcount Skill + +This skill gives you instant access to HelloPrint's workforce numbers by maintaining a local +anonymized snapshot of Personio data. It connects to Personio via the **"N8N Personio"** MCP +server (URL: `https://helloprint-staging.app.n8n.cloud/mcp/personio`). + +## How It Works + +There are three modes: + +1. **Snapshot mode** (fast) — uses cached data to answer headcount questions instantly +2. **Refresh mode** — fetches latest from Personio via `Call_Personio_Get_Employees_` MCP tool in batches of 50 +3. **Report mode** — generates a professional PDF workforce insights report + +The skill stores anonymized employee records — only IDs, status, team, department, office, and +dates. Never names, emails, or personal information. + +### Data Location + +``` +/data/headcount_snapshot.json — anonymized employee records +/data/team_mapping.json — team_id ↔ team name lookup (derived) +/data/department_mapping.json — department_id ↔ department name + team groupings (derived) +/data/office_mapping.json — office_id with headcounts (derived) +/data/team-mapping.csv — SOURCE OF TRUTH for team ID → name +/data/department-mapping.csv — SOURCE OF TRUTH for department ID → name +/scripts/build_snapshot.py — merge/deduplicate employee data, rebuild mappings +/scripts/build_report.py — generate workforce insights PDF report +``` + +The two CSV files (`team-mapping.csv` and `department-mapping.csv`) are the **single source of +truth** for human-readable names. The JSON mapping files are derived from these CSVs combined with +headcount data from the snapshot. When rebuilding mappings, always read names from the CSVs. + +Read the snapshot first. If it exists and `last_refreshed` is recent enough for the user's needs, +just report from the cached data. If it's stale or the user explicitly asks for fresh data, +run a refresh. Always load the team mapping alongside the snapshot to resolve team names. + +### Snapshot Structure + +```json +{ + "last_refreshed": "2026-03-04T19:00:00Z", + "total_records": 755, + "summary": { + "ACTIVE": 177, + "INACTIVE": 574, + "ONBOARDING": 1, + "LEAVE": 3 + }, + "employees": [ + { + "personio_id": "35653993", + "status": "ONBOARDING", + "team_id": "417186", + "department_id": "1291054", + "office_id": "501083", + "start_date": "2026-05-01", + "end_date": null + } + ] +} +``` + +### Team Mapping Structure + +The team mapping at `data/team_mapping.json` maps real Personio team IDs to human-readable names: + +```json +{ + "total_teams": 65, + "teams": [ + { + "team_id": "4400959", + "team_name": "AI & Automation", + "active_headcount": 6, + "total_records": 7 + } + ] +} +``` + +Use `team_id` in the snapshot to look up the human-readable name from the mapping. + +### Department Mapping Structure + +The department mapping at `data/department_mapping.json` groups teams under departments with +real names from `department-mapping.csv`: + +```json +{ + "total_departments": 19, + "departments": [ + { + "department_id": "1291054", + "department_name": "Finance & Business Navigation", + "active_headcount": 11, + "total_records": 45, + "team_ids": ["417186", "417184", "3442693", "3443053", "572110"], + "teams": ["Data", "Finance & Administration", "M&A", "Pricing", "Rev Ops"] + } + ] +} +``` + +### Office Mapping Structure + +The office mapping at `data/office_mapping.json` tracks headcount per office location: + +```json +{ + "total_offices": 8, + "offices": [ + { + "office_id": "501083", + "active_headcount": 120, + "total_records": 500 + } + ] +} +``` + +## Answering Headcount Questions + +When the user asks about headcount: + +1. **Read the snapshot** from `data/headcount_snapshot.json` +2. **Read the team mapping** from `data/team_mapping.json` +3. **Check freshness** — if `last_refreshed` is today, use it directly. If older, mention the + date and offer to refresh. +4. **Report the answer** — the "current headcount" is the count of employees with `status = "ACTIVE"`. + +Common questions and how to derive them: +- **"Current headcount"** → count where status = ACTIVE +- **"Total employees in system"** → total_records +- **"How many people left?"** → count where status = INACTIVE +- **"Who's onboarding?"** → count where status = ONBOARDING +- **"People on leave"** → count where status = LEAVE +- **"Headcount by team"** → group ACTIVE employees by `team_id`, look up names from team_mapping.json +- **"Headcount by department"** → group ACTIVE employees by `department_id`, use department_mapping.json for team groupings +- **"Headcount by office"** → group ACTIVE employees by `office_id` +- **"How big is team X?"** → find team by name in mapping, count ACTIVE employees with that team_id +- **"New hires this year"** → ACTIVE employees where start_date >= 2026-01-01 + +## Refreshing the Data + +When fresh data is needed, fetch from Personio via the **N8N Personio** MCP tool. + +### Full Refresh Process + +Use the `Call_Personio_Get_Employees_` MCP tool from the "N8N Personio" server: + +``` +Step 1: Fetch first page + Call_Personio_Get_Employees_ with input: {"limit": 50} + +Step 2: Parse result + The result is a JSON array containing one object with: + - employees: array of employee objects + - totalCount: number of records returned + - nextCursor: pagination cursor string + - hasMore: boolean + +Step 3: If hasMore is true, fetch next page + Call_Personio_Get_Employees_ with input: {"limit": 50, "cursor": ""} + +Step 4: Repeat until hasMore is false +``` + +Each employee record from the MCP tool contains: +- `id` → store as `personio_id` +- `status` → ACTIVE, INACTIVE, ONBOARDING, or LEAVE +- `team_id` → Personio team ID (look up name in team_mapping.json) +- `department_id` → Personio department ID +- `office_id` → Personio office ID +- `start_date` → employment start +- `end_date` → contract end date (null if ongoing) +- `first_name`, `last_name`, `email` → **NEVER STORE THESE** — strip before saving + +### Incremental Refresh (Delta Update) + +For efficiency, you don't always need a full refresh. The incremental approach: + +1. Read the existing snapshot +2. Fetch all pages from Personio (the API doesn't support a "modified since" filter yet) +3. Compare each fetched employee against the snapshot by `personio_id`: + - If ID exists → update the record (status, team_id, etc. may have changed) + - If ID is new → add it + - IDs in snapshot but not in fresh data → keep them (they're historical) +4. Recalculate the summary counts +5. Update `last_refreshed` timestamp +6. Rebuild team_mapping.json counts (active_headcount and total_records per team) +7. Save all files + +The helper script at `scripts/build_snapshot.py` automates the merge logic. After collecting +all employee batches into a list, run: + +```bash +python3 /scripts/build_snapshot.py \ + --existing /data/headcount_snapshot.json \ + --new-data /tmp/fresh_employees.json \ + --output /data/headcount_snapshot.json +``` + +### Important: Stripping PII + +Before storing ANY data, remove all personally identifiable information: +- Remove `first_name` and `last_name` +- Remove `email` +- Keep only: personio_id, status, team_id, department_id, office_id, start_date, end_date + +The `personio_id` is an internal system identifier, not PII — it's needed for deduplication +during incremental updates. + +## MCP Tool Reference + +**MCP Server:** n8n-personio (local config in `claude_desktop_config.json`) +**URL:** `https://helloprint-staging.app.n8n.cloud/mcp/personio` +**Auth:** Bearer token +**Tool:** `Call_Personio_Get_Employees_` (full ID: `mcp__n8n-personio__Call_Personio_Get_Employees_`) + +| Parameter | Type | Default | Description | +|-----------|--------|---------|--------------------------------------| +| limit | number | 50 | Records per page (use 50 for balance)| +| cursor | string | "" | Pagination cursor from previous page | + +Pass parameters as a JSON string in the `input` field: +- First page: `{"limit": 50}` +- Subsequent pages: `{"limit": 50, "cursor": ""}` + +### Authentication + +The N8N MCP Server Trigger uses **Bearer token authentication**. This is configured in the local +`claude_desktop_config.json` under the MCP server name `n8n-personio`. The Bearer token must +match the one set in N8N's MCP Server Trigger node. + +Note: Cowork's built-in MCP connector UI does not support Bearer or Header Auth — the local +config approach via `claude_desktop_config.json` is the working method. + +## Generating a PDF Report + +When the user asks for a workforce report, headcount report, or PDF report, use the +`build_report.py` script to generate a professional PDF. + +### Running the Report Script + +```bash +pip install reportlab --break-system-packages +python3 /scripts/build_report.py \ + --data-dir /data \ + --output /path/to/output/helloprint_workforce_report.pdf +``` + +### What the Report Contains + +The PDF report has 6 pages: + +1. **Cover page** — KPI cards showing active employees, onboarding, on leave, and inactive counts +2. **Section 1: Headcount by Team** — all active teams ranked by headcount with percentages +3. **Section 2: Headcount by Department** — departments with team listings +4. **Section 3: New Joiners** — monthly breakdown since January 2025, with team attribution +5. **Section 4: Departures** — monthly breakdown since January 2025, with team attribution +6. **Section 5: Net Workforce Movement** — monthly joiners vs leavers with cumulative net change + +Each section includes insight paragraphs highlighting trends (peak hiring months, largest teams, +year-start turnover patterns). + +### XML Escaping + +The report script uses `xml_escape()` to handle special characters in team and department names +(e.g., "M&A" → "M&A"). This is necessary because reportlab's Paragraph class uses an XML +parser internally. The `make_table()` helper automatically escapes all cell content. + +### Customizing the Report + +The script is parameterized via `--data-dir` and `--output`. The color scheme uses HelloPrint +branding (HP_BLUE `#1a3a5c`, HP_LIGHT_BLUE `#2980b9`). To change the date range for joiners/ +leavers analysis, modify the `datetime(2025, 1, 1)` threshold in `build_report()`. + +## Output Guidelines + +When reporting headcount: +- Lead with the headline number: **"Current active headcount: X"** +- Mention the snapshot date so the user knows how fresh the data is +- Offer a breakdown if relevant (by status, team, department, office) +- If the data is more than 1 day old, proactively offer to refresh \ No newline at end of file