12 KiB
name, description
| name | description |
|---|---|
| personio-headcount | 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:
- Snapshot mode (fast) — uses cached data to answer headcount questions instantly
- Refresh mode — fetches latest from Personio via
Call_Personio_Get_Employees_MCP tool in batches of 50 - 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
<skill-directory>/data/headcount_snapshot.json — anonymized employee records
<skill-directory>/data/team_mapping.json — team_id ↔ team name lookup (derived)
<skill-directory>/data/department_mapping.json — department_id ↔ department name + team groupings (derived)
<skill-directory>/data/office_mapping.json — office_id with headcounts (derived)
<skill-directory>/data/team-mapping.csv — SOURCE OF TRUTH for team ID → name
<skill-directory>/data/department-mapping.csv — SOURCE OF TRUTH for department ID → name
<skill-directory>/scripts/build_snapshot.py — merge/deduplicate employee data, rebuild mappings
<skill-directory>/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
{
"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:
{
"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:
{
"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:
{
"total_offices": 8,
"offices": [
{
"office_id": "501083",
"active_headcount": 120,
"total_records": 500
}
]
}
Answering Headcount Questions
When the user asks about headcount:
- Read the snapshot from
data/headcount_snapshot.json - Read the team mapping from
data/team_mapping.json - Check freshness — if
last_refreshedis today, use it directly. If older, mention the date and offer to refresh. - 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": "<nextCursor>"}
Step 4: Repeat until hasMore is false
Each employee record from the MCP tool contains:
id→ store aspersonio_idstatus→ ACTIVE, INACTIVE, ONBOARDING, or LEAVEteam_id→ Personio team ID (look up name in team_mapping.json)department_id→ Personio department IDoffice_id→ Personio office IDstart_date→ employment startend_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:
- Read the existing snapshot
- Fetch all pages from Personio (the API doesn't support a "modified since" filter yet)
- 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)
- Recalculate the summary counts
- Update
last_refreshedtimestamp - Rebuild team_mapping.json counts (active_headcount and total_records per team)
- Save all files
The helper script at scripts/build_snapshot.py automates the merge logic. After collecting
all employee batches into a list, run:
python3 <skill-dir>/scripts/build_snapshot.py \
--existing <skill-dir>/data/headcount_snapshot.json \
--new-data /tmp/fresh_employees.json \
--output <skill-dir>/data/headcount_snapshot.json
Important: Stripping PII
Before storing ANY data, remove all personally identifiable information:
- Remove
first_nameandlast_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": "<nextCursor>"}
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
pip install reportlab --break-system-packages
python3 <skill-dir>/scripts/build_report.py \
--data-dir <skill-dir>/data \
--output /path/to/output/helloprint_workforce_report.pdf
What the Report Contains
The PDF report has 6 pages:
- Cover page — KPI cards showing active employees, onboarding, on leave, and inactive counts
- Section 1: Headcount by Team — all active teams ranked by headcount with percentages
- Section 2: Headcount by Department — departments with team listings
- Section 3: New Joiners — monthly breakdown since January 2025, with team attribution
- Section 4: Departures — monthly breakdown since January 2025, with team attribution
- 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