Files
skills-build-team/personio-headcount/SKILL.md
T

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:

  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

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

  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": "<nextCursor>"}

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:

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_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": "<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:

  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