FS Garage Utilisation - Metrics and Calculations
Complete metrics catalog for the Utilisation scorecard: project-wise stream segregation, KPI card formulas, employee utilisation, and project analysis.
Audience: engineers, ops, leadership
Repos: base-fs-garage-fe + fs-garage-base
UI route: /utilization
Code source of truth:
- BE join + stream segregation:
pb_hooks/utilisation.pb.js(streamOf, project maps) - BE / FE derived engine:
pb_hooks/utilisation_derived.js,lib/utilization/derivedMetrics.js - KPI click explanations:
lib/utilization/derivedMetricExplain.js
Snapshot field: snapshot.derived with version: 3 after Run Now.
1. How numbers are produced
1. Publish Garage allocations (person x project x week planned hours)
2. Run Now pulls Timesheet entries for the week
3. Canon project names (TS_PROJECT_MAP) then segregate each row into a stream
4. Build person segments (person x project planned/actual)
5. applyDerivedMetrics() -> badges, streams, projects, KPIs, key actions
6. Store utilisation_snapshots.snapshot JSON
7. FE renders cards (and can enrich older snapshots for display)
Grain of segregation: every planned or logged hour is attributed to a person x project segment, then rolled up to streams, people, and projects.
2. Constants and terminology
| Term | Meaning |
|---|---|
STD_HOURS (40) | Standard week capacity per person |
| Standard capacity | headcount x 40 |
| Allocated / TEAM CAPACITY | sum(employee.planned) from published Garage allocations |
| Logged | All Timesheet hours for the person/week (work + leave entries) |
| Leave hours | Leave detected from Timesheet leave projects / reports |
| Holiday hours | Hours on Holiday projects (own bar bucket; not overhead) |
Productive / actual | max(0, logged - leaveHours - holidayHours) |
| Unlogged | max(0, planned - logged) |
| Cohort | People with published allocations (TEAM SIZE). Interns are listed separately and excluded from scorecard KPI totals |
| On-plan band | +/- 10% around 100% for leave-adjusted util interpretation |
| Week window | Run Now blocked for future weeks |
What is materiality?
Materiality is the minimum hour threshold used to decide whether a variance or risk is large enough to flag.
| Item | Value |
|---|---|
| Constant | MATERIALITY_H = 8 |
| Plain meaning | One person-day (about 8 hours in a standard week) |
| Why it exists | Small noise (1-2h miss) should not flood Key Actions / "material" project flags. Only gaps at least about one full day are treated as actionable. |
Where it is used:
- Project Material flag (P3):
abs(actual - planned) >= 8 - Key Actions named rules: only fire when severity / hours at risk are material (typically
>= 8h; some rules use higher bars like16h) - Billable gap company signal: gross bench
>= 8h
So if a project is 3h over plan, it is not material. If it is 8h+ over/under, it is material.
3. Project-wise stream segregation
3.1 Name canonicalization first
Timesheet project names are mapped onto Garage names via TS_PROJECT_MAP / canonProject() before segregation. Examples:
| Timesheet name (examples) | Canonical Garage name |
|---|---|
ABA, ABA-IndieCommerce | ABA / IndieCommerce |
Thirstie on-demand | Thirstie on demand |
FS Labs, FS-Labs, workshops | FS Labs / R&D |
FS-Garage Base | FS-Garage |
TA | TA / HR |
| Fleet Studio Administration / Operations long names | Operations |
HR / Talent long names | TA / HR |
If no map hit, the raw name is kept (then rule-based classification may still apply).
3.2 Stream keys
Every segment lands in exactly one of:
| Stream key | UI label | Meaning |
|---|---|---|
billable | Billable | Client delivery books |
fslabs | FS Labs / R&D | Labs, academy, internal product R&D |
hrops | HR / Operations | Ops, TA/HR, interview, admin |
overhead | Overhead | Leave, learning, GPTW/town hall, onboarding, other |
3.3 Explicit project maps (primary segregation)
These maps win when the canonical project name matches:
Billable (BILLABLE)
ABA / IndieCommerceMWECocktail CourierThirstie on demandTIBAWally SEBICFOOCEO
FS Labs (FSLABS)
FS Labs / R&DFS-AcademyFS-Labs-SiftHireWallyWally MarketingFS-Garage
HR / Ops (HROPS)
OperationsTA / HRTA
3.4 Rule-based fallback (streamOf)
If not in the maps above, classification uses name rules (order matters):
- Leave project (
LEAVEin name) ->overhead - Onboarding ->
overhead - Structured / unstructured Learning ->
overhead - HR combined bucket (Interview, Fleet admin/ops, names starting with HR, ADMIN, etc.) ->
hrops
(this wins over bare GPTW/TOWN so combined HR projects stay hrops) - Pure GPTW / Town Hall (
02-GPTW, GPTW, TOWN) ->overhead - Name contains
FSorWALLY->fslabs - Else ->
overhead
Segment flag: segment.billable = true when the project is in the billable map (used for billable planned/actual rollups).
3.5 How planned vs actual are segregated
| Source | What happens |
|---|---|
| Garage allocation row | Hours added to person planned and to that project's planned; stream planned totals use streamOf(project) |
| Timesheet entry | Hours added to person logged and to segment actual for canonical project; also increment stream actual + billable/labs/hrops/overhead person fields |
Person x project segment fields (core grain):
project- canonical nameplanned- Garage allocation hoursactual- Timesheet hours on that projectbillable- boolean from billable map
Important exclusions
- Intern hours appear in Intern sections but are excluded from main KPI totals
- Intern Training can sit under overhead stream label historically but is excluded from
overheadActualto avoid double-count with its own bar - Holiday is its own employee bar bucket (not folded into overhead composition bars)
3.6 Category splits inside streams
Overhead composition categories (classifyOverheadCat):
- Leave
- Holiday
- Onboarding
- Learning - Structured / Learning - Unstructured (or raw Timesheet name when present)
- GPTW / Town Hall
- Other
HR / Ops categories (classifyHropsCat):
- Interview (Panelists and HM)
- IT Administration
- Fleet Studio Ops
- HR / Other
4. Primary KPI cards (top matrix)
These are the main scorecard KPI tiles (labels match UI / derivedMetricExplain.js).
| Metric (UI label) | Definition |
|---|---|
TEAM SIZE (B1) | People in the allocated cohort. Formula: count(employees with published allocations). Same cohort for capacity, streams, and employee util. Interns excluded from main KPI totals. |
TEAM CAPACITY (B3) | Total planned hours (main capacity figure). Formula: sum(employee.planned). Meta: standard capacity headcount x 40 and coverage% = allocated / standardCapacity x 100. |
TOTAL LOGGED (B4) | All Timesheet hours for the cohort. Formula: sum(employee.logged). Includes work + leave. Often shows vs allocated: logged / allocated x 100. |
BILLABLE HOURS (B7) | Hours on billable projects. Formula: sum(employee.billableActual). Share of logged: billableActual / logged x 100. |
FS LABS / R&D (S-LABS) | Labs / R&D stream hours. Formula: sum(fslabs stream actual). Often compared to billable: labs / billableActual x 100. |
HR / OPERATIONS (S-HROPS) | HR/Ops stream hours. Formula: sum(hrops stream actual). Interview, IT admin, Fleet ops, related. |
OVERHEAD (S-OH) | Overhead stream hours. Formula: sum(overhead stream actual). Should match Overhead Composition total (learning / onboarding / GPTW / leave-project style load as classified). |
INTERN TEAM SIZE (INT1) | Interns in Timesheet weekly report. Formula: count(interns in reports/filter for week). Intern hours shown separately; not in other KPI totals. |
5. Derived insight KPI cards
Clickable cards; drawer shows meaning, formula, and this-week computation.
| Metric (UI label) | Definition |
|---|---|
BILLABLE UTIL % (K2) | Share of allocated hours that went to billable work. Formula: billableActual / allocated x 100. Denominator is TEAM CAPACITY (Garage plan), not logged. |
EFFECTIVE BILLABLE % (K3) | Billable vs capacity after leave. Formula: billableActual / (standardCapacity - leave) x 100. Leave-adjusted capacity denominator. |
HOURLY BILLABLE CONVERSION (K4) | Of productive hours by people allocated to included books, how much was billable. Formula: sum(billableActual) / sum(productive) over eligible staff x 100. Eligible = people with planned hours on included project list (default TIBA). Productive = leave/holiday-net actual. |
BILLABLE / FTE (K5) | Average billable hours per person. Formula: billableActual / headcount. Result is hours, not %. |
DELIVERY ATTAINMENT (K6) | Billable delivery vs billable plan. Formula: billableActual / billablePlan x 100. 100% means billable actual matched billable allocation. |
OVERHEAD RATIO (K7) | Non-delivery load share of logged. Formula: (hropsHours + overheadHours) / logged x 100. |
UNBASELINED (K8) | Share of logged hours with no allocation on that segment. Formula: unbaselinedHours / logged x 100, where unbaselined hours = sum(segment.actual where planned = 0). |
PLAN DEVIATION (K9) | Average % miss vs plan (MAPE). Lower is better. Formula: average( abs(logged - planned) / planned ) x 100. Only people with planned > 0. |
ON-PLAN RATE (K10) | Share of people with ON PLAN badge. Formula: count(onPlan badges) / headcount x 100. Badge uses productive vs planned (see employee section). |
R&D INTENSITY (R1) | Labs relative to billable. Formula: labsActual / billableActual x 100. Not share of total logged. |
DISPLACEMENT (R4) | Non-billable load on people who had a billable plan. Formula: sum(learning + overhead_net + max(0, labsActual - labsPlanned)) for people with billablePlanned > 0. Hours that may displace billable work. |
ALLOCATION DRIFT (K14) | Hours logged with no allocation for that person/project. Formula: sum(project.driftHours) from contributors with actual > 0 and planned = 0. |
6. Employee utilisation
6.1 Per-employee fields
| Field | Meaning / formula |
|---|---|
planned | Garage planned hours = sum of allocation segments |
logged | Timesheet hours = sum of entries for person/week |
leaveHours | Leave from leave projects / report |
holidayHours | Holiday project hours |
actual / productive | max(0, logged - leaveHours - holidayHours) |
unloggedHours | max(0, planned - logged) |
billablePlanned / billableActual | Billable stream hours (segments with billable=true, or stored fields) |
labsPlanned / labsActual | Labs stream hours |
hropsPlanned / hropsActual | HR/Ops stream hours |
learningHours | Learning classification hours |
overheadActual | Overhead load after folding learning/GPTW/onboarding style projects |
benchHours | max(0, billablePlanned - billableActual) |
utilizationPct | productive / planned x 100 (if planned > 0) |
adjUtilPct | Leave-adjusted util % (see below) |
segments[] | Person x project rows: { project, planned, actual, billable } |
6.2 Leave-adjusted utilisation % (stored)
adj_denom = planned - leaveHours
adjUtil% = productive / adj_denom x 100
(null if adj_denom <= 0)
Band for leave-adjusted interpretation: ON PLAN if within 100 +/- 10, else below/above.
6.3 Employee badge (what the UI shows)
Current FE/BE enrich uses plan vs productive for the badge (not the +/-10% leave-adjusted band):
| Condition | Badge |
|---|---|
productive > planned | ABOVE PLAN |
productive < planned | BELOW PLAN |
productive = planned | ON PLAN |
| both zero | N/A / neutral |
Status meta typically: {productive}h productive.
Missing timesheet icon when unloggedHours > 0.
6.4 Employee bar / stream stacks
Person bars segregate hours by project/stream after folding:
- Overhead-fold projects (structured/unstructured learning, onboarding, GPTW/town hall, bare Overhead) roll into overhead totals and are not shown as separate delivery project bars
- HR combined projects stay visible as HR/Ops segments
- Holiday and Intern Training have special handling so they are not double-counted in overhead
7. Project analysis
Projects are enriched from all person segments with matching project name, then sorted by absolute variance hours (not %).
| Metric | Meaning / formula |
|---|---|
| Planned / Actual | Project planned hours / project actual hours |
| P1 Variance hours | actual - planned |
| Variance % | variance / planned x 100 (if planned > 0; else 100 if actual only) |
| P2 Attainment % | actual / planned x 100 (null if no plan) |
| P3 Material | abs(variance) >= 8 (materiality = one person-day; see section 2) |
| P10 Contributors | Distinct people with actual > 0 (or planned/actual touch) |
| P11 Concentration % | max(person actual) / project actual x 100 |
| Concentration lead(s) | People tied for max hours on the project |
| P12 Bus factor | Smallest k such that top-k people cover >= 50% of project actual |
| Top contributors | Top 3 by hours (ties extended) |
| P13 Delivery state | See table below |
| P14 Zero-loggers | People with planned > 0 and actual = 0 on that project |
| P15 Drift contributors | People with actual > 0 and planned = 0 on that project |
| Drift hours | Sum of drift contributor actuals |
Delivery state (P13)
| Condition | State |
|---|---|
planned > 0 and actual = 0 | NOT STARTED |
no plan and actual > 0 | UNBASELINED |
| no plan and no actual | N/A |
actual / planned < 0.25 | STALLED |
0.25 <= ratio < 0.85 | UNDER-DELIVERED |
ratio > 1.1 | OVER-DELIVERED |
| otherwise (near plan) | ON PLAN |
Status color key: stalled / under / not started -> belowPlan; over -> abovePlan; on plan -> onPlan.
8. Stream mix analysis (S1-S7)
For each stream (billable, fslabs, hrops, overhead):
| Metric | Formula |
|---|---|
| S1 Attainment % | actual / planned x 100 (null if no plan) |
| S2 Actual share % | actual / logged x 100 |
| S3 Planned share % | planned / allocated x 100 |
| S4 Mix drift (pts) | S2 - S3 |
| S5 Target band | Compare S2 to band below |
| S6 Variance hours | actual - planned |
| S7 Baseline | If planned = 0 -> no baseline / unbaselined style label with actual hours |
Target bands (actual share of logged)
| Stream | Band |
|---|---|
| Billable | 60-100% |
| FS Labs | 15-20% |
| HR / Ops | 0-15% |
| Overhead | 0-8% |
Band status: BELOW BAND / IN BAND / ABOVE BAND.
Badge style copy: {attainment}% OF PLAN · {+/- mixDrift}pts mix
- % of plan = delivery of allocated hours for that stream
- mix pts = whether the stream's share of the week matched the plan shape
9. Billable gap attribution
Company totals
grossBench = sum max(0, billablePlanned_i - billableActual_i)
overdelivery = sum max(0, billableActual_i - billablePlanned_i)
netGap = sum (billableActual_i - billablePlanned_i)
Per-person attribution (priority order)
For each person with billable bench > 0, remaining balance is consumed in order:
- leave (
leaveHours) - unlogged (
unloggedHours) - learning (
learningHours) - labs overrun
max(0, labsActual - labsPlanned) - residual
Company attribution = sum of those buckets. Shown in Billable Gap panel + Key Action BILLABLE_GAP.
10. Leave summary
| ID | Metric | Formula |
|---|---|---|
| L1 | Absence rate | leaveTotal / standardCapacity x 100 |
| L2 | Planning compliance | plannedLeave / leaveTotal x 100 |
| L3 | Unplanned hours | leave rows typed UNPLANNED |
| L5 | Concentration | max(person leave) / leaveTotal x 100 |
11. Reconciliation
loggedDelta = org_wide_logged - cohort_logged
billableDelta = org_wide_billable - cohort_billable
Non-zero deltas surface as banners / DATA_RECONCILIATION key action.
12. Key Actions (hours at risk)
Severity = hours (no money / bill rate).
Company signals (pinned)
| Rule | When | Severity |
|---|---|---|
DATA_RECONCILIATION | abs logged/billable delta > 0 | sum of abs deltas |
MIX_DRIFT | billable abs(mixDriftPts) > 5 | abs(mix)/100 x logged |
UNLOGGED_CAPACITY | total unlogged >= 40h | unlogged hours |
BILLABLE_GAP | gross bench >= 8h | gross bench |
Named actions (top by hours, materiality >= 8h)
| Rule | When | Severity |
|---|---|---|
ZERO_LOGGER | planned > 0, logged = 0 | planned hours |
STALLED_PROJECT | delivery state STALLED | planned - actual |
BILLABLE_SHORTFALL | attainment < 85%, abs(var) >= 16, not stalled | abs(variance) |
BENCH_CONCENTRATION | person bench >= 16h | bench hours |
ALLOCATION_DRIFT | project drift >= 8h | drift hours |
BUS_FACTOR | bus factor = 1 and actual >= 8h | project actual |
13. Quick formula cheat sheet
coverage% = allocated / (headcount x 40) x 100
productive = max(0, logged - leave - holiday)
unlogged = max(0, planned - logged)
utilisation_headline = logged / allocated x 100 (TOTAL LOGGED vs TEAM CAPACITY)
billable_util% = billableActual / allocated x 100 (K2)
effective_billable% = billableActual / (capacity - leave) x 100 (K3)
conversion% = billableActual_elig / productive_elig x 100 (K4, TIBA cohort by default)
delivery_attainment% = billableActual / billablePlan x 100 (K6)
project_variance = actual - planned
project_attainment% = actual / planned x 100
stream_mix_drift_pts = (actual/logged x 100) - (planned/allocated x 100)
14. How to verify
- Publish allocations for the week.
- Run Now (ceo / c-suite) ->
derived.version = 3. - Open
/utilizationand click KPI cards - drawer text comes fromderivedMetricExplain.js. - Spot-check one person: segments should sum to planned/logged; streams should match
streamOf(project). - Spot-check one project: contributors / zero-loggers / drift should match person segments.
15. Code map
| Concern | Location |
|---|---|
Project maps + streamOf segregation | fs-garage-base pb_hooks/utilisation.pb.js |
| Derived metrics apply | fs-garage-base pb_hooks/utilisation_derived.js |
| FE enrich / project / employee | base-fs-garage-fe lib/utilization/derivedMetrics.js |
| KPI card meanings + formulas | base-fs-garage-fe lib/utilization/derivedMetricExplain.js |
| Hourly conversion project list | lib/utilization/hourlyBillableConversion.js (default TIBA) |
| UI sections | components/utilization/* (KpiCard, DerivedInsights, StreamMix, ProjectVariance, EmployeeUtilisation, KeyActions, Leave) |