Workforce Planning — plain-language explainer

Workforce Planning is the canonical position spine: it tracks every budgeted, filled, and open seat in an organization, reconciles what HR, the ATS, and Finance each think the headcount is, and forecasts where it lands 30/60/90 days out.

A People Analytics Toolbox component. Built to the portfolio Explainer Standard v1.0. Every claim below is grounded in the spoke's own code and contracts (src/spokes/workforce-planning/, contract 0.5.0); anything not yet built is marked (TBD).


1. What is it?

Workforce Planning is the toolbox's position-control engine: it holds a canonical row for every position — budgeted, filled, and open headcount keyed by job family, level, org unit, and location — and keeps that row aligned with what the surrounding systems report.

It does three jobs on top of that spine. It reconciles the three numbers that never quite agree (HR's filled count, the applicant-tracking system's open requisitions, and Finance's budgeted seats), freezing the deltas into an immutable snapshot. It forecasts headcount forward over 30/60/90/365-day windows from trailing hire/exit pacing and ATS funnel throughput. And it matches incoming ATS requisitions to the right open seat — deciding, probabilistically, whether a new req fills an existing open position or is a genuinely new one.

Person-level facts stay in segmentation-studio; this spoke deliberately aggregates to position grain — the seat, not the seat-holder.

Visual — Tier B (typographic spine). The position-grain spine, as the contract defines it:

Position { jobFamilyId · jobLevelId · organizationUnitId · locationId · budgetedHeadcount · filledHeadcount · openHeadcount · status } — where status is one of filled · open-newly-created · open-from-exit · reconciled-by-operator (PositionLifecycleStatusSchema, contract 0.5.0).

2. What problem does it solve — and why is it different?

The pain it removes: three systems hold three different headcount numbers and nobody can defend which is right. HR's HRIS says 37 people are in seats. The ATS says 7 reqs are open. Finance budgeted 50. Those numbers are produced by different teams, on different cadences, against different definitions of "a job" — and at month-end someone has to explain the gap.

The difference, stated as a shift:

  • FROM a spreadsheet that reconciles headcount by hand once a quarter, with the reconciliation logic living in one analyst's head and no record of why the numbers diverged.
  • TO a position-grain spine where the deltas are computed deterministically, frozen as an immutable snapshot with an asOf date, and queryable as history — plus a forward forecast and a probabilistic answer to "is this new req actually a new seat?"

How it differs from the obvious substitutes:

  • vs. doing it by hand in a headcount spreadsheet — the spreadsheet can't tell you, three months later, what the numbers were on a given date or why they diverged. Workforce Planning persists reconciliation_snapshots as authoritative, immutable history (not a live stream that overwrites itself).
  • vs. a generic BI dashboard over the same tables — BI shows you the three counts side by side; it doesn't reconcile them (compute filledVsBudgeted, openVsUnfilledBudget), doesn't carry an append-only position-event audit behind each row, and has no opinion on whether an inbound requisition fills an existing open seat or warrants a new position.
  • vs. the ATS's own reporting — the ATS knows its own reqs; it has no model of the HRIS-filled or Finance-budgeted side, and no canonical position id to bridge them.

3. How does it work?

Inputs → method → outputs, concretely, framed as the questions a planner actually asks.

"What's my real headcount?" — reconciliation. Inputs are position-grain rows (budgeted / filled / open headcount per org unit). The pure core (computeHeadcountReconciliation) sums them and computes the deltas:

  • filledVsBudgeted = hrisFilled − financeBudgeted
  • openVsUnfilledBudget = atsOpen − max(0, financeBudgeted − hrisFilled)

Output is a HeadcountReconciliation snapshot — { asOf, hris.filled, hris.byUnit, ats.open, finance.budgeted, deltas } — persisted immutably.

"Where does headcount land next quarter?" — forecasting. The v1 TrailingAverageForecaster reads trailing hire/exit pacing from the append-only position_events history and projects it over the window (forecastWindowDays(window)). Layered on top (PAT-D1-B-FU-D): an ExitRatePerSegmentForecaster (segment-level exit pacing, with regretted-exit attribution), a FunnelThroughputForecaster (projected hires from the ATS application funnel + offer states), and a combined projectHeadcount convolver that produces a balance-sheet rollup: begin + plannedHires − projectedExits = end. Output is a ForecastSnapshot / ProjectedHeadcountResult.

"Does this new req fill an existing seat?" — Bayesian matching. When an ATS requisition arrives, the engine (proposeMatches, PAT-D1-B-FU-C) scores each eligible open position against the req on seven likelihood-ratio features — org-exact, manager-exact, manager-chain overlap, job-family, location-distance, time-since-exit, and an exit-derived-slot preference — multiplies them against a tenant Beta–Bernoulli prior, and competes the whole mixture against a "phantom" brand-new-position hypothesis weighted by 1 − prior. Output is a ranked MatchProposal with a posterior per candidate, the top-vs-phantom recommendation, and a mostLikelyKind of fills-existing-open-position | new-position | ambiguous.

Differentiation beat: the planner's real question is not "what are the three numbers" — it's "which gap do I owe an explanation for, and is this hire net-new or backfill?" The reconciliation deltas localize the gap (and deltas.byUnit localizes it to an org unit); the matcher answers net-new-vs-backfill with a posterior and an audit row (match_engine_audit) recording the prior and candidate list at run time. Both are defensible after the fact, not just at the moment of the query.

Data sources / science: trailing-average and per-segment exit-rate forecasting; a conjugate Beta–Bernoulli prior updated on each accepted/rejected match; likelihood-ratio feature scoring for the mixture model. Inputs are uploaded HRIS position data, ATS requisitions (Greenhouse webhook with HMAC signature verification, vendor-neutral DDL for other systems), and Finance allocations (with FX snapshot columns). Position ids align with job-family-agent / segmentation-studio semantics.

Visual — Tier B (step flow). HRIS positions + ATS reqs + Finance allocations → { reconcile → frozen delta snapshot · forecast → 30/60/90/365 projection · match → ranked posterior per open seat }.

4. What does it enable?

Concrete uses a practitioner would recognize:

  • Month-end headcount attestation — freeze a reconciliation snapshot tying HRIS filled vs ATS open vs Finance budgeted, with deltas localized by org unit, as authoritative history you can point back to.
  • 30/60/90 (and 365-day) headcount forecast — project filled headcount and budget variance forward from trailing hire/exit pacing for scenario planning and board-deck anchors.
  • Net-new vs. backfill triage — when a req opens, get a probabilistic call on whether it fills an existing open seat (so it doesn't double-count against budget) or is a genuinely new position.
  • Exit-pacing forecast with regret attribution — segment-level projected exits, separating regretted from non-regretted, from the position-event taxonomy.
  • ATS funnel-throughput hiring forecast — project hires from application-status and offer-state pacing rather than guessing fill rates.
  • Operator correction queue — route ambiguous matches to a human (/conductor/matches) who accepts, rejects-and-creates-new, or overrides with a reason — and that decision updates the tenant prior.

Visual — (TBD — a rendered reconciliation-delta waterfall: budgeted → filled → open → variance for one tenant).

5. How it fits in the toolbox

Data flow:

  • Consumes — uploaded HRIS position rows, ATS requisitions (Greenhouse webhook + vendor-neutral envelope), and Finance allocations. It reads other spokes' contracts only (job-family / anycomp semantics for ids and FX columns) — never their internal core/, db/, or routes.
  • Aggregates fromsegmentation-studio owns the person-level facts; Workforce Planning rolls those up to position grain (it does not duplicate person records).
  • Emits — the workforce-planning contract (Position, HeadcountReconciliation, ForecastSnapshot / ProjectedHeadcountResult, MatchProposal, plus the ATS-parity and position-event taxonomy types). Consumers vendor src/spokes/workforce-planning/contracts/types.ts.
  • Feeds — registered consumer performix (status planned in the registry). The position-control + forecast surface aligns with the planning deck (slide 11).
  • Transports — REST under /api/spokes/workforce-planning/* (POST routes gated by requireServiceKey, tenant binding via PAT-N7 header/body) and MCP tools under the workforce-planning.* namespace.

Visual — Tier B (typographic data-flow). segmentation-studio (person grain) → Workforce Planning (position grain) → { reconciliation snapshots · forecast snapshots · match proposals } → performix (planned), with HRIS / ATS (Greenhouse) / Finance as upstream sources.

6. Commercialization / packaging

Workforce Planning is a service component, not a standalone product — it is the position-control + forecasting + req-matching layer a workforce-planning or finance-partnering offering composes. It sits behind operator and consumer surfaces rather than being sold on its own.

  • Data posture: it operates on tenant-uploaded HRIS / ATS / Finance data, multi-tenant and tenant-scoped (PAT-N7 binding on every call); writes are service-key-gated (PAT-11). The diversity-snapshot path carries a floorApplied flag with the min-N reconciliation against data-anonymizer explicitly deferred — so team-level diversity rollups are not a shipped guarantee yet (TBD).
  • Anything about pricing tiers or packaged offerings is (TBD) — not earned yet, so not stated.

Visual — (TBD — product-tier placement diagram).

7. The vision

One canonical seat-level spine where the headcount the three systems report is always reconciled, the forecast is always current, and every "is this a new hire?" decision is both probabilistic and auditable.

The forecaster is built as a pluggable Forecaster interface precisely so the v1 trailing-average model can be swapped for stronger ones (the code notes ARIMA-grade modeling and tenant-specific coefficients as the intended swap-in) without changing the contract. The matcher's Beta–Bernoulli prior is designed to learn per tenant from operator corrections, so the net-new-vs-backfill call sharpens over time. ATS coverage starts Greenhouse-first with a vendor-neutral DDL so other systems fold in without re-modeling.

8. Current status

Grounded in the real code state (contract 0.5.0, status: "live" in src/lib/contracts/registry.ts, src/spokes/workforce-planning/):

  • Shipped: the position spine (append-only position_events + denormalized positions); cross-system reconciliation (computeHeadcountReconciliation, immutable snapshots); the TrailingAverageForecaster v1 plus per-segment exit forecasting, ATS funnel throughput, and the combined projectHeadcount convolver (30/60/90/365-day windows); the Conductor Bayesian requisition↔open-slot matcher with a tenant Beta–Bernoulli prior ledger and an operator correction queue (/conductor/matches); Greenhouse-aligned ATS parity (job/application/offer status, hiring-manager + sourcer attribution); the position-event taxonomy (type × reason × regret/rehire flags). Live routes across /positions, /forecast/{run,snapshots,exits,funnel-throughput,projection}, /reconciliation/{run,snapshots}, /ats/webhook/greenhouse, /matches/*, /health. MCP tools registered. Demo seeds present.
  • In flight / planned: ARIMA-grade / tenant-coefficient forecaster swap-in (the pluggable interface is in place); data-anonymizer min-N reconciliation for the diversity-snapshot path (floorApplied is a v0 flag only); the registered performix consumer (status planned); the other (non-Greenhouse) ATS webhook returns HTTP 501 today.

Visual — Tier A (live capture). GET /api/spokes/workforce-planning/health reports schema reachability + contract version at request time; GET .../reconciliation/snapshots and .../forecast/snapshots return the real persisted snapshots.


Load-bearing worked example — month-end reconciliation (real seed data)

Grounded in the spoke's own demo seed (seeds/2026-05-22-workforce-planning-demo.sql, tenant pat-d1-b-demo, five positions). The computeHeadcountReconciliation core sums the position rows and freezes the deltas:

Position rows (budgeted / filled / open), by org unit:

  • org_us_west — 12 / 10 / 2 (Eng L4) and 4 / 2 / 1 (Sales L6)
  • org_us_east — 6 / 6 / 0 (Eng L5)
  • org_latam — 20 / 15 / 3 (Ops L3)
  • org_emea — 8 / 4 / 1 (Ops L4)

Reconciled totals (the frozen reconciliation_snapshots row, asOf 2026-05-22):

asOf:                2026-05-22
hris.filled:         37        (10 + 6 + 15 + 4 + 2)
ats.open:            7         (2 + 0 + 3 + 1 + 1)
finance.budgeted:    50        (12 + 6 + 20 + 8 + 4)
deltas.filledVsBudgeted:    -13   (37 − 50)
deltas.openVsUnfilledBudget: -6   (7 − max(0, 50 − 37))

What a practitioner does with it: the -13 says HR is 13 seats below budget; the -6 says even counting the 7 open reqs, the org is still 6 seats short of the unfilled budget (13) — i.e. there is budgeted headcount with no requisition opened against it yet. That gap is the action item the month-end attestation surfaces, and the snapshot is now immutable history the planner can defend in the next finance review.

(Every figure above is the arithmetic of computeHeadcountReconciliation over the in-repo demo seed; the seed itself stores the same reconciled totals. No figure here is invented.)