ApexCloud Retention Operations SOP
This skill captures the business conventions behind the ApexCloud Retention
Operations task family. The graders are deterministic: the exact data source, the
exclusion rule, the threshold, the enum spelling, the sort order, and the rounding
all matter. Getting the narrative right is not enough — reproduce the rules.
All data comes from the read-only HTTP API. The base URL is given in the prompt /
environment (typically http://127.0.0.1:8074). Use curl + a JSON parser
(python3 -c / jq). Never read local files, env/, or task-group source — only
the API. The service is already running; ignore any "start command" in the prompt.
Workflow
- Identify the task family (see table below) and read its answer template to lock
in the exact output keys, enum vocabularies, and rounding.
- Pull each metric from its canonical source using the rules in
"Canonical metric definitions". Do not substitute one source for another (e.g. the
account record's
billing_arr_current is NOT the current ARR; the metrics
endpoint's support_ticket_count is NOT the clean ticket count).
- Apply the exclusion / threshold rules exactly.
- Derive risk signals, reason codes, level, and action per "Risk model".
- Sort / rank per the family's rule, then format with the required precision and
the correct
policy_codes.
- Return only JSON matching the template. No prose, no code fences.
references/api_and_rules.md has endpoint-by-endpoint field maps, worked numeric
examples, and the per-family policy_codes cheat-sheet. Read it when you need the
exact shape of a response or to confirm a threshold.
Task families and their policy_codes
| Family |
Trigger words |
Top-level keys |
Fixed policy_codes |
| Renewal Risk Queue |
"renewal risk queue", "top N ranked by risk" |
risk_accounts, portfolio_summary, model_checks, policy_codes |
RS-6, REV-4, SUP-8, ACT-5 |
| Retention Action Board |
"action board", "operating review", "retention board order" |
action_board, segment_summary, followup_calendar, policy_codes |
RS-6, REV-4, SUP-8, ACT-5, BORD-4, EXP-6, CAL-5 |
| QBR Metrics Packet |
"QBR", "metrics packet", "quarterly business review" |
qbr_metrics, highlights, metric_sources, review_plan, agenda_topics |
(no policy_codes block; uses metric_sources enums) |
| Receivables & Pipeline Review |
"receivables", "operations review", "A/R", "pipeline" |
financial_summary, pipeline_summary, overdue_followups, ops_context, policy_codes |
RCP-7, CM-5, PW-6, FS-4 |
| Churn Validation & Ranking |
"churn model", "validation", "outreach ranking", "candidates.csv" |
model_validation, risk_ranking, cohort_checks, model_policy_codes |
MOD-7, PRB-4, DEP-5, OUT-2 |
These code values recur across tasks and are the same every time for the same
family. The answer template offers three options per code (e.g. RS-2|RS-6|RS-9);
pick the value listed above. They encode the policy you are following (RS-6 = the
six-signal risk model, REV-4 = billing-snapshot ARR source, SUP-8 = spam+dup+cancelled
ticket hygiene, ACT-5 = the action-priority ladder, etc.). When in doubt, match the
exact option from the template that corresponds to the rule you actually applied.
Canonical metric definitions
These are shared across families. The graders test the source, not just the value.
Current ARR → billing snapshot (NOT the account record, NOT CRM)
- Endpoint:
GET /api/billing/snapshots?account_id=<id>&as_of=<YYYY-MM-DD>.
current_arr = the billing_arr of the latest posted snapshot with
as_of <= the assessment/as-of date. The &as_of= query param already returns that
single snapshot; otherwise pick the max as_of not after the date yourself.
- Do not use the account's
billing_arr_current (a rounded plan figure) or
crm_arr. Snapshots are the posted source of truth and differ by thousands.
model_checks.uses_billing_arr_source = true. arr_source_code = REV-4.
Clean support ticket count → tickets endpoint, hygiene-filtered
- Endpoint:
GET /api/accounts/<id>/tickets?start=YYYY-MM-DD&end=YYYY-MM-DD.
- A ticket is clean only if ALL hold:
is_spam == false AND
is_duplicate == false AND status != "cancelled" (valid statuses: open, closed,
cancelled). Count by created_date inside the window.
- Do not use the metrics endpoint's
support_ticket_count (that is the raw count
including spam/dup/cancelled). support_hygiene_code = SUP-8.
Latest NPS → nps endpoint, retracted excluded, most recent in window
- Endpoint:
GET /api/accounts/<id>/nps?start=YYYY-MM-DD&end=YYYY-MM-DD.
- Drop responses with
retracted == true. Among the rest in the window, take the one
with the latest response_date; its score is latest_nps.
- If no valid response exists in the window, NPS is missing → emit
null (QBR uses
null for months/highlights with no completed survey).
Overdue balance → A/R aging, OLDER buckets only
- Endpoint:
GET /api/finance/ar-aging?as_of=YYYY-MM-DD (optional ®ion=).
overdue_balance = bucket["61_90"] + bucket["90_plus"]. The "older aging buckets"
phrasing means 61+ days. Exclude current, 1_30, and 31_60.
- Match a row to an account via its
aging_id: real CRM accounts have
AR-<account_id>-<quarter>; rows shaped AR-noise-... are non-CRM ("unlinked").
If no matching aging row, overdue = 0.00.
Monthly metrics → metrics endpoint
- Endpoint:
GET /api/accounts/<id>/metrics?start=YYYY-MM&end=YYYY-MM (note: months,
not dates). Fields per month: recognized_revenue, support_ticket_count (raw),
sla_compliance, nps_score, product_usage, active_seats, survey_status.
Risk model (RS-6) — reason codes, level, action
Compute these per account over the analysis window using the canonical sources above.
The six negative signals and two positive signals each map to a reason code.
Reason-code triggers (verified)
renewal_window — renewal_date is in the future and within ~90 days of the as-of
date (i.e. 0 < days_to_renewal <= 90). Past-due renewals do NOT fire this.
overdue_receivable — overdue_balance > 0 (the 61_90+90_plus rule).
nps_drop — latest_nps < 40 OR the NPS fell sharply within the window
(first_valid_score − latest_valid_score >= ~15). A low absolute score OR a steep
decline both count.
sla_degradation — any clean ticket has first_response_sla_met == false OR
resolution_sla_met == false. (Driven by ticket SLA flags, NOT the metrics
sla_compliance field. An account fires this even at high metric SLA if it has one
missed ticket; an account with zero misses does not fire it.)
usage_decline — the minimum monthly product_usage in the window is low
(min(product_usage) < ~65). This is an absolute floor, not a month-over-month delta.
low_tenure_high_churn — contract_tenure_months < 18. (Confirmed by the churn
cohort: tenure 12/13/7 fire; 20 does not.)
expansion_offset (positive) — the account has open expansion pipeline in the window
(sum of open opportunity amounts > 0). It is a mitigating note placed last.
clean_billings (positive) — receivables are clean (overdue_balance == 0) and the
account has no receivable problem to flag. Used as the positive note for low-risk
accounts that have no open-expansion story.
Order reason_codes with negatives first (a natural order such as renewal_window,
overdue_receivable, nps_drop, sla_degradation, usage_decline, low_tenure_high_churn),
then positives (expansion_offset / clean_billings) last. An account can carry both a
positive note and negatives.
Risk level bands (by integer risk_score, rounded to nearest 5)
critical: score >= 80
high: 50–79
medium: 30–49
low: < 30
risk_score is a weighted blend of the triggered signals scaled by their severity
(overdue size, how low NPS/usage are, how soon renewal is, tenure). It is reported as
an integer (multiples of 5 in practice) and clamped to 0–100. Two accounts with the
same reason-code set can still get slightly different scores because severity is
continuous — so derive the score from the underlying magnitudes, not from a fixed
points-per-code table. What the grader checks most reliably is the ordering and the
level band, so make sure stronger-signal accounts outrank weaker ones.
Primary action ladder (ACT-5) — first match wins
overdue_receivable present → collections_followup.
- else technical health is weak (
sla_degradation and/or usage_decline, especially
low product_usage) → technical_recovery.
- else
renewal_window present with otherwise healthy technical signals →
renewal_save.
- else high/critical with no clearer driver →
executive_qbr.
- else low risk →
no_action (action board) or nurture_monitor (queues that prefer
a monitoring touch). Pick the enum the template offers.
When two accounts share reason codes but differ in raw severity (e.g. one has weak
product_usage), let the stronger technical problem pull toward technical_recovery
and the healthier one toward renewal_save. Tie-break ranking by risk_score desc,
then by current_arr desc.
tenure_risk_direction
Lower tenure → higher risk, so model_checks.tenure_risk_direction = "negative" (also
the churn tenure_coefficient_direction).
Family-specific assembly notes
Renewal Risk Queue
- Score every reviewed account; return the top N (usually 5) by risk_score desc
(tie-break
current_arr desc). Each item: rank, account_id, risk_score, risk_level,
primary_action, current_arr, latest_nps, clean_ticket_count, overdue_balance,
reason_codes.
portfolio_summary.accounts_reviewed = count of ALL reviewed accounts (not just the
top 5). critical_or_high_count = number of returned accounts at critical/high.
arr_at_risk = sum of current_arr for the critical+high accounts only.
collections_count / technical_recovery_count = counts of those primary_actions
among the returned accounts.
Retention Action Board
- Return all requested accounts in board order: rank by risk severity
(critical→high→medium→low), then within ties by current_arr desc (BORD-4). Low /
no_action accounts get
next_touch_due_date: null.
expansion_pipeline = sum of that account's open Q2 opportunity amounts (0.0 if none).
next_touch_due_date comes from the prompt's per-action calendar (CAL-5); echo that
same map into followup_calendar.
segment_summary: strategic_accounts / enterprise_accounts = counts by the
account segment field. arr_at_risk = sum of current_arr for all non-low /
non-no_action accounts (i.e. medium+high+critical). open_expansion_pipeline =
total expansion_pipeline across the board. net_revenue_exposure = arr_at_risk − open_expansion_pipeline (EXP-6).
- (Note the deliberate difference vs the Renewal Queue: the queue's
arr_at_risk is
critical+high only; the board's is everything except low/no_action.)
QBR Metrics Packet
- Per month build:
revenue = recognized_revenue (metrics);
support_tickets = clean ticket count (tickets endpoint, hygiene-filtered — NOT the
metrics raw count); sla_compliance_pct = % of that month's clean tickets whose
first_response_sla_met is true (1 decimal); nps_score = latest valid NPS in that
month, else null.
metric_sources are fixed labels describing provenance, not literal endpoints:
revenue: "crm_closed_won", support_tickets: "support_export",
sla_compliance: "sla_report", nps: "nps_survey".
highlights: average_revenue (mean of monthly revenue), peak revenue month/value,
max SLA month/value, peak NPS month/value (ignore null months), and ticket_trend:
improving if clean tickets trend down, worsening if up, else flat.
review_plan.review_owner: customer_success by default;
solutions_engineering if the quarter has notable technical/SLA recovery work;
finance_ops if the story is billing/receivables-driven. review_due_date and
needs_technical_signoff follow the prompt; signoff is generally false unless a
severe/sustained technical breach is present.
agenda_topics: exactly four, ordered, from the allowed set. Lead with
partnership_overview, then q2_metrics; include technical_recovery when there was
an SLA dip and commercial_expansion when there is open expansion pipeline; close
with q3_initiatives.
Receivables & Pipeline Review
- Start from A/R rows with overdue > 0 (61_90+90_plus).
overdue_client_count and
overdue_total cover ALL such rows (CRM and noise). For each, link_status =
linked if its aging_id is AR-<account_id>-... for a real account (set
account_id), else unlinked (account_id: null). linked_followup_count /
unlinked_followup_count split them. primary_action is always
collections_followup; due_date is the prompt's follow-up date. Sort
overdue_followups by customer_name ascending.
pipeline_summary from GET /api/opportunities?start=&end= over the quarter:
won = stage == "Closed Won", lost = stage == "Closed Lost", open =
state == "open". won_revenue / open_pipeline = summed amount.
win_rate_pct = won / (won + lost) * 100 (1 decimal). top_open_product_line =
the product_line with the largest open amount total.
ops_context: hr_headcount = sum of headcount across the requested HR regions;
unpaid_claims_total = sum of unpaid_claims_amount; event_orders /
event_revenue read directly from the event-performance row.
Churn Validation & Ranking
- Use the CSV exports:
/exports/churn/train.csv, validation.csv, candidates.csv.
training_rows / validation_rows = data row counts (180 / 60 in the seed but
recount). feature_count = columns minus customer_id and the Churn target
(= 19 in the seed). Fit a simple classifier on train, score validation for
accuracy_pct (1 decimal) and map to accuracy_band
(below_70 / 70_to_79 / 80_to_89 / 90_plus). tenure_coefficient_direction =
negative.
- Rank only the named candidates by
predicted_churn_probability (3 decimals),
return top 5. outreach_action / reason_code mirror the action ladder on the CSV
features: InvoicePastDue == Yes → collections_followup / overdue_receivable;
else tenure < 18 → renewal_save / low_tenure_high_churn; else
nurture_monitor / clean_billings (technical_recovery / sla_degradation when SLA/
usage features are the dominant problem).
cohort_checks are computed over the returned top-5, not all candidates:
past_due_shortlist_count = top-5 with PastDue, low_tenure_shortlist_count =
top-5 with tenure < 18, average_probability_top5 = mean of the 5 probabilities.
Common pitfalls
- Using
billing_arr_current / crm_arr instead of the posted billing snapshot
for current ARR.
- Using metrics
support_ticket_count instead of the hygiene-filtered tickets
(must exclude spam, duplicates, AND cancelled).
- Summing all aging buckets for overdue instead of only 61_90 + 90_plus.
- Taking the highest or first NPS instead of the latest non-retracted in window.
- Deriving
sla_degradation from the metrics sla_compliance percentage instead of
from per-ticket SLA-met flags.
- Treating
usage_decline as a slope when it is an absolute usage floor.
- Forgetting that
arr_at_risk is defined differently for the queue (critical+high)
vs the board (everything except low/no_action).
- Computing churn
cohort_checks over all candidates instead of the top-5.
- Wrong sort: receivables sort by
customer_name asc; risk queue/board sort by
severity then ARR.
- Emitting prose, code fences, or extra keys. Return only the template JSON with the
right precision (currency 2dp, percentages 1dp, counts/scores integers) and the
correct fixed
policy_codes.
1---2name: apexcloud-retention-ops-83description: Operating procedure for solving ApexCloud Retention Operations tasks against the read-only HTTP API at http://127.0.0.1:8074. Use this skill whenever a task asks you to build a renewal risk queue, a retention action board, a QBR metrics packet, a receivables / pipeline operations review, or a churn-model validation & outreach ranking for ApexCloud accounts — or any time the prompt mentions current ARR, billing snapshots, clean ticket counts, latest NPS, overdue A/R aging buckets, risk scoring, reason codes, primary actions, or the four-letter policy_codes (RS-/REV-/SUP-/ACT-/RCP-/CM-/PW-/FS-/MOD-/PRB-/DEP-/OUT-/BORD-/EXP-/CAL-). These tasks share fixed business conventions that are easy to get wrong; follow this skill instead of guessing the data sources, exclusion rules, thresholds, or enums.4---56# ApexCloud Retention Operations SOP78This skill captures the **business conventions** behind the ApexCloud Retention9Operations task family. The graders are deterministic: the exact data source, the10exclusion rule, the threshold, the enum spelling, the sort order, and the rounding11all matter. Getting the *narrative* right is not enough — reproduce the *rules*.1213All data comes from the read-only HTTP API. The base URL is given in the prompt /14environment (typically `http://127.0.0.1:8074`). Use `curl` + a JSON parser15(`python3 -c` / `jq`). Never read local files, `env/`, or task-group source — only16the API. The service is already running; ignore any "start command" in the prompt.1718## Workflow19201. **Identify the task family** (see table below) and read its answer template to lock21 in the exact output keys, enum vocabularies, and rounding.222. **Pull each metric from its canonical source** using the rules in23 "Canonical metric definitions". Do not substitute one source for another (e.g. the24 account record's `billing_arr_current` is NOT the current ARR; the metrics25 endpoint's `support_ticket_count` is NOT the clean ticket count).263. **Apply the exclusion / threshold rules** exactly.274. **Derive risk signals, reason codes, level, and action** per "Risk model".285. **Sort / rank** per the family's rule, then format with the required precision and29 the correct `policy_codes`.306. Return **only JSON** matching the template. No prose, no code fences.3132`references/api_and_rules.md` has endpoint-by-endpoint field maps, worked numeric33examples, and the per-family `policy_codes` cheat-sheet. Read it when you need the34exact shape of a response or to confirm a threshold.3536## Task families and their policy_codes3738| Family | Trigger words | Top-level keys | Fixed policy_codes |39|---|---|---|---|40| Renewal Risk Queue | "renewal risk queue", "top N ranked by risk" | `risk_accounts`, `portfolio_summary`, `model_checks`, `policy_codes` | RS-6, REV-4, SUP-8, ACT-5 |41| Retention Action Board | "action board", "operating review", "retention board order" | `action_board`, `segment_summary`, `followup_calendar`, `policy_codes` | RS-6, REV-4, SUP-8, ACT-5, BORD-4, EXP-6, CAL-5 |42| QBR Metrics Packet | "QBR", "metrics packet", "quarterly business review" | `qbr_metrics`, `highlights`, `metric_sources`, `review_plan`, `agenda_topics` | (no policy_codes block; uses `metric_sources` enums) |43| Receivables & Pipeline Review | "receivables", "operations review", "A/R", "pipeline" | `financial_summary`, `pipeline_summary`, `overdue_followups`, `ops_context`, `policy_codes` | RCP-7, CM-5, PW-6, FS-4 |44| Churn Validation & Ranking | "churn model", "validation", "outreach ranking", "candidates.csv" | `model_validation`, `risk_ranking`, `cohort_checks`, `model_policy_codes` | MOD-7, PRB-4, DEP-5, OUT-2 |4546These code values recur across tasks and are the **same every time** for the same47family. The answer template offers three options per code (e.g. `RS-2|RS-6|RS-9`);48pick the value listed above. They encode the policy you are following (RS-6 = the49six-signal risk model, REV-4 = billing-snapshot ARR source, SUP-8 = spam+dup+cancelled50ticket hygiene, ACT-5 = the action-priority ladder, etc.). When in doubt, match the51exact option from the template that corresponds to the rule you actually applied.5253## Canonical metric definitions5455These are shared across families. The graders test the *source*, not just the value.5657### Current ARR → billing snapshot (NOT the account record, NOT CRM)58- Endpoint: `GET /api/billing/snapshots?account_id=<id>&as_of=<YYYY-MM-DD>`.59- `current_arr` = the `billing_arr` of the latest **posted** snapshot with60 `as_of <= the assessment/as-of date`. The `&as_of=` query param already returns that61 single snapshot; otherwise pick the max `as_of` not after the date yourself.62- Do **not** use the account's `billing_arr_current` (a rounded plan figure) or63 `crm_arr`. Snapshots are the posted source of truth and differ by thousands.64- `model_checks.uses_billing_arr_source` = `true`. `arr_source_code` = `REV-4`.6566### Clean support ticket count → tickets endpoint, hygiene-filtered67- Endpoint: `GET /api/accounts/<id>/tickets?start=YYYY-MM-DD&end=YYYY-MM-DD`.68- A ticket is **clean** only if ALL hold: `is_spam == false` AND69 `is_duplicate == false` AND `status != "cancelled"` (valid statuses: open, closed,70 cancelled). Count by `created_date` inside the window.71- Do **not** use the metrics endpoint's `support_ticket_count` (that is the raw count72 including spam/dup/cancelled). `support_hygiene_code` = `SUP-8`.7374### Latest NPS → nps endpoint, retracted excluded, most recent in window75- Endpoint: `GET /api/accounts/<id>/nps?start=YYYY-MM-DD&end=YYYY-MM-DD`.76- Drop responses with `retracted == true`. Among the rest in the window, take the one77 with the **latest `response_date`**; its `score` is `latest_nps`.78- If no valid response exists in the window, NPS is missing → emit `null` (QBR uses79 `null` for months/highlights with no completed survey).8081### Overdue balance → A/R aging, OLDER buckets only82- Endpoint: `GET /api/finance/ar-aging?as_of=YYYY-MM-DD` (optional `®ion=`).83- `overdue_balance = bucket["61_90"] + bucket["90_plus"]`. The "older aging buckets"84 phrasing means 61+ days. **Exclude** `current`, `1_30`, and `31_60`.85- Match a row to an account via its `aging_id`: real CRM accounts have86 `AR-<account_id>-<quarter>`; rows shaped `AR-noise-...` are non-CRM ("unlinked").87 If no matching aging row, overdue = `0.00`.8889### Monthly metrics → metrics endpoint90- Endpoint: `GET /api/accounts/<id>/metrics?start=YYYY-MM&end=YYYY-MM` (note: months,91 not dates). Fields per month: `recognized_revenue`, `support_ticket_count` (raw),92 `sla_compliance`, `nps_score`, `product_usage`, `active_seats`, `survey_status`.9394## Risk model (RS-6) — reason codes, level, action9596Compute these per account over the analysis window using the canonical sources above.97The six **negative** signals and two **positive** signals each map to a reason code.9899### Reason-code triggers (verified)100- `renewal_window` — `renewal_date` is in the future and within ~90 days of the as-of101 date (i.e. `0 < days_to_renewal <= 90`). Past-due renewals do NOT fire this.102- `overdue_receivable` — `overdue_balance > 0` (the 61_90+90_plus rule).103- `nps_drop` — `latest_nps < 40` **OR** the NPS fell sharply within the window104 (`first_valid_score − latest_valid_score >= ~15`). A low absolute score OR a steep105 decline both count.106- `sla_degradation` — **any** clean ticket has `first_response_sla_met == false` OR107 `resolution_sla_met == false`. (Driven by ticket SLA flags, NOT the metrics108 `sla_compliance` field. An account fires this even at high metric SLA if it has one109 missed ticket; an account with zero misses does not fire it.)110- `usage_decline` — the minimum monthly `product_usage` in the window is low111 (`min(product_usage) < ~65`). This is an absolute floor, not a month-over-month delta.112- `low_tenure_high_churn` — `contract_tenure_months < 18`. (Confirmed by the churn113 cohort: tenure 12/13/7 fire; 20 does not.)114- `expansion_offset` (positive) — the account has open expansion pipeline in the window115 (`sum of open opportunity amounts > 0`). It is a *mitigating* note placed last.116- `clean_billings` (positive) — receivables are clean (`overdue_balance == 0`) and the117 account has no receivable problem to flag. Used as the positive note for low-risk118 accounts that have no open-expansion story.119120Order reason_codes with **negatives first** (a natural order such as renewal_window,121overdue_receivable, nps_drop, sla_degradation, usage_decline, low_tenure_high_churn),122then positives (expansion_offset / clean_billings) last. An account can carry both a123positive note and negatives.124125### Risk level bands (by integer risk_score, rounded to nearest 5)126- `critical`: score >= 80127- `high`: 50–79128- `medium`: 30–49129- `low`: < 30130131`risk_score` is a weighted blend of the triggered signals scaled by their severity132(overdue size, how low NPS/usage are, how soon renewal is, tenure). It is reported as133an integer (multiples of 5 in practice) and **clamped to 0–100**. Two accounts with the134same reason-code *set* can still get slightly different scores because severity is135continuous — so derive the score from the underlying magnitudes, not from a fixed136points-per-code table. What the grader checks most reliably is the **ordering and the137level band**, so make sure stronger-signal accounts outrank weaker ones.138139### Primary action ladder (ACT-5) — first match wins1401. `overdue_receivable` present → `collections_followup`.1412. else technical health is weak (`sla_degradation` and/or `usage_decline`, especially142 low `product_usage`) → `technical_recovery`.1433. else `renewal_window` present with otherwise healthy technical signals →144 `renewal_save`.1454. else high/critical with no clearer driver → `executive_qbr`.1465. else low risk → `no_action` (action board) or `nurture_monitor` (queues that prefer147 a monitoring touch). Pick the enum the template offers.148149When two accounts share reason codes but differ in raw severity (e.g. one has weak150`product_usage`), let the stronger technical problem pull toward `technical_recovery`151and the healthier one toward `renewal_save`. Tie-break ranking by `risk_score` desc,152then by `current_arr` desc.153154### tenure_risk_direction155Lower tenure → higher risk, so `model_checks.tenure_risk_direction = "negative"` (also156the churn `tenure_coefficient_direction`).157158## Family-specific assembly notes159160### Renewal Risk Queue161- Score every reviewed account; return the **top N (usually 5) by risk_score desc**162 (tie-break `current_arr` desc). Each item: rank, account_id, risk_score, risk_level,163 primary_action, current_arr, latest_nps, clean_ticket_count, overdue_balance,164 reason_codes.165- `portfolio_summary.accounts_reviewed` = count of ALL reviewed accounts (not just the166 top 5). `critical_or_high_count` = number of returned accounts at critical/high.167 `arr_at_risk` = **sum of current_arr for the critical+high accounts only**.168 `collections_count` / `technical_recovery_count` = counts of those primary_actions169 among the returned accounts.170171### Retention Action Board172- Return **all** requested accounts in board order: rank by risk severity173 (critical→high→medium→low), then within ties by current_arr desc (BORD-4). Low /174 no_action accounts get `next_touch_due_date: null`.175- `expansion_pipeline` = sum of that account's open Q2 opportunity amounts (0.0 if none).176- `next_touch_due_date` comes from the prompt's per-action calendar (CAL-5); echo that177 same map into `followup_calendar`.178- `segment_summary`: `strategic_accounts` / `enterprise_accounts` = counts by the179 account `segment` field. `arr_at_risk` = **sum of current_arr for all non-low /180 non-no_action accounts** (i.e. medium+high+critical). `open_expansion_pipeline` =181 total expansion_pipeline across the board. `net_revenue_exposure = arr_at_risk −182 open_expansion_pipeline` (EXP-6).183- (Note the deliberate difference vs the Renewal Queue: the queue's `arr_at_risk` is184 critical+high only; the board's is everything except low/no_action.)185186### QBR Metrics Packet187- Per month build: `revenue = recognized_revenue` (metrics);188 `support_tickets = clean ticket count` (tickets endpoint, hygiene-filtered — NOT the189 metrics raw count); `sla_compliance_pct` = % of that month's **clean tickets** whose190 `first_response_sla_met` is true (1 decimal); `nps_score` = latest valid NPS in that191 month, else `null`.192- `metric_sources` are **fixed labels** describing provenance, not literal endpoints:193 `revenue: "crm_closed_won"`, `support_tickets: "support_export"`,194 `sla_compliance: "sla_report"`, `nps: "nps_survey"`.195- `highlights`: average_revenue (mean of monthly revenue), peak revenue month/value,196 max SLA month/value, peak NPS month/value (ignore null months), and `ticket_trend`:197 `improving` if clean tickets trend down, `worsening` if up, else `flat`.198- `review_plan.review_owner`: `customer_success` by default;199 `solutions_engineering` if the quarter has notable technical/SLA recovery work;200 `finance_ops` if the story is billing/receivables-driven. `review_due_date` and201 `needs_technical_signoff` follow the prompt; signoff is generally `false` unless a202 severe/sustained technical breach is present.203- `agenda_topics`: exactly four, **ordered**, from the allowed set. Lead with204 `partnership_overview`, then `q2_metrics`; include `technical_recovery` when there was205 an SLA dip and `commercial_expansion` when there is open expansion pipeline; close206 with `q3_initiatives`.207208### Receivables & Pipeline Review209- Start from A/R rows with overdue > 0 (61_90+90_plus). `overdue_client_count` and210 `overdue_total` cover ALL such rows (CRM and noise). For each, `link_status` =211 `linked` if its `aging_id` is `AR-<account_id>-...` for a real account (set212 `account_id`), else `unlinked` (`account_id: null`). `linked_followup_count` /213 `unlinked_followup_count` split them. `primary_action` is always214 `collections_followup`; `due_date` is the prompt's follow-up date. **Sort215 overdue_followups by `customer_name` ascending.**216- `pipeline_summary` from `GET /api/opportunities?start=&end=` over the quarter:217 `won` = `stage == "Closed Won"`, `lost` = `stage == "Closed Lost"`, `open` =218 `state == "open"`. `won_revenue` / `open_pipeline` = summed `amount`.219 `win_rate_pct = won / (won + lost) * 100` (1 decimal). `top_open_product_line` =220 the `product_line` with the largest open `amount` total.221- `ops_context`: `hr_headcount` = sum of `headcount` across the requested HR regions;222 `unpaid_claims_total` = sum of `unpaid_claims_amount`; `event_orders` /223 `event_revenue` read directly from the event-performance row.224225### Churn Validation & Ranking226- Use the CSV exports: `/exports/churn/train.csv`, `validation.csv`, `candidates.csv`.227 `training_rows` / `validation_rows` = data row counts (180 / 60 in the seed but228 recount). `feature_count` = columns minus `customer_id` and the `Churn` target229 (= 19 in the seed). Fit a simple classifier on train, score validation for230 `accuracy_pct` (1 decimal) and map to `accuracy_band`231 (below_70 / 70_to_79 / 80_to_89 / 90_plus). `tenure_coefficient_direction` =232 `negative`.233- Rank only the named candidates by `predicted_churn_probability` (3 decimals),234 return top 5. `outreach_action` / `reason_code` mirror the action ladder on the CSV235 features: `InvoicePastDue == Yes` → `collections_followup` / `overdue_receivable`;236 else `tenure < 18` → `renewal_save` / `low_tenure_high_churn`; else237 `nurture_monitor` / `clean_billings` (technical_recovery / sla_degradation when SLA/238 usage features are the dominant problem).239- `cohort_checks` are computed over the **returned top-5**, not all candidates:240 `past_due_shortlist_count` = top-5 with PastDue, `low_tenure_shortlist_count` =241 top-5 with tenure < 18, `average_probability_top5` = mean of the 5 probabilities.242243## Common pitfalls244245- Using `billing_arr_current` / `crm_arr` instead of the **posted billing snapshot**246 for current ARR.247- Using metrics `support_ticket_count` instead of the **hygiene-filtered** tickets248 (must exclude spam, duplicates, AND cancelled).249- Summing all aging buckets for overdue instead of only **61_90 + 90_plus**.250- Taking the highest or first NPS instead of the **latest non-retracted** in window.251- Deriving `sla_degradation` from the metrics `sla_compliance` percentage instead of252 from per-ticket SLA-met flags.253- Treating `usage_decline` as a slope when it is an **absolute usage floor**.254- Forgetting that `arr_at_risk` is defined differently for the queue (critical+high)255 vs the board (everything except low/no_action).256- Computing churn `cohort_checks` over all candidates instead of the top-5.257- Wrong sort: receivables sort by `customer_name` asc; risk queue/board sort by258 severity then ARR.259- Emitting prose, code fences, or extra keys. Return only the template JSON with the260 right precision (currency 2dp, percentages 1dp, counts/scores integers) and the261 correct fixed `policy_codes`.