Skip to main content

Signal — KPI Targets

Written by Schae Lilley

KPI Targets is an admin-only screen in Signal (Power Digital's internal BI web app) at route /kpi-targets. It is the control room where every client-facing job title gets its official revenue benchmarks — RPE (revenue per employee), MRR (monthly recurring revenue book size), and client retention rate — and where an admin verifies that every employee in Signal is actually matched to one of those benchmarks. It answers: "What target is each role supposed to hit, and which employees are covered by a target?" Target numbers originate in a People-team Google Sheet (the CX and SS "JD Review Process" tabs); the employee roster and job titles originate in Nova (Power Digital's HR/ops platform); Signal's own records (Supabase tables written by Signal itself) hold the working copy of roles, the title-match log, and the sync history.

Field

Value

Route

/kpi-targets

Access

Admin or Gandalf (super-admin) — route wrapped in AdminRoute; nav link flagged adminOnly

Nav section

People (alongside Employees, Staffing, ETCR, Employee Pulse)

Primary data sources

Google Sheet "JD Review Process" tabs (target numbers); Nova (employee roster + job titles); Signal's own records (roles working copy, match log, sync log); curated title map hardcoded in the app (server/jd-title-map.ts)

Writes

One action: "Re-sync Matches" (POST /api/kpi-targets/sync) — rewrites Signal's own role/match/sync records and rebuilds server caches

Key code refs

src/views/KpiTargetsView.tsx; src/App.tsx (route + nav); server/index.ts (/api/kpi-targets*, /api/employee-kpi-targets, runKpiMatchSync); server/google-sheets.ts (fetchKpiTargetsFromSheet, KPI_SHEET_ID); server/jd-title-map.ts (JD_TITLE_MAP); src/context/DataContext.tsx (kpiClassification)

Last verified

staging @ 98df0c4, verified 2026-07-07

Purpose

The KPI Targets module exists because Signal grades employees against role-based revenue benchmarks: an Account Manager is expected to manage a certain monthly recurring revenue (MRR) book, a Strategist is expected to generate a certain revenue per employee (RPE), and client-facing roles carry a client retention benchmark. Those benchmark numbers are maintained by the People team in a Google Sheet (the "CX JD Review Process" and "SS JD Review Process" tabs), while employees and their job titles live in Nova, Power Digital's HR/ops platform. Nothing connects the two automatically — job titles are messy ("Sr. Paid Media Strategist, B2B" vs "Paid Advertising Strategist"), and an unmatched title means an employee silently has no target and drops out of performance views like Company Capacity and Role Capacity.

The KPI Targets screen solves that: it shows the canonical roster of roles and their targets (Signal's working copy, refreshed from the Google Sheet on every sync), shows how many real Nova employees matched each role, quantifies coverage ("X of Y assigned employees have KPI targets"), and lists the job titles that could not be matched so an admin can get the mapping fixed.

Access & visibility

The KPI Targets view at /kpi-targets is gated in two places, both in src/App.tsx (verified staging @ 98df0c4):

  • Route guard: the route is wrapped in AdminRoute, which allows users where isAdmin OR isGandalf is true and redirects everyone else to /summary. "Gandalf" is Signal's super-admin tier, above Admin.

  • Nav link: the "KPI Targets" link sits in the People nav section with the adminOnly: true flag; the nav filter shows adminOnly links to users where isAdmin || isGandalf.

How the flags are granted: isAdmin and isGandalf come from the is_admin / is_gandalf columns of the app_users table in Signal's own records (Supabase), read by src/context/AuthContext.tsx on sign-in and cached in sessionStorage. The app_users roles are managed in Signal's "General Access" screen (/users, itself Gandalf-gated). Two code-constant fallbacks exist in AuthContext.tsx as of Jul 2026: a SUPER_ADMIN_EMAILS list (always admin regardless of database state) and a GANDALF_ALLOWLIST (always Gandalf regardless of database state). Sign-in itself is Google OAuth restricted to Power Digital email domains.

Note: the gate is enforced in the frontend router; the backend /api/kpi-targets* endpoints themselves do not perform per-user authorization checks (as of staging @ 98df0c4).

Data sources & lineage

Origin conventions: "Nova" = Power Digital's HR/ops platform (source of who works here and their job title). "Google Sheet" = the People-team KPI sheet, ID 1JdhP_o1LHq44X9jJPSGiHAe2UiMHS9HZfrJ4wc1_NrY (overridable via KPI_SHEET_ID env var), tabs "CX JD Review Process" and "SS JD Review Process". "Signal's own records" = data entered/written by Signal itself (Supabase tables signal_roles, signal_match_log, signal_sync_log). The curated title map JD_TITLE_MAP is a hardcoded constant in the app repo (server/jd-title-map.ts, 165 entries, generated from a "Signal Title / JD" Excel export dated 2026-04-17).

On-screen element

Origin system

Code path

Refresh

Caveat

Roles table (Job Title, RPE Target, MRR Target, Retention Target)

Google Sheet targets, held in Signal's own records (signal_roles working copy)

GET /api/kpi-targets in server/index.ts reads signal_roles

Read live on page load; signal_roles itself refreshed from the Google Sheet during every sync

The table shows the working copy, not the sheet directly — if a sync hasn't run since a sheet edit, numbers lag the sheet

Matched Employees count (per role)

Signal's own records (signal_match_log), fallback to in-memory server cache cachedMatchCounts

GET /api/kpi-targets; fallback documented in server/index.ts (Supabase row-level security can block the read)

Reflects the last sync run

Rows with match_type unresolved/excluded are not counted

KPI Type pill (MRR / RPE / Excluded)

Derived in the frontend from the role's stored targets

classifyRoleType in src/views/KpiTargetsView.tsx

Computed on render

MRR-only target → MRR; RPE-only → RPE; neither → Excluded; both → titles containing "account director"/"account manager" → MRR, else RPE. Pure presentation logic, not stored

"Last synced" timestamp (header)

Signal's own records (signal_sync_log, latest success row), fallback to in-memory cachedSyncTime

GET /api/kpi-targets

Updated at the end of every sync

Employee Coverage bar ("N employees with client assignments"; MRR / RPE / No Targets segments)

Computed client-side: Nova employee roster joined to the title→targets map from the last sync

getEmployeeMetrics() in src/context/DataContext.tsx, using the map from GET /api/employee-kpi-targets

Recomputed whenever DataContext data or the targets map refreshes

Population = active employees assigned to at least one retained client account — deliberately the same population as Company Capacity and Role Capacity, so percentages agree across views. This population differs from the sync banner's employee total (the sync counts all active Nova employees with a title, no assignment requirement)

Unresolved-titles list (expandable under the coverage bar)

Computed during the last sync: Nova titles not present in the curated title map

GET /api/kpi-targets/coverage → in-memory cachedCoverage in server/index.ts

Rebuilt on every sync; empty (all zeros) until a sync has run since the last server restart

The startup auto-sync fills it ~30 seconds after server boot (code constant as of Jul 2026)

Sync result banner ("Sync complete — N employees: matched / excluded / unresolved")

Computed during the sync run

Response of POST /api/kpi-targets/sync

Per manual sync

The "auto-created" slot always reports 0 — auto-creation is dormant in the current strict-curated matching mode

CSV export & column show/hide (toolbar)

The rows already on screen

TableToolbar + useTableConfig (kpi_targets config key) in the frontend

Client-side only

Column visibility persists per browser, not per account

Behind the scenes, the sync pipeline (runKpiMatchSync in server/index.ts) orchestrates three origins: (1) the Nova employee roster (active, non-terminated employees with job titles), (2) the Google Sheet KPI tabs (free-text KPI cells parsed into structured RPE/MRR/retention numbers), and (3) the curated title map JD_TITLE_MAP (deterministically maps each Nova title to a role, or marks it "Exclude"). It writes results into Signal's own records and repopulates all in-memory caches.

Metrics & definitions

All metrics on the KPI Targets view are targets (benchmarks), not actuals. Actuals (each person's real MRR/RPE/retention) are computed elsewhere in Signal by the revenue engine (server/vena-kpi-engine.ts, from Vena contract data) and compared against these targets in downstream views.

  • RPE Target — Revenue Per Employee benchmark, in dollars: the revenue an individual in this role is expected to generate (e.g. a Strategist-level role). Origin: parsed from the free-text KPI cells of the Google Sheet "JD Review Process" tabs; stored in Signal's own records (signal_roles.rpe_target). Displayed dimmed when RPE is not the role's primary KPI.

  • MRR Target — Monthly Recurring Revenue book-size benchmark, in dollars: the size of the client revenue book an account-management role is expected to manage (e.g. an Account Manager or Account Director). Origin: same Google Sheet parse; stored in signal_roles.mrr_target. Displayed dimmed when MRR is not primary.

  • Retention Target — client retention percentage benchmark for the role (typically 90–95%). Origin: same Google Sheet parse; stored in signal_roles.retention_target. Downstream, DataContext.tsx defaults a missing retention target to 90% (code constant as of Jul 2026).

  • KPI Type (MRR / RPE / Excluded) — which benchmark is the role's primary KPI. Derived from the stored targets: MRR-only → MRR; RPE-only → RPE; neither → Excluded (role exists but carries no revenue target, e.g. leadership or project management); both set → title hint decides ("account director"/"account manager" in the title → MRR, otherwise RPE). Computed in KpiTargetsView.tsx and, identically, server-side during sync.

  • Matched Employees — count of real Nova employees whose job title mapped to this role in the last sync (curated matches only; excluded and unresolved titles don't count). Origin: Signal's own records (signal_match_log) or the server's in-memory fallback cache. A zero badge is styled as a warning, and note the sync's cleanup step deletes zero-match roles at the end of each run.

  • Employee Coverage (MRR / RPE / No Targets) — a stacked bar over the population of active employees with at least one retained client assignment: how many have an MRR-type target, an RPE-type target, or no target at all. Summary line: "X of Y assigned employees have KPI targets." Computed client-side from the same population Company Capacity and Role Capacity use.

  • Unresolved titles — Nova job titles held by real active employees that appear in neither the curated title map nor its exclusion list. These employees have no KPI target. Origin: computed during sync from the Nova roster vs JD_TITLE_MAP.

  • Sync counts (matched / excluded / unresolved / total) — per-employee tallies of the last sync run: total = active Nova employees with a title; matched = title found in the curated map and resolved to a role; excluded = title explicitly marked "Exclude" in the curated map; unresolved = title absent from the map.

How it's used

  1. Push updated benchmarks live after the People team revises the JD Review sheet. The People team changes, say, a Senior Strategist RPE in the Google Sheet. An admin opens KPI Targets and clicks Re-sync Matches. The sync overwrites Signal's working copy (signal_roles) with the new numbers — including clearing stale targets and propagating targets to division-suffixed title variants like "…, B2B" — and every downstream performance view immediately grades people against the new bar.

  2. Audit coverage before a performance review cycle. Before leadership pulls Company Capacity / Role Capacity numbers, an admin checks the coverage bar. If "X of Y assigned employees have KPI targets" has slipped (new hires, retitles), the unmatched people are silently missing from target-vs-actual views. The question answered: is the performance data complete enough to run reviews on?

  3. Chase down unresolved titles. Expand "Show N unresolved titles." Each chip is a Nova job title on a real employee that isn't in the curated title map. The fix happens outside this screen — either the curated title map (server/jd-title-map.ts, regenerated from the "Signal Title / JD" Excel — a code change) gets a new entry, or the People team adds the role to the JD Review sheet — then re-sync and confirm the chip disappears.

  4. Settle "what is this person supposed to be hitting?" disputes. A manager questions a target shown on the Employees view. The admin searches the job title on KPI Targets to see the authoritative RPE/MRR/retention numbers, which role the title matched to, and when it was last synced from the sheet.

  5. Sanity-check the MRR vs RPE split after a sync. The type counts line ("N titles — a MRR, b RPE, c Excluded") and the coverage bar show the shape of the org: account-management heads carrying books vs delivery heads measured on RPE. A skewed shift right after a sync is an early warning that matching went wrong.

  6. Export for offline review. Search, sort, hide columns, and export CSV (kpi_targets.csv) to share the full target roster with People/Finance for benchmark planning.

What users can change here

The KPI Targets screen has exactly one user action that writes data: the Re-sync Matches button (POST /api/kpi-targets/sync, handled by runKpiMatchSync in server/index.ts). One sync run, server-side:

  • Updates/creates/renames/deletes rows in signal_roles (Signal's own records): targets overwritten from the Google Sheet; targets propagated to division-suffix title variants (fuzzy match score ≥ 0.7, code constant as of Jul 2026); synonym-variant titles renamed to canonical form (e.g. "Paid Media …" → "Paid Advertising …") or deleted as duplicates; and roles with zero matched employees deleted at the end of the run.

  • Rewrites signal_match_log (Signal's own records): the full employee-title → role match log is cleared and re-inserted each run.

  • Appends to signal_sync_log (Signal's own records): the success record that powers the "Last synced" header.

  • Rebuilds in-memory server caches: per-role match counts, the title→targets map served app-wide via GET /api/employee-kpi-targets, and the coverage breakdown served via GET /api/kpi-targets/coverage.

The same sync also runs automatically ~30 seconds after every backend server restart (code constant as of Jul 2026), so the data self-heals after deploys without anyone clicking the button.

You cannot edit target numbers on this screen. Targets are only editable at their origin — the People-team Google Sheet — and the title-to-role mapping is only editable in the curated map in code (server/jd-title-map.ts). This is by design: one source of truth, no side-channel edits. Column show/hide and search are per-browser presentation preferences, not data writes.

Caveats & data quality

  • Matching is strict-curated mode. Only Nova titles explicitly listed in the curated map (JD_TITLE_MAP, 165 entries as of Jul 2026) get targets; there is no fuzzy fallback and no auto-creation of roles from employee titles. The sync banner's "auto-created" count is a legacy slot that always reports 0.

  • The curated title map is a hardcoded code snapshot generated from a "Signal Title / JD" Excel export dated 2026-04-17. Fixing an unresolved title requires a code change and deploy (or a People-team sheet change); it cannot be fixed from the UI.

  • Dormant default targets exist in code. ROLE_LEVEL_DEFAULTS in server/index.ts (e.g. Account Manager $175K MRR, Strategist $28K RPE) belongs to the dormant auto-create path and is NOT active policy — the Google Sheet is the policy source. Do not quote those constants as benchmarks.

  • In-memory caches reset on server restart. The coverage breakdown, unresolved-title list, and the app-wide title→targets map are empty for roughly the first 30 seconds after a deploy, until the startup auto-sync completes. A page loaded right after a deploy may show an empty or zeroed coverage bar.

  • Match counts have a known fallback path. If Supabase row-level security blocks reading signal_match_log, the server silently serves cached counts from the last sync — cosmetically identical, but numbers can be stale relative to the database.

  • Two different "total employees" figures. The sync banner total counts all active Nova employees with a job title (no assignment requirement); the coverage bar total counts active employees with at least one retained client assignment (the Company/Role Capacity population). They are not expected to match.

  • Sheet parsing is heuristic. Tabs are found by name hints ("CX JD Review Process", "SS JD Review Process") with auto-discovery fallback; columns are auto-detected from headers; KPI numbers are parsed from free text (e.g. "$28K RPE, 95% retention"); dollar values under $200 are assumed to be thousands; archived rows (CX Status column) are skipped. A People-team sheet reformat can silently change what syncs. A diagnostic endpoint exists at GET /api/kpi-targets/debug-sheet (server-side, not linked in the UI).

  • Orphan cleanup deletes zero-match roles every sync — a role can vanish from the table when its last holder leaves. The table is not a permanent catalog of all roles ever defined.

  • The division column exists in signal_roles and is returned by the API but is not displayed by the view; whether it is vestigial or reserved is not defined in the app repo.

  • Business-process unknowns (not defined in the app repo): who owns the Google Sheet and its update cadence; the official fix path for unresolved titles (curated-map code change vs sheet addition); whether retention targets are enforced as strictly as MRR/RPE downstream; and whether there is an expected manual sync rhythm beyond the automatic on-restart sync.

Related views

  • Company Capacity (/company-capacity) and Role Capacity (/capacity) — consume the title→targets map produced by the KPI Targets sync (via GET /api/employee-kpi-targets → kpiClassification on every employee in DataContext.tsx) to grade employees on/off track. The KPI Targets coverage bar intentionally uses the same employee population as these views.

  • Employees (/employees) — shows each person's primary KPI, target, and performance status, all sourced from this module's sync output. Note DataContext.tsx matches an employee's title to targets exactly, with a fallback that appends the employee's division (e.g. "Account Manager" + Core → "account manager, core") when the title has no comma suffix.

  • Insights (/insights) — reads the same employee KPI classification/performance status.

  • JD Sync (/jd-sync) and JD links — sibling feature: the same curated map file (server/jd-title-map.ts) also maps titles to Job Description documents; fixing a title for JD links and for KPI matching often means editing the same curated Excel.

  • General Access (/users) — where Admin/Gandalf roles (the flags that gate the KPI Targets view) are granted, in the app_users table of Signal's own records.

  • Revenue/actuals views — the KPI Targets module supplies targets only; actual MRR/RPE/retention per person is computed by the revenue engine (server/vena-kpi-engine.ts) from Vena contract data and compared to these targets downstream.

FAQ

Q: Who can see the KPI Targets page? A: Signal Admins and Gandalf-tier (super-admin) users. The route /kpi-targets redirects everyone else to the Summary page, and the nav link (People section) is hidden for non-admins. Admin/Gandalf status is set per user in Signal's app_users records via the General Access screen (/users).

Q: How do I change a target number (RPE, MRR, or retention) for a role? A: You can't change it in Signal. Target numbers are owned by the People team's Google Sheet ("CX JD Review Process" and "SS JD Review Process" tabs). Edit the sheet (via the People team), then click Re-sync Matches on the KPI Targets page — the sync overwrites Signal's working copy with the sheet values and all downstream views pick up the new targets immediately.

Q: Why does an employee show "no target" in Company Capacity or the Employees view? A: Their Nova job title either isn't in Signal's curated title map (it will appear under "Show N unresolved titles" on the KPI Targets page), is explicitly excluded (non-KPI roles like engineering, finance, leadership), or matched a role that carries no revenue targets. Unresolved titles can't be fixed from the UI — the curated map in code (server/jd-title-map.ts) or the People-team sheet has to be updated, then re-synced.

Q: What does the "Re-sync Matches" button actually do? A: It runs the full pipeline server-side: pulls the current target numbers from the People-team Google Sheet into Signal's role records (updating, creating, renaming, and de-duplicating roles), pulls the active employee roster and job titles from Nova, matches every title through the curated title map, rewrites the match log, updates the "Last synced" timestamp, rebuilds the coverage breakdown, and deletes roles that matched zero employees. The same pipeline also runs automatically about 30 seconds after every backend restart.

Q: Why doesn't the sync banner's employee total match the coverage bar's total? A: They count different populations by design. The sync banner counts all active Nova employees who have a job title. The coverage bar counts only active employees assigned to at least one retained client account — the same population used by Company Capacity and Role Capacity, so those views agree with the coverage percentages.

Q: What decides whether a role is "MRR", "RPE", or "Excluded"? A: The targets stored on the role. MRR target only → MRR (book-of-business roles like Account Managers/Directors). RPE target only → RPE (delivery roles like strategists, specialists, analysts). Neither → Excluded (the role exists but carries no revenue target, e.g. leadership, project managers). If a role has both targets, the title decides: titles containing "account director" or "account manager" are MRR, everything else RPE.

Q: A role disappeared from the KPI Targets table — where did it go? A: Every sync deletes roles that matched zero employees (orphan cleanup). If the last person holding a title left or was retitled, the role is removed on the next sync. It will reappear if the Google Sheet still defines it and someone holds a matching title again, or if the sheet sync recreates it.

Q: The coverage bar is empty or shows zeros right after a deploy — is something broken? A: Usually not. The unresolved-titles list and coverage breakdown live in server memory and are rebuilt by an automatic sync roughly 30 seconds after the backend starts. Wait a minute and reload; if it's still empty, run "Re-sync Matches" manually.

Q: Can I trust the dollar defaults I've heard about, like "$175K MRR for Account Managers"? A: No — those figures are dormant code constants (ROLE_LEVEL_DEFAULTS) from an inactive auto-create path, not policy. The authoritative numbers are whatever the People-team Google Sheet says, as shown in the KPI Targets table after a sync.

Q: Why doesn't the revenue here match Vena? A: The KPI Targets page shows no revenue actuals at all — only target benchmarks (RPE/MRR/retention) per job title. Actual revenue figures come from Vena contract data and are computed in other views (Employees, Company Capacity, Role Capacity). If a target-vs-actual comparison looks wrong elsewhere, check here that the person's title matched a role and that the targets were recently synced from the sheet.

Q: Where do the "Matched Employees" counts come from, and can they be stale? A: They come from the match log written during the last sync (Signal's own records). If the database read is blocked (a known row-level-security quirk), the server silently falls back to in-memory counts cached at the last sync — so counts always reflect the last sync run, never live HR data. Re-sync to refresh them.

Did this answer your question?