--- 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