Method paper

How every number is calculated

Every score, rate, projection and settlement figure mdvin puts on a screen — the question it answers, the logic behind the way it is built, the formula, a worked example in real numbers, and how to check it without taking our word for anything.

mdvin · prior authorization and value-based care orchestration · BvLogic Solutions LLC

What this paper is

A platform that ranks people for clinical outreach and estimates money owed under a contract is making claims about the world. Those claims are worth exactly what the arithmetic behind them is worth, so this document states all of it: not a summary of the approach, but the formula behind each output with the function that computes it named beside it.

Every calculation here is held to three commitments, and they are the reason the engines look the way they do rather than the way a modelling exercise would look:

The worked examples are not illustrations. Each is the engine's own output on the demonstration population, printed by Docs/evidence/2026-09/2026-09-24_worked_examples.py and quoted here unchanged.

1 · The method

Four decisions that shape every engine in this paper

These are not style preferences. Each was taken for a reason specific to running clinical outreach under a regulated contract, and each costs something — which is why they are stated rather than assumed.

Rules, not a learned model

The obvious instinct for risk stratification is gradient boosting. It is the wrong first move here, for three reasons. The output format already demands reasons — the member panel renders each risk factor with its provenance, which a rule engine produces natively and a tree ensemble only ever reconstructs afterwards. A risk tier drives outreach, and outreach is regulated — a score that affects what care somebody is offered must be explainable, disclosed and defensible in discovery. And there is no training data: a synthetic cohort is not a training set, and fitting to it would produce a model whose version string meant nothing. When real longitudinal data exists these rules become the baseline a learned model has to beat, and the factor format they already emit is what makes that comparison possible.

Computed at read time, never stored

No score in this paper has a column in the database. A persisted priority is stale the moment a clock moves, and it would need its own recompute job that can drift from what the screen shows. The cost is that every page load does the arithmetic; the benefit is that the number on screen is always the number the current facts produce. Risk scores are the deliberate exception: each run is appended as history, so what a score meant last quarter is still recoverable.

Clinical logic is code; policy is data

Measure specifications, targets, contract terms and SLA lengths are versioned records rather than constants in a source file, so changing one is a data change with an audit row behind it and a measure's numerator can be restated without a release. What stays in code is the logic that must not vary by tenant: what it means for a gap to be open, who may write to a member's record, when an authorization blocks scheduling.

Nothing here decides

Every recommendation names the endpoint a human would call and calls none of them. The server applies its own guards regardless of what the screen suggested — scheduling stays blocked while a linked authorization is pending, and the refusal is visible rather than pre-empted. The platform connects the workflow; the clinician provides the judgment.

2 · Population health

Clinical risk score and tier

app/services/risk.py · score_patient() · model apex-risk-rules v2
The question

Of the people this plan is responsible for, who is most likely to have an expensive, avoidable year — and why, in terms a clinician can argue with?

The logic

Risk is additive across independent signals, each drawn from a different source system, because that is how a clinician reasons about it: a diagnosis list, recent labs, social circumstances, what the hospital sent over, whether the member is actually taking the medication. Each rule that fires contributes its points and records what it saw and where it came from, so the score is never more than the sum of statements you can check one at a time. Only the most severe band per lab fires, so one bad result is counted once rather than twice.

The formula
score = the sum of the points of every rule that fires
tier  = HIGH if score ≥ 60 · RISING if ≥ 35 · MEDIUM if ≥ 15 · else LOW
Active conditionPoints
Heart failure I50.914
Chronic kidney disease stage 3 N18.312
Coronary artery disease I25.1010
Diabetes with hyperglycemia E11.6510
Type 2 diabetes E11.98
Hypertension I105
Obstructive sleep apnea G47.334
Hyperlipidemia E78.54
Three or more chronic conditions+8
Measured value — latest, 730-day lookbackPoints
HbA1c above 9.0 4548-418
HbA1c above 8.010
Systolic BP above 160 8480-614
Systolic BP above 1409
Creatinine above 1.5 2160-010
Total cholesterol above 240 2093-36

Only the most severe band per lab fires: an HbA1c of 9.4 scores 18, not 28.

Social risk — the newest screening answer per Z-code, counted when it is not negative (0, false, negative, no, no risk, none) and not blank; once per Z-code; no lookbackPoints
Homelessness Z59.010
Food insecurity Z59.48
Transportation insecurity Z59.827
Social isolation Z60.25
Utilisation (365-day lookback) and adherence (current PDC, no lookback)Points
ED visits, 6 eachcapped 18
Inpatient admissions, 10 eachcapped 20
Each active medication below 80% PDC7
Worked example — John Doe, exactly as the engine returns him
PointsFactor that firedSource
12Chronic kidney disease stage 3 (N18.3)problem_list
10Diabetes with hyperglycemia (E11.65)problem_list
10HbA1c above 8% (was 8.9%)lab
10Creatinine above 1.5 (was 1.8 mg/dL)lab
9Systolic BP above 140 (was 148.0 mmHg)ehr
83 chronic conditionsproblem_list
8Food insecurity (Z59.4)survey
7Atorvastatin 40mg adherence 62% (below 80%)pharmacy
61 ED visit in 12 monthsadt
5Hypertension (I10)problem_list
85→ tier HIGH (60 or above)10 factors, 6 source systems
How to check it. Open the member and add up the factor list on screen — it is the same list, in descending order, and it must total the score shown. Each row names the system that asserted it, so a disputed factor can be traced back to the feed it came from. If a weight ever changes, the model version changes with it and past scores keep their meaning: the history is the audit trail, the tier on the member record is only a convenience.

3 · Quality measurement

One member against one measure

app/services/quality.py · evaluate_patient()
The question

For this measure, is this member counted — and if they are counted, have they had the care it asks for?

The logic

Four buckets, mutually exclusive, so no member is ever both excluded and counted. The distinction that matters most is between not in the denominator (the measure does not apply to them) and excluded (it would apply, but a clinical exclusion releases them): conflating those two is the error that quietly inflates a rate. Denominator, numerator, exclusions and target all come from the versioned measure package rather than from code, so a specification change is a data change with a version attached.

BucketRule
Not in denominatorAge today outside the package's minimum or maximum, or the package names condition codes and the member has no active condition among them
ExcludedIn the denominator, but holds an active condition listed in the package's exclusions
CompliantIn the denominator, not excluded, and the numerator is met
Open gapIn the denominator, not excluded, and the numerator is not met
The formula — three ways a numerator can be met

Always inside the package's lookback window, 365 days by default:

  • observation — the latest qualifying result inside the window; where the package sets a maximum, compliant only if that value is below it (HbA1c below 8.0, systolic below 140 — a reading of exactly 8.0 or 140 is not controlled; the package states the comparison, so a “≤” measure is met at its maximum). Latest, not best: a member who improved and then relapsed is not compliant.
  • procedure — any qualifying procedure inside the window (retinal exam, annual wellness visit). Any, not latest: having had it twice is not better than once.
  • medication_pdc — an active medication in the named class whose best proportion of days covered is at least 80%, the PQA / CMS Star Ratings adherence threshold, overridable per measure.
How to check it. Every member in a measure's denominator is listed by the drill-down behind its rate, and each row states which bucket they are in and why. The rate is computed from the same rows that list renders — so a KPI can never disagree with the list behind it, which is a requirement of the build rather than a happy property.

A measure across a population

app/services/quality.py · evaluate_tenant()
The question

How is the plan performing on this measure, and how much of that performance is still recoverable this year?

The formula
D      = members in the denominator and not excluded   (denominator)
N      = of those, the compliant                       (numerator)
X      = the excluded                                  (reported, outside D)
G      = D − N                                           (open gaps)
rate%  = round(100 × N / D, 1)      undefined, shown as a dash, when D = 0
Worked example — all five rated measures on the demonstration population
MeasureDNXOpenRateTarget
Diabetes: Eye Exam Performed60060.0%70
Diabetes: HbA1c Control (<8.0%)620433.3%75
Statin Adherence (PDC ≥ 80%)820625.0%80
Annual Wellness Visit820625.0%60
Controlling High Blood Pressure1040640.0%75

Eye exam: 100 × 0 / 6 = 0.0%. Blood pressure: 100 × 4 / 10 = 40.0%. In every row N + open = D — the numerator and the open gaps account for the whole denominator, with the excluded reported separately. The rows count the members attributed for the current period — the contract's population, the same one PMPM and settlement divide by. Every member is evaluated, so the demo's unattributed duplicate record (“Jon Doe”) still has his wellness-visit gap opened and worked, but he is not in the wellness visit's D of 8.

A measure is rated when D is greater than zero, and only rated measures reach the composite. That one definition is why an unrated measure never quietly scores zero — a composite of nothing is not zero, because zero is a score somebody earned.

The quality composite

app/services/quality.py · composite()
The question

One number for quality, because the contract settles on one number.

The formula
composite% = round( mean( rate% of every rated measure ), 1 )
Worked example

(0.0 + 33.3 + 25.0 + 25.0 + 40.0) / 5 = 123.3 / 5 = 24.66, shown as 24.7% — the composite the contract is judged against.

Unweighted, because that is the demonstration contract's stated scoring method. A programme that weights its measures would change every number in the opportunity table below, and the weighting would live in the contract terms rather than in code.

A property of the method, said out loud on the screen: because the mean is unweighted, measures with small denominators move the composite fastest per gap closed. Six eligible members means each closure is worth more than one in a measure with ten. That is arithmetic, not a defect — and it is precisely why the next section ranks by points per gap rather than by how many gaps are open.

4 · Value-based contract management

Quality opportunity, and what one closed gap is worth

app/services/vbc_manager.py · quality_opportunity()
The question

There are twenty-eight open gaps and a team that can work a handful this month. Which ones move the number the contract settles on?

The logic

“Opportunity” is defined here rather than left to intuition: it is the number of composite points the score would gain if every open gap in that measure closed. That is a real, checkable quantity, and it is what makes the ranking arguable — a measure twenty points below target with two eligible members matters less than one five points below target with thirty. Ranking by “most gaps” or “furthest below target” gets both of those backwards.

The formula

With n = the number of rated measures and target = the package's own target percentage:

Column on screenFormula
Performancerate%
Targetfrom the versioned measure package; “no target set” when absent
Gap to target, in pointsround(target − rate%, 1) — positive means short
“N members would close it”max( ceil(D × target / 100) − N , 0 )
Open gapsG
Composite points per gapround(100 / D / n, 2)
Composite points availableround(100 / D / n × G, 2) — from the unrounded points per gap, so this figure and the scenario engine cannot drift apart
Opportunity bandLow if at or above target · else High if available ≥ 3.0 · Medium if ≥ 1.0 · else Low
Why 100 / D / n — two separate facts, one after the other:
closing one gap raises that measure's rate by 100 / D points,
and the composite is the unweighted mean of n measures,
so the composite moves by 1/n of that.
Worked example — one rule, five different answers
MeasureDOpen100 / D÷ n = 5On screenAvailable
Diabetes: Eye Exam Performed6616.673.33333.3320.0
Diabetes: HbA1c Control6416.673.33333.3313.33
Statin Adherence8612.502.50002.515.0
Annual Wellness Visit8612.502.50002.515.0
Controlling High Blood Pressure10610.002.00002.012.0

Five different numbers from one rule, with no exceptions. A formula that explains only the row being questioned is a rationalisation; one that explains all five is the method. Note also that the ranking is not by open gaps — the wellness visit, statin adherence and blood pressure each have six open gaps, as many as the eye exam, yet are worth 15.0, 15.0 and 12.0 against the eye exam's 20.0. Equal points rank by title, so the wellness visit is second.

How to check it, from a photograph of the screen. Each row prints N open and X% now, and those two give the denominator with no other source: D = open ÷ (1 − rate). Eye exam: 6 ÷ (1 − 0) = 6. Wellness visit: 6 ÷ (1 − 0.25) = 8. Then 100 ÷ D ÷ 5 reproduces the points per gap. Nothing else is needed, and nothing has to be taken on trust.

Path to the quality gate

app/services/vbc_manager.py · gate_path()
The question

The contract pays nothing on savings unless quality clears a gate. What is the cheapest route from where we are to where we must be, counted in gaps closed?

The logic

Greedy — take the measure with the highest points per gap first, then the next. Greedy is not an approximation here, it is optimal: every gap within one measure is worth exactly the same number of composite points, so taking the highest-value measure first can never cost a cheaper route later. That is a property of the unweighted mean, and it would stop being true under a weighted contract — at which point this function would have to change, and this paper would say so.

The formula
gate      = contract.minimum_quality_pct
shortfall = round(gate − composite, 1)            (0 when already met)

then, measures sorted by points_per_gap descending:
  need        = min( G , ceil( (shortfall − gained) / points_per_gap ) )
  gained     += need × points_per_gap
  rate_after% = round(100 × (N + need) / D, 1)
until gained ≥ shortfall

projected%    = round(composite + gained, 1)
achievable    = shortfall ≤ 0 or projected ≥ gate
gaps_to_close = the sum of need
Worked example — the engine's own plan

Current 24.7%, gate 60.0%, shortfall 35.3 points.

StepMeasureCloseOf openRate after
1Diabetes: Eye Exam Performed (3.33 per gap — the richest)66100.0%
2Diabetes: HbA1c Control (3.33 per gap)44100.0%
3Annual Wellness Visit (2.5 per gap, tied with statin adherence and first by title — stops as soon as the gate is cleared)1637.5%
Projected composite 60.5% — gate cleared11

Eleven gaps, not twenty-eight, and the third measure is only partly worked — the plan stops the moment the gate is cleared rather than finishing a measure for tidiness.

Scenario modelling — “what if we close more gaps?”

app/services/vbc_manager.py · scenario()
The question

If the team commits to closing a specific number of specific gaps, where does that leave the composite, the gate, and the money?

The formula
for each measure with k chosen closures, capped at G:
rate_after%      = round(100 × (N + k) / D, 1)
composite_after% = round( mean( rate_after% of every rated measure ), 1 )
gate_met_after   = composite_after ≥ gate
settlement_after = gross_savings × shared_savings_rate × (1 − quality_withhold_pct)
                   if the gate is met after and gross_savings > 0, else 0
settlement_delta = settlement_after − settlement_before
Worked example — the eye-exam measure
ClosuresMeasure rateCompositeGate met after?
Close 10.0% → 16.7%24.7% → 28.0%No
Close all 60.0% → 100.0%24.7% → 44.7%No

One closure is worth 3.33 composite points. The screen reports the composite exactly as the live score is computed — each measure's rate rounded to one decimal, then their mean rounded to one decimal — so the eye exam's 16.7% gives (16.7 + 25.0 + 25.0 + 33.3 + 40.0) ÷ 5 = 28.0, and the sentence under the result reads “Composite moves +3.3 points”: the difference of the two displayed figures. Six closures move it 20.0, matching the row's stated points available exactly. And even perfect performance on one measure does not clear a 60% gate: that is the honest answer, and it is the one the screen gives.

Assumptions the screen carries with the number: closures add to the numerator only — denominators do not move; cost performance is held constant; closures are capped at the gaps actually open. It is a projection, not a forecast.

Contract performance and shared-savings settlement

app/services/finance.py · contract_performance()
The question

Against the benchmark this contract set, are we spending less — and does any of that saving actually become money?

The logic

Two independent conditions, and the second is the one that surprises people. Savings are real only if spending came in under benchmark, and they are only payable if quality clears the gate. A programme can generate savings and receive nothing. That is why the quality composite is presented as a financial number on the contract screen rather than a clinical one, and why the gate path above exists at all.

The formula
member_months = from the cost summary for the current period
actual_pmpm   = allowed cost per member per month
variance_pmpm = round(benchmark_pmpm − actual_pmpm, 2)
gross_savings = round(variance_pmpm × member_months, 2)
gate_met      = composite ≥ contract.minimum_quality_pct
settlement    = round(gross_savings × shared_savings_rate × (1 − quality_withhold_pct), 2)
                when gross_savings > 0 and the gate is met; otherwise 0, and the note says which
Worked example — the demonstration contract
Benchmark PMPM1,750.00
Actual PMPM1,554.32
Variance per member per month195.68
Member months in the period105.12
Gross savings — 195.68 × 105.1220,569.88
Quality composite against the gate24.7% vs 60.0%
Estimated settlement — gate not met0.00

Twenty thousand dollars of real savings, and nothing payable. This is the most useful number on the contract screen: it converts a quality problem into a financial one, and it is why the platform ranks gaps by composite points rather than by clinical severity alone.

Every figure in this section is labelled an estimate on screen. Real settlement is reconciled by the payer against run-out claims months later. The platform's job is to show the trajectory and what moves it, not to promise a cheque.

5 · Prioritisation — how work gets ordered

Work-queue ranking

app/services/ranking.py · rank()
The question

A care manager opens the day with thirty-two open tasks and time for eight. Which eight?

The logic

The authored priority label is an input, not the ranking. A manual “Critical” is a snapshot of what somebody believed when they created the task; the clinical signals move afterwards. A member discharged yesterday with a contact window closing today outranks a task somebody marked Critical last month — and the earlier weighting, which put the human label at 40 points against 15 for clinical risk, could not express that. The label still counts, because discarding a clinician's judgment would be its own failure, but it can now be outscored by what is actually happening to the member.

The formula

The score is the sum of every component that fires, and each component is returned with the sentence that justifies it.

ComponentPoints
Authored priority — Critical / High / Medium / Low20 / 12 / 6 / 2
Member risk tier — HIGH / RISING / MEDIUM / LOW22 / 15 / 6 / 0
Worst open gap's priority — Critical / High / Medium / Low18 / 12 / 6 / 2
Post-discharge 48-hour contact window still open28
Contact window missed, visit window still open (7 days)20
Outreach stalled — no reply for 21 days (the tenant's engagement.stalled wait), or the member declined or could not be reached14
Ageing — from day 3, points = days open (capped 21)3–21
Due date and authorization clockPoints
Overdue — 37 at 1 day late, +2 per further day, capped at 10 days late37–55
Due today25
Due in 1–2 days18
Due in 3–7 days10
Due in 8 or more days3
Authorization past its decision deadline30
Decision due within 24h — expedited / standard25 / 20
Decision due within 72h — expedited / standard18 / 12
On the SLA clock, beyond 72h5
Worked example — the task at the top of the queue
PointsComponentWhy
12prioritymarked High
55dueoverdue by 60 days
22patient_riskhigh-risk patient
18gap_prioritycritical-priority gap: HbA1c Control — Glycemic Assessment, 5 open in total
107TotalComplete the health risk assessment

The care manager sees the composed sentence and the score: “overdue by 60 days + high risk + HbA1c Control — Glycemic Assessment” in the Reason column, and score 107 under the priority badge, whose tooltip lists every line above. Note that the authored label contributed 12 of 107 — the queue is being driven by the member's situation, which is the whole point of the design.

How to check it. Every task exposes its component list. Ties break on due date, then creation time, so the order is stable across reloads rather than falling back to whatever the database returned.

My Panel — ranking members rather than tasks

app/services/panel.py · panel()
The question

Which people need me today — including the ones nobody has raised a task for?

The logic

A care manager thinks in members, not tickets. Ranking tasks splits one member with three gaps into three small jobs, so the person who needs the most attention can look like three minor items. Worse: a member who needs attention but has no task is not in the queue at all — high risk, several gaps open, nobody raised anything, invisible. This ranks the panel instead, so a member with no task can outrank a member with a stale one. The same engine scores a cohort list, so two screens cannot disagree about how much a member needs attention.

The formula
SignalPointsWhy it carries weight
Risk tier HIGH / RISING / MEDIUM / LOW30 / 20 / 8 / 0The strongest single predictor the plan holds
Each open gap nobody is working8Unworked, not merely open
Each gap already in flight2Still present, much quieter
Discharge follow-up owed: the 7-day visit window is open and no follow-up visit has been attended25It closes whether or not anybody acts
Plus, from day 3: the 48-hour contact window was missed (added to the visit-window points)15The visit window may still be saved
No active coverage while a gap is open18As hard a block as a pending authorization, and quieter: nothing is pending, so nothing prompts anyone to look
Outreach stalled: no reply for 21 days, or the member declined or could not be reached14Needs a different approach, and nothing else surfaces that
Authorization on the clock12A deadline the plan does not control
Medication adherence below 80% PDC10Precedes the admission it causes
High risk and in no programme10Eligible and unenrolled is a decision nobody made
Worked example — the member at the top of the panel
PointsSignalWhy
40unworked_gaps5 open gaps with nothing running × 8
30riskhigh risk
10adherenceAtorvastatin 40mg adherence 62%, below the 80% threshold
80Total — John Doenext action: start an intervention on the HbA1c gap

The same member scores 85 on clinical risk and 80 on panel attention — two different questions with two different answers. Risk asks how likely a bad year is; the panel asks how much of that is unattended right now. A high-risk member with everything already in flight scores lower here, and should.

Next best action — expected value, and why the number is not on screen

app/services/nba.py · recommend()
The question

For this population: which member, which action, why them, why now, and who should do it?

The logic

Ranked by expected value — how much the gap matters, weighted by the member's risk, discounted by whether anything can actually be done today. A recommendation nobody can act on is noise, and the fastest way to lose a work queue is to fill it with noise. But the score itself never reaches the screen. The customer review's words were that numeric scoring is difficult for users to interpret, and that is not a cosmetic complaint: a recommendation justified by “32.0” is one nobody can argue with and — the part that matters — nobody can catch being wrong. So the arithmetic orders the list, and what a person sees is a band, the reasons with their sources, and the single next step.

The formula
impact = 10 × risk_weight × gap_priority_weight

  risk_weight          HIGH 1.6 · RISING 1.3 · MEDIUM 1.0 · LOW 0.8
  gap_priority_weight  Critical 2.0 · High 1.5 · Medium 1.0 · Low 0.6

score  = impact × 1.0   actionable today
       = impact × 0.9   previous outreach ended unreachable or declined
       = impact × 0.5   blocked — an authorization is pending

order  = band first (Critical, High, Medium), then score, then member

The band is derived from the facts, never from a cut point on the score, so the band and the reasons beneath it can never disagree:

  • Critical — a critical-priority gap; or a high or rising-risk member with a gap 90 days or more late
  • High — a high or rising-risk member, or a high-priority gap, or a gap 90 days or more late
  • Late means days past the gap's due date once it is overdue, and days since the gap opened while it is not yet due.
  • Medium — an open gap with no aggravating factor
Worked example — the top recommendation

Record HbA1c Control — band Critical, because a critical-priority gap on a high-risk member.

impact = 10 × 1.6 (HIGH risk) × 2.0 (Critical gap) = 32.0
score  = 32.0 × 1.0 (actionable now)          = 32.0   sort key only, never displayed

The eleven reasons the engine returned, each naming the system that asserted it — this is what the user actually reads:

Open quality gap — HbA1c ControlCare Gap
Overdue 97 daysCare Gap
Critical priority on the measure packageCare Gap
High-risk memberRisk
Impacts Diabetes: HbA1c Control (<8.0%) — a tracked VBC quality measure with a 75% targetQuality
1 ED visit in the last 6 monthsUtilization
Coverage not verified — check eligibility before bookingEligibility
No outreach attempted for this gap yetOutreach
Food insecurity recorded and unaddressed — may prevent attendanceSDOH
Enrolled in Complex Care ManagementProgram/Contract
Actionable nowWorkflow

Next step: “No prior authorization is required — this can be scheduled as soon as the member is reached.” That sentence is the answer from the authorization engine, not a guess made on this screen, which is why the two can never contradict each other.

The engine recommends: it creates nothing, transitions nothing and sends nothing. Each action is returned already marked enabled or not, with the reason when not — a control that looks live and then refuses teaches the user that the screen lies.

6 · Clocks and windows

Transition-of-care windows

app/services/transitions.py
The question

Somebody came out of hospital. How long do we have, and for what?

The logic

This is the highest-yield window in value-based care and the one most often missed, because the trigger arrives from outside — an ADT message, not a person's decision. Every window is measured from the discharge date, not from the day the notification arrived: a feed that runs a day late shortens the window, it does not extend it.

WindowLengthMeasured from
Contact after discharge48 hoursthe discharge date, not the day the notification arrived
Follow-up visit7 daysthe discharge date
Readmission counted in30 daysthe discharge date
A discharge still treated as actionable120 dayslookback horizon
Worked example

Discharged 2026-09-22, two days ago: contact due by 2026-09-24 — window still open — and the follow-up visit due by 2026-09-29. While that contact window is open it contributes 28 points to every related task in the queue and 25 to the member's panel score, which is how a discharge reorders somebody's morning without anyone re-prioritising anything by hand.

Prior-authorization decision clock

app/services/workflow.py · submit_pa() · app/core/config.py
The question

When is this decision due, and what does the platform do as that moment approaches?

The formula
decision_due = submitted_at + 72 hours   when urgency is expedited
             = submitted_at + 7 days     standard
Worked example

PA-2026-40188, standard urgency, submitted 2026-09-20 12:29 → decision due 2026-09-27 12:29. Exactly seven days, to the minute, from submission — not from receipt, and not from the start of the next business day.

Configurable per deployment and modelled on CMS-0057-F. Every queue in the product reads this same field, which is why an authorization nearing its deadline lifts its member's task without anybody re-prioritising anything. The API also refuses a denial that does not carry a specific reason code and text.

Engagement ladder, wait times and the contact ceiling

app/services/engagement.py
The question

How do we reach somebody who has not replied — without becoming the plan that harasses its members?

The logic

A ladder starts on the member's preferred channel when they have named one, then walks the other channels in the order below, least intrusive first, skipping any that cannot be used (no address on file, opted out, and so on). Each rung waits long enough for a real person to have replied. With no preference on file the ladder opens on SMS and ends on Voice; a member who prefers Voice is contacted that way first. Consent is checked by a single authority that every channel calls, and a refusal is written to the audit chain before it is raised — so a member who was not contacted still leaves a record of why they were not contacted.

Rung, when no channel is preferredWait before the next
1 · SMS2 days
2 · Email3 days
3 · Portal7 days
4 · Voice2 days
LimitValue
Contacts allowed per member (default)4
within a rolling window of7 days
Consent disclosure validity365 days
A ladder that stalls is itself a signal. Outreach with no reply for the tenant's stall wait (21 days by default), or outreach the member declined or could not be reached on, adds 14 points in the work queue and 14 on My Panel, because it means the channel is wrong and no other screen would ever surface that.

7 · Programmes, providers and vendors

Care-management eligibility

app/services/enrollment.py · evaluate()
The question

Who should be offered care management, and what is the evidence for saying so?

The logic

Four named criteria, each recording its own evidence. The function returns the reasons, never a bare boolean — “eligible” with nothing behind it cannot answer an auditor, a medical director or the member. Before this existed, eligibility was a condition inline in the ranking engine that wrote nothing down, which meant eligible was a state no member could actually be in.

CriterionTestWhy the plan considers it
High-risk memberrisk tier is HIGHThe strongest predictor the plan has of the next twelve months
Rising risk with open gapstier RISING and three or more open care gapsRising risk is where care management is cheapest and most effective — before the admission, not after it
Recent acute eventan admission or ED visit within 180 daysThe strongest short-term predictor of the next admission is the last one
Unmet social barrierstwo or more unaddressed social needsNo clinical intervention reaches a member who cannot execute the plan
Worked example — six members qualify, on stated evidence
MemberCriterionEvidence recorded
John Doehigh_riskrisk tier HIGH
John Doerecent_acute1 acute encounter(s) since today − 180 days
Maria Gonzaleshigh_riskrisk tier HIGH
Maria Gonzalesrecent_acute1 acute encounter(s) since today − 180 days
Robert Kimrecent_acute2 acute encounter(s) since today − 180 days
Sam Okaforrecent_acute2 acute encounter(s) since today − 180 days
Linda Tranrising_with_gaps4 open gaps, rising risk
Fatima Hassanrising_with_gaps4 open gaps, rising risk

Eight criteria met by six members; two members qualify on two criteria each (John Doe and Maria Gonzales: high risk and a recent admission or ED visit). Note that Linda Tran and Fatima Hassan are not high risk — they are the rising-risk members the programme exists to reach early — and Robert Kim and Sam Okafor qualify on a recent acute event alone. A HIGH-only rule would have missed all four.

Deliberately not tunable from the screen. Who a plan enrols is clinical policy, and a threshold anybody can drag is a policy nobody has approved. ACCEPTED is the member's yes; ENROLLED is the plan's delivery. ENROLLED may be recorded directly (a payer file, an older programme); the panel flags any enrolment with no care manager or no care plan. The Start programme button assigns a care manager and opens a plan first. A member who declined is never re-offered by an automated sweep.

Provider response metrics

app/services/opportunities.py · summary()
The question

We send gap-closure opportunities to attributed providers. Which of them engage, and how fast?

The formula
acceptance_rate%     = round(100 × accepted / responded)
disputed_rate%       = round(100 × disputed / responded)
evidence_returned%   = round(100 × with_evidence / all opportunities)
response_median_days = the median of the response times in days: the middle value,
                       or the mean of the two middle values for an even count, rounded once to 1 decimal
overdue              = an answer is still owed (NEW or VIEWED) and the due date is past
                       (by more than the chase rule's grace; the clock stops at the first answer)
Two different denominators, deliberately. Acceptance is judged only against providers who answered — scoring silence as a refusal would misrepresent the ones still working the list, and a provider who has not replied yet is a different problem from one who declined. Evidence return is judged against every opportunity, because a claim closed without evidence is exactly what that denominator exists to count.
A provider's say-so never closes a gap. “Already completed” routes to the measure engine, which looks for the result; if the result is not there the gap stays open and the disagreement becomes visible, instead of resolving silently in the plan's favour — or the provider's.
Worked example — and what an empty rate means

Dr. P. Osei currently holds 2 opportunities and has answered neither, so the Acceptance column prints the words “no responses yet”, not 0%. That distinction is the formula doing its job: zero would be a statement about this provider's judgment, and the honest statement is that there is nothing to divide by yet.

Referral SLA

app/services/referrals.py · sla_days()
The question

We sent this member to somebody else. When should we have heard back, and who finds out if we have not?

The formula
due_on   = the due date typed on the referral (a real date, or the request is refused),
           else today + sla_days(queue)
sla_days = the tenant's setting for that queue, else its default, else 3 days

Per queue, because one queue's standard is not another's — hard-coding three days for all of them is the assumption a customer's operations team overturns on day one. A redirect restarts the clock for the new queue and does not settle the referral; unable to contact does settle it, because a team that tried and could not reach somebody has produced a result worth keeping rather than an absence. The person who raised the referral (when one is recorded) is notified when it is received, accepted, completed or outcome confirmed, and when it ends declined or unable to contact; routing, work starting, a redirect and a cancellation do not notify. A confirmed outcome is appended to the linked intervention's own trail, so the loop visibly closes where it started.

8 · Looking forward

Trends and projection

app/services/trends.py · forecast()
The question

On the current trajectory, where does this metric land at the end of the performance period?

The logic

Ordinary least squares through the captured points — deliberately the simplest method that can be checked by hand, because a projection is arithmetic and a forecast is a claim, and this produces the first. What makes it trustworthy is not the fit but the refusals: the function would rather return a sentence than a number it cannot stand behind.

The formula
method    = ordinary least squares through the captured points
slope     = Σ(x − x̄)(y − ȳ) / Σ(x − x̄)²
intercept = ȳ − slope × x̄
R²        = 1 − ss_res / ss_tot
projected = slope × (days to the horizon) + intercept

And the three refusals, each returned with its reason in words rather than a number nobody should trust:

  • Fewer than three captured points, or a span under 14 days → no projection. Two points define a line through anything.
  • R² below 0.25 → no projection, with the R² stated. A poor straight-line fit means the slope is noise from a single week, not a trend.
  • No open performance period → no projection: there is no horizon to project to. Any result outside a metric's natural bounds is clamped and flagged — a percentage projecting past 100 is arithmetic escaping its own units.
Worked example — ten metrics, nine projections and one refusal
MetricR²Projected at 2026-12-31
Quality composite0.97751.16%
Cost PMPM (allowed)0.9761,361.69
Open care gaps0.9619.68
Measure: HbA1c control0.97863.21%
Measure: retinal exam1.00.0%
High and rising-risk members0.8930.93
Attributed members—refused: only 1 captured point

Two rows worth reading together. The retinal measure fits perfectly (R² = 1.0) and projects 0.0% — a flat line through a measure where nothing has ever closed is an excellent fit and a useless forecast, which is why a high R² is a licence to draw the line, not evidence that the future will follow it. And attributed members refuses outright on a single point rather than drawing a line through it.

Note the honest tension on this screen: the composite projects to 51.16% against a 60% gate. The projection says the current trajectory does not clear the gate — and the gate path above says eleven gap closures would. Those two numbers answer different questions, and the screen shows both.

9 · Limits and conventions

Rounding, and what deliberately has no formula

QuantityRounding
Rates and composites1 decimal
Composite points2 decimals
Money2 decimals
Members needed to reach a targetwhole, rounded up
Percentages of a provider's responseswhole
  • Rounded once, at the end — with two stated exceptions. A displayed figure is a rendering of the exact value, never an input to the next step — otherwise two screens reporting one quantity drift apart by a hundredth, and the customer checking the arithmetic is the one who finds it. The exceptions are deliberate, so that a customer can reproduce a number from the figures printed beside it: the quality composite is the mean of the measure rates as displayed (each already to one decimal), and gross savings is the displayed per-member variance (to the cent) times the displayed member-months. Every screen that shows either number computes it the same way, in one function.
  • No learned model sits behind any of this. Every rule above is deterministic, versioned and readable.
  • Scores are computed at read time — no nightly job can drift from what the screen shows.
  • Nothing here decides. The server applies its own guards regardless of what the screen suggested.
What these numbers are not
  • The measure packages are synthetic. Real HEDIS and Stars specifications are licensed content with weights and cut points this engine does not implement. Every rate in this paper is correct arithmetic over a synthetic specification.
  • The risk weights are clinically reasoned, not empirically fitted. They encode conventional thresholds — 80% PDC, 140 systolic, 8.0% HbA1c — and their value is that they are arguable. A plan that disagrees with a weight can say so, and the version string makes the change visible in the score history.
  • Settlement figures are estimates. The payer reconciles against run-out claims months later.
  • A projection is not a forecast, and an unweighted composite is a scoring method chosen by a contract rather than a fact about quality.
How this paper stays true. Every constant published here is read back out of this page by an automated check and compared with the constant that actually computes it — risk weights and tier bands, lab thresholds, the utilisation caps, PDC, both ranking tables, the expected-value weights, every clock, the ladder and its ceiling. If a weight changes in the code and this page is not updated in the same change, the check fails and names the row. The honest limits above are asserted the same way, because removing one is a compliance change rather than an edit.

Part II · Every number on every screen

Point at any number and find how it was made

Part I explains the calculations that drive the product. This part is the dictionary: 457 values across 25 screens, each with the formula in plain words, why it is computed that way, the function that computes it, and its value in the demonstration tenant. Those demo values are not typed by a person: they are read from the product itself, on a freshly reset demo tenant, by signing in as the persona who sees the screen and calling the same endpoint the screen calls. The demo tenant is reset every day, and this page's values are regenerated on the server from that day's reset, so they match what the demo shows today: values as of 2026-10-12.

Command Center

18 values

The VBC manager, the executive and the platform administrator open this screen as their Dashboard (the care manager's Dashboard is a different screen). It shows six population counts, each linking to the list behind it, and today's priorities: groups of members who share a reason to act now.

Members

KPI card 1, top row

Formula

count of every Patient row in the tenant no attribution filter, no status filter, no panel filter

Why this wayIt answers 'how many people are on file', not 'how many we are responsible for'. That costs a mismatch with Contract Health, whose rows are built over the members attributed in the current contract period, so the two screens can show different member totals on the same day. For a panelled login (a provider) every card counts that login's panel only.

In the demo13 members

Check it yourselfClick the card: it opens Members, and that list has the same number of rows.

app/services/command_center.py · kpis()app/static/app.js · rCommandCenter()

High / rising risk

KPI card 2, top row

Formula

count of Patient rows whose stored risk_tier, upper-cased, is HIGH or RISING over every member in the tenant

Why this wayIt reads the tier stored on the member record, which Part I (Clinical risk score and tier) calls a convenience: the score history is the authority, and the tier bands (HIGH 60 or more, RISING 35 or more) are defined there. It is not re-scored at read time. Contract Health's 'High / rising risk members (attributed)' uses the same two tiers over attributed members only and says so in its label; the care manager's Dashboard card of the same name matches the tier without upper-casing. The card is marked amber when the count is above zero, and shows the hint 'monitor'.

In the demo6 members

Check it yourselfOpen Members and count the HIGH and RISING tier badges; the total matches the card.

app/services/command_center.py · kpis()app/services/risk.py · score_patient()

Open care gaps

KPI card 3, top row

Formula

count of CareGap rows where gap_is_open(status) open = status not in CLOSED, EXCLUDED, NOT_APPLICABLE, GAP_CLOSED, RESOLVED, DUPLICATE the database refuses any status outside the open and closed lists (and a blank one)

Why this wayGaps being worked (IN_PROGRESS, SCHEDULED, AWAITING_AUTH, SERVICE_COMPLETED, REOPENED) still count, because a gap is open until it is closed, not until someone touches it. It counts gap rows, not members, over every member and every measure. Contract Health's 'Open care gaps (attributed)' counts only attributed members' gaps and names that population in its label, so it can be lower (28 against 29 in the demo). The care manager's Dashboard counts gaps whose status is in the open list; because the database accepts only the known states, that and this card's rule always agree.

In the demo29 gaps

Check it yourselfClick the card: Care Gaps opens with the same total in its header, computed by the same gap_is_open rule.

app/services/command_center.py · kpis()app/services/workflow.py · gap_is_open()

Discharges (30d)

KPI card 4, top row

Formula

the Transitions list's inpatient discharges whose discharge date is 0 to 30 days before today (US business date)

Why this wayOne definition of a discharge: the card is the last 30 days of the very list it opens (inpatient stays, never a future date), so the card and the list behind it cannot disagree. It counts discharges, not members. For a provider login it counts the provider's panel only.

In the demo1 discharges

Check it yourselfClick the card: Transitions lists the inpatient discharges; count the rows discharged in the last 30 days.

app/services/command_center.py · kpis()app/services/transitions.py · recent_discharges()

Active interventions

KPI card 5, top row

Formula

count of Intervention rows whose status is not CANCELLED, COMPLETED, GAP_CLOSED, DECLINED or UNREACHABLE

Why this wayDeclined and unreachable interventions are treated as finished, because nobody is working them. The Interventions list the card opens uses the same five states, so card and list agree. The care manager's Dashboard shows a card with the same label that excludes only GAP_CLOSED, CANCELLED and COMPLETED, so it counts declined and unreachable interventions as active and can be higher.

In the demo1 interventions

Check it yourselfClick the card: Interventions opens with the same total in its header.

app/services/command_center.py · kpis()app/services/worklists.py · interventions()

Tasks overdue

KPI card 6, top row

Formula

count of WorkTask rows whose status is open (any case) and whose due date is before today today = the US business date, not the server's date; every owner in the tenant, assigned or not

Why this wayA task due today is not overdue until tomorrow. The count is tenant-wide, so the card opens My Work on its 'Team' tab, which lists the same open tasks; the rows marked OVERDUE there are the ones counted. The care manager's Dashboard 'Overdue tasks' card uses the same date rule.

In the demo8 tasks

Check it yourselfClick the card: My Work opens on 'Team'; count the rows marked OVERDUE.

app/services/command_center.py · kpis()app/core/clock.py · business_today()

'needs attention' / 'monitor' hint under a KPI card

KPI cards, under the number

Formula

a hint for every tone the server sends: red → 'needs attention' (Tasks overdue, when above 0); amber → 'monitor' (High / rising risk, when above 0)

Why this wayOnly a breached commitment (work already past due) earns the warning; a count of risk earns 'monitor', because risk is to be watched rather than a deadline missed. Every tone the server can send has a hint on the card.

In the demo('High / rising risk', 'amber'), ('Tasks overdue', 'red')

Check it yourself'needs attention' sits under Tasks overdue and 'monitor' under High / rising risk; each disappears when its count is 0.

app/services/command_center.py · kpis()app/static/app.js · rCommandCenter()

Today's priorities (count)

Today's priorities panel, number beside the title

Formula

number of clusters returned by the six detectors (at most 6) a detector with nobody in it is left out, not shown as 0

Why this wayAn empty row would read as a metric; its absence reads as 'nothing to do'. A detector that raises an error is not treated as empty: it is named above the table ('N checks could not run'), and the 'Nothing needs attention' all-clear is never shown while any check has failed.

In the demo4 clusters

Check it yourselfCount the table rows; it matches the number beside the title.

app/services/command_center.py · priorities()app/core/isolate.py · run_isolated()app/static/app.js · rCommandCenter()

Priority (CRITICAL / HIGH / MEDIUM)

Today's priorities table, Priority column

Formula

fixed per detector, not scored: CRITICAL = post-discharge follow-up, assessments overdue · HIGH = measure below target, vendor referrals overdue · MEDIUM = duplicate outreach, members we cannot reach colour: CRITICAL red, HIGH amber, anything else grey

Why this wayThe priority says which kind of clock is running: a window that cannot be reopened (Critical), a commitment already breached (High), or waste that breaches nothing (Medium). It does not grow with the size of the cluster; one member in a closing window outranks a hundred with a missed target.

In the demoHealth risk assessments overdue CRITICAL, Post-discharge follow-up CRITICAL, Diabetic Retinal Exam HIGH, Duplicate outreach MEDIUM

app/services/command_center.py · priorities()app/static/app.js · rCommandCenter()

Row order ('worst clock first')

Today's priorities table, top to bottom

Formula

sort by priority (Critical 0, High 1, Medium 2), then by member count, largest first

Why this wayThe kind of deadline decides first; size only breaks ties within the same priority. It is not the Work-queue score in Part I (Work-queue ranking): no member-level signal enters it.

In the demoHealth risk assessments overdue, Post-discharge follow-up, Diabetic Retinal Exam, Duplicate outreach

Check it yourselfRead the Priority and Members columns: priorities never go up as you read down, and within one priority the member counts never go up.

app/services/command_center.py · priorities()

Members (per cluster)

Today's priorities table, Members column (bold number)

Formula

number of distinct members in the cluster

Why this wayThis counts people. The 'Why' text beside it sometimes counts something else (discharges, referrals, outreach ladders), so the two numbers on one row can differ; where the sentence counts members (Duplicate outreach, Health risk assessments overdue) it is this number. For a panelled login every detector reads only the panel's rows, so the count, the example names and the sentence all describe the provider's own members.

In the demoHealth risk assessments overdue 2, Post-discharge follow-up 1, Diabetic Retinal Exam 6, Duplicate outreach 3 members

Check it yourselfClick the row's action button; the list it opens contains these members, among others.

app/services/command_center.py · priorities()app/api/population.py · command_center()

Example names under the member count

Today's priorities table, Members column (small text)

Formula

names of the first 3 members of the cluster, ranked by the panel's attention score (highest first, ties by name) … is added when the cluster has more than 3 members

Why this wayThe names are the cluster's members who most need attention, by the one 'needs attention' ranking the Risk panel uses (its score is computed at request time), so for a large cluster they are its most urgent three rather than an arbitrary three. The demo value below reports how many names are shown rather than which.

In the demoHealth risk assessments overdue 2 name(s), Post-discharge follow-up 1 name(s), Diabetic Retinal Exam 3 name(s) + …, Duplicate outreach 3 name(s)

app/services/command_center.py · priorities()app/services/panel.py · panel()app/static/app.js · rCommandCenter()

Post-discharge follow-up: 'N discharge(s) inside the 48-hour contact window, M more discharged 3-7 days ago, still inside the 7-day visit window'

Today's priorities table, Why column

Formula

windows = transitions.windows(tenant): the tenant's transition.contact / transition.visit rule params (48 hours and 7 days by default) rows = the Transitions list's discharges (inpatient stays discharged 0 to 120 days ago) whose visit window is open: discharged visit_days days ago or less N = those whose contact window is open: discharged contact_days days ago or less (the contact hours rounded up to whole days: 48 h = 2) M = the rest, discharged 3 to 7 days ago by default · members = distinct members in rows

Why this wayThe windows and the definition of a discharge are the Transitions module's (Part I, Transition-of-care windows), read through the same functions as the Transitions screen, the work queue and My Panel, so tuning the rule moves all of them and this row, the Discharges card and the Transitions list count the same discharges: an ED visit is not a transition, and a discharge dated in the future is not one yet. N and M count discharges, not members, and together they are every discharge still inside the visit window. For a provider login only the panel's discharges count.

In the demo1 discharge(s) inside the 48-hour contact window, 0 more discharged 3-7 days ago, still inside the 7-day visit window

Check it yourselfClick View transitions: the contact window is still open on each discharge counted in N.

app/services/command_center.py · _post_discharge()app/services/transitions.py · recent_discharges()app/services/transitions.py · windows()

Measure below target: 'X% against a Y% target, Z points short'

Today's priorities table, the High row named after a measure

Formula

for each measure's ACTIVE version with a target (quality.active_measures): rate = the measure's own rate from the measure engine; short = target − rate the measure with the largest positive short, among those with at least one open gap in the login's scope, is shown members = members in scope with an OPEN gap on that measure

Why this wayOne measure is reported, not all of them, so the row is a decision rather than a report. The rate is the measure rate in Part I (A measure across a population), compliant members over eligible members, read from the same engine as the Quality screen, so the two show the same rate. The target and title are the active version's; a retired version is never quoted. For a provider login the rate is still the plan's (the sentence says so) while the members are the provider's own.

In the demoDiabetic Retinal Exam: 0% against a 70% target — 70.0 points short

Check it yourselfOpen Quality and find the same measure: its rate is the one this row quotes.

app/services/command_center.py · _measure_below_target()app/services/quality.py · active_measures()app/services/quality.py · evaluate_tenant()

Health risk assessments overdue: 'N enrolled member(s) past their contractual HRA window, completion is P% of D enrolled'

Today's priorities table, Why column

Formula

for every ENROLLED member: the HRA governing the current period (the latest HRA whose due date has arrived, else the earliest) N = members whose governing HRA is not COMPLETED, REFUSED or UNREACHABLE and is past its due date P = round( 100 × members whose governing HRA was completed on or before its due date ÷ D ), D = enrolled members

Why this wayIt is the HRA completion KPI itself, over the denominator the plan does not choose: every enrolled member, so an HRA nobody attempted counts as much as one the member refused. Only HRAs count; a reassessment is a different obligation with its own clock. Missing HRAs are raised before the row is read, so a member enrolled since the last sweep is counted. The action opens the Assessments worklist, whose headline is the same KPI.

In the demo2 enrolled member(s) past their contractual HRA window

app/services/command_center.py · _assessments_overdue()app/services/assessments.py · hra_standing()app/services/assessments.py · kpis()app/services/assessments.py · ensure_due()

Vendor referrals overdue: 'N past their contracted SLA, the worst is <vendor> at D days over'

Today's priorities table, Why column

Formula

late = VendorReferral rows whose due date is before today (the date it is held to (vendors.due_on, A-058): the vendor's contracted SLA (referral date + the vendor's SLA days, 7 when none) until the referral is SCHEDULED; once SCHEDULED with an appointment date, that appointment; nothing once DELIVERED or settled) N = number of late referrals · D = the largest today - due date in days, with that referral's vendor

Why this wayIt calls the same is_overdue rule as the vendor scorecard, so this row and Vendor Performance cannot disagree about which referral is late. N counts referrals; the Members column counts members. The row is absent when nothing is late.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNothing is late in the freshly reset demo tenant, so the row does not appear. When it does, click Vendor performance: the breaches there are the same referrals.

app/services/command_center.py · _vendor_sla()app/services/vendors.py · is_overdue()

Duplicate outreach: 'N member(s) have more than one live outreach ladder, one has K, for different reasons: …'

Today's priorities table, Why column

Formula

live ladder = OutreachSequence whose status is not COMPLETED, EXHAUSTED or STOPPED N = members with 2 or more live ladders · K = the most live ladders any one member has reasons = the purposes of that member's ladders (at most 3 named)

Why this wayIt flags members being contacted about several things at once, which the member experiences as being pestered. A ladder records its PURPOSE (a care gap, an intervention, a transition, a task), and the sentence names those purposes. Two live ladders for the same purpose cannot exist: the store holds one per member per purpose (Part I, Engagement ladder, describes one ladder at a time).

In the demo3 member(s) have more than one live outreach ladder — one has 3

Check it yourselfOpen any listed member's Engagement tab: more than one ladder is live.

app/services/command_center.py · _duplicate_outreach()

Members we cannot reach: 'N outreach ladder(s) have run out of usable channels'

Today's priorities table, Why column

Formula

N = OutreachSequence rows with status EXHAUSTED, with no time limit members = distinct members among them

Why this wayA ladder is exhausted when every usable channel has been tried (Part I, Engagement ladder, wait times and the contact ceiling). N counts ladders; the Members column counts members. Because there is no time limit, a ladder that ran out months ago still counts. This is a different signal from the 'outreach stalled' ranking factor, which reads intervention status.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo ladder is exhausted in the freshly reset demo tenant, so the row does not appear. It appears once a ladder has used every channel.

app/services/command_center.py · _unreachable()

Dashboard

31 values

The Care Manager's "what needs me today" screen (payer personas — VBC Manager, Executive, Platform Admin — get the Command Center under the same menu item instead): eight operational counts, the top of the ranked member panel, three progress strips and the population totals.

Tasks due today

Operational card row, first card

Formula

count of work tasks with status open whose due date (first 10 characters, YYYY-MM-DD) equals today's US business date. Tenant-wide: every owner, not only mine

Why this wayRead against the US business calendar (business_today()), not the server's time zone, so a task due today does not jump to tomorrow at 19:00 Eastern. It counts tasks, not members: one member with three tasks due today adds three. It is a subset of Population "Open tasks".

In the demo15 tasks

Check it yourselfMy Work, Team: sort by Due and count the open tasks dated today.

app/api/vbc.py · summary()

Overdue tasks

Operational card row, second card (red badge when above 0)

Formula

count of work tasks with status open that have a due date and whose due date (due[:10]) is earlier than today's US business date, compared as ISO text. Tenant-wide

Why this wayA task due today is not overdue until tomorrow. The Command Center's "Tasks overdue" card computes the same idea in a second function (command_center.py · kpis()): it lower-cases the status and parses the date, so a task stored as Open or with a malformed due value is counted on one screen and not the other. On the demo data both show the same number. See Part I, Prioritisation, for how overdue work is ranked.

In the demo8 tasks

Check it yourselfMy Work, Team: sort by Due and count the open tasks dated before today; as VBC Manager, the Command Center's "Tasks overdue" card shows the same number on the demo.

app/api/vbc.py · summary()app/services/command_center.py · kpis()

High / rising risk

Operational card row, third card (never badged)

Formula

count of members whose stored risk tier is exactly HIGH or RISING. Tenant-wide, every member row including unresolved duplicates

Why this wayThe tier is the one stored by the last rescore (see Part I, Population health), not recomputed on read. It equals the High plus Rising bars of the Risk distribution strip on this page. The Command Center card of the same name upper-cases the tier first, so it would also count a tier stored as high; the demo stores upper case and both screens agree.

In the demo6 members

Check it yourselfAdd the High and Rising numbers in the Risk distribution strip lower on the page.

app/api/vbc.py · summary()app/services/risk.py · score_patient()

Discharges to follow up

Operational card row, fourth card

Formula

count of inpatient discharges 0 to visit_days days before today (the tenant's transition.visit window, 7 by default, via transitions.windows) whose follow-up is still owed: no appointment on or after the discharge date has been attended (transitions.open_followup). Counts discharges, not distinct members

Why this wayAnchored to the discharge date, so a discharge we learned about late has less window left, not a fresh one (see Part I, Clocks and windows). A discharge whose follow-up visit has already been attended drops off: the same test the transition plan uses for its 'visit completed' step, so the card and the plan agree. The Transitions screen's "Visit window open" count is the window alone and can be higher. The Command Center's "Discharges (30d)" is a different number: 30 days of the Transitions list.

In the demo1 discharges

Check it yourselfOpen Transitions: the "Visit window open" card shows the same number.

app/api/vbc.py · summary()app/services/transitions.py · recent_discharges()

PA / eligibility blocking care

Operational card row, fifth card

Formula

number of MEMBERS with a pending authorization (status SUBMITTED, IN_REVIEW or PENDED) or with at least one open gap and no current coverage (services/coverage: a coverage that is active with effective_from ≤ today ≤ effective_to) — each member counted once

Why this wayBoth causes stop the same service, so they share one card; the note under Care gap progress splits them, and gives the authorization count separately. A member with two pending authorizations and no coverage is one member blocked, not three. Pending is the one definition in models.pa_is_pending, read from the status, which My Panel's 'authorization on the clock' signal also uses.

In the demo1

Check it yourselfIt equals the sum of the four numbers in the "Blocking care" note under Care gap progress.

app/api/vbc.py · summary()

Outreach needing follow-up

Operational card row, sixth card

Formula

count of interventions that need a different approach: an intervention at IDENTIFIED or OUTREACH whose last event is at least the tenant's engagement.stalled wait old (stalled_days, 21 by default), or one that ended DECLINED or UNREACHABLE (models.iv_follow_up_why). Counts interventions, not members

Why this wayThe same test as the "Outreach stalled" signal in the work queue and on My Panel, and the same wait as the rule that raises 'Outreach with no movement', so an outreach sent this morning is not counted until the wait has passed. A member who DECLINED or could not be reached still counts, and each reason says which it was; whether a member who declined should count at all is an owner decision.

In the demo0 interventions

Check it yourselfMy Panel, "all factors": members with an outreach_stalled factor are where these sit; one member can hold several such interventions.

app/api/vbc.py · summary()

Active interventions

Operational card row, seventh card (never badged)

Formula

count of interventions whose status is not GAP_CLOSED, CANCELLED or COMPLETED

Why this wayAnything not finished counts as active here, including DECLINED and UNREACHABLE. The Command Center card with the same label, and the Interventions worklist it opens, both leave out DECLINED and UNREACHABLE, so this card shows a larger number than those screens as soon as one outreach fails. On the fresh demo there is one intervention (AUTH_APPROVED) and all three show 1.

In the demo1 interventions

Check it yourselfAs VBC Manager, the Command Center's "Active interventions" card and its Interventions worklist show the same number while no outreach has ended declined or unreachable.

app/api/vbc.py · summary()app/services/command_center.py · kpis()app/services/worklists.py · interventions()

Gaps closed this month

Operational card row, eighth card

Formula

count of care gaps with status CLOSED and a closure date (closed_on, YYYY-MM-DD) on or after the first day of the current US business month

Why this wayOnly CLOSED counts; EXCLUDED, NOT_APPLICABLE and the other closed states do not. Gaps closed before closure dates were recorded have no date and are reported separately (see "Gaps closed without a closure date") instead of being counted as not this month, which would read as a measured zero. Reopening a gap clears its closure date.

In the demo0 gaps

Check it yourselfMember 360, Care gaps: a gap closed this month shows Closed with this month's closure date; the fresh demo has none closed yet.

app/api/vbc.py · summary()

"needs attention" badge

Under five of the eight operational cards

Formula

shown when the card's value is above 0, on Tasks due today (amber), Overdue tasks (red), Discharges to follow up, PA / eligibility blocking care and Outreach needing follow-up (amber). Never shown on High / rising risk, Active interventions or Gaps closed this month

Why this wayA plain non-zero test with no threshold: the five badged cards are things to do, the other three are things to know. Red is kept for work already past its date.

In the demoTasks due today, Overdue tasks, Discharges to follow up, PA / eligibility blocking care

Check it yourselfEvery badged card shows a number above 0; the unbadged ones never carry the badge.

app/static/app.js · rDash()

Today's priorities (count beside the title)

Today's priorities panel, title

Formula

number of rows shown = the smaller of 8 (the screen asks for limit=8) and the number of members scoring above 0

Why this wayThe same ranking as My Panel (see Part I, Prioritisation, My Panel), cut at 8 so the first screenful stays short. My Panel asks for 25, so the two screens show different row counts from one ranking.

In the demo8 members

Check it yourselfCount the table rows; the first 8 rows of My Panel are the same members in the same order.

app/static/app.js · rDash()app/api/population.py · my_panel()app/services/panel.py · panel()

of N needing attention

Today's priorities panel, right of the title

Formula

number of members visible to the viewer whose panel score is above 0 (all of them, not only the 8 shown)

Why this wayVisible means the whole tenant for a care manager and a provider's attributed members for a provider. It is not "my" members: the ranking function's description says it narrows to the members a care manager owns work for, but the code only uses the viewer to set a mine flag, so every care manager sees the same N.

In the demo12 members

Check it yourselfMy Panel lists the same members (up to 25 rows); for the demo's 12 the whole list is on one page.

app/services/panel.py · panel()app/core/scope.py · visible_ids()

N with no task

Today's priorities panel, right of the title

Formula

of the members scoring above 0, the number with zero work tasks in status open (any owner)

Why this wayCounts across all scored members, not only the 8 rows shown, so it can be larger than the number of NO TASK badges visible on this page. These are the members the task queue cannot show, because nobody has raised work for them.

In the demo2 members

Check it yourselfOn My Panel, count the rows with a NO TASK badge.

app/services/panel.py · panel()

Priority

Today's priorities table, Priority column (shown for John Doe)

Formula

sum of signal points (see Part I, Prioritisation, My Panel for the table); rows sorted by score, highest first, ties by member name; members scoring 0 are not listed

Why this wayThe same computation as My Panel, from the same weight table (panel.PANEL_WEIGHTS), which Part I's table is rendered from. The 25 points apply while the member's newest discharge still owes its follow-up (the tenant's visit window, 7 days by default, open and no visit attended), and the extra 15 once the contact window has passed, so days 3 to 7 score 40 by default. The 10 for 'high risk and in no programme' reads the member's enrolment, whatever the top next-best action is. Adherence counts a PDC measured at 0% as below the threshold, as the risk score does. A gap whose only intervention was declined, went unreachable or completed without closing counts as unworked (8), not in flight (2).

In the demo80 points

Check it yourselfMy Panel, open "all factors" on John Doe: +40 + 30 + 10 = 80, Part I's worked example.

app/services/panel.py · panel()

Reason

Today's priorities table, Reason column (shown for John Doe)

Formula

the explanation text of the member's largest score component. Components are sorted by points, highest first; a tie keeps the order risk, unworked gaps, in flight, discharge, contact missed, adherence, authorization, stalled outreach, coverage, programme

Why this wayOne line, not all of them, so the column stays readable; the full list is on My Panel. For John Doe the 40-point unworked-gaps component outranks the 30-point risk tier. Adherence percentages are rounded, the same figure Member 360 prints, and a stalled-outreach line names what happened: "no reply for N days", "not started in N days", "member declined" or "member could not be reached".

In the demo5 open gap(s) with nothing running: HbA1c Control — Glycemic Assessment, Diabetic Retinal Exam, Statin Adherence (PDC >= 80%)

Check it yourselfMy Panel, "all factors": the Reason is the text of the factor with the most points.

app/services/panel.py · panel()

NO TASK badge

Today's priorities table, Member column

Formula

shown when the member has zero work tasks in status open, whoever owns them

Why this wayIt says only that no task exists. It says nothing about interventions: a member can carry NO TASK while a gap is being worked.

In the demoJon Doe

Check it yourselfOpen the member's 360: the Tasks list has no open task.

app/services/panel.py · panel()

"X risk" under the member name

Today's priorities table, Member column

Formula

the member's stored risk tier, from the rules engine: HIGH ≥ 60 points, RISING ≥ 35, MEDIUM ≥ 15, else LOW (see Part I, Population health)

Why this wayIt is the tier as of the last rescore, not recomputed on this read. A tier can also be set by hand through the records interface; the demo's duplicate record "Jon Doe" is seeded LOW after the tenant rescore, with no factors behind it.

In the demoHIGH

Check it yourselfMember 360, Risk: the factors listed add up to a score inside that tier's band.

app/services/risk.py · score_patient()app/services/risk.py · rescore()

Recommended action, grey reason line

Today's priorities table, under the Recommended action (shown for John Doe)

Formula

the first three reason texts of that action joined with ; , then cut to the first 90 characters in the browser

Why this wayThree reasons, not the full list of up to eleven, because the full list lives on the member 360. The cut is a plain character count, not a word boundary: John Doe's line is 91 characters, so the screen ends "…measure packag".

In the demoOpen quality gap — HbA1c Control; Overdue 97 days; Critical priority on the measure packag

Check it yourselfMy Panel shows the same reason uncut, and the member 360's Next best action lists the reasons in the same order.

app/services/nba.py · _recommend_for_gap()app/static/app.js · rDash()

Due

Today's priorities table, Due column (shown for John Doe)

Formula

the earliest date among: each open task's due date, the discharge follow-up visit date (discharge + 7 days) while that follow-up is still owed, and each pending authorization's decision deadline (converted to the US business date). The reason text of that date is printed under it; no deadline when there is none

Why this wayThree different clocks can be running, so the reason travels with the date (see Part I, Clocks and windows, for the discharge and authorization clocks). The earliest date may already be past: for John Doe it is a task 97 days overdue. The discharge clock is the member's NEWEST discharge (transitions.latest_discharge_by_member), and it only counts while the follow-up is owed (transitions.open_followup: the 7-day window is open and no appointment since the discharge has been attended) — the same test the +25 score uses, so a row never shows a closed window as its next deadline.

In the demo97 days before the reset

Check it yourselfMember 360: the earliest date among the member's open tasks, discharge window and pending authorizations is the one shown.

app/services/panel.py · _soonest_due()app/services/panel.py · panel()

Care gap progress: Open / In progress / Closed

Care gap progress strip (Closed is also the Population "Gaps closed" card)

Formula

Open = gaps not yet started (OPEN or REOPENED, models.GAP_NOT_STARTED); In progress = every other open state (IN_PROGRESS, SCHEDULED, AWAITING_AUTH, SERVICE_COMPLETED, models.GAP_IN_PROGRESS, derived from the open set); Closed = CLOSED only

Why this wayOpen and In progress partition the open states, so together they always equal the Population 'Open care gaps' card on the same page; a gap whose member attended and whose evidence is still on its way (SERVICE_COMPLETED) is in progress. EXCLUDED, NOT_APPLICABLE and the other closed states are not shown. Closed counts gap rows, not the measure numerator in Part I, Quality measurement.

In the demoOpen 28, In progress 1, Closed 0

Check it yourselfOpen + In progress equals Population "Open care gaps" whenever no gap is waiting on evidence.

app/api/vbc.py · summary()

Care gap progress bar lengths

Care gap progress strip, the three bars

Formula

bar length = round(100 × value ÷ (Open + In progress + Closed)) %, or 0% when that total is 0. Whole percent

Why this wayEach bar is a share of the three states drawn, not of all gaps, so gaps in states outside the bars do not shorten them. Whole-percent rounding can make the bars total 99% or 101%.

In the demoOpen 97%, In progress 3%, Closed 0%

Check it yourselfDivide each count by the sum of the three and round.

app/static/app.js · rDash()

Blocking care: authorization(s) on the clock

Note under Care gap progress (shown only when PA / eligibility blocking care is above 0)

Formula

count of prior authorizations with status SUBMITTED, IN_REVIEW or PENDED. Tenant-wide; counts authorizations, not members

Why this wayAn authorization still on its clock stops the member being scheduled, so it belongs on an operational board (see Part I, Clocks and windows, Prior-authorization decision clock). Pending is read from the status alone (models.pa_is_pending), the same test My Panel and the scheduling gate use.

In the demo0 authorizations

Check it yourselfAuthorizations screen: count the rows at Submitted, In review or Pended.

app/api/vbc.py · summary()

Blocking care: no coverage on file / terminated / lapsed

Note under Care gap progress

Formula

for each member with at least one open gap and no current coverage, by the reason services/coverage gives from the member's latest coverage row: no rows at all → no coverage on file; latest row not active → terminated; latest row active but ended, or not yet started → lapsed

Why this wayEach member lands in exactly one bucket, because the three causes need different fixes: a missing record is often an unmerged duplicate, terminated means the member left the plan, lapsed means the dates ran out. Members with no open gap are skipped: nothing is waiting on cover. The same function (services/coverage) drives the panel's "no active coverage" signal and every other screen that names a member's coverage.

In the demono coverage on file 1, terminated 0, lapsed 0

Check it yourselfThe demo's 1 is Jon Doe, the unmerged duplicate of John Doe: his 360 has no coverage.

app/api/vbc.py · summary()

Gaps closed without a closure date

Note under Care gap progress (shown only when above 0)

Formula

count of care gaps with status CLOSED and no closure date

Why this wayThe closure date was added later, so older closures carry none. They are counted as closed but left out of "this month" and named here, rather than silently treated as closed in some other month.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo gap is closed in the fresh demo, so the note does not appear. It appears once a gap with status Closed has no closure date, for example a record migrated from before closure dates existed.

app/api/vbc.py · summary()

Engagement funnel: Interventions created / Outreach or beyond / Reached (engaged or beyond) / Scheduled or beyond / Completed or gap closed

Engagement funnel strip

Formula

the one funnel table (models.IV_FUNNEL_STAGES, via models.iv_funnel), the same stages and labels as the Analytics funnel: Interventions created = every intervention in any state; each later stage = interventions whose status is at or past that stage in the funnel order (models.IV_STAGE): IDENTIFIED, ASSIGNED, OUTREACH, ENGAGED, AWAITING_AUTH, AUTH_APPROVED, SCHEDULED, IN_PROGRESS, COMPLETED, GAP_CLOSED, with REOPENED placed at ENGAGED; UNREACHABLE, DECLINED and CANCELLED count only as created. Outreach ≥ OUTREACH, Reached ≥ ENGAGED, Scheduled ≥ SCHEDULED, Completed ≥ COMPLETED

Why this wayStages are cumulative, so a member already scheduled still counts as reached. Consequences: an intervention waiting on or holding an authorization counts as reached but not scheduled; one that was reached and later declined drops out of every stage; a REOPENED intervention counts as reached, the same as My Panel's follow-through 'Member reached'. It counts interventions, not members.

In the demoInterventions created 1, Outreach or beyond 1, Reached (engaged or beyond) 1, Scheduled or beyond 0, Completed or gap closed 0

Check it yourselfThe demo's one intervention is at Auth approved: it counts in Outreach and Reached only.

app/api/vbc.py · summary()app/models.py · iv_funnel()

Engagement funnel bar lengths

Engagement funnel strip, the five bars

Formula

bar length = round(100 × stage ÷ Interventions created) %, dividing by 1 when nothing was created. Whole percent

Why this wayEach stage is shown as a share of every intervention created, the same base the Analytics funnel draws against, so the two screens give the same bar for the same count.

In the demoInterventions created 100%, Outreach or beyond 100%, Reached (engaged or beyond) 100%, Scheduled or beyond 0%, Completed or gap closed 0%

Check it yourselfDivide each stage by the Interventions created number and round.

app/static/app.js · rDash()

Risk distribution: High / Rising / Medium / Low

Risk distribution strip

Formula

count of members by stored risk tier; a tier with no members shows 0. Tenant-wide

Why this wayThe tier as of the last rescore (see Part I, Population health). It includes hand-set tiers and the demo's unmerged duplicate "Jon Doe", seeded LOW with no factors, so the Low bar holds one member no rule scored.

In the demoHigh 2, Rising 4, Medium 5, Low 2

Check it yourselfThe four numbers add up to Population "Patients"; Patients screen, filter by risk.

app/api/vbc.py · summary()app/services/risk.py · score_patient()

Risk distribution bar lengths

Risk distribution strip, the four bars

Formula

bar length = round(100 × tier count ÷ total) %, where total sums every tier returned, including any member with no tier, which is not drawn. Whole percent

Why this wayA member with no tier shortens all four bars without appearing, so the bars can total under 100%; rounding alone can make them total 99% or 101%. The demo totals 99%.

In the demoHigh 15%, Rising 31%, Medium 38%, Low 15%

Check it yourselfDivide each tier count by Population "Patients" and round.

app/static/app.js · rDash()

Patients

Population panel, first card

Formula

count of every member record in the tenant

Why this wayNot attributed members and not de-duplicated: an unmerged duplicate (the demo's "Jon Doe") counts as a second member. The whole tenant for a care manager; a provider who opens this screen sees the count of their own attributed panel, like every other figure on it.

In the demo13 members

Check it yourselfThe Patients screen lists the same number of rows; the Command Center's "Members" card agrees.

app/api/vbc.py · summary()

Open care gaps

Population panel, second card

Formula

count of care-gap rows whose status is in the open set OPEN, IN_PROGRESS, SCHEDULED, AWAITING_AUTH, SERVICE_COMPLETED, REOPENED

Why this wayGap rows, including gaps already scheduled or attended and waiting on evidence. Part I, Quality measurement defines open gaps per measure as denominator minus numerator and totals twenty-nine; this card shows 29 on the demo too. The Command Center card counts any status not in the closed set, which agrees for every known status.

In the demo29 gaps

Check it yourselfAs VBC Manager, the Command Center's "Open care gaps" card and the Open care gaps worklist behind it show the same number.

app/api/vbc.py · summary()

Open tasks

Population panel, fourth card

Formula

count of work tasks with status open, any owner. Tenant-wide

Why this wayThe denominator the two task cards come from: Tasks due today and Overdue tasks are subsets of it (Part I, Prioritisation, opens with the same thirty-two). A started task keeps status open, so it stays in this count.

In the demo31 tasks

Check it yourselfMy Work: the Team button carries the same number.

app/api/vbc.py · summary()

My Work

34 values

Care managers open My Work to see their own open tasks, ranked by what is happening to each member and with the reason each task exists. VBC managers use the Team view of the same screen to find everyone's open work, including tasks nobody owns.

My Work N (count in the panel title)

Panel title, beside 'My Work' (or 'Team work' in the Team view)

Formula

number of rows in the table: the open tasks in the selected view (Mine = open tasks you own; Team = every open task in the tenant). When the queue was opened from an Exception Center row, only that rule's tasks are kept (filter on rule_key). 'Open' means status == 'open' exactly, so tasks someone has started are still counted.

Why this wayThe title counts what is on screen, while the Mine and Team buttons always count the whole tenant. So when the queue is filtered to one exception (an amber badge names the rule), the title can be smaller than the buttons beside it. That is intended: the filter is visible and 'all work' removes it. Neither view is narrowed by panel or attribution: Team is the whole tenant.

In the demo30 tasks

Check it yourselfcount the rows in the table; with no exception filter the title equals the Mine button in the Mine view and the Team button in the Team view

app/static/app.js · rWork()app/api/vbc.py · my_tasks()

Mine N

Scope toggle, top right

Formula

number of tasks in the tenant with status == 'open' and owner_id equal to the signed-in user

Why this wayThe count is the same whichever view is selected, because both counts come back with every response. That lets the toggle show what is on the other side without a second request. Started (in-progress) tasks are included, because a started task is still open.

In the demo30 tasks

Check it yourselfin the Mine view with no exception filter, the table has exactly this many rows

app/api/vbc.py · my_tasks()

Team N (also 'of N open task(s)' in the footer note and in the empty state)

Scope toggle; 'Where this work came from' note; the 'Your queue is clear' message

Formula

number of tasks in the tenant with status == 'open', whoever owns them and including tasks with no owner

Why this wayOne number, printed in three places on this screen. It is tenant-wide, not limited to the signed-in user's panel. The empty state compares it with Mine (team > mine) to tell a care manager with a clear queue that work is waiting in the Team view.

In the demo31 tasks

Check it yourselfswitch to Team: the table has this many rows

app/api/vbc.py · my_tasks()app/static/app.js · rWork()

N unassigned, and the UNASSIGNED row badge

Team button ('Team 32 · N unassigned'); Team-view note; empty state; Owner column badge

Formula

badge: the task has no owner_id. Count: number of open tasks in the tenant with no owner_id, from every source (rule, campaign, transitions sweep, typed by hand)

Why this wayA task is unassigned when it was typed in without an owner, or when a rule found nobody to give it to. Rules assign by continuity first (the member's current owner), then to the holder of the target role with the fewest open tasks, and leave the task unowned only when nobody holds that role (see 'Owner' below). The count and the badges describe the same set, so they agree whenever the table shows the whole Team view.

In the demo1 tasks

Check it yourselfswitch to Team and count the amber UNASSIGNED badges; they equal N

app/api/vbc.py · my_tasks()app/services/task_rules.py · _assign()

AUTO and CAMPAIGN badges, and 'X of N open task(s) were raised by a rule from an event; Y came from a campaign a person launched'

Member column badge; 'Where this work came from' note under the table

Formula

origin = task_origin(task): rule for source rule or transitions, campaign for source campaign, else manual AUTO badge: origin rule · CAMPAIGN badge: origin campaign X = open tasks in the tenant whose origin is rule · Y = those whose origin is campaign

Why this wayThe note exists to report how much of the queue came from events rather than from typing. A campaign is launched by a person, so it is reported as its own origin rather than as rule work. The legacy transitions source is counted as rule work, because the transitions sweep now delegates to the rule engine. X and Y count the whole tenant whichever view is shown.

In the demo25 tasks

Check it yourselfswitch to Team and count the blue AUTO badges; they equal X

app/api/vbc.py · my_tasks()app/static/app.js · rWork()

'Z were typed in by a person'

'Where this work came from' note under the table

Formula

number of open tasks in the tenant whose origin is manual. Every task has exactly one origin, so X + Y + Z = Team N always.

Why this wayReported, not asserted: the note sits beside the claim that work should come from events and rules, so a reader can see how far that is true of this queue.

In the demo6 tasks

Check it yourselfin the Team view, the rows reading 'created by hand' under the member name number Z

app/api/vbc.py · my_tasks()

IN PROGRESS badge, and 'N are in progress'

Reason column badge ('<name> started this'); 'Where this work came from' note

Formula

badge: started_at is set. N = number of open tasks in the tenant with started_at set. Derived, never stored as a status.

Why this wayIn progress is not a status. The task stays open, so it stays in the queue and in every open count, and it does not disappear from other screens that filter on open. Pressing Start sets started_at and also claims an unowned task, so two people do not work the same call.

In the demo0 tasks

Check it yourselfzero after a reset. Press Start on any row: the badge appears on that row and N goes up by one

app/api/vbc.py · my_tasks()app/static/app.js · taskStart()

Priority badge (Critical / High / Medium / Low)

Priority column, top of each row

Formula

the task's stored priority. Typed in by hand: the label chosen. Raised by a rule: the rule's own grade where it has one (abnormal lab: Critical at 30% or more outside the range, else High), otherwise the rule's configured priority. A few rules may later raise it one band (see 'Escalated priority').

Why this wayThe badge prints the word the record holds, coloured red, amber, blue and green. It is one input to the ranking, worth 20 / 12 / 6 / 2 points, and not the ranking itself (see Part I, Work-queue ranking). So a Medium task can sit above a Critical one.

In the demoHigh

Check it yourselfthe top row's badge. Hover 'score N' under it: the first line is '+points marked <label>'

app/static/app.js · priorityBadge()app/services/task_rules.py · evaluate()

Escalated priority, and '[escalated to vbc_manager]' in the reason

Priority badge and the grey reason line

Formula

for an open rule-raised task whose rule sets escalate_after_days and escalate_to_role (as shipped: gap.evidence_missing and appointment.not_rebooked, 7 days, vbc_manager): when today − (due date, else the business date it was created) >= 7, the priority moves up one band (Low→Medium→High→Critical; Critical stays Critical) and [escalated to vbc_manager] is added to the reason. It happens once per task, guarded by a task.escalated audit row.

Why this wayEscalation adds urgency without taking the task away from its owner: the owner is not changed. Both rules raise their tasks with no due date, so in practice the 7 days run from the day the task was created. The Exception Center's 'Escalated' card states exactly this, from the server's own sentence (ESCALATION_MEANING): no reassignment happens and nobody is notified; the role appears only as text in the reason.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone in a fresh tenant. It appears on a task-rule sweep once an 'evidence missing' or 'missed visit not rebooked' task is 7 days old

app/services/task_rules.py · _escalate_overdue()

score N

Priority column, under the badge

Formula

sum of the points of every component that fires: authored priority + due date + age + member risk + post-discharge window + authorization clock + worst open gap + stalled outreach (see Part I, Work-queue ranking). Maximum 20+55+21+22+28+30+18+14 = 208.

Why this wayComputed each time the screen loads and never stored, so the order moves as the calendar does, with no recompute job. The screen shows both the composed sentence (Reason column) and this number; Part I's worked example is the team queue's top row, generated from this endpoint. In a freshly reset tenant only five of the eight components can fire; age, the authorization clock and stalled outreach have nothing to act on.

In the demo107 points (top row)

Check it yourselfhover 'score N': the +points in the tooltip add up to N

app/services/ranking.py · rank()

Score tooltip ('+points why · +points why …')

Hover 'score N' in the Priority column

Formula

every component that fired, each written +points why, joined with · , in a fixed order: priority, due, age, member risk, post-discharge window, authorization, gap, engagement. The 'marked <label>' line is always first, even at +0.

Why this wayThe tooltip shows the evidence behind the order, so the queue explains its ranking rather than just asserting it. It is in engine order, not sorted by points. The worked example in Part I is sorted by points and shortens the gap line to 'HbA1c Control'. The screen shows the full measure name and adds ', N open in total' when the member has more than one open gap.

In the demo+12 marked High · +55 overdue by 60 days · +22 high-risk patient · +18 critical-priority gap: HbA1c Control — Glycemic Assessment, 5 open in total

Check it yourselfhover the top row's score and compare it word for word

app/static/app.js · rWork()app/services/ranking.py · rank()

'+N marked <label>' (authored-priority points)

Score tooltip, first line

Formula

Critical 20, High 12, Medium 6, Low 2, any other label 0

Why this wayThe human label counts, but it can be outscored by what is happening to the member (see Part I, Work-queue ranking). It used to be the heaviest component at 40.

In the demo12 points (top row)

app/services/ranking.py · rank()

'+N overdue by D days' / 'due today' / 'due in D days' (due-date points)

Score tooltip; also the first phrase of many Reason headlines

Formula

D = due date − today, where today is the US business date (America/New_York). D < 0: 35 + 2 × min(days late, 10), i.e. 37 to 55. D = 0: 25. D = 1–2: 18. D = 3–7: 10. D > 7: 3. No due date, or one that is not a date: no line.

Why this wayLateness counts most, and stops adding after 10 days so an ancient task cannot bury today's work. Part I's range, 37–55, is generated by calling this function at 1 to 30 days late, so it cannot drift from it. The business-date clock stops the queue from calling today's tasks overdue after 20:00 Eastern, when UTC has already rolled over.

In the demo55 points (top row)

Check it yourselfa task more than 10 days late shows the +55 cap; one 1 day late shows +37, and one 4 days late shows +43

app/services/ranking.py · _due_component()

'+N open D days' (age points)

Score tooltip

Formula

D = whole 24-hour periods since the task was created (elapsed time, UTC). Under 3: no line. From 3: min(D, 21) points.

Why this wayAge breaks ties. It lets low-priority work with no due date eventually surface. Part I's row is generated from this function: from day 3, points = days open, so 3 points on day 3 and 21 from day 21.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfa reset creates every task at reset time, so no task is 3 days old yet. The line appears on any task still open 3 days later

app/services/ranking.py · _age_component()

'+N high-risk patient' (member risk points)

Score tooltip; 'high risk' / 'rising risk' in the Reason headline

Formula

the member's stored risk tier: HIGH 22, RISING 15, MEDIUM 6, LOW 0 (no line)

Why this wayIt applies to every task of the member, whatever the task is about. The tier itself comes from the clinical risk score (see Part I, Clinical risk score and tier). A task with no member gets only the priority, due and age components.

In the demo22 points (top row)

app/services/ranking.py · rank()

'+28 discharged N day(s) ago, 48-hour contact window open until DATE' / '+20 … contact window missed'

Score tooltip; 'post-discharge contact window open' in the Reason headline

Formula

the member's most recent inpatient discharge within 120 days. N = business today − discharge date, in days. N ≤ contact days (the tenant's contact_hours, 48 by default, rounded UP to whole days by transitions.windows: 2): 28 points, open until discharge + 2 days. Otherwise N ≤ visit days (7 by default): 20 points, visit due by discharge + 7. Otherwise: no line.

Why this wayThe window closes whether or not anyone acts, so it ranks above almost everything while it is open (see Part I, Transition-of-care windows). It lifts EVERY open task of that member, not only the discharge tasks. The '48 hours' are counted as whole calendar days, so a member discharged just after midnight stays 'open' for almost 72 hours.

In the demo28 points (first row that has it)

Check it yourselfevery row for the member discharged two days before the reset carries +28, including a social-barrier task that has nothing to do with the discharge

app/services/ranking.py · rank()app/services/transitions.py · recent_discharges()

'+N <PA ref> decision due in Hh' / 'is past its decision deadline' / 'is on the SLA clock'

Score tooltip; 'authorization on the SLA clock' in the Reason headline

Formula

the member's undecided authorization with the soonest decision_due. H = hours until it (elapsed time, printed whole with the fraction dropped). Past it: 30. H < 24: 25 expedited / 20 standard. H < 72: 18 / 12. Otherwise: 5.

Why this wayThis is the regulatory clock the authored priority could never express (see Part I, Prior-authorization decision clock). Only the soonest authorization counts, and it lifts every task of that member, not only the task about the authorization.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone of the demo queue's members has an undecided authorization with a deadline, so no line shows after a reset. Submit an authorization for a member with a task, and every task of that member gains the line

app/services/ranking.py · _sla_component()

'+N <priority>-priority gap: <measure>, K open in total' (worst open gap points)

Score tooltip; the measure name in the Reason headline

Formula

among the member's open gaps (OPEN, IN_PROGRESS, SCHEDULED, AWAITING_AUTH, SERVICE_COMPLETED, REOPENED), the one with the highest priority: Critical 18, High 12, Medium 6, Low 2. ', K open in total' is added when K > 1.

Why this wayIt uses the worst gap's priority, not the number of gaps, because three Low gaps are not one Critical gap. A gap the member has attended but that has no evidence yet (SERVICE_COMPLETED) still counts as open here.

In the demo18 points (top row)

app/services/ranking.py · rank()

'+14 <intervention>: no reply for N days' (or 'not started in N days' / 'member declined' / 'member could not be reached')

Score tooltip; 'outreach stalled' in the Reason headline

Formula

14 points if any intervention of the member is an intervention at IDENTIFIED or OUTREACH whose last event is at least the tenant's engagement.stalled wait old (stalled_days, 21 by default), or one that ended DECLINED or UNREACHABLE (models.iv_follow_up_why). The sentence names a silent one before a member-side stop.

Why this wayIt flags members whose outreach needs a different channel. It waits exactly as long as the rule 'Outreach with no movement' (engagement.stalled) does, so outreach sent today scores nothing until the wait has passed, and the queue, My Panel and the Dashboard read the same test. A member who DECLINED or could not be reached still scores 14, with a reason that says which; whether a declined member should earn queue points is an owner decision.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfno queue member has outreach at those statuses after a reset. Press 'Begin outreach' on a member's intervention, and every task of that member gains +14 at once

app/services/ranking.py · rank()

Row order

The table, top to bottom

Formula

score, highest first; ties broken by due date, earliest first (no or unreadable due date last), then by creation time, oldest first

Why this wayThe tie-breaks make the order stable across reloads instead of whatever the database returned (see Part I, Work-queue ranking). The order is the only output; no rank number is shown. It applies to the rows in view, Mine or Team, after any exception filter.

In the demo107, 107, 101, 94, 94 (scores of the first five rows)

Check it yourselfthe first two rows tie on score; the one due earlier is above the other

app/services/ranking.py · rank()

Reason headline (bold), e.g. 'overdue by 60 days + high risk + HbA1c Control — Glycemic Assessment'

Reason column, bold line

Formula

sort the components by points, highest first (ties keep engine order); take the first three; drop any worth 0; join their short phrases with + . Phrases: the due/age text as-is, 'marked <label>', '<tier> risk', the gap's measure name, 'post-discharge contact window open' / 'overdue', 'authorization on the SLA clock', 'outreach stalled'.

Why this wayThe headline says what is true about the member in a few words, not what somebody typed. Three phrases fit a table cell; the tooltip keeps the full list. The grey line under it is the rule's own reason text (see the rows below).

In the demooverdue by 60 days + high risk + HbA1c Control — Glycemic Assessment

Check it yourselfthe three phrases are the three largest +points in that row's tooltip

app/services/ranking.py · priority_reason()

Reason headline fallback (single top factor)

Reason column, bold line, only when the composed headline is empty

Formula

the full 'why' sentence of the single highest-point component (the first one on a tie)

Why this wayKept as a safety net, and effectively unreachable. The authored-priority component always exists and is worth at least 2 for every standard label, so the headline is empty only for a non-standard label.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnever shown in the demo: every row has a composed headline

app/services/ranking.py · top_reason()app/static/app.js · rWork()

OVERDUE badge

Due column, under the date

Formula

the first 10 characters of due sort before today's date, where today is the US business date (America/New_York). A task due today is not overdue. A due that is not a date never shows the badge.

Why this wayIt is the same boundary the ranking uses for its overdue points, so the badge and the '+N overdue by D days' line always agree.

In the demo8 rows badged in the Mine view

Check it yourselfcount the red OVERDUE badges; every one of those rows has an 'overdue by' line in its tooltip, and no other row does

app/api/vbc.py · my_tasks()

Origin line under the member name (rule title / 'campaign' / 'transitions sweep' / 'created by hand')

Member column, grey line

Formula

the title of the rule that raised the task, looked up from the rule catalogue by rule_key; with no rule: 'campaign' if source == 'campaign', 'transitions sweep' if source == 'transitions', else 'created by hand'

Why this wayIt names the rule in a care manager's words ('Post-discharge contact window'), not the mechanism, so the person can see why a task they did not create is in their queue.

In the demoHRA past its contractual window

app/static/app.js · rWork()app/api/vbc.py · my_tasks()

Due date of a rule-raised task

Due column

Formula

set once, when the rule raises the task. Post-discharge: discharge + 2 days for contact (ceil(48h ÷ 24)), + 3 for medication reconciliation and barriers, + 7 for the visit and the assessment. Care gap due soon: the gap's date; today if the gap says 'now', 'overdue' or 'asap'; otherwise today + 14. Abnormal lab: result date + 7. Authorization at risk: the date of decision_due. Appealable denial: decision date + 60. Coverage ending: the coverage end date. SLA breach and HRA: the missed date. Social barrier, enrolment and engagement tasks: the day raised. Evidence missing and missed visit not rebooked: no date.

Why this wayDischarge windows count from the discharge date, not from the day the record arrived (see Part I, Transition-of-care windows). A later sweep never re-dates an existing task: one task per rule and trigger. The authorization date is its business date (America/New_York, core.clock.business_iso), so a late-evening Eastern deadline is due that evening, not the next day. The task rule and every screen read the contact window through transitions.windows, rounded up the same way, so a tenant that tunes the rule moves the task and the screens together.

In the demo60 days before the reset

Check it yourselfthe 'Discharged … — follow-up visit due within 7 days' row is due 7 days after the discharge date in its reason

app/services/task_rules.py · _r_transition_contact()app/services/task_rules.py · _transition_step()app/services/task_rules.py · _r_gap_due_soon()app/services/task_rules.py · _r_pa_sla_at_risk()

'N% outside the reference range' (abnormal-result tasks)

Reason column, grey line of an 'Abnormal result with no follow-up' task

Formula

above the range: 100 × (value − upper) ÷ |upper|; below: 100 × (lower − value) ÷ |lower|; rounded to a whole percent. Only the latest abnormal result per member per test in the last 30 days. Under 10%: no task. 30% or more: Critical, else High.

Why this wayThe lab's abnormal flag has no size, so the rule measures how far outside the range the result is before it interrupts anyone. A result with no reference range is not treated as normal. It still raises a High task, which says the platform could not grade it.

In the demo14 % (highest-ranked lab task)

Check it yourselfevery graded lab task in the demo is below 30% and shows High; the HbA1c with no range says it could not be graded

app/services/task_rules.py · _deviation_pct()app/services/task_rules.py · _r_lab_critical()

'<PA ref> (urgency) decision due in Hh' / 'decision overdue' (authorization-at-risk tasks)

Reason column, grey line

Formula

for an authorization at SUBMITTED, IN_REVIEW or PENDED: H = hours until decision_due, with the fraction dropped (23.9 prints '23h'). The task is raised when H ≤ 24, and says 'overdue' once H < 0.

Why this wayIt warns the UM nurse a day before the regulatory deadline (see Part I, Prior-authorization decision clock). The hours are frozen into the reason when the task is raised; the score tooltip's authorization line is recomputed on every load.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone after a reset. Raised by a task-rule sweep within 24 hours of a pending authorization's deadline

app/services/task_rules.py · _r_pa_sla_at_risk()

'appeal window closes DATE' (denied-authorization tasks)

Reason column, grey line; the same date is the Due date

Formula

for a DENIED or PARTIALLY_APPROVED authorization: raised while appeals.is_appealable — today is on or before the window's closing business date; closes on decision time + the line of business's window (appeals.appeal_window: 180 days by default, 60 for Medicare Advantage, tenant-configurable)

Why this wayIt keeps a denial in front of a care manager while an appeal is still possible. The window is the appeals engine's own, by the coverage's line of business, so the task's closing date is the date filing an appeal stops being accepted, and the member attention panel names the same window.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone after a reset. Deny an authorization, run the task rules, and the task appears with the closing date 180 days out (60 for a Medicare Advantage member)

app/services/task_rules.py · _r_pa_denied_appealable()

'coverage ends DATE (N days)' (coverage-ending tasks)

Reason column, grey line

Formula

N = coverage end date − today, in days; raised when 0 ≤ N ≤ 30, and only for members enrolled in a programme

Why this wayA member in care management who loses coverage drops out of the programme; 30 days leaves time to confirm continuing coverage. Coverage with no end date never raises a task.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone after a reset

app/services/task_rules.py · _r_coverage_lapsing()

'<intervention> has sat in outreach for N days' (outreach-with-no-movement tasks)

Reason column, grey line

Formula

for an intervention at OUTREACH or IDENTIFIED: N = today − date of its last recorded event; raised when N ≥ 21; due the day raised

Why this wayThe last event is the last deliberate act on the intervention. The '+14 outreach stalled' score line uses the same wait and the same last event (see that row).

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone after a reset

app/services/task_rules.py · _r_engagement_stalled()

SLA-breach tasks: vendor, internal referral, provider response, social-need follow-up, HRA

Reason column, e.g. '<vendor> has not delivered <service> — SLA was DATE', 'HRA was due DATE — N days overdue'

Formula

raised when is_past_due(due, today, grace_days) — today − the object's due date > grace_days (grace 0, so from the day AFTER the due date) — and the object is not settled: vendor vendors.due_on (the SLA until SCHEDULED, then the appointment; A-058), referral due_on, provider opportunity sla_due_on (NEW or VIEWED only), social-need follow_up_on, HRA due_on. The task's due date is that same date.

Why this wayA deadline is missed the day after it, not on it. The rules and every OVERDUE badge and overdue flag (My Work, the vendor chain, referrals, social-need follow-ups, the provider portal) ask the same function (core.clock.is_past_due), so a breach task never appears while its own row still reads 'due today'. A tenant's grace_days adds whole days after the due date. The HRA reason says how late it is, always 1 day or more.

In the demo2 tasks in the Team view

Check it yourselfa vendor task says 'SLA was <date>' or, for a scheduled service, 'the appointment was <date>'; the referral's own row shows the same date

app/services/task_rules.py · _r_vendor_sla_breach()app/services/task_rules.py · _r_referral_sla_breach()app/services/task_rules.py · _r_opportunity_unanswered()app/services/task_rules.py · _r_sdoh_follow_up()app/services/task_rules.py · _r_hra_overdue()

'no evidence has closed the gap in 7+ days' / 'visit … was never rebooked'

Reason column, grey line

Formula

evidence missing: the gap is SERVICE_COMPLETED, and today − the date it reached that status ≥ 7. Missed visit: the member's most recent appointment is NO_SHOW or CANCELLED, and today − its date ≥ 5. Both are raised with no due date.

Why this wayAttendance is not the outcome. A gap stuck at 'attended' stays open on the measure, and a missed visit nobody rebooks is a member falling out of care. With no due date these tasks can never show OVERDUE, and their 7-day escalation clock runs from creation (see 'Escalated priority').

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnone after a reset

app/services/task_rules.py · _r_gap_evidence_missing()app/services/task_rules.py · _r_appointment_not_rebooked()

'Meets care-management criteria — …' tasks (how many appear)

Reason column, grey line

Formula

each evaluation takes the members currently ELIGIBLE, sorts them by number of criteria met (most first), and proposes the first 5 (max_per_run). A member who already has an offer task still takes one of the 5 places.

Why this wayThe rule is rationed because care management has limited capacity, and a queue with every eligible member in it gets scrolled past. The cap is in effect 'the top 5 eligible members each evaluation', not '5 new offers each run'. Ties between members with the same number of criteria keep database order. The eligibility criteria are in Part I, Care-management eligibility.

In the demo1 offer tasks in the Team view

app/services/task_rules.py · _r_enrollment_offer()

Owner of a rule-raised task

Owner column

Formula

continuity first: the owner of the member's most recently created open, owned task. Otherwise, the holder of the rule's target role with the fewest open owned tasks (ties by user id). Otherwise, no owner (UNASSIGNED).

Why this wayA member with three care managers effectively has none, so new work joins whoever already has the member. Continuity overrides the rule's target role: an authorization task meant for a UM nurse can go to the member's care manager. Tasks raised by a provider's response (review, dispute, more information) are assigned without a caseload count, so unless continuity applies they go to the lowest-id holder of the role, not the least-loaded one.

In the demoJane Smith

Check it yourselfreassign from the Owner drop-down; the next rule-raised task for that member goes to the new owner

app/services/task_rules.py · _assign()app/services/task_rules.py · _caseload()

My Panel

22 values

The Care Manager's first screen: every member who currently needs attention, ranked by a points score that explains itself, with the next best action and what is already running. Below it, a follow-through panel counts what happened to the interventions the signed-in care manager owns.

My Panel (N) — members needing attention

Count badge beside the My Panel title

Formula

number of members whose panel priority score is > 0, counted BEFORE the top-25 cut. For a care manager: every member in the tenant. For a provider login: only the provider's attributed members (scope.visible_ids).

Why this wayCounted before the cut so the badge says how many members need attention, not how many rows fit on the screen: the table shows at most 25 of them. The list is the whole visible scope by design (A-099): unassigned members stay visible to somebody. A MINE badge marks the members the signed-in user owns an open task or a live intervention on, and the Mine button asks the server for those alone, filtered before the top-25 cut; the badge count then counts them. A care manager therefore sees the whole tenant's attention list under All; the Dashboard's Today's priorities shows the same computation with 8 rows and the same total ('of N needing attention').

In the demo12 members

Check it yourselfCount the rows: with fewer than 25 members needing attention the table has exactly this many rows. Open Dashboard: Today's priorities says 'of N needing attention' with the same N.

app/services/panel.py · panel()app/api/population.py · my_panel()app/core/scope.py · visible_ids()

N of them have no task

Under the My Panel title; the same number is repeated in the bold note under the table ('… N of them right now')

Formula

number of members with score > 0 that have no WorkTask with status open, whoever owns the task. Counted over all members needing attention, not only the 25 rows shown.

Why this wayThis is the reason the panel exists: the task queue can only rank work somebody created, so a member who needs attention but has no task is invisible there. Any open task counts, including one owned by another care manager, so the number answers 'has nobody raised anything', not 'have I raised anything'. When more than 25 members need attention, N can be larger than the NO TASK badges visible in the table.

In the demo2 members

Check it yourselfCount the amber NO TASK badges in the Member column; with 25 or fewer rows they equal N.

app/services/panel.py · panel()

NO TASK badge

Member column, under the member's risk tier

Formula

shown when the member has no WorkTask in status open (has_task is false). Owner and task subject are not considered.

Why this wayThe badge is a presence test, not an ownership test: a member whose only open task belongs to someone else shows no badge, and a member whose tasks are all closed shows the badge even when an intervention is running (Derrick Boateng in the demo: an intervention at AUTH_APPROVED, no open task).

In the demoJon Doe, Derrick Boateng

Check it yourselfOpen the member's 360 and look at open tasks: a badged member has none.

app/services/panel.py · panel()app/static/app.js · rPanel()

Risk tier (e.g. 'HIGH risk')

Member column, under the name

Formula

the member's stored Patient.risk_tier: HIGH when the risk score is >= 60, RISING >= 35, MEDIUM >= 15, else LOW. See Part I, Population health.

Why this wayThe panel reads the tier stored on the member; it does not recompute it. The stored tier is refreshed only when risk is recomputed, and a user with record-edit rights can also set it by hand: a member created by hand carries the model version 'manual entry', and an edit changes the tier without changing the version. Until the next recompute a hand-set tier can differ from the model's latest score, which is what the risk header on Member 360 shows. The tier is worth 30 / 20 / 8 / 0 points in the priority score.

In the demoHIGH (John Doe)

Check it yourselfOpen the member's 360: the risk header shows the model's tier, which matches unless the tier was set by hand since the last recompute.

app/services/risk.py · score_patient()app/services/risk.py · rescore()

Priority

Priority column, bold number

Formula

sum of the points in the one weight table, panel.PANEL_WEIGHTS (Part I's My Panel table is rendered from it): risk tier HIGH 30 / RISING 20 / MEDIUM 8; 8 per open gap with no live intervention; 2 per gap already being worked; 25 while the post-discharge follow-up is owed (the member's newest discharge, the tenant's visit window, 7 days by default, open, no follow-up visit attended), plus 15 more once the contact window (48 hours by default) has passed (days 3–7 by default); 10 if any active medication has a measured PDC below 0.80; 12 if a prior authorization is pending (SUBMITTED, IN_REVIEW or PENDED); 14 if an intervention needs a different approach (at IDENTIFIED or OUTREACH with no event for the tenant's engagement.stalled wait, 21 days by default, or ended UNREACHABLE or DECLINED); 18 if the member has an open gap and no active coverage today; 10 if the member is HIGH risk and enrolled in no care-management programme. Members scoring 0 are not listed. Computed at request time, never stored. See Part I, My Panel.

Why this wayA points table rather than a model, so a care manager can argue with each weight, and the table the paper prints is the table the scorer reads, so the two cannot drift. Each signal reads the fact itself: 'in no programme' is the member's enrolment, not which recommendation ranked first (in the demo Maria Gonzales, HIGH risk and not enrolled, scores 80 although her top recommendation is a Critical gap); a PDC measured at 0% is below the threshold and an unmeasured one flags nothing. Adherence, authorization and stalled outreach each count once per member however many medications, authorizations or interventions qualify. This is a different scale from the risk score (Part I's worked example: John Doe 85 on risk, 80 here) and from the My Work task score.

In the demo80 points (John Doe)

Check it yourselfOpen 'all factors' on the row: the + numbers add up to the Priority.

app/services/panel.py · panel()app/services/transitions.py · recent_discharges()app/services/nba.py · recommend()

N factor(s)

Priority column, under the score

Formula

number of entries in the member's component list (client-side components.length). One per signal that fired, at most 10.

Why this wayIt counts signals, not problems: five unworked gaps are ONE factor worth 40 points. A high count means the attention comes from several independent directions; it says nothing about size.

In the demo3 factors (John Doe)

Check it yourselfOpen 'all factors': it lists exactly this many lines.

app/static/app.js · rPanel()app/services/panel.py · panel()

Why (the sentence)

Why column, first line

Formula

the why text of the single highest-points component. Ties keep a fixed order: risk, unworked gaps, gaps in flight, post-discharge, contact overdue, adherence, authorization, stalled outreach, no coverage, no programme.

Why this wayOne sentence, so the reason for the rank is readable at a glance; the rest is one click away. The sentence carries computed values: the unworked-gap sentence counts all unworked gaps but names only the first three; the adherence sentence names the lowest-PDC medication with its percentage rounded, the same figure Member 360 prints (attention.pdc_pct); the authorization sentence names the pending authorization due soonest, the same one the Due column and the auth line name; a stalled intervention reads 'reached out, nothing back' or 'member declined', whichever it is.

In the demo5 open gap(s) with nothing running: HbA1c Control — Glycemic Assessment, Diabetic Retinal Exam, Statin Adherence (PDC >= 80%)

Check it yourselfOpen 'all factors': the Why sentence is the first (largest) line.

app/services/panel.py · panel()

all factors: '+points factor — why'

Why column, expandable 'all factors' (shown when a member has more than one factor)

Formula

every component that fired, sorted by points, largest first, each printed as +points factor — why.

Why this wayThe score ships with its own explanation, so a member's position can be checked rather than trusted. The factor names are the internal keys (unworked_gaps, post_discharge, …), not translated labels.

In the demo+40 unworked_gaps, +30 risk, +10 adherence (John Doe)

Check it yourselfThe + numbers sum to the Priority in the same row.

app/services/panel.py · panel()app/static/app.js · rPanel()

Row order (the ranking)

Table rows, top to bottom

Formula

sort by Priority, highest first; ties by member name A–Z; show the first 25.

Why this wayA name tie-break keeps the order stable across reloads. The order moves by itself as clocks move (a discharge window opening, an authorization going pending) because nothing is stored. The Dashboard's Today's priorities is the first 8 rows of the same ranking, and a cohort's member list is ranked by the same engine restricted to the cohort, so the three cannot disagree about a member.

In the demoJohn Doe 80, Maria Gonzales 80, Linda Tran 62

Check it yourselfOpen Dashboard: Today's priorities lists the same members in the same order.

app/services/panel.py · panel()

Next best action (e.g. 'Start intervention')

Next best action column, bold line ('nothing recommended' when empty)

Formula

the member's first recommendation in the engine's order (band Critical > High > Medium, then higher score, then name), from one tenant-wide pass capped at 500 recommendations. Labels: 'Start intervention', 'Track authorization <ref>', 'Try an alternate channel', 'Offer care-management enrollment'. See Part I, Next best action.

Why this wayOne pass for the whole tenant keeps the screen to one read of each table. Deferred or dismissed recommendations are left out. 'Nothing recommended' means every gap is already in hand (for example an intervention running) and the member is not a high-risk unenrolled member. The band that decided the order is not shown on this row.

In the demoStart intervention (John Doe)

Check it yourselfOpen the member's 360: its next-best-action list (same engine, same order) starts with the same action.

app/services/nba.py · recommend()app/services/panel.py · panel()

Reason under the action

Next best action column, grey line

Formula

the first 3 reason texts of that recommendation joined with ; . For an enrollment offer, the fixed sentence 'high-risk member is not enrolled in any care-management program'.

Why this wayThree reasons keep the row short; the full reasoning is on the member's 360. The Dashboard shows the same text cut to 90 characters.

In the demoOpen quality gap — HbA1c Control; Overdue 97 days; Critical priority on the measure package

Check it yourselfOpen the member's 360: the first recommendation's reasons begin with the same three.

app/services/nba.py · recommend()

Status: interventions in flight ('nothing running' when none)

Status column, one status badge per intervention

Formula

every live intervention on the member, whoever owns it: status not CANCELLED, COMPLETED, GAP_CLOSED, DECLINED or UNREACHABLE (models.iv_works_gap, the same as iv_is_live).

Why this way'In flight' means somebody still has work to do on it. The same test decides whether a gap counts as 'already being worked' (2 points) or 'unworked' (8 points), and Cost Concentration's '(N unworked)' uses it too, so the two screens agree: a gap whose only intervention was declined, went unreachable, or completed without the gap closing is unworked on both.

In the demoDiabetic retinal exam AUTH_APPROVED (Derrick Boateng)

Check it yourselfOpen the member's 360, Care tab: the same interventions are listed as open.

app/services/panel.py · panel()

Status: 'auth <ref> · <status>'

Status column, grey line

Formula

the member's pending prior authorization (status SUBMITTED, IN_REVIEW or PENDED) whose decision is due soonest; ties on the reference.

Why this wayShown because a pending authorization blocks scheduling. It is the same authorization the Due column and the 'authorization on the clock' reason name, because all three take the first of models.pending_auths. Pending is read from the status: a pended request stays pending, and a DRAFT (nothing sent to the payer) is not pending, here or on the Start button. A pending authorization not linked to the gap shows here while Start is still offered, because only an authorization on the gap's own intervention blocks booking it. See Part I, Prior-authorization decision clock.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfSubmit a prior authorization for a member on the panel; after a reload the line appears and Priority rises by 12.

app/services/panel.py · panel()

Status: 'appt <date>'

Status column, grey line

Formula

the earliest appointment with status BOOKED or CONFIRMED whose business-time date is today or later; the screen prints that business-time date (next_appointment.date).

Why this wayOnly live bookings count, so a cancelled or no-show appointment never reads as care arranged. The filter and the printed date use the same clock, US business time (core/clock), so an appointment in the US evening, after midnight UTC, prints the day the member is expected. The server sends the time already in business time (business_iso), and the page slices no UTC timestamp.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfBook an appointment for a panel member from the 360 Care tab; after a reload its date appears here.

app/services/panel.py · _next_appt()app/static/app.js · rPanel()

Status: 'N closed'

Status column, last line

Formula

the member's care gaps whose current status is CLOSED, all time, with no date window.

Why this wayIt tells a care manager what has already been achieved for this member. Gaps retired as not applicable or excluded are not counted, and a closed gap that was later reopened drops out. It counts the member's gaps, whoever closed them; it is not the same number as the follow-through 'Gaps closed' below, which counts the signed-in user's interventions.

In the demo0 closed (John Doe)

Check it yourselfOpen the member's 360, Care tab: count the gaps shown as closed.

app/services/panel.py · panel()

Status: 'last contact <date>' / 'never contacted'

Status column, last line

Formula

the business-time date of the member's most recent engagement record, of any channel, direction or delivery status; 'never contacted' when the member has none.

Why this wayAny engagement counts, so an inbound message or a failed send updates it. 'Never contacted' means no engagement is recorded in the platform, not that nobody tried by another route.

In the demonever contacted

Check it yourselfRecord an engagement (a call or a message) for the member; after a reload this line shows today's date.

app/services/panel.py · _last_contact()

Start button (shown or not)

Last column, beside Open 360

Formula

shown when all three hold: the signed-in user may write (vbc.write); the recommendation is actionable (no undecided authorization linked to the gap's intervention, of any status including DRAFT); and the recommendation is for a gap.

Why this wayThe screen only offers what the server would accept. An enrollment offer is actionable but is not about a gap, so it never gets a Start button. Starting still runs the authorization-requirement check on the server first.

In the demo10 rows show Start (care manager)

Check it yourselfRows showing 'nothing recommended' or an enrollment offer have no Start button.

app/static/app.js · rPanel()app/services/nba.py · recommend()

Interventions started

Did what I started land? — first card

Formula

count of interventions owned by the signed-in user, in any status (including CANCELLED), all time.

Why this wayThe denominator of the follow-through funnel. It is ownership, not authorship: a reassigned intervention moves to its new owner's count. There is no date window: every count on this panel is all time, and the panel's basis line says so.

In the demo0 interventions

Check it yourselfClick Start on a panel row; after a reload this card goes up by one.

app/services/panel.py · follow_through()

Member reached

Did what I started land? — second card

Formula

owned interventions at or past ENGAGED in the one funnel order (models.iv_reached): ENGAGED, AWAITING_AUTH, AUTH_APPROVED, SCHEDULED, IN_PROGRESS, COMPLETED, GAP_CLOSED and REOPENED, all time.

Why this way'Reached' means the member responded, so an intervention that only sent a message does not count, and a stopped one (UNREACHABLE, DECLINED, CANCELLED) never does. It always includes 'Service completed'. The Dashboard's engagement funnel 'Reached' reads the same order, so the two agree, including on a REOPENED intervention.

In the demo0 interventions

Check it yourselfMove one of your interventions to ENGAGED; this card goes up by one.

app/services/panel.py · follow_through()

Service completed

Did what I started land? — third card

Formula

owned interventions whose status is COMPLETED or GAP_CLOSED, all time.

Why this wayThe service happened. Started ≥ Reached ≥ Completed always holds, so the three cards read as a funnel.

In the demo0 interventions

app/services/panel.py · follow_through()

Gaps closed

Did what I started land? — fourth card

Formula

owned interventions whose linked care gap's CURRENT status is CLOSED, all time.

Why this wayIt counts interventions, not distinct gaps. A gap closes on clinical evidence that can arrive for reasons unrelated to the outreach, which the caveat under the panel says in bold: read it as what happened to the work, not what the work caused. A gap that later reopened drops out. Different from the per-member 'N closed' in the table and from a quality measure's numerator.

In the demo0 interventions

app/services/panel.py · follow_through()

N stalled

Note under the follow-through cards (shown only when N > 0), followed by each intervention's name and status

Formula

owned interventions that need a different approach: an intervention at IDENTIFIED or OUTREACH whose last event is at least the tenant's engagement.stalled wait old (stalled_days, 21 by default), or one that ended DECLINED or UNREACHABLE (models.iv_follow_up_why).

Why this wayThese need a different channel or a caregiver contact, and nothing else surfaces them. An outreach counts only once it has been quiet for the tenant's wait, the same wait the rule 'Outreach with no movement' uses. DECLINED and UNREACHABLE are included and named as such, not described as silence. The Dashboard's 'Outreach needing follow-up' uses the same test for the whole tenant; this note counts only your own.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfIn the fresh demo tenant the care manager owns no interventions, so the note is absent. Start an intervention and move it to OUTREACH: the note appears with N = 1.

app/services/panel.py · follow_through()app/static/app.js · rPanel()

Worklists

19 values

The lists behind the Command Center cards and priorities (open care gaps, care plans, interventions in flight, referrals, vendors, vendor performance, open assessments). A VBC manager, executive or administrator opens them by clicking a card; each is a list you can act on, not a report.

Open care gaps (list count)

Open care gaps worklist, header count

Formula

count of care-gap rows whose status is not closed closed = CLOSED, EXCLUDED, NOT_APPLICABLE, GAP_CLOSED, RESOLVED, DUPLICATE

Why this wayThis is the list the Command Center's 'Open care gaps' card opens, and it uses the same rule (gap_is_open), so the card and the list agree. A gap being worked (in progress, scheduled, awaiting authorization) is still open: a gap closes on evidence, not on effort. The list shows at most 200 rows; the count is the full total.

In the demo29 open gaps

Check it yourselfOpen the Command Center, note 'Open care gaps', click it: the header count here is the same number.

app/services/worklists.py · care_gaps()app/services/workflow.py · gap_is_open()

Priority (per gap)

Open care gaps worklist, Priority column

Formula

looked up from the member's risk tier and the measure's domain: clinical/chronic measure: HIGH → Critical · RISING → High · MEDIUM → Medium · LOW → Medium any other measure: HIGH → High · RISING → Medium · MEDIUM → Medium · LOW → Low a member with no tier counts as MEDIUM

Why this wayThe same missed measure matters more on a high-risk member with a chronic condition, so priority is a lookup rather than a score: two inputs, sixteen fixed answers, nothing hidden. It is re-derived every time the measure is evaluated, so it follows the member's tier. The list is sorted Critical first, then by member name. This priority feeds both ranking engines (Part I, Work-queue ranking and Next best action).

In the demoCritical 6, High 10, Medium 12, Low 1

Check it yourselfOpen a Critical row's member: their risk tier is HIGH and the measure is a clinical one.

app/services/quality.py · gap_priority()app/services/worklists.py · care_gaps()

Why open

Open care gaps worklist, last column

Formula

the measure engine's own reason the member is not in the numerator, stored when the gap is created and refreshed on every evaluation: 'no qualifying result in the last N days' · 'latest <test> <value> on <date> is at or above the <threshold> threshold' (a '<' measure; 'is above' for a '<=' one) · '<drug class> adherence NN%, below the 80% threshold' (NN rounded, the same figure every screen prints) · '<drug> adherence not measured' · 'no qualifying procedure in the last N days'

Why this wayThe sentence is not written for display; it is the evidence the numerator test produced, so it cannot say something the engine did not decide. See Part I, One member, one measure.

In the demolatest Systolic blood pressure 148.0mmHg on 2026-09-12 is at or above the 140 threshold

Check it yourselfOpen the member and the gap: the lab or procedure history shows exactly what the sentence says.

app/services/quality.py · meets_numerator()app/services/quality.py · evaluate_patient()

Active care plans (list count)

Active care plans worklist, header count

Formula

count of members with an ACTIVE care plan

Why this wayOne row per active plan. Rows are sorted with overdue reviews first, then by member name, so the plans that need a person come to the top.

In the demo1 plans

app/services/worklists.py · care_plans()

Goals and Met (per plan)

Active care plans worklist, Goals and Met columns

Formula

Goals = number of goals on the member's active plan Met = goals whose DERIVED status is MET: a goal linked to a care gap is met when that gap is CLOSED or RESOLVED a goal linked to a vendor referral is met when the referral is CONFIRMED a goal with no linked work is met only when a person marked it met

Why this wayA goal's status is derived from the work beneath it, so nobody can type 'met' over an open gap; only a goal with nothing linked relies on a person's word. The cost: a goal whose gap closed for an unrelated reason also reads as met.

In the demoJohn Doe: 8 goals, 0 met

Check it yourselfOpen the member's Care plan tab: each goal's status and the gap or referral it is linked to are shown side by side.

app/services/worklists.py · care_plans()app/services/careplan.py · _goal_status()

Next review / OVERDUE

Active care plans worklist, Next review column

Formula

OVERDUE when the plan's next review date is before today (today = the US business date, America/New_York)

Why this wayA review date in the past is a commitment already missed, so it is flagged rather than merely shown. Dates are judged on the US business calendar, not the server's clock, so the flag does not flip at the wrong midnight.

In the demo0 overdue reviews

app/services/worklists.py · care_plans()app/core/clock.py · business_today()

Interventions in flight (list count)

Interventions in flight worklist, header count

Formula

count of interventions whose status is not CANCELLED, COMPLETED, GAP_CLOSED, DECLINED or UNREACHABLE

Why this wayIn flight means somebody still has something to do. This is the same rule as the Command Center's 'Active interventions' card, which opens this list, so they agree. The care manager's Dashboard card of the same name excludes only GAP_CLOSED, CANCELLED and COMPLETED, so a declined or unreachable intervention counts there and not here.

In the demo1 interventions

app/services/worklists.py · interventions()

Stage and row order

Interventions in flight worklist, Stage column

Formula

Stage = the intervention's current state in its state machine (no arithmetic) row order: IDENTIFIED, OUTREACH, ENGAGED, SCHEDULED, then every other state, then member name

Why this wayEarliest stage first, because an intervention nobody has contacted yet is the one most likely to stall.

In the demoAUTH_APPROVED 1

app/services/worklists.py · interventions()

Last movement

Interventions in flight worklist, last column

Formula

the detail of the most recently APPENDED entry in the intervention's event trail; '—' when there is none

Why this wayIt reads the trail in the order entries were written, which is the order things happened, rather than re-sorting by timestamp.

In the demoauthorization approved; ready to schedule

app/services/worklists.py · interventions()

Referrals (list count)

Referrals worklist, header count

Formula

social (SDOH) referrals + vendor referrals + internal team referrals, one row each

Why this wayAll three kinds of referral in one list, because to the member they are all 'somebody was asked to help'. Sorted overdue first, then by due date.

In the demo3 referrals

app/services/worklists.py · referrals()

Due / OVERDUE (per referral)

Referrals worklist, Due column

Formula

social referral: due = created + 14 days (follow-up) vendor referral: due = referred + the vendor's contracted SLA days (7 when none) until it is SCHEDULED; SCHEDULED with an appointment date → the appointment (A-058) internal referral: due = created + the receiving queue's SLA (3 days unless the tenant sets one) OVERDUE = due before today AND the referral has not settled

Why this wayEach kind keeps its own clock because each is a different promise: a follow-up call, a contract, an internal service level. A referral that has settled is never overdue, however late it settled. See Part I, Referral SLA.

In the demo0 overdue now

Check it yourselfThe header's 'N overdue' chip is the count of red OVERDUE badges in the list.

app/services/worklists.py · referrals()app/services/referrals.py · sla_days()

Closed loop %

Referrals worklist, header badge

Formula

round( 100 × closed ÷ settled ) settled = referrals in a final state (any kind) closed = social RESOLVED + vendor CONFIRMED + internal OUTCOME_CONFIRMED none settled → no percentage, with the reason

Why this wayThe denominator is settled referrals, not all referrals, so a referral still in progress neither helps nor hurts the rate. Closed means the outcome was confirmed by the plan, not claimed by the vendor. Green at 80% or above. For a login limited to a panel, the list, the overdue count and this percentage are all computed over the panel's referrals; settled is judged per referral kind.

In the demo100 %

Check it yourselfIf nothing has settled yet the badge reads 'closed loop: n/a' and says why.

app/services/worklists.py · referrals()

Contracted vendors, SLA, open referrals

Contracted vendors worklist

Formula

vendors = every vendor on file (active or not) SLA = the vendor's contracted days (stored, not computed) open referrals = that vendor's referrals not in CONFIRMED, DECLINED, FAILED, CANCELLED

Why this wayA vendor is a contracted party, so its SLA is a contract term, typed once and read everywhere. Open referrals is everything that has not reached a final state.

In the demo5 vendors; SLA days 3, 5, 14, 7, 5; 2 open referrals

app/services/worklists.py · vendors()app/services/vendors.py · ensure_vendors()

Referrals and Confirmed (per vendor)

Vendor performance worklist

Formula

Referrals = every referral ever sent to the vendor Confirmed = those the plan confirmed as delivered (CONFIRMED)

Why this wayConfirmed, not delivered: 'delivered' is the vendor's claim, 'confirmed' is the plan's evidence.

In the demoMedRide NEMT 0/1, Nourish Home Meals 1/1, HouseCall Health Assessments 0/1

app/services/vendors.py · performance()

Closed loop (per vendor)

Vendor performance worklist

Formula

round( 100 × CONFIRMED ÷ settled ), settled = CONFIRMED + DECLINED + FAILED + CANCELLED no settled referrals → 'n/a' with the reason

Why this waySame denominator rule as the referrals list: only finished referrals are judged.

In the demoNourish Home Meals 100%

app/services/vendors.py · performance()

Median days (per vendor)

Vendor performance worklist

Formula

the days from referral to DELIVERED for each referral that has both dates; value = their median (app/core/stats.median: the mean of the two middle values for an even count, so it can end in .5)

Why this wayIt measures the vendor's own turnaround (to delivery), not the plan's confirmation lag. A median, the same function every median on every screen uses, so one slow ride does not define the vendor; for an even count it is the average of the two middle values, not either one.

In the demoNourish Home Meals 4 d

app/services/vendors.py · performance()app/core/stats.py · median()

SLA breaches (per vendor) and row order

Vendor performance worklist, last column

Formula

referrals past the date they are held to (the SLA until SCHEDULED, then the appointment; A-058) that have NOT been delivered or settled rows sorted by breaches (most first), then by referral volume

Why this wayIt counts breaches that are still live, the ones a person can still chase; a referral that was late but has since been confirmed drops out. It is therefore a current-risk count, not a historical breach rate.

In the demo0 live breaches, all vendors

app/services/vendors.py · performance()app/services/vendors.py · is_overdue()

Open assessments (list count) and OVERDUE badge

Assessments worklist, header count and Due column

Formula

count of Assessment rows of any kind whose status is not COMPLETED, REFUSED or UNREACHABLE OVERDUE = due date before today (US business date) rows sorted by due date, soonest first

Why this wayThe list the Command Center's 'Health risk assessments overdue' row opens. It lists assessment ROWS of every kind (an HRA or a reassessment), because each is a piece of work somebody can do; the Command Center row counts MEMBERS by their governing HRA. So the header counts open assessments, not overdue members, and the two numbers are not meant to match. The list shows at most 200 rows; the count is the full total.

In the demo8 open, 2 overdue

Check it yourselfCount the rows; the header shows the same number, and the OVERDUE badges sit on the rows whose due date has passed.

app/services/worklists.py · assessments()app/static/app.js · rWlAssess()

HRA completion headline ('HRA completion P% of D enrolled · N overdue · R refused · U unreachable')

Assessments worklist, beside the title

Formula

for every ENROLLED member: the HRA governing the current period (the latest HRA whose due date has arrived, else the earliest) P = round( 100 × members whose governing HRA was completed on or before its due date ÷ D ), D = enrolled members N = governing HRA not settled and past due · R = governing HRA refused · U = governing HRA unreachable

Why this wayThe HRA completion KPI, over the denominator the plan does not choose: every enrolled member, so an HRA nobody attempted counts as much as one the member refused, and one completed late is not a completion. N is the number on the Command Center's overdue-HRA row, and P is the rate the operational board's 'HRA completion' shows. Refused and unreachable are reported apart because they call for different responses.

In the demo62% of 8 enrolled; 2 overdue, 1 refused, 0 unreachable

Check it yourselfOpen the Command Center: its 'Health risk assessments overdue' row names the same N and P.

app/services/assessments.py · kpis()app/services/assessments.py · hra_standing()app/static/app.js · rWlAssess()

Patients list

3 values

The member roster: every staff role with patient read access (Care Manager, VBC Manager, Executive, PA Coordinator, Auditor, Administrator) sees every member of the tenant here; a Provider sees the same screen as "My Panel" in the sidebar, narrowed to the members attributed to them. It is a lookup list, not a work queue: it counts and classifies, it does not rank.

Patients (N)

Count badge beside the Patients title

Formula

number of rows returned by GET /vbc/patients: every Patient record in the tenant, then narrowed by scope.filter_patient_rows (a no-op for staff roles, the attributed panel for a provider). Counted in the browser as rows.length; the search box hides rows but does not change N.

Why this wayEvery member record, not only attributed members and not de-duplicated: the demo's unmerged duplicate "Jon Doe" (same date of birth and MRN as John Doe, arriving from the EHR feed) is counted as a second member until someone merges it. No row is dropped for missing coverage or a closed attribution period. The Dashboard's "Patients" card counts the same records and agrees for every staff role; it agrees for a provider too: both the Dashboard card and this list count the provider's panel. Rows are listed in the order the database returns them. The query sets no order, so the list is not ranked by risk or by anything else.

In the demo13 members

Check it yourselfCount the table rows, or download the CSV with the search box empty: same number. As Care Manager, Dashboard > Population > "Patients" shows the same N.

app/api/vbc.py · list_patients()app/core/scope.py · filter_patient_rows()app/static/app.js · rPatients()

My Panel (N), provider sign-in

The same count badge, when a provider opens "My Panel" in the sidebar (the page header still reads "Patients")

Formula

members with a live PatientAttribution row (no end date, or one after today) where provider_id = the signed-in provider, in the CURRENT period (open and containing today). A provider login with no provider binding, or no current period, sees an empty list (never the whole tenant).

Why this wayAttribution is by provider identity, not by the free-text PCP name, so the PCP column on this screen does not decide who is on the panel. In the demo every one of Dr. P. Osei's five members shows a different declared PCP (Dr. Sarah Smith, Dr. Robert Jones or Dr. Karen White); the demo seed moves these five attribution rows to Dr. Osei after they were created from the declared PCP. Those rows still read method declared_pcp and "Declared PCP on record: Dr. …", which names a different physician. That is a known inconsistency in the demo data. A member outside the panel is not merely hidden: opening them by link answers not-found rather than forbidden, so a provider cannot learn the member exists. Two things share the name here: the Care Manager's "My Panel" is a different screen (members ranked by a priority score, see the My Panel chapter) and counts something else.

In the demo5 (John Doe, Sarah Martinez, Linda Tran, Ahmed Ali, Rose Chen) members

Check it yourselfSign in as doc@demo.provider: the badge shows the number above and exactly those names. Sign in as the Care Manager: the same screen lists the whole tenant (see "Patients (N)").

app/core/scope.py · visible_ids()app/core/scope.py · _provider_panel()app/api/vbc.py · list_patients()

Risk tier badge (HIGH / RISING / MEDIUM / LOW)

Risk column, one badge per member (column label and visibility are configurable in Administration)

Formula

the stored Patient.risk_tier, printed as-is. When the risk model last scored the member: HIGH if score >= 60, RISING >= 35, MEDIUM >= 15, else LOW. See Part I, Population health. Colour: HIGH red, RISING amber, MEDIUM blue, LOW green.

Why this wayThe list reads the tier stored on the member; it does not rescore. The stored tier is refreshed in only two places: the nightly demo reset, and the recompute call POST /population/risk/recompute (VBC Manager permission). No screen offers that call. New labs, conditions or admissions do not rescore the member on arrival, so the badge can lag the clinical record. A user with record-edit rights can also overwrite the tier by hand. A hand edit does not change the model name stored on the member, so a hand-set tier still carries the model's name. A member no model has scored shows a badge too: the column defaults to LOW. In the demo, "Jon Doe" (the unmerged duplicate) shows a green LOW with no score and no factors behind it. His Member 360 says "No risk model has scored this member" and the list does not. The Dashboard's Risk distribution counts that unscored LOW as a Low member.

In the demoJohn Doe HIGH, Sarah Martinez MEDIUM, Maria Gonzales HIGH, Sam Okafor RISING, Linda Tran RISING, Ahmed Ali RISING, Rose Chen MEDIUM, Derrick Boateng MEDIUM, Elena Petrova MEDIUM, Robert Kim MEDIUM, Fatima Hassan RISING, Grace Nwosu LOW, Jon Doe LOW (never scored)

Check it yourselfOpen a member: the Member 360 risk header shows the score and the factor list, and the tier matches the badge (for a scored member the factors add up to a score inside the band). For Jon Doe the 360 says no model has scored him.

app/services/risk.py · score_patient()app/services/risk.py · rescore()app/api/records.py · update_patient()app/api/vbc.py · list_patients()app/static/app.js · statusBadge()

Patient 360: header, gaps, interventions and authorizations

14 values

The care manager's record of one member (VBC managers, executives, PA coordinators, providers for their own panel and auditors can open it too). This chapter covers the header, the Overview's service cases, partner activity and recent activity, the care gaps and interventions on the Care tab, the transition badge on the Utilization tab, and coverage and authorizations on the Coverage tab. Figures are shown as the care manager Jane Smith sees them.

Age ('John Doe | 65')

Header, after the member's name

Formula

whole years from the date of birth to today's business date (America/New_York): year difference, minus 1 if this year's birthday has not happened yet

Why this wayWorked out by the server each time the record is read, never stored, because a stored age is wrong on the member's next birthday. Known defect: when the date of birth is missing or unreadable, the age function returns -1 rather than no value, so the header prints '| -1' where it should leave the age out.

In the demo65 years

Check it yourselfThe date of birth is printed two lines below the name. Subtract it from today's date.

app/api/vbc.py · get_patient()app/services/quality.py · _age_on()

Sex, plan line and 'not recorded: …'

Header, identity lines

Formula

sex: stored value male / female / other (any case) shown as Male / Female / Other; anything else is not shown plan: payer + plan of the member's current coverage (services/coverage.current_coverage: status active and today within its dates); none → 'no active coverage on file' not recorded: names each of sex, PCP and active coverage that is empty

Why this wayThe header names a missing fact instead of leaving a blank, because a blank looks like a display fault. The plan line is the same coverage the summary strip's Coverage card and the Coverage tab's line name: one function answers which coverage is current.

In the demoJohn Doe: Male | UnitedHealthcare MA Choice; not recorded: nothing, Jon Doe: (sex not shown) | no active coverage on file; not recorded: sex, active coverage

Check it yourselfOpen Jon Doe (the unmerged duplicate): no sex is shown, the plan line reads 'no active coverage on file' and the header lists both under 'not recorded'.

app/api/vbc.py · get_patient()

Header risk badge (HIGH / RISING / MEDIUM / LOW)

Header, after the name

Formula

the tier stored on the member (Patient.risk_tier), the same field as the Patients list badge. When the risk model sets it: HIGH ≥ 60 · RISING ≥ 35 · MEDIUM ≥ 15 · LOW

Why this waySee Part I, Clinical risk score and tier, and the Patients list row on the risk tier. Known inconsistency on this screen: the Primary risk drivers panel on the Overview prints the tier of the latest scoring run, not this stored field. Editing the tier by hand (Edit patient) changes this badge and not the panel, so the two can show different tiers for one member. They agree in the fresh demo tenant.

In the demoheader HIGH; risk drivers panel HIGH

Check it yourselfCompare the header badge with the badge on the Primary risk drivers panel on the Overview tab.

app/api/vbc.py · get_patient()app/services/member360.py · risk_drivers()app/api/records.py · update_patient()

Service cases: 'N open', and each case's Open / Closed label and colour

Overview tab, Service cases panel (shown only when the member has a case)

Formula

N = the member's mirrored service cases with no closed date each case: 'Open' when it has no closed date, else 'Closed' colour: grey when closed · amber when the status text contains 'progress', 'open' or 'new' · blue otherwise

Why this waySalesforce is the system of record for cases; this panel is a copy, so a care manager does not work the member while a complaint is open without knowing about it. Open or closed depends on the closed date Salesforce sent, not on its status text, and the status is shown verbatim. A case sent with a closed-family status ('Closed', 'Resolved') but no closed date still reads 'Open', and since 2026-09-25 (A-048) it carries a red flag 'status says Closed but no closed date was sent' rather than being silently remapped. The count is only as current as the last sync (next row).

In the demo1 open: Transportation complaint (In Progress)

Check it yourselfEach case card below the count says Open or Closed; count the Open ones.

app/api/population.py · member_cases()app/static/app.js · casesPanel()

Service cases: 'as of <date time>'

Overview tab, Service cases panel header

Formula

the latest sync time across the member's cases, in business time with the zone named (clock.business_stamp: 'YYYY-MM-DD HH:MM EDT')

Why this wayA copied status without the time of the last copy invites a stale answer to be read as current. Since 2026-09-25 (A-048) the time is shown in business time with its zone label; before that it was the UTC time with no zone, so after 8 pm Eastern (daylight time) it already showed the next day's date.

In the demomoves with the clock — read it on screen

Check it yourselfIn the demo it is the time of the nightly reset, in US Eastern time, with the zone shown.

app/api/population.py · member_cases()

Partner Activity: status badge and its colour

Overview tab, Partner Activity table, Status column

Formula

label: each referral state printed from one table (DELIVERED 'Vendor reports it completed', CONFIRMED 'Completion confirmed by the plan, with evidence', …) overdue: the due date is before today (vendors.due_on: the SLA due date until the referral is SCHEDULED, then its appointment date; nothing once DELIVERED or settled = CONFIRMED, DECLINED, FAILED, CANCELLED) SLA due date = referral date + the vendor's contracted SLA days (7 when the vendor has none) colour: red if overdue, FAILED or DECLINED · green if CONFIRMED · grey if otherwise settled (CANCELLED) · blue otherwise rows: newest last activity first, by full timestamp

Why this wayDelivered is the vendor's claim and confirmed is the plan's evidence, so only confirmed turns green. The SLA clock starts at the referral, not when the vendor acknowledges it, so a slow acknowledgement cannot move the deadline. Part I, Referral SLA covers team referrals; vendor referrals use the vendor's own contracted days instead. A vendor with an SLA of 0 days is treated as 7.

In the demoMedRide NEMT: Service scheduled (blue), Nourish Home Meals: Completion confirmed by the plan, with evidence (green), HouseCall Health Assessments: Vendor acknowledged (blue)

Check it yourselfClick 'Closed-loop detail': the Referrals tab's Vendor referrals table shows the same referrals, with the SLA date in red when overdue.

app/services/vendors.py · member_view()app/services/vendors.py · is_overdue()app/static/app.js · p360Tab()

Most recent activity (six entries)

Overview tab, Most recent activity panel

Formula

every dated event on the member across all areas (tasks, referrals, gaps, authorizations, encounters, outreach, documents, …), sorted by the moment it happened, newest first; the first 6. Each is dated on the business day (US Eastern); an event with only a date counts as 00:00 that day

Why this wayOne list across all areas, so the order of events is visible. Events on the same day are ordered by their time, and events at the same instant keep the order they were read in; the kind of event never decides the order. Until 2026-09-25 (A-047) the time of day was dropped and same-day events were ordered by the kind's name (tasks ahead of gaps), and an event after 8 pm Eastern was dated the next day.

In the demo2026-10-12 Care plan — active, 2026-10-12 Task raised — Review result with member, 2026-10-12 Outreach planned — Food insecurity recorded on 2026-07-14 with no ref, 2026-10-12 Outreach planned — HRA was due 2026-08-13 — 60 days overdue, 2026-10-12 Outreach planned — HbA1c Control — Glycemic Assessment — due 2026-07-, 2026-10-12 Task raised — Assign a care manager and open a care plan

Check it yourself'Full timeline' opens the Timeline tab. Its first six entries are the same six, in the same order.

app/services/member360.py · timeline()app/static/app.js · p360Tab()

Care Gaps (N open of M) and each gap's progress

Care tab, Care Gaps panel: title count, and under each gap's status

Formula

N = the member's care gaps that are open by the one open rule (workflow.gap_is_open, served per gap as open); M = every care-gap record, closed and excluded included progress = the gap orchestration bar 'X of Y steps complete' (eligibility, authorization and the clinical steps); the gap's own status is shown as its badge

Why this wayN is the same count as the summary strip's 'Open gaps', from the same rule, and the Open outreach ladder and Send to provider buttons show for exactly those gaps (A-063). Progress has one source, the orchestration bar (see the Gap orchestration row in the Patient 360 summary-strip chapter); the old six-step 'step N of 6' line read from the status is removed (A-040), because it showed a second, disagreeing progress figure for the same gap.

In the demoJohn Doe: 5 open of 5 gaps, Derrick Boateng, Diabetic Retinal Exam: orchestration 5 of 8 steps complete

Check it yourselfOn the Care tab, the Care Gaps title's open count matches the summary strip's Open gaps; each gap shows one progress bar.

app/api/vbc.py · get_patient()app/services/workflow.py · gap_is_open()app/static/app.js · p360Tab()

Interventions (N) and each intervention's recent trail

Care tab, Interventions panel

Formula

N = every intervention for the member, in any status (completed and cancelled included) each card: its status and the last 5 entries of its event trail, oldest of the five first

Why this wayThe panel is the member's whole history of interventions. The summary strip's 'Active interventions' counts only those still in progress, so the two numbers differ once one finishes or is cancelled. Only five events are shown to keep the card short; the full trail is on the Timeline tab.

In the demoJohn Doe: 0, Derrick Boateng: 1 (Diabetic retinal exam AUTH_APPROVED)

Check it yourselfJohn Doe has none until the demo starts one from a gap. After that, the count here and the strip's Active interventions both read 1.

app/api/vbc.py · get_patient()app/static/app.js · p360Tab()

Transition of care: badge ('4/7 completed'), its colour, and the closing note

Utilization tab, Transition of care panel header and footer

Formula

plan for the member's latest inpatient discharge when it is inside the actionable window (discharged within the last 120 days, the same rule Transitions lists by); an older discharge is shown as 'historical — outside the 120-day actionable window' with its outcome, not as a live plan badge = done / total actions, the same arithmetic as 'X of Y actions completed' on Transitions outcome (stated by the server, transitions.plan): success · closed_with_misses · failed (readmitted) · watching (30-day readmission watch running) · open colour = the outcome's tone: green success · amber closed with missed steps · red readmitted · blue watching · amber open note = the plan's close state detail (see the Transitions chapter, Readmission monitoring)

Why this wayThe member's own record shows how far the transition has got, not only a date. Since 2026-09-25 the member record and Transitions share one actionable window (A-039) and the badge colours the outcome, not the lifecycle (A-038): a plan that closed with missed steps is amber, not green. The Utilization panel below counts a separate, stated 180-day window. Dismissed steps count as completed.

In the demoSam Okafor: discharged 2026-10-10, 4/7 completed (amber) — 3 step(s) still outstanding: Clinical assessment, Follow-up appointment, Visit completion, Grace Nwosu: historical — outside the 120-day actionable window, discharged 2026-03-30 — closed with missed steps

Check it yourselfOpen Sam Okafor, Utilization tab: the badge matches the plan's steps counted by hand (the 30-day watch and the closing step are not counted). Open Grace Nwosu: her March discharge is labelled historical, and its outcome reads 'closed with missed steps'.

app/api/population.py · member_transition()app/services/transitions.py · latest_discharge()app/services/transitions.py · plan()app/static/app.js · p360Tab()

Coverage (N)

Coverage tab, Coverage panel title

Formula

every coverage record for the member, active and terminated; the green 'active' badge = status active

Why this wayOnly one coverage is active at a time: adding a plan terminates the previous one and sets its end date to today. Known defect: if the coverage request fails, the panel shows 'No coverage on file' and a count of 0 instead of an error. That is a false absence, and coverage is what decides whether a service can be booked.

In the demoUnitedHealthcare MA Choice (active, 2026-01-01 to 2026-12-31)

Check it yourselfCount the table's rows.

app/api/records.py · list_coverage()app/static/app.js · coveragePanelHtml()

Eligibility & authorizations: 'Coverage' line

Coverage tab, Eligibility & authorizations panel

Formula

the member's current coverage (services/coverage.current_coverage); with none current, the latest coverage row (largest end date) with its status, shown as payer · plan (status); none → 'none on file'

Why this wayThe status is printed next to the plan, so a terminated plan says so. It is the same coverage the header plan line and the summary strip name whenever the member has a current one; only when none is current does it fall back to the latest row, so the reader sees why.

In the demoUnitedHealthcare · MA Choice (active)

Check it yourselfCompare with the plan line in the header and the Coverage table directly above.

app/api/population.py · member_authorizations()

'Last verified with the payer' (or 'never')

Coverage tab, Eligibility & authorizations panel

Formula

the business date (America/New_York) of the member's most recent eligibility check, ordered by time then id (eligibility.latest_check), and its transaction number; no check → 'never' plus the sentence explaining it

Why this wayVerification is something done on a date, not a property of the record: 'active' on file is not the payer saying so today. This line and the Check eligibility step in each gap's orchestration chain (Care tab) read the same query since 2026-09-25 (A-041), so they name the same check and the same business date; before that this line took the UTC date with no tie-break.

In the demothe reset day

Check it yourselfOpen Derrick Boateng (verified on the reset day). John Doe reads 'never' until someone clicks Check eligibility in the header.

app/api/population.py · member_authorizations()app/services/orchestration.py · gap_chain()

Authorizations (N), headline ('Retinal exam PA — Approved'), status colour, 'scheduling unlocked / held'

Coverage tab, Eligibility & authorizations panel: title count, green badge, table

Formula

N = every prior authorization for the member, any status, newest submitted first headline = the service's short name from the one terminology registry (terminology.service_label: short name, else the full CPT name, else the code) + ' PA — ' + the status label from models.PA_STATUS_LABEL ('In review', 'Partially approved', …) Decision column = 'Decided <date>' once the authorization is decided, else 'Due <date>' — both business-calendar dates scheduling unlocked when status is APPROVED; otherwise 'scheduling held' (a denial shows its reason) green badge by the title = the newest approved authorization colour: green APPROVED · red DENIED · amber PENDED, IN_REVIEW · blue otherwise

Why this wayThe member's own record answers whether a service can be booked; the server applies the same rule when booking (see Part I, The method: scheduling stays blocked while a linked authorization is pending). Limit: only an approval unlocks scheduling, so a partial approval reads 'scheduling held'. Status and service names come from one registry each since 2026-09-25 (A-045); before that the endpoint title-cased the raw status ('In_Review'), kept its own five-code name list without G0439, and printed an open authorization's raw decision-due timestamp under 'Decided'.

In the demoPA-2026-40188: Retinal exam PA — Approved, scheduling unlocked

Check it yourselfOpen Derrick Boateng, Coverage tab: one authorization, approved, with the green headline badge. The Authorizations screen shows the same reference and status.

app/api/population.py · member_authorizations()app/api/population.py · _decision()app/core/terminology.py · service_label()app/static/app.js · authorizationsPanel()

Patient 360: summary strip, risk drivers, clinical panels, what needs attention, timeline

23 values

The care manager's single view of one member. Every figure below is shown for the demonstration member John Doe, as the care manager Jane Smith sees him.

Attributed provider (and 'why?': method, basis, since)

Header, attribution line

Formula

the member's attribution row for the open period that contains today (not ended); method, provider, the recorded basis, and the period start no row → 'Not attributed this period'

Why this wayAccountability is a recorded assertion with its method, because the method is the argument when a payer disputes the denominator. The method says how the member came to this provider: 'declared_pcp' when their declared PCP is the provider, 'roster' when they were assigned to the provider's panel (the demo provider's five members), with the overridden PCP kept in the basis.

In the demoDr. P. Osei by roster since 2026-01-01 (basis: Assigned to Dr. P. Osei's panel (synthetic demo roster); declared PCP on record: Dr. Sarah Smith)

app/services/member360.py · summary()

Summary: Risk tier and 'N factor(s)'

Summary strip, card 1

Formula

the tier stored on the member by the last scoring run (HIGH ≥ 60 · RISING ≥ 35 · MEDIUM ≥ 15 · LOW); N = factors recorded by that run

Why this wayThe tier is the risk engine's; see Part I, Clinical risk score. It is kept current by the record, not by whoever remembers: any change to a member's conditions, results, encounters or medications — through ingestion, the API or a feed — re-scores that member when it is saved, and a score is refreshed once a day when time alone moves it (a lab ageing out of its window). A tier set by hand is left as the person set it.

In the demoHIGH, 10 factors

app/services/member360.py · summary()app/services/risk.py · rescore()

Summary: Open gaps ('N critical · N high' / 'N closed')

Summary strip, card 2

Formula

the member's care gaps whose status is not closed; hint: open gaps with priority Critical and High (or, when there are none, gaps closed)

Why this wayThe same open rule as every other gap count (gap_is_open), for one member.

In the demo5 open (3 critical, 2 high), 0 closed

app/services/member360.py · summary()app/services/workflow.py · gap_is_open()

Summary: Recent utilization, 'N in 180d', 'discharged Nd ago'

Summary strip, card 3

Formula

window = encounters whose discharge date (or admission date, if not discharged) is within the last 180 days big number = inpatient + emergency encounters in the window (added in the browser) 'N in 180d' = every encounter in the window · 'discharged Nd ago' = today − the latest inpatient discharge in the window

Why this waySix months is long enough to show a pattern and short enough to be about now. The big number counts only acute use (inpatient and ED), which is the utilisation a care manager acts on.

In the demo1 acute (0 inpatient + 1 ED) of 2 encounters in 180 days

app/services/member360.py · summary()app/static/app.js · summaryStrip()

Summary: Active interventions (and their states / 'N total')

Summary strip, card 4

Formula

interventions in IDENTIFIED, OUTREACH, ENGAGED, SCHEDULED, AWAITING_AUTH, AUTH_APPROVED or IN_PROGRESS; hint = those states, or the total when none is active

Why this wayActive means the intervention is still moving toward its outcome. The worklists and Command Center use a slightly different list of 'not finished' states, so their counts can differ for an intervention in an unusual state.

In the demo0 active of 0

app/services/member360.py · summary()

Summary: Upcoming actions, 'N overdue', 'next <date>' and the Next action line

Summary strip, card 5 and the next-action line

Formula

open tasks for the member; overdue = open tasks due before today; next = the open task with the earliest due date (its action and reason)

Why this wayThe member's own queue in one line, so the care manager sees what is due before scrolling.

In the demo7 open, 3 overdue; next: Close care gap

app/services/member360.py · summary()

Summary: Coverage (Active / None, payer, ends)

Summary strip, card 6

Formula

Active when the member has a current coverage (the one coverage rule (services/coverage.current_coverage): the coverage whose status is active and whose dates contain today (the most recently started, if several)); payer, plan and end date from it

Why this wayCoverage decides who pays and whether an authorization rule applies. The same function names the coverage on the header, My Panel, the Dashboard and the readiness check, so they cannot disagree.

In the demoUnitedHealthcare MA Choice, ends 2026-12-31

app/services/member360.py · summary()

What needs attention: total, 'N needing attention first', 'N already in flight'

What needs attention panel, header

Formula

items = one per open gap (per measure) + labs over target not covered by a gap + low adherence + social barriers + a recent discharge + authorizations + coverage first = items of high severity · in flight = items with work already under way (a live intervention, open transition tasks, a PA with a reviewer)

Why this wayOne list across every source, so nothing is only visible on a tab. 'In flight' stops a care manager from starting work that someone else has already started.

In the demo7 items, 4 first, 0 in flight

app/services/attention.py · attention()

What needs attention: severity (dot colour) and order

What needs attention panel, each line

Formula

care gap: high if past its due date or Critical priority · medium if High · else low lab over target: high if ≥ 10% above the target · else medium adherence: high if PDC < 60% · else medium social barrier: medium · discharge: high within 7 days, else medium (shown for 30 days) denied authorization (last 60 days): high · no active coverage: high · coverage ending within 60 days: medium order: high, medium, low, then title

Why this wayEvery severity is a written threshold, and each line shows the arithmetic that fired it. Targets are population defaults, not individualised clinical targets, and the panel says so.

In the demohigh: Creatinine above target, high: Diabetic Retinal Exam, high: HbA1c above target, high: Systolic BP above target, medium: Annual Wellness Visit, medium: Social barrier identified, medium: Statin Adherence (PDC >= 80%)

app/services/attention.py · attention()app/services/attention.py · _severely_over()

What needs attention: value and trend ('8.9%; improved from 9.4%')

What needs attention, lab and gap lines

Formula

latest reading of the tracked result within 400 days; trend word only when there is more than one reading and the value changed: improved when latest < first (all tracked results are lower-is-better), else worsened targets: HbA1c 8.0% · systolic 140 · total cholesterol 200 · creatinine 1.3

Why this wayA number without its direction invites the wrong conversation: 8.9% is above target but moving the right way.

In the demo8.9%; improved from 9.4%

app/services/attention.py · _value_phrase()app/services/attention.py · _latest_by_code()

What needs attention: 'Due <date> — overdue' and the PA note

What needs attention, care-gap lines

Formula

a gap's due date = the end of the current attribution period (else 31 December), set when the gap is created; overdue when before today PA note: the payer policy's answer for the gap's service code under the member's active plan ('required' / 'not required'; no note when unknown)

Why this wayThe due date is when the measure year closes, because that is when an open gap becomes a missed one. The PA note comes from the same policy lookup the authorization screens use, so the two cannot disagree.

In the demo1 overdue gap lines

app/services/quality.py · gap_due_on()app/services/workflow.py · pa_required()

What needs attention: 'PDC N% vs 80% threshold'

What needs attention, adherence line

Formula

each active medication with PDC below 80%, shown as round(PDC × 100)%; left out when a statin-adherence gap already covers that drug class

Why this wayPDC (proportion of days covered) is a stored pharmacy input, not computed by the platform; 80% is the standard adherence threshold.

In the demoAtorvastatin 40mg adherence 62%, below the 80% threshold

app/services/attention.py · _adherence()

What needs attention: discharge ('N day(s) ago'), authorizations, coverage ending

What needs attention, other lines

Formula

discharge: today − the member's most recent discharge on the Transitions list (inpatient, never a future date), shown for 0–30 days authorizations: pending ones (submitted, in review, pended) collapsed to one line with the soonest decision due; each denied or partially approved one whose appeal window is still open (appeals.is_appealable: 180 days, 60 for Medicare Advantage), with the window's length and closing date coverage: 'ends <date> (N days)' when 0–60 days away

Why this wayEach is a clock the member is on. The discharge line uses a 30-day window, while the Transitions screen keeps a discharge actionable for 120 days.

In the demoRecently discharged from hospital: Discharged 2026-10-10 (2 day(s) ago) from Apex Demo Medical Center.

Check it yourselfShown for Sam Okafor, discharged two days before the reset.

app/services/attention.py · _transition()app/services/transitions.py · recent_discharges()app/services/attention.py · _authorizations()app/services/attention.py · _coverage()

Risk panel: 'N-point score', tier, model and date

Primary risk drivers panel, header

Formula

score = Σ points of every rule that fires (conditions, the 3+ chronic bonus, the most severe band of each lab within 730 days, social Z-codes, ED and inpatient use in 365 days with caps, each medication below 80% PDC) tier: HIGH ≥ 60 · RISING ≥ 35 · MEDIUM ≥ 15 · LOW read from the latest stored scoring run (model apex-risk-rules v2)

Why this wayA transparent points model, not a statistical one: every point names the fact that earned it. See Part I, Clinical risk score, for every weight. The panel shows the latest stored scoring run, and that run always matches the member's current record: saving a new result re-scores the member, and the score history keeps every earlier run. For John Doe the 9.4% HbA1c band (18 points) gave way to the 8.9% band (10 points) when the September result arrived: 85 points.

In the demo85 points, HIGH, model apex-risk-rules v2

app/services/member360.py · risk_drivers()app/services/risk.py · score_patient()

Risk panel: each driver's 'N pts', 'X% of score' and impact label

Primary risk drivers panel, each row

Formula

share = round( 100 × driver points ÷ total points, 1 ) impact: High impact ≥ 10 points · Moderate impact ≥ 6 · else Supporting factor

Why this wayShare tells a care manager which fact is doing the work; the impact label is a reading aid over the same points.

In the demoChronic kidney disease stage 3 (N18.3): 12 pts, 14.1%, High impact, Diabetes with hyperglycemia (E11.65): 10 pts, 11.8%, High impact, HbA1c above 8% (was 8.9%): 10 pts, 11.8%, High impact, Creatinine above 1.5 (was 1.8mg/dL): 10 pts, 11.8%, High impact

app/services/member360.py · risk_drivers()

Risk panel: the action on each driver

Primary risk drivers panel, each row

Formula

matched by subject (HbA1c, blood pressure, cholesterol, creatinine, adherence, ED/inpatient use, social) to a 'What needs attention' line: its action, or its in-flight label; a standing condition gets a note instead of a button

Why this wayA diagnosis explains the tier but is not something to close, so it gets an explanation rather than a false call to action.

In the demo5 drivers with an action button

app/services/member360.py · _action_for_driver()

Change over time: readings, first, latest, change, direction, at target

Change over time panel

Formula

per tracked result (HbA1c, systolic BP, total cholesterol, creatinine), over every reading on file: change = round( latest − first, 2 ); improved when change < 0, unchanged at 0, worsened above (all lower-is-better); a single reading is not evaluable at target when latest ≤ target (8.0% · 140 · 200 · 1.3)

Why this wayAn observed change, not a claim about what caused it; the panel says so. No time window, so the whole record on file is compared.

In the demoHbA1c 9.4 → 8.9 (-0.5, improved, target 8.0), Systolic BP 162.0 → 148.0 (-14, improved, target 140.0), Creatinine 1.9 → 1.8 (-0.1, improved, target 1.3)

app/services/outcomes.py · patient_trajectory()

Clinical panels: lab 'abnormal' highlight, problem count, medications with PDC %, encounters count

Labs, Problems, Medications, Encounters panels

Formula

abnormal (red) = the flag the sending lab put on the result (HL7 OBX-8 H/L/A/AA/HH/LL, or the file's abnormal column); not recomputed problems = every condition on the record · encounters = every encounter on the record PDC % = round(PDC × 100), red below 80%

Why this wayThe abnormal flag is the laboratory's statement, kept as sent: re-deriving it from a reference range would put words in the lab's mouth. PDC is a pharmacy input the platform stores, not a figure it computes.

In the demo3 problems, 6 of 6 lab results flagged abnormal, 2 encounters, PDC Atorvastatin 40mg 62%

app/services/ingest.py · promote()app/static/app.js · panelMeds()

Gap orchestration: 'X of Y steps complete', PA REQUIRED / NO PA NEEDED, '<step> blocked'

Each care gap's orchestration chain

Formula

eight steps, each read from its own record: gap · eligibility · PA requirement · launch PA · obtain authorization · schedule · confirm service · close gap Y = steps that apply (steps not required or not applicable are left out); X = steps done; bar green ≥ 80%, amber ≥ 40% blocked badges = steps blocked, denied, stale or unknown

Why this wayA progress bar that can never reach the end is one nobody reads, so a service with no authorization requirement has 6 steps, not 8. Each step is derived, never ticked.

In the demoHbA1c Control — Glycemic Assessment: 2 of 6 steps complete, Diabetic Retinal Exam: 2 of 8 steps complete, PA required, Statin Adherence (PDC >= 80%): 2 of 6 steps complete

app/services/orchestration.py · gap_chain()

Gap orchestration: eligibility 'verified Nd ago' / stale

Orchestration chain, Check eligibility step

Formula

age = today − the date of the latest eligibility check done when age ≤ 30 days · stale when older · no check: open when coverage is on file, blocked when there is none

Why this wayAn eligibility answer goes stale; a 30-day-old check is not evidence of coverage today.

In the demoopen: coverage on file (UnitedHealthcare MA Choice) but never verified with the payer

app/services/orchestration.py · gap_chain()

Close gap: when evidence closes a gap

Orchestration chain, Close gap step; completion modal

Formula

a gap closes only when the evidence supplied is an encounter, lab or procedure AND (its service code equals the gap's service code OR it names the gap's measure)

Why this wayApproval of an authorization never closes a gap; only clinical evidence of the service does. The check is on the evidence supplied with the event.

In the demo0 gaps closed on evidence so far

app/services/workflow.py · evaluate_completion()

Timeline: total, per-kind and per-team chip counts, 'Showing the most recent N of M'

Timeline tab

Formula

every dated event across encounters, results, medications, procedures, conditions, gaps, interventions, referrals, appointments, provider opportunities, care plan, assessments, authorizations, messages, calls, ladders, tasks and documents chip counts are taken BEFORE a filter is applied (a zero chip stays visible, disabled) shows the most recent 120 (up to 400 on request); 'N of M' when there are more

Why this wayOne chronology across every system, so the member's story reads in order. Counting before filtering lets a reader see what a filter hides.

In the demo43 events; Clinical 11, Encounters 2, Care Gaps 6, Tasks 7, Referrals 12, Provider 1, Care Plan 1, Engagement 3

app/services/member360.py · timeline()

Timeline: entry colour (red / amber / green)

Timeline tab, each entry

Formula

red: abnormal result, inpatient or ED admission, denied authorization, failed or declined referral amber: discharge, a fill below 80% PDC green: gap closed, authorization approved, referral confirmed or resolved, appointment completed

Why this wayColour marks events that change what should happen next, so a long timeline can be skimmed.

In the demored 7, amber 1, green 1

app/services/member360.py · timeline()

Patient 360: care plan, HRA, SDOH, referrals, appointments and programmes

25 values

The care manager's working tabs on one member: care-management enrolment, the health risk assessment, the care plan, social barriers, appointments and documents (Care tab), team, social and vendor referrals (Referrals tab), and Partner Activity on the Overview. Every figure below is read as the care manager Jane Smith sees it, for John Doe unless another member is named.

Care management: status badge (ENROLLED, OFFERED, … / MEETS CRITERIA / NOT ELIGIBLE), the four stage dots and 'Previously declined'

Care tab, Care management panel, header and stage row

Formula

live = the member's live Complex Care Management (cm-complex) enrolment: status ELIGIBLE, OFFERED, ACCEPTED or ENROLLED (other programmes are listed, never shown as this one) badge = the live status; with nothing live: MEETS CRITERIA when at least one eligibility criterion is met today, else NOT ELIGIBLE a stage dot is ticked when it is at or before the live status in ELIGIBLE → OFFERED → ACCEPTED → ENROLLED 'Previously declined / disenrolled' = the member's DECLINED or DISENROLLED row for cm-complex, shown only when nothing is live

Why this wayThe badge is the recorded lifecycle; the criteria are re-evaluated on every view. The two can differ, and the panel shows both rather than choosing: an ENROLLED member who no longer meets any criterion keeps ENROLLED (Sarah Martinez shows ENROLLED above 'does not currently meet the criteria'). A member who declined is shown as declined and is not re-offered automatically; offering again is a person's decision. One limit. The Recommended action card uses a different rule (see the Next best action chapter): it offers enrolment to every HIGH-risk member not yet ENROLLED, so Maria Gonzales, already OFFERED, is still recommended an offer (ranked sixth, so it is below the three cards Patient 360 shows, but it is on the Population list), while rising-risk members who meet the criteria get no offer card.

In the demoENROLLED; stages reached: ELIGIBLE, OFFERED, ACCEPTED, ENROLLED

Check it yourselfOpen John Doe → Care. Sam Okafor shows MEETS CRITERIA with a 'Previously declined' line and an 'Offer again' button.

app/services/enrollment.py · member_view()app/static/app.js · enrollmentPanel()

Care management: 'Why this member qualifies' (criteria met and their evidence)

Care tab, Care management panel

Formula

the four criteria of Part I, evaluated at the moment the panel loads: HIGH tier · RISING tier with 3 or more open gaps · an inpatient or ED admission in the last 180 days · 2 or more unaddressed social barriers each line prints the evidence the test used; none met → 'does not currently meet the criteria'

Why this waySee Part I, Care-management eligibility. The criteria are computed on every view and never stored as a flag, so the reasons are current. The acute test counts encounters by admission date. The social test uses the same barrier classification as this tab's Social drivers of health panel, so a declined referral still counts as unaddressed. Part I's worked example is out of date: the product today finds six members on eight criteria. Maria Gonzales also meets the recent-acute test, and Robert Kim qualifies on it; neither line is in Part I's table.

In the demoHigh-risk member (risk tier HIGH), Admission or ED visit in the last 6 months (1 acute encounter(s))

Check it yourselfOpen John Doe → Care: two lines, 'High-risk member' and 'Admission or ED visit in the last 6 months'. His Utilization tab shows the admission.

app/services/enrollment.py · evaluate()

Care management: 'NOT ACTUALLY STARTED' and the care manager

Care tab, Care management panel

Formula

only for a member whose Complex Care (cm-complex) enrolment is ENROLLED: started when a care manager owns the member (the active care plan's owner, else the owner of the newest open task) AND an active care plan exists otherwise the red badge shows and the missing items are listed; when started, the owner is shown as Care manager

Why this wayEnrolment can be recorded directly, for example from a payer file or a programme older than the platform. So the platform reports an enrolment with nothing behind it rather than refusing to record it. The panel's note states the rule in one sentence (enrollment.ENROLLED_RULE, which Part I quotes word for word): ENROLLED may be recorded directly, the panel flags an enrolment with no care manager or no care plan, and only the Start programme button assigns a care manager and opens a plan first. The check runs only for a Complex Care (cm-complex) enrolment: in the fresh demo tenant 3 of the 4 members ENROLLED in Complex Care have no care plan and show this badge; John Doe is the exception. (The nightly 'enrolled but not started' task rule still checks ENROLLED rows of every programme.)

In the demoNOT ACTUALLY STARTED: no active care plan

Check it yourselfOpen Sarah Martinez → Care: ENROLLED, NOT ACTUALLY STARTED, 'no active care plan'. John Doe shows Care manager Jane Smith and no red badge.

app/services/enrollment.py · delivery_state()app/static/app.js · enrollmentPanel()

Health risk assessment: status badge and PAST WINDOW

Care tab, Health risk assessment panel, header (and the red Due date on each row)

Formula

HRA rows for the member, latest due date first; live = the first one not settled (COMPLETED, REFUSED and UNREACHABLE are settled) badge = the live status; else COMPLETED if any HRA was completed; else NONE DUE PAST WINDOW, and a red Due date = due date before today (business date) and not settled

Why this wayThe window is the contract's, so an HRA nobody attempted shows as past its window as plainly as one the member refused. Two limits. A late completion is shown as COMPLETED, with nothing on this screen marking it late; the in-window test exists only in the HRA completion KPI, which no screen renders. And this panel does not know about an in-home HRA referred to a vendor. John Doe shows DUE and PAST WINDOW while his Referrals tab shows HouseCall Health Assessments working an annual in-home HRA for him; the two records are not linked.

In the demoDUE, PAST WINDOW

Check it yourselfOpen John Doe → Care: DUE and a red PAST WINDOW; the row's Due date is red.

app/services/assessments.py · member_view()app/static/app.js · hraPanel()

Health risk assessment: Window and Due dates

Care tab, Health risk assessment table

Formula

raised automatically for every ENROLLED member with no live HRA first HRA: window from the enrolment date; due = enrolment date + 90 days later HRAs: window from the last settled HRA (its completion date, else its due date); due = that date + 365 days

Why this wayThe clock runs from enrolment, not from when somebody created the record: a window that starts when the plan notices is not a window. A refused or unreachable HRA starts the next annual cycle from its due date, so the member is asked again next year rather than on the next sweep. The 90 and 365 days are constants in code, not tenant settings. The missing HRA rows are created when this panel is read (the read also writes).

In the demo60 days before the reset

Check it yourselfJohn Doe enrolled 150 days before the reset; his first HRA was due 90 days later, so it is 60 days past due.

app/services/assessments.py · ensure_due()

Care plan: goal count

Care tab, Care plan panel, title

Formula

goals on the member's single ACTIVE care plan, every status including MET; proposals are not counted; a removed goal is deleted and leaves the count no plan → 'No care plan yet' and 'N goal(s) can be proposed from the record'

Why this wayThe count of lines on the plan, not a measure of progress. The plan's own summary also computes met and PA-blocked counts, but this panel does not show them.

In the demo8 goals

Check it yourselfCount the rows of John Doe's care plan table.

app/services/careplan.py · read()

Care plan: 'review due' date and REVIEW OVERDUE

Care tab, Care plan panel, header

Formula

review due = the day the plan was opened + 90 days; 'Mark reviewed' resets it to that day + 90 REVIEW OVERDUE when the review date is before today (business date)

Why this wayA plan nobody revisits is a document, not a plan. The 90 days are a constant in code, not a tenant setting. 'Started' is the day the plan record was opened, not the enrolment date. John Doe's plan is opened by the demo reset, so it reads as started on the reset day although he enrolled five months earlier.

In the demo90 days after the reset

Check it yourselfClick Mark reviewed: the date moves to today + 90 days.

app/services/careplan.py · ensure_plan()app/services/careplan.py · review()app/services/careplan.py · read()

Care plan: goal Status and 'from the work' / 'set by hand'

Care tab, Care plan table, Status column

Formula

read from the linked work; the first rule that applies wins vendor referral: SENT→REFERRED · ACKNOWLEDGED, MEMBER_CONTACTED→ACTIVE · SCHEDULED · DELIVERED→AWAITING EVIDENCE · CONFIRMED→MET · DECLINED, FAILED→BLOCKED · CANCELLED→CLOSED care gap: closed as CLOSED/RESOLVED→MET, closed otherwise→CLOSED · AWAITING_AUTH→PA REQUIRED · SERVICE_COMPLETED→AWAITING EVIDENCE · SCHEDULED · intervention AUTH_APPROVED→PA APPROVED · OUTREACH, ENGAGED, IN_PROGRESS→ACTIVE · else OPEN no link: the status a person set (OPEN until marked met) = 'set by hand'; everything else = 'from the work'

Why this wayA typed status can read 'active' over a gap nobody has touched, so a linked goal takes its status from the work and cannot be edited. Hovering the badge shows the basis. MET for a vendor line means CONFIRMED, with evidence, not the vendor's own DELIVERED. A proposal and the add-goal request are one shape, so accepting a vendor proposal keeps its vendor link and the line reads its status from the referral. For a gap worked more than once, the plan reads the gap's current intervention: a live one before a finished one, then the one with the latest activity, the same pick every other screen uses.

In the demoComplete retinal screening: OPEN (from the work), Improve diabetes control: OPEN (from the work), Improve blood pressure control: OPEN (from the work), Complete annual wellness visit: OPEN (from the work), Address food insecurity: OPEN (set by hand), Improve medication adherence: OPEN (from the work), Address ride to retinal exam: SCHEDULED (from the work), Address annual in-home health risk assessment: ACTIVE (from the work)

Check it yourselfHover a status: the tooltip names the gap, intervention, authorization or vendor referral it was read from.

app/services/careplan.py · _goal_status()app/services/careplan.py · _vendor_goal_status()app/services/workflow.py · current_intervention()

Care plan: 'now X ↓/↑ (date)' and 'at target' under a goal's target

Care tab, Care plan table, Target column

Formula

for a goal with a target code and value: every numeric reading of that code on file, with no time window; latest = the newest by date at target = latest ≤ target (lower is better) or latest ≥ target (higher is better) arrow: ↓ improving or ↑ worsening, comparing the latest with the EARLIEST reading; no arrow with one reading or no change

Why this wayA target is shown with the reading and its date, because a target met months ago on a value nobody has taken since is not met today. Three things to know. The test is inclusive while the target text prints a strict '<', so an HbA1c of exactly 8.0 shows 'at target' beside 'HbA1c <8%'. There is no lookback window, unlike What needs attention (400 days). The arrow is always drawn ↓ for improving. That is right for every target the product proposes today, all lower-is-better, but a hand-entered higher-is-better goal would show the wrong arrow. Change over time on the Overview (Part II, the Patient 360 summary-strip chapter) compares latest with first in the same way.

In the demoImprove diabetes control: now 8.9% ↓ (2 readings), Improve blood pressure control: now 148mmHg ↓ (2 readings)

Check it yourselfCompare with the HbA1c and systolic readings in the Clinical tab's Labs & vitals table.

app/services/careplan.py · _progress()app/static/app.js · planProgress()

Care plan: 'N goal(s) are owned outside this system' (and 'outside this system' under an owner)

Care tab, Care plan panel, note under the table

Formula

goals whose owner is a label and not a platform user: Pharmacist, Social Work, Dietitian, Behavioral Health, Community Health Worker, Primary Care, or Vendor a goal given neither an owner nor a label falls to the plan owner and is not counted

Why this wayThe platform cannot send a task to a pharmacist or a social worker it has no account for, so it says so instead of pretending to assign the work. Adherence proposals default to Pharmacist, social-barrier proposals to Social Work, vendor proposals to Vendor.

In the demo4 goals

Check it yourselfCount the rows whose Owner column says 'outside this system'.

app/services/careplan.py · read()app/services/careplan.py · add_goal()

Care plan: 'Proposed from the record N' and each proposal's target

Care tab, Care plan panel (or the empty plan's 'N goal(s) can be proposed')

Formula

one proposal per What needs attention line that is a care gap, low adherence or a social barrier, plus one per unsettled vendor referral; minus anything already on the active plan (matched by care gap, else vendor referral, else title) target: a tracked result '<name> <target><unit>' (HbA1c <8%, Systolic BP <140mmHg) · adherence 'PDC ≥ 80%' · other gaps 'completed by <gap due date>' · social 'Barrier addressed' · vendor 'appointment <when>' or 'delivery by <SLA date>'

Why this wayNothing is invented: each proposal names the fact behind it, and adding it to the plan is a person's decision. Social proposals come from the attention list, which reads the same current barriers as the Social drivers of health panel (the newest answer per code, when it is positive) and proposes only those nobody is working: a barrier with a live referral, a vendor on it, or a resolved referral is not proposed, and a re-screen 'no' retires it. A proposal is added exactly as offered: the proposal and the add-goal request are one shape, and a field the request does not know is refused rather than dropped.

In the demoComplete retinal screening → completed by the gap due date, Address transportation insecurity → Barrier addressed, Complete annual wellness visit → completed by the gap due date, Improve diabetes control → HbA1c <8%

Check it yourselfOpen Sarah Martinez → Care: no plan yet, and the proposals listed here, each with its reason.

app/services/careplan.py · propose()app/services/attention.py · _social()app/services/sdoh.py · current_findings()

Social drivers of health: barrier count and 'N unaddressed · N in progress · N resolved'

Care tab and Referrals tab (the same panel on both)

Formula

barriers = one per Z-code: the newest screening answer for it, kept if it is positive; an answer of no, none, negative, no risk, false, 0 or blank is negative, so a newer negative answer retires the barrier each barrier: RESOLVED if its latest community referral is RESOLVED · IN PROGRESS if that referral is not settled, or a contracted vendor in the matching category (transport, meals) holds an unsettled referral · else UNADDRESSED, including a DECLINED or UNRESOLVED referral the three counts add up to the barrier count

Why this wayA member who declined help still has the barrier, so it returns to unaddressed. A barrier a vendor is already working is not reported as ignored. The enrolment criterion and the population views use the same classification. A later 'no' on re-screening clears an earlier 'yes' here, in the enrolment criterion, the population views, the attention list and the risk score, which all read the same current findings. The Clinical tab's Social screening answers panel lists every answer instead (next value).

In the demo1 barrier(s): 1 unaddressed, 0 in progress, 0 resolved

Check it yourselfOpen John Doe → Care or Referrals: one barrier, food insecurity, not addressed.

app/services/sdoh.py · barriers()app/services/sdoh.py · barrier_state()app/services/sdoh.py · current_findings()

Clinical tab: 'Social screening answers N'

Clinical tab, Social screening answers table

Formula

every social-history observation on the member, positive or negative, every repeat screening, all dates

Why this wayThis is the raw record of screening answers, newest first. It counts a different thing from the Care tab's Social drivers of health panel, which counts the barriers these answers raise now: a member screened twice, or with a 'no' answer, shows more here than there. The two panels have different titles, and this one's note says the barriers are on the Care tab.

In the demo1 screening answers

Check it yourselfCount the rows of the table; compare with the barrier count on the Care tab's Social drivers of health panel.

app/api/population.py · clinical_record()app/static/app.js · panelSdoh()

Barrier badge: NOT ADDRESSED / IN PROGRESS / RESOLVED

Care and Referrals tabs, each barrier, top right

Formula

the barrier's state as the server classifies it (the same classification the header counts come from); the first rule that applies wins latest community referral RESOLVED → RESOLVED · latest community referral still open (not RESOLVED, UNRESOLVED or DECLINED) → IN PROGRESS · community referral a contracted vendor has a live referral for it → IN PROGRESS · vendor — <name> otherwise, including a DECLINED or UNRESOLVED referral → NOT ADDRESSED the latest referral's own status is shown beside the badge as 'referral <status>'

Why this wayIt shows who holds the barrier, so a care manager does not start a second referral for a ride already booked, and a referral the member turned down comes back as not addressed. The badge, the referral box, the vendor box and the Start referral control each show when their work exists, so a declined referral and the vendor who later took the barrier both appear. Counting the badges gives the header's counts.

In the demoFood insecurity: NOT ADDRESSED

Check it yourselfJohn Doe's food-insecurity barrier reads NOT ADDRESSED, and the header says 1 unaddressed.

app/static/app.js · sdohStateBadge()app/static/app.js · sdohBarrier()app/services/sdoh.py · barrier_state()

Barrier chain: Finding → Intervention chosen → Referral made → Follow-up → Resolution, and 'Follow-up due'

Care and Referrals tabs, each barrier

Formula

stage reached: RECOMMENDED 1 · SENT 2 · ACCEPTED, DECLINED, UNREACHABLE 3 · RESOLVED, UNRESOLVED 4 · no referral 0 Finding always done · Intervention chosen done at 1+ · Referral made done at 2+ · Follow-up overdue when the follow-up date is before today and the referral is not settled, else done at 3+ · Resolution done on RESOLVED, failed on UNRESOLVED or DECLINED follow-up date = the day the referral was OPENED + 14 days; shown as 'Follow-up due' until the referral settles

Why this wayEach step is read from the referral, so the chain shows how far a barrier has really got. A referral here is a record, not a transmission: a person sends it and marks it sent. Limits. 'Overdue' takes precedence over 'done', so an ACCEPTED or UNREACHABLE referral past day 14 shows Follow-up overdue beside 'member reached — accepted the referral'. A referral opened without a resource still shows Intervention chosen as done, with 'no resource chosen yet'. The 14 days are a constant and run from opening, not from sending.

In the demoFinding: done, Intervention chosen: open, Referral made: open, Follow-up: open, Resolution: open

Check it yourselfStart a referral on John Doe's barrier: Intervention chosen turns done, and 'Follow-up due' shows the date 14 days out.

app/services/sdoh.py · _chain()app/services/sdoh.py · start()

Appointments N

Care tab, Appointments panel, title

Formula

every appointment booked for the member, in any status (cancelled and no-show included), earliest first

Why this wayAttendance is recorded here but does not close a gap: the gap waits at SERVICE_COMPLETED for clinical evidence. Known inconsistency: if the request fails, the panel shows 0 and 'No appointments' rather than saying it could not load, unlike the other panels on this tab.

In the demo0 appointments

Check it yourself0 in the fresh tenant; after booking in Act 3 it reads 1.

app/api/integration.py · appointments()app/static/app.js · p360Tab()

Documents: count, Size (KB), version (v) and Checksum

Care tab, Documents panel

Formula

count = documents stored for the member size = bytes ÷ 1024, one decimal, labelled KB (strictly KiB) v = the highest earlier version of the same file name for this member + 1, else 1 (the name after unsafe characters are replaced with '_') checksum = the first 16 hex characters of the file's SHA-256

Why this wayA re-upload under the same name is a new version, not an overwrite, and every download re-checks the full SHA-256, so a changed file refuses to serve. Limits: two names that differ only in replaced characters share one version line; and a failed request shows 0 and 'No documents' rather than an error.

In the demoreferral: 0.2 KB, v1, checksum of 16 characters

Check it yourselfOpen Sarah Martinez → Care: one referral letter, 0.2 KB, v1.

app/api/documents.py · upload()app/api/documents.py · list_documents()app/static/app.js · docsPanelHtml()

Team referrals N

Referrals tab, Team referrals panel, title

Formula

every internal referral for the member, in any status (settled included), newest first; a redirect updates the same referral, so it is not counted twice

Why this wayThe member's whole history of hand-offs between teams, not only the open ones; the Due column and the LOOP CLOSED badge say which are still live.

In the demo0 referrals

Check it yourself0 in the fresh tenant; Create referral adds one.

app/services/referrals.py · member_view()

Team referral: Due date (red when overdue)

Referrals tab, Team referrals table, Due column

Formula

due = the date typed in the Create referral form, if any; else the day raised + the queue's SLA (the tenant's referral_sla setting for that queue, else its default, else 3 days); a redirect resets it to that day + the new queue's SLA red when due is before today and the referral is not settled (OUTCOME_CONFIRMED, DECLINED, CANCELLED, UNABLE_TO_CONTACT)

Why this waySee Part I, Referral SLA. A typed due date wins over the queue's SLA, so it must be a real date: text that is not a YYYY-MM-DD date is refused when the referral is created, never stored to read later as 'no date'.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfCreate a referral to Care Management with Due blank: Due is 3 days out, and it turns red on the fourth day if still open.

app/services/referrals.py · create()app/services/referrals.py · sla_days()app/services/referrals.py · _row()app/api/fields.py · _iso_date()

Team referral: LOOP CLOSED badge

Referrals tab, Team referrals table, Status column

Formula

shown when the referral's status is OUTCOME_CONFIRMED; COMPLETED (the receiving team's own report) does not show it; DECLINED, CANCELLED and UNABLE_TO_CONTACT settle the referral without it

Why this wayA loop that closes on the doer's own word measures effort, not outcome. Only a confirmed outcome with evidence closes it, and that evidence is printed in the same row.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfWalk a referral to COMPLETED: no badge. Confirm the outcome with evidence: LOOP CLOSED appears.

app/services/referrals.py · _row()

Vendor referrals N (Referrals tab) and Partner Activity N (Overview)

Referrals tab, Vendor referrals title; Overview, Partner Activity title

Formula

every vendor referral for the member, in any status (confirmed, declined, failed and cancelled included), all time; both panels count the same rows

Why this wayPartner Activity is a rollup of the same records, so the two numbers cannot differ. Community (social) referrals and team referrals are not included; they have their own panels.

In the demo3 vendor referrals; Partner Activity 3

Check it yourselfJohn Doe: MedRide NEMT, Nourish Home Meals and HouseCall Health Assessments on both tabs.

app/services/vendors.py · member_view()

Vendor referral: Progress markers (● ▲ ✕ ○)

Referrals tab, Vendor referrals table, Progress column

Formula

chain: SENT → ACKNOWLEDGED → MEMBER_CONTACTED → SCHEDULED → DELIVERED → CONFIRMED reached = the current status's step; for DECLINED, FAILED or CANCELLED (not steps on the chain), the last step the event trail moved it to, and at least SENT ● every step up to reached · ▲ the next step when the SLA has passed · ✕ every step after reached when DECLINED, FAILED or CANCELLED · ○ otherwise

Why this wayDELIVERED is the vendor's claim and CONFIRMED is the plan's evidence, so they are separate steps. The chain is drawn in full, including the part that has not happened. A stopped referral keeps the steps it really reached: one acknowledged and then declined reads ● ● ✕ ✕ ✕ ✕. The internal referral chain on the Referrals tab reads its stops the same way.

In the demoMedRide NEMT ● ● ● ● ○ ○, HouseCall Health Assessments ● ● ○ ○ ○ ○, Nourish Home Meals ● ● ● ● ● ●

Check it yourselfHover a marker: the tooltip names the step.

app/services/vendors.py · chain()app/services/workflow.py · reached_index()

Vendor referral: due date, 'SLA <date>' or 'appt <date>' (red when overdue), also 'SLA due' / 'appointment' in a social barrier's vendor box

Referrals tab, Vendor referrals table, SLA column; Care and Referrals tabs, social barrier vendor box

Formula

SLA date = the day the referral was made + the vendor's contracted SLA in calendar days (7 when the vendor has none, or 0) due date = the SLA date until the referral is SCHEDULED; SCHEDULED with an appointment date → the appointment ('appt'); DELIVERED or settled → nothing owed red when the due date is before today starter contracts: MedRide NEMT 3 · Nourish Home Meals 5 · HouseCall Health Assessments 14 · Regional DME Supply 7 · Bridge Behavioural Network 5

Why this wayThe clock starts at the referral, not at the vendor's acknowledgement, so a vendor who is slow to acknowledge gains nothing. The SLA measures SENT → SCHEDULED (owner decision A-058): once the vendor has scheduled the service with a date, the service is due on that appointment, so John Doe's MedRide ride, booked for two days after the reset, is not overdue in between even though its 3-day SLA ended on the reset day. The day after the appointment, if it has not been delivered, it is red and Partner Activity says 'appointment passed <date> — follow up with the vendor'. Vendor SLAs are per contract. They are not the team-referral SLA in Part I (3 days per queue). Referring the member again to the same vendor for the same service returns the live referral, so its SLA does not restart.

In the demo2 days after the reset

Check it yourselfMedRide NEMT was referred 3 days before the reset with a 3-day SLA (SLA date: the reset day) and is SCHEDULED for two days after it, so its column reads 'appt' and that date, not red.

app/services/vendors.py · refer()app/services/vendors.py · is_overdue()

Partner Activity: Last interaction (date and what happened)

Overview, Partner Activity table

Formula

the event with the latest timestamp in the referral's trail: its date and the label of the status it moved to; with no events, the referral date and 'Referral sent' rows sorted by that timestamp, newest first

Why this wayRead from the event trail, never from a 'last contacted' field somebody has to maintain. The date is the Eastern business date of the event's UTC timestamp, like every other date on this screen, so an event logged after 8 pm Eastern is dated that evening.

In the demo1 day before the reset

Check it yourselfThe first row, MedRide NEMT 'Service scheduled', matches the newest event in that referral's trail on the Referrals tab.

app/services/vendors.py · _last_interaction()app/services/vendors.py · member_view()app/core/clock.py · business_iso()

Partner Activity: 'Outcome / next'

Overview, Partner Activity table, Outcome / next column

Formula

settled: the recorded outcome, or 'settled without a recorded outcome' not settled, first that applies: due date passed → 'SLA passed <date>' or 'appointment passed <date>', then ' — follow up with the vendor' · appointment on file → 'appointment <when>' · else 'awaiting: <next step> · due <SLA date>'

Why this wayWhat happens next is derived from the record, so it cannot go stale. A passed due date outranks everything else, because the vendor owes a response; the due date is the SLA until the vendor schedules and the appointment after (A-058), so a booked service is not chased before its day. The consequence is described under the SLA date above. The Status column and its colour are in Part II, Patient 360: header, gaps, interventions and authorizations.

In the demoMedRide NEMT: appointment, Nourish Home Meals: 14 meals delivered over two weeks; member confirmed, HouseCall Health Assessments: awaiting: vendor reached the member

Check it yourselfHouseCall Health Assessments reads 'awaiting: vendor reached the member · due …'; Nourish Home Meals shows its recorded outcome.

app/services/vendors.py · _next_step()app/static/app.js · p360Tab()

Patient 360: coordination

9 values

The care manager's view of who is already talking to one member and on what authority: the Coordination line in the member header, the contact lists on the Engagement tab, and the disclosure consent a vendor referral needs on the Referrals tab. Every figure below is shown for the demonstration member John Doe, as the care manager Jane Smith sees him.

Coordination: 'N live outreach ladder(s)' (or 'no live outreach')

Member header, Coordination line

Formula

count of the member's outreach ladders whose status is not settled (COMPLETED, EXHAUSTED, STOPPED); every programme's ladders are counted together counted in the browser as the length of the list the server returns; 0 → 'no live outreach'

Why this wayOne number across programmes, because each programme could always see its own outreach and none could see the others'. A ladder counts from the moment it is planned, before anything has been sent: in the fresh demo tenant John Doe has three live ladders (all PLANNED) and no contact history yet, so a live ladder means 'somebody intends to reach this member', not 'somebody has'. The ladder rungs and waits are explained in Part I, Engagement ladder, wait times and the contact ceiling.

In the demo3 live outreach ladders

Check it yourselfopen the Engagement tab; the Outreach panel lists the same ladders. A ladder that is completed, exhausted or stopped drops out of the count.

app/services/engagement.py · who_is_engaging()app/static/app.js · coordinationLine()

Coordination: last decision badge (PROCEED / COMBINE / DELAY / SUPPRESS / ESCALATE), 'with … on …', or 'nothing refused'

Member header, Coordination line

Formula

the newest coordination.decided event in the audit chain for this member (any programme, any age); badge = its decision, hover = its reason, 'with X on D' = what it was combined with the decision itself is the gate's first matching check, in order: do-not-contact → SUPPRESS; no channel usable → SUPPRESS (permanent) or DELAY (temporary); inside quiet hours → DELAY (ESCALATE if the caller marked it critical); a booked appointment or planned contact about something else within lookahead_days (default 2) → COMBINE; another live ladder about something else → COMBINE; the attributed provider already has an open opportunity for this gap → COMBINE; caller marked it urgent or critical → ESCALATE; member at the weekly contact ceiling → DELAY (automation is held at the channel check instead); otherwise PROCEED no event → 'nothing refused'

Why this wayThe header replays the gate's own record rather than recomputing it, so it cannot disagree with what the gate actually did. PROCEED is never written to the audit chain, so the badge only ever shows a decision that stopped, folded or escalated something; 'nothing refused' means no such decision has been recorded, not that the member has never been contacted. The decision is shown however old it is (its date is printed beside it). The contact ceiling it uses (4 contacts in 7 days) is in Part I, Engagement ladder, wait times and the contact ceiling; Part I does not yet describe the gate's order of checks. A campaign's fold into another campaign's task is recorded under the same event, so the badge can show COMBINE with task.

In the demonothing refused

Check it yourselfhover the badge for the reason. Try to send outreach to a member who already has a live ladder: the Outreach coordination window shows the full check chain, and after you choose, the header shows that decision.

app/services/coordination.py · latest()app/services/coordination.py · decide()app/static/app.js · coordinationLine()

Coordination: what happened next ('proceeded anyway: …', 'folded into existing work', 'automation skipped this member', 'refused')

Member header, Coordination line, after the badge

Formula

from the same audit event's outcome: overridden → 'proceeded anyway' with the person's recorded reason; combined → 'folded into existing work'; skipped → 'automation skipped this member'; ANY other outcome → 'refused'

Why this wayA person may override COMBINE, DELAY or ESCALATE with a reason of at least 3 characters; SUPPRESS (consent) cannot be overridden, and automated contact takes no override. Known defect: a campaign that finds another campaign's task owned by someone else creates its own task anyway and records the outcome kept_separate; the browser has no label for that outcome, so the header says 'refused' for work that in fact went ahead.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnothing to show in the fresh demo tenant: no coordination decision exists after the reset. Override a COMBINE in the Outreach coordination window with a reason; the header then reads 'proceeded anyway' followed by that reason.

app/services/coordination.py · gate()app/services/coordination.py · record_task_combined()app/static/app.js · coordinationLine()

Contact history — all channels (count)

Engagement tab, Contact history panel title

Formula

every contact with the member, one row per contact (all channels, both directions; a voice call is one row however it ended), newest first, cut at 40 the panel is not drawn at all when the count is 0

Why this wayOne list across SMS, email, portal and voice, so 'when did we last speak to this person' needs one screen. It reads the same contact ledger the contact ceiling and the who-is-engaging header count, so a call is listed once, as the call. The count stops at 40 without saying so: a member with 60 contacts shows 40. The Engagement panel lower on the same tab is the raw message log and counts differently (see the next rows).

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnothing to show in the fresh demo tenant: nothing has been sent yet, so the panel is hidden. Send one outreach from the ladder; the panel appears with 1. Place a simulated call to the end; the count rises by 2.

app/services/engagement.py · history()app/api/engagement.py · history()app/static/app.js · p360Tab()

Contact history: 'Voice … N member turn(s) · outcome'

Engagement tab, Contact history, each voice call line

Formula

per voice call: N = transcript entries spoken by the member (who == 'member'); the call's outcome is appended when it has one; status is the call's status in lower case

Why this wayMember turns show whether the member actually engaged or the call rang out. The count is of turns, not minutes or words, and it is the same number the Voice tab's calls list shows. The call's transcript is not listed again as a separate message: it is the call's own record.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnothing to show in the fresh demo tenant. Run a simulated call from Engagement → Voice and answer twice; the line reads '… 2 member turn(s)'.

app/services/engagement.py · history()app/services/voice.py · _apply_outcome()

Engagement (count)

Engagement tab, Engagement panel title

Formula

the member's latest 15 message records by time, both directions; voice calls themselves are not included, but the transcript message each call writes is no messages → 'No outreach recorded'

Why this wayThe raw message log, with template and status. It is a different count from Contact history on the same tab: capped at 15 rather than 40, and without call records, so the two titles differ for any member with calls or more than 15 messages. Neither says 'of M'. The channel is printed as stored (lower case) and the timestamp without its year.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfnothing to show in the fresh demo tenant (the panel reads 'No outreach recorded'). After one send, both this panel and Contact history show 1; after one finished call, this shows 1 more and Contact history 2 more.

app/api/vbc.py · get_patient()app/static/app.js · p360Tab()

Consent & Communication Preferences: Channels ('… (opted out)')

Engagement tab, bottom panel

Formula

the four channels sms · email · phone · portal; a channel is marked '(opted out)' only when the stored opt-in is explicitly false

Why this wayA quick read of the stored preference. It does not distinguish 'never asked' from 'opted in': both print the bare channel name. The Consent panel at the top of the same tab does distinguish them (NEVER ASKED), so for John Doe the top panel shows phone as never asked while this line shows 'phone' exactly like the channels he agreed to. Outreach itself is enforced on the server, not from this line.

In the demosms · email · phone · portal

Check it yourselfcompare with the Consent panel at the top of the tab: a channel shown NEVER ASKED there is shown without a mark here.

app/static/app.js · p360Tab()app/services/engagement.py · consent_state()

Create vendor referral: 'Member consent — confirmed' or 'No disclosure consent on file', and whether Send referral is enabled

Referrals tab → Create vendor referral window, consent line

Formula

for the selected need's vendor category: the member's authorization for that category, else their blanket (*) authorization, else none none → never_asked; recorded as declined → refused; expiry date before today → expired (valid through the expiry date itself); expiry not a date → unreadable; otherwise granted Send referral is enabled only for granted; the server applies the same test again when the referral is sent a recorded agreement expires 365 days after it was recorded unless another date is given

Why this waySharing a member's name, address and need with a vendor is disclosure to a third party, a different consent from permission to contact them, so no channel opt-in counts. A category record wins over the blanket one even when it is a refusal or has expired. The window defaults to the first vendor's category (transportation). Known limits: the heading reads 'No disclosure consent on file' for a refusal or an expired authorization as well as for 'never asked' (the reason line underneath says which); the expiry date of a valid authorization is not shown; and an expiry typed in a form is stored without being checked, which is how unreadable arises. Part I's 'Consent disclosure validity 365 days' is this default.

In the demotransportation (never_asked): No disclosure consent on file; Send referral disabled

Check it yourselfopen Create vendor referral for John Doe: the line says no consent on file and Send referral is greyed out. Click 'They agreed'; the line turns to confirmed. Maria Gonzales carries a blanket authorization from the reset and opens confirmed for every need.

app/services/engagement.py · disclosure_state()app/services/engagement.py · record_disclosure()app/api/population.py · disclosure_consent()app/static/app.js · vendorModal()

Next best action

7 values

The recommendation cards on Population (for the VBC manager) and on Patient 360 (for the care manager). Advisory only: nothing happens until a person acts, and every card says why it exists in reasons a care manager can check.

Next best actions (count and order)

Population → Next best actions panel; Patient 360 → Recommended actions

Formula

one recommendation per open care gap (plus an enrolment offer for a high-risk member outside every programme) sorted by band (Critical, High, Medium), then by an internal score, then member name; the top 8 on Population, the top 3 on a member internal score = 10 × risk weight × gap-priority weight × stage factor risk weight: HIGH 1.6 · RISING 1.3 · MEDIUM 1.0 · LOW 0.8 · gap priority: Critical 2.0 · High 1.5 · Medium 1.0 · Low 0.6 stage factor: start 1.0 · retry outreach 0.9 · waiting on an authorization 0.5 (an enrolment offer scores a fixed 12.8)

Why this wayThe score only orders cards within a band and is never shown: a number a care manager cannot interpret is a justification nobody can check. The band and the reasons are what the card shows. See Part I, Next best action.

In the demoJohn Doe — Record HbA1c Control (Critical), John Doe — Complete Diabetic Retinal Exam (Critical), John Doe — Record Blood Pressure Control (Critical), Maria Gonzales — Record HbA1c Control (Critical), Maria Gonzales — Complete Diabetic Retinal Exam (Critical)

app/services/nba.py · recommend()app/services/nba.py · _recommend_for_gap()

Band: CRITICAL / HIGH / MEDIUM PRIORITY, and its one-line justification

Each card, top badge and the line under the title

Formula

Critical: the gap's priority is Critical, or a HIGH/RISING-risk member's gap is 90+ days late High: a HIGH/RISING-risk member, or a High-priority gap, or 90+ days late Medium: everything else (including a gap blocked pending authorization) late = days past the due date once the gap is overdue, days since it opened while it is not yet due an enrolment offer is always High

Why this wayBands are derived from facts, not cut from a score, so each one comes with the sentence that set it ('critical-priority gap on a high-risk member').

In the demoCritical: critical-priority gap on a high-risk member

app/services/nba.py · _band()app/services/nba.py · _gap_age()

'Why Apex recommends this' (the reason lines, each with its source)

Each card, reasons list

Formula

one line per input the engine consumed, in fixed order: Care Gap: 'Overdue N days' (today − due) or 'Open for N days' (today − first event), or 'Due <date> — N days left'; the gap's priority Risk: '<tier>-risk member' · Quality: 'Impacts <measure> — a tracked VBC quality measure with an X% target' Utilization: ED visits and inpatient stays in the last 180 days · Eligibility: 'verified N days ago' (fresh ≤ 30 days) or 'not verified' Outreach: attempts on this gap's ladder · SDOH: the first open barrier and 'N other barriers' · Program/Contract: enrolled or not

Why this wayEight named inputs, every one checkable on the member's record. The card lists which inputs it used, so a missing input is visible, not silent.

In the demoOpen quality gap — HbA1c Control [Care Gap], Overdue 97 days [Care Gap], Critical priority on the measure package [Care Gap], High-risk member [Risk], Impacts Diabetes: HbA1c Control (<8.0%) — a tracked VBC quality measure with a 75% target [Quality], 1 ED visit in the last 6 months [Utilization]

app/services/nba.py · _reasons()

Next step and the card title's verb

Each card

Formula

from the authorization state and the payer's policy for the gap's service: blocked (an undecided authorization) · retry (outreach unreachable or declined) · 'no prior authorization is required' · 'prior authorization is required' · unknown title verb by how the measure is met: Complete (a procedure) · Record (a lab result) · Improve (adherence) · else Close

Why this wayThe next step is the same policy lookup the authorization screens use, so a card can never tell a care manager a service is free of authorization when the policy says otherwise.

In the demoRecord HbA1c Control — No prior authorization is required — this can be scheduled as soon as the member is reached.

app/services/nba.py · _recommend_for_gap()app/services/nba.py · _title()app/services/workflow.py · pa_required()

BLOCKED badge

Each card, top

Formula

the gap's intervention has an authorization with no decision yet

Why this wayA blocked card still shows (the member still needs the care) but it sinks in the order (stage factor 0.5) and its band falls to Medium, because nobody on the care team can move it until the payer decides.

In the demo0 blocked cards in the top 8

app/services/nba.py · _recommend_for_gap()

Action buttons: enabled or disabled, and why

Each card, buttons

Formula

Start Intervention: off when an authorization is pending or an intervention is already running Launch PA: on only when the policy says authorization is required and none is pending Send to Provider: on only when the member has an attributed provider with no live request all off (with the reason) when the signed-in role lacks care-work permission

Why this wayThe button state asks the same permission system the server enforces, so a button that looks enabled always works, and a disabled one says why.

In the demoas the VBC manager: Start Intervention off (your role is read-only for care work); Launch PA off (your role is read-only for care work); Send to Provider off (your role cannot route work to a provider); Assign off (your role cannot assign w

app/services/nba.py · _actions()

'Deferred to <date>' and 'returned' notes

Each card

Formula

dismissing defers a card to today + N days (30 from the card, at most 90) and needs a reason it returns early when its fingerprint changes: gap status · priority · member's risk tier · band · the 30-day bucket of lateness

Why this wayWhat is being deferred is an open gap on a real member, so a deferral expires, and a material change in the case brings the card back with the reason.

In the demo0 deferred in the fresh tenant

Check it yourselfDismiss a card for 30 days, then change the member's gap priority: the card comes back with the reason.

app/services/nba.py · dismiss()app/services/nba.py · fingerprint()

Engagement and voice

26 values

The care manager's outreach screens: the Engagement page (Voice, SMS, Email, Portal, History and Preferences tabs), the Outreach ladder on a member's Patient 360 Engagement tab, the coordination window that opens when outreach is held, and the HRA findings line on the Care tab. Everything is shown as the care manager Jane Smith sees it; member-level figures are for John Doe unless a row names another member. Campaigns has its own chapter; the consent table, the Coordination line and Contact history are in Patient 360: coordination; the Care management and HRA status panels are in the Patient 360 care chapter.

Outreach sequences (count)

Engagement → History tab, panel title

Formula

the newest 50 OutreachSequence rows, by creation time, of every status (settled ladders included), among the members this login may see — the panel is applied in the query, before the 50 are taken the number is the length of that list; when more exist, the title adds 'newest 50 of N'

Why this wayA sequence is one conversation with one member about one thing, across channels, so this counts conversations, not messages. The list stops at 50 and says so ('newest 50 of N') when there are more. Because the panel is applied before the limit, a provider gets their own newest 50, not what survives of the tenant's newest 50. In the fresh demo the provider sees 5 of the 10.

In the demo10 sequences

Check it yourselfCount the rows in the History table; they match the number in the title.

app/api/engagement.py · sequences()app/static/app.js · engSequences()

SIMULATED DELIVERY badge

Engagement → History tab, panel header; Patient 360 → Engagement tab, Outreach ladder header

Formula

shown when can_really_send() is false can_really_send() is true only when a real messaging provider is configured AND the deployment is not synthetic-only

Why this wayBoth conditions are required, so a synthetic-data deployment can never message a real person even if someone configures a provider. The demo is synthetic-only, so the badge always shows. It is one deployment-wide answer: each sequence also carries its own 'any real send' flag, which no screen renders.

In the demoshown on both

Check it yourselfThe amber badge sits beside the History title and beside the ladder title on John Doe's Engagement tab.

app/services/messaging.py · can_really_send()app/api/engagement.py · sequences()app/api/engagement.py · plan()

Sequence status (PLANNED, ATTEMPTED, ESCALATED, REACHED, SCHEDULED, REMINDED, COMPLETED, EXHAUSTED, STOPPED)

Engagement → History tab, Status column; Patient 360 → Outreach ladder header badge

Formula

set only by the ladder's own actions: opened → PLANNED · a send → ATTEMPTED · moved to the next usable channel → ESCALATED · an outcome that means we spoke to the member → REACHED · a real booked appointment → SCHEDULED · REMINDED · COMPLETED (also set when the care gap closes) STOPPED = the member opted out · EXHAUSTED = no untried channel can ever be used when nothing is usable now: any untried channel blocked only by quiet hours or the contact ceiling → wait; otherwise → EXHAUSTED

Why this wayThe rule separates 'not yet' from 'never'. Quiet hours and the contact ceiling are clocks, so a ladder they block waits instead of being written off. Opt-out, no address on file and do-not-contact are walls, and only walls end a ladder. An EXHAUSTED ladder goes back to PLANNED when the task-rule sweep finds a usable channel again. SCHEDULED cannot be set without a real, uncancelled appointment. When the task-rule sweep runs, a stalled ATTEMPTED or ESCALATED ladder whose wait has passed is escalated automatically. The event written when a ladder waits gives the hold's own reason (quiet hours, or the contact ceiling) and when it clears. A send, a call or an intervention's outreach made through another door is recorded on the ladder too (an attempt; a call's ending as its outcome) when it names the ladder, is about the ladder's purpose, or the member has only one live ladder.

In the demoPLANNED 8, REACHED 2

Check it yourselfHistory: count the Status badges; Maria Gonzales and Elena Petrova are REACHED, the other eight PLANNED.

app/services/engagement.py · open_sequence()app/services/engagement.py · reachability()app/services/engagement.py · escalate()app/services/engagement.py · record_outcome()app/api/engagement.py · step()app/services/task_rules.py · _r_engagement_escalate()app/services/task_rules.py · _r_engagement_exhausted()

Channel (SMS / Email / Portal / Voice)

Engagement → History tab, Channel column

Formula

the ladder's current channel when opened: the first usable channel in the order preferred channel first, then SMS, Email, Portal, Voice afterwards: the channel of the latest send, or the channel escalation moved to (the next usable untried channel in that same order) '—' when the ladder opened with no usable channel

Why this wayThe ladder starts where the member asked to be reached, as Part I (Engagement ladder, wait times and the contact ceiling) states; its fixed order, SMS first and Voice last, applies only to the channels the member did not choose. Sam Okafor's two ladders start on Email because Email is his stated preference, and a member who prefers phone is called first. A preferred channel that is blocked is skipped, not waited for.

In the demoEmail 2, SMS 7, Voice 1 ladders

Check it yourselfHistory: Sam Okafor's rows read Email; Elena Petrova's reads Voice after SMS was tried.

app/services/engagement.py · channels()app/services/engagement.py · open_sequence()app/services/engagement.py · escalate()

Tried ('SMS → Voice') and 'N attempt(s)'

Engagement → History tab, Tried column; Patient 360 → Outreach ladder, Outreach step

Formula

Tried = the distinct channels on which a send was recorded, in the order first used, joined with → N attempt(s) = the number of sends recorded, repeats on the same channel included; hidden when 0

Why this wayOne conversation, several channels: a text on Monday and a call on Wednesday about the same exam are one row, not two. Escalating alone adds nothing to Tried; a channel is added when a send is recorded on it. On this deployment every send is simulated, and the ladder's event says so. A call placed from the Voice tab is not a send on any ladder (see Call status), so it never appears here.

In the demoSMS → Voice · 2 attempt(s)

Check it yourselfHistory: Elena Petrova's row; her Patient 360 ladder's Outreach step reads '2 attempt(s) across SMS, Voice'.

app/services/engagement.py · record_attempt()app/services/engagement.py · _steps()app/static/app.js · engSequences()

Waiting on (amber next act, or 'retry <date>')

Engagement → History tab, Waiting on column

Formula

if the member's recorded answer demands a next act that has not happened: that act (amber badge) else the server's label for the next date, chosen from the ladder's status: PLANNED → 'first send due ' + date · ATTEMPTED or ESCALATED → 'retry ' + date · any other live status → 'next step ' + date; else '—' next attempt date = the day of the send + the channel's wait (SMS 2, Email 3, Portal 7, Voice 2 days; 3 otherwise); a voicemail restarts that wait from the day it is recorded; escalating sets today; opening a ladder sets today; a reached or wrong-number outcome clears it

Why this wayWaits are Part I's (Engagement ladder, wait times and the contact ceiling). Whether the demanded act has happened is read from the record that would prove it, as in the 'Member said' row. The words come from the status, not from the date: a PLANNED ladder has sent nothing, so its date is the first send, not a retry. In the fresh demo all eight PLANNED ladders read 'first send due' with the reset day. A wrong number clears the date on purpose, so the escalation rule cannot redial a bad number.

In the demoMaria Gonzales: refer to the contracted transportation vendor; Elena Petrova: book an appointment from an open slot; 8 ladder(s) show a first-send date

Check it yourselfHistory: Maria Gonzales and Elena Petrova show amber badges; open their Patient 360 ladders and the 'Member said' line names the same act.

app/api/engagement.py · sequences()app/services/engagement.py · next_trigger()app/services/engagement.py · record_attempt()app/services/engagement.py · record_outcome()

SMS / Email / Portal templates (count) and the template picker

Engagement → SMS, Email and Portal tabs: the templates panel title and the Send picker

Formula

the library table: every MessageTemplate row in the tenant whose channel is this tab's — every version, locale and status, not only approved ones the picker: the server's list of the distinct APPROVED keys on this channel (GET /engagement/templates?channel=), one entry per key, shown as 'key — purpose'

Why this wayShowing the library per channel makes 'is there an approved text for this' answerable before a send. The picker offers only what a send can use: a draft is never offered, and a key approved in two locales (the demo's T-OUTREACH on SMS, en-US and es-US) appears once, because the version and locale are chosen per member at send — the highest approved version of that key in the member's own locale, falling back to en-US. The 'No approved … template exists' note appears when the picker is empty, so a draft cannot hide it.

In the demosms 4, email 1, portal 0 templates

Check it yourselfOpen each tab: the panel titles read 4, 1 and 0, and the Portal tab says no approved Portal template exists.

app/api/engagement.py · templates()app/static/app.js · engChannel()app/services/messaging.py · resolve_template()

Send result: 'Recorded on <channel> — simulated, nothing transmitted'

Engagement → SMS, Email and Portal tabs, toast after Send

Formula

channel = the tab the Send button is on; the server refuses a send that does not name one text = the approved template for that channel and key, in the member's locale, filled from the same values a call to the member uses: {first_name} = the member's name up to the first space · {org} = the tenant's name · {service} = 'a check-up' (this screen names no service) a placeholder nothing fills is refused (422), never sent

Why this wayThe channel tabs choose the channel: a send from the Email tab for John Doe, who prefers SMS, is recorded as an email and the toast says 'Recorded on email'. A template with no approved version for that channel is refused. Consent, quiet hours and the coordination gate apply as for any other contact, on the channel sent. When the member has exactly one live ladder, the send is recorded on it as an attempt and the gate does not tell that ladder to combine with itself.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfEngagement → Email → pick John Doe → Send, and choose Proceed separately with a reason in the coordination window. The toast names sms. The message then appears in Patient 360 → Engagement with '{org}' unreplaced.

app/services/messaging.py · send_outreach()app/services/messaging.py · outreach_values()app/services/messaging.py · fill()app/api/engagement.py · send()app/static/app.js · engChannel()

Recent simulated calls (count)

Engagement → Voice tab, bottom panel title

Formula

the newest 20 VoiceCall rows, by start time, among the members this login may see — the panel is applied in the query, before the 20 are taken the number is the length of that list

Why this wayA short recent list, not a call log: it stops at 20 (the response carries the full count as total, which this screen does not print). A provider gets their own newest 20. The fresh demo tenant has no calls; each call placed during the demo adds one.

In the demo0 calls

Check it yourselfPlace a call and finish it: the title goes up by one and the call is the top row.

app/api/voice.py · calls()app/static/app.js · rVoice()

Call status (IN_PROGRESS, COMPLETED, ESCALATED, OPTED_OUT, VOICEMAIL, NO_ANSWER, ABANDONED)

Engagement → Voice tab: the live call's badge and the Status column of Recent simulated calls

Formula

IN_PROGRESS when the call starts when a reply reaches an ending: booked, callback or declined → COMPLETED; escalated → ESCALATED; opted_out → OPTED_OUT; voicemail → VOICEMAIL; no_answer → NO_ANSWER 'Member hangs up' while the call is live → ABANDONED

Why this wayStatus says how the call ended; Outcome says what it achieved, which is why three different results share COMPLETED. The live badge and the table badge are the same stored field. The code also treats RINGING as a live state, but nothing ever sets it. Every ending, abandoned included, writes the transcript to the member's contact history. A call is a separate record from the member's outreach ladder: booking, declining or opting out on a call does not move the ladder, and the ladder's Outreach step still reads 'not attempted'.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfStart a call for John Doe, answer 'Yes, please' then 'Mornings': the badge turns COMPLETED and the same status appears in the table.

app/services/voice.py · start()app/services/voice.py · respond()app/services/voice.py · hang_up()app/api/voice.py · calls()

Call outcome text

Engagement → Voice tab: 'Outcome:' under an ended call, and the Outcome column

Formula

booked 'Appointment requested — care team to confirm' · callback 'Member asked to be called back' · declined 'Member declined for now' · escalated 'Transferred to a person' · opted_out 'Member opted out of calls' · voicemail 'Voicemail left' · no_answer 'No answer' · hang-up 'Call ended before it finished' the live panel shows the status when there is no outcome

Why this wayThe outcome is what the workflow acts on. An opt-out sets the member's phone opt-in to 'no', so Voice then reads 'opted out' wherever consent is shown. 'booked' moves the linked intervention from OUTREACH to ENGAGED only when the call was started from an intervention. The Voice tab starts calls without one, so a booking made here books nothing and changes no intervention; the words 'care team to confirm' mean exactly that.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfEnd a call with 'Stop calling me': the outcome reads 'Member opted out of calls', and John Doe's Voice channel then shows opted out on his Engagement tab.

app/services/voice.py · respond()app/services/voice.py · _apply_outcome()app/services/voice.py · hang_up()app/static/app.js · rVoice()

Turns

Engagement → Voice tab, Recent simulated calls, Turns column

Formula

the number of times the MEMBER spoke on the call: transcript lines whose speaker is the member (voice.turn_count) the same number Patient 360's Contact history prints as 'N member turn(s)'

Why this wayIt counts the member's replies, not every spoken line: a call answered twice ('Yes, please', 'Mornings') shows 2, and a call hung up before any reply shows 0. The column is headed 'Member turns'. The scripts list reports a script's size as steps (9 and 7), a different thing, and no screen shows it.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfOpen the call's Transcript: count the Agent and Member bubbles; the total equals Turns.

app/api/voice.py · calls()app/services/voice.py · turn_count()app/services/engagement.py · history()

Reply buttons, or 'Outcome' and Close (call ended)

Engagement → Voice tab, the live call panel

Formula

ended = a call is open on screen and its status is neither IN_PROGRESS nor RINGING → show 'Outcome: …' and Close otherwise the buttons = the replies of the script step the call is at, in script order, plus 'Member hangs up'

Why this wayThe member's side is picked from a fixed list, because a simulated call must not imply speech recognition. The server enforces the same rule the screen draws: a reply that is not on the current step's list is refused, and so is any reply after the call has ended ('call already COMPLETED'). Example: the Care gap outreach script opens with five replies: 'Yes, please', 'Not right now', 'Who is this? I want a person', 'Stop calling me', '(no answer)'.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfStart a Care gap outreach call: five reply buttons appear; answer until the call ends and they are replaced by the outcome and Close.

app/services/voice.py · options()app/services/voice.py · respond()app/static/app.js · rVoice()

What the agent says (personalised script line)

Engagement → Voice tab, Agent bubbles in the live call and in a transcript

Formula

the approved script's text for the step, filled from the same values an SMS to the member uses (messaging.outreach_values): {first_name} = the member's name up to the first space · {org} = the tenant's name, or 'your care team' if it has none · {service} = the Service typed at Start call, else the linked intervention's name, else 'a check-up' a placeholder nothing fills refuses the call (422) rather than speaking the hole

Why this wayThe words come from a versioned script a person approved (v1 of each); the system only fills in names. Only the Care gap outreach greeting uses {service}. Replies are rendered without the typed service, which changes nothing today because no later step uses it. The first-name rule is simple: 'Mary Ann Smith' is greeted as 'Mary'.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfStart a Care gap outreach call for John Doe with Service 'a diabetic eye exam': the greeting reads 'Hello John, this is an automated call from Demo Health ACO … due for a diabetic eye exam'.

app/services/messaging.py · outreach_values()app/services/messaging.py · fill()app/services/voice.py · _line()app/api/voice.py · start()

Outreach coordination window: decision (COMBINE / DELAY / SUPPRESS / ESCALATE), 'What the engine checked', and the options

Opens on Start call (Voice), Send (SMS / Email / Portal) and Send outreach (Patient 360 ladder) when the engine does not say PROCEED

Formula

checks in order; the first decisive one wins: do-not-contact → SUPPRESS · no usable channel → SUPPRESS if every channel is blocked by a wall, else DELAY · quiet hours → ESCALATE if marked critical, else DELAY · contact ceiling → automation is held here (DELAY), a person gets a warning · an appointment or a ladder send planned within 2 days (tenant setting) about something else → COMBINE · any other live ladder → COMBINE · vendor already involved → warning · the attributed provider already has an open opportunity for this gap → COMBINE · marked urgent or critical → ESCALATE · ceiling warning on routine → DELAY · else PROCEED (no window) proceeding anyway needs a reason of 3 or more characters; SUPPRESS cannot be overridden, and automated contact takes no override

Why this wayThe window is the gate's refusal, shown with its working, so a person decides and the decision is recorded. The Coordination line in the member header shows the last one. Every door on these screens sends urgency 'routine', so ESCALATE cannot come from them. The Voice and channel tabs send no purpose: when the member has exactly one live ladder, the call or message is that ladder's own next step (recorded on it, and not told to combine with it); with two or more, every live ladder counts as 'something else'. In the fresh demo, Start call for John Doe opens with COMBINE because three of his ladders are planned for today. When the thing to combine with is a ladder, it is always labelled 'Care Management', whichever programme opened it.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfEngagement → Voice → John Doe → Start call: the window shows COMBINE with 'Future contact scheduled' marked. Choose Proceed separately with a reason and the call starts.

app/services/coordination.py · decide()app/services/coordination.py · gate()app/static/app.js · coordModal()

Outreach ladder: which ladder is shown, and NONE RUNNING

Patient 360 → Engagement tab, Outreach ladder header

Formula

the member's newest ladder (by creation time) whose status is not COMPLETED, EXHAUSTED or STOPPED none → NONE RUNNING, and no progress bar

Why this wayThe panel shows one conversation, the newest one still live. A member with several live ladders shows only one here, and History lists them all. A finished ladder is never shown, so a ladder that just COMPLETED makes the panel read NONE RUNNING, not a full bar.

In the demoPLANNED, one of 3 live ladders

Check it yourselfEngagement → History: John Doe has three rows; his Patient 360 ladder shows one of them.

app/api/engagement.py · plan()app/static/app.js · engagementLadder()

Outreach ladder: 'X of 7 steps complete' and the progress bar

Patient 360 → Engagement tab, Outreach ladder, bar under the header

Formula

X = the steps whose state is 'done'; the total counts only the steps that apply — a step 'not needed' (Escalation, when the member answered on the first channel) is left out bar = round(100 × X / total)%; green at 80% or more, amber at 40% or more, else red 'waiting', 'blocked' and 'failed' do not count as done

Why this wayOne rule for every 'N of M' in the product (progress.step_progress, shared with the Transitions plan and the gap orchestration): a step nobody has to do is not outstanding work. So a first-channel success at REMINDED reads 5 of 6 and a ladder that needed escalation 6 of 7 — each one step (Completion) from done. Only live ladders are shown, and a completed ladder leaves the panel.

In the demo1 of 7 steps complete, bar 14% (red)

Check it yourselfCount the ticked steps in the chain under the bar.

app/services/engagement.py · plan()app/static/app.js · engagementLadder()

Outreach ladder: the seven steps and their state (done / open / waiting / blocked / failed / not needed)

Patient 360 → Engagement tab, Outreach ladder chain

Formula

stage rank: PLANNED 1, ATTEMPTED 2, ESCALATED 3, REACHED 4, SCHEDULED 5, REMINDED 6, COMPLETED 7 (EXHAUSTED and STOPPED 3) Preferred channel: done if the preferred channel, else the first usable one, exists; blocked if none · Outreach: done from rank 2 · Response: done from rank 4, failed if EXHAUSTED or STOPPED, else waiting from rank 2 · Escalation: not needed if reached on one channel, done if more than one channel was tried · next step: done when the member's answer was acted on (see 'Member said'); with no answer recorded, done from rank 5 · Reminder: from rank 6 · Completion: at 7

Why this wayThe step after Escalation is named after what the member's answer demands, for example 'Vendor referral', and not always 'Scheduling'. Its done flag is read from the record that proves it. Response's detail uses the ladder's own label for its next date: 'not attempted — first send due <day>' while PLANNED, 'no answer yet — retry <day>' once something has been sent.

In the demoPreferred channel blocked, Outreach done, Response done, Escalation not_needed, Vendor referral open, Reminder open, Completion open

Check it yourselfMaria Gonzales, Engagement tab: Escalation shows a dash ('the member answered on the first channel') and Vendor referral is still open.

app/services/engagement.py · _steps()app/static/app.js · engagementLadder()

Outreach ladder: each channel 'available' or the reason it is not

Patient 360 → Engagement tab, Outreach ladder, channel list (preferred first)

Formula

checked in order, first failure shown: do-not-contact · opted out of the channel · no recorded consent where the tenant requires it (SMS, Voice) · contact ceiling · quiet hours · no address on file (where the member has an address record) contact ceiling = 4 outbound contacts in the last 7 days (or the member's own limit), counted from the one contact ledger in which a message or a call is one contact; it holds automated outreach until the oldest contact + 7 days and warns a person

Why this wayOne rule set for every channel, voice included; see Part I, Engagement ladder, wait times and the contact ceiling. This list asks as the person looking at it, so a member at the ceiling shows each channel as available with the ceiling's warning; automation (the task-rule sweep) is held. Every caller says whether it is a person or automation; none defaults. The count comes from one ledger that the ceiling, the who-is-engaging header and the Contact history all read, so a call counts once however it ended. The text says 'contacted N times in 7 days' and counts every programme's contacts, the care manager's own included.

In the demoSMS available, Email available, Portal available, Voice available

Check it yourselfSam Okafor's list reads 'no mobile on file' for SMS; Derrick Boateng's reads 'asked not to be contacted at all' on every channel.

app/services/engagement.py · contactability()app/services/engagement.py · contact_ledger()app/services/engagement.py · channels()app/services/voice.py · _apply_outcome()

'Member said: …' with TRIGGERS <act> (amber) or <act> DONE (green), and its evidence

Patient 360 → Engagement tab, Outreach ladder, under the channel list

Formula

each of the 12 recorded outcomes names one next act; done is read from the record that would prove it, tied to THIS ladder: scheduling = a linked appointment that is not cancelled · vendor referral = one opened from this ladder · benefits check, clinical referral = the record this ladder's trigger created (the trigger records it on the ladder) · prior auth = the policy check this ladder's trigger ran: done if not required, or if required and an authorization was submitted after that check; never while the requirement is unknown · evidence = the gap CLOSED with evidence · escalate = more than one channel tried · settle = ladder finished · wait = finished or REACHED · contact details, reminder and next best action = SCHEDULED, REMINDED or finished · a bare 'reached' = never

Why this wayThe ladder's own status never vouches for itself: 'appointment booked' is printed only when an appointment row exists. Every trigger is tied to this ladder: an eligibility check or authorization from last year does not mark today's 'Cost concern' or 'PA required' as done; only the act this ladder's trigger performed does.

In the demoTransportation barrier: TRIGGERS VENDOR REFERRAL

Check it yourselfMaria Gonzales: the amber badge and a button to refer her to the transportation vendor; after the referral is sent the badge turns green.

app/services/engagement.py · next_trigger()app/static/app.js · engTriggerBanner()

Health risk assessment: 'What it found'

Patient 360 → Care tab, Health risk assessment table, last column

Formula

the assessment's stored outcome; for a completed HRA with none, '—' when an HRA is completed with answers and nobody typed an outcome, the stored line is 'P positive finding(s) — <findings>; D of M actioned' or 'A items answered, no positive findings' A = answers to the 8 instrument questions (unknown keys dropped) · positive = 'yes' (also y, true, 1), except the housing question, where 'no' is the finding · M = actions tried, one per positive finding · D = those that succeeded

Why this wayUnlike most values in this paper, the line is a snapshot written once at completion and never recomputed. A referral later closed or refused does not change it. A typed outcome always wins: refused and unreachable HRAs must carry a reason, and that reason is what shows here. Linda Tran's refused HRA shows 'member declined the HRA'. 'D of M' can be below M on purpose: a transport referral needs disclosure consent, and without it the finding is recorded but not actioned.

In the demoREFUSED: member declined the HRA

Check it yourselfComplete John Doe's HRA with two Yes answers: the table's last column shows the one-line summary, and the assessment's event trail carries the same line.

app/services/hra.py · summary()app/services/hra.py · _is_positive()app/api/population.py · assessment_step()app/static/app.js · hraPanel()

Assessment completed window: 'N positive finding(s)', Started, Could not start

Patient 360 → Care tab → Complete HRA → Complete assessment (result window)

Formula

N = the answers judged positive (same rule as 'What it found'), counted in the browser Started = the actions that succeeded, with what was opened; Could not start = the ones that failed, with the reason

Why this wayThe window shows what the answers set off, not only that they were saved: each positive answer opens a referral, a vendor request or a task through that module's own service. A failed action is shown with its reason and the finding stays on the record, so the social-needs rules keep chasing it. An HRA cannot be completed with no answers.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfAnswer Yes to transportation and food and complete: the window lists what started and anything that could not start (for example, a transport referral with no disclosure consent).

app/static/app.js · hraSubmit()app/services/hra.py · extract()app/services/hra.py · generate()

Members (count)

Engagement → Preferences tab, first card and the Members table title

Formula

every Patient in the tenant that this login may see — the panel is applied in the query the number is the length of that list

Why this wayThe Preferences page lists everyone this login could contact, so a provider sees their own panel and a care manager the tenant. Each row is the member's consent record exactly as their Patient 360 consent table shows it — one authority, consent_state(), so the list and the member cannot disagree.

In the demo13 members

Check it yourselfCount the rows of the Members table; they match the card and the title.

app/api/engagement.py · preferences()app/static/app.js · engPreferences()app/services/engagement.py · consent_state()

Do not contact (count)

Engagement → Preferences tab, second card

Formula

the members in the list whose preferences carry do_not_contact = true

Why this wayDo not contact is a wall on every channel at once, whatever each channel's own opt-in says, so it is counted apart from the per-channel figures. The member's row carries the DO NOT CONTACT badge.

In the demo1 members

Check it yourselfCount the DO NOT CONTACT badges in the Members table; they match the card.

app/api/engagement.py · preferences()app/static/app.js · engPreferences()app/services/engagement.py · consent_state()

Preferred channel set (count)

Engagement → Preferences tab, third card

Formula

the members in the list whose stored preferred_channel is not empty, shown 'of' the member count

Why this wayThe stored preference is counted even when that channel is currently blocked: 'they asked for SMS and we cannot use it' is a different fact from 'they have no preference'.

In the demo12 members

Check it yourselfCount the rows whose Preferred column is not a dash; they match the card.

app/api/engagement.py · preferences()app/static/app.js · engPreferences()app/services/engagement.py · consent_state()

Recorded consent by channel (opted in / opted out / never asked / no address)

Engagement → Preferences tab, the Recorded consent by channel table

Formula

for each channel (SMS, Email, Portal, Voice), over the members in the list: opted in — the recorded opt-in is true opted out — it is false never asked — nothing is recorded no address on file — the channel's contact point (mobile, email, phone, portal account) is empty the first three add up to the member count

Why this wayNever asked is neither yes nor no, so it is its own column rather than folded into either (A-184). These are what is RECORDED, not whether outreach would go out right now: quiet hours and the contact ceiling change by the hour and are shown on each member's Patient 360, where 'Usable right now' is asked of the same authority outreach uses.

In the demoSMS {'opted_in': 9, 'opted_out': 0, 'never_asked': 4, 'no_address': 4}, Email {'opted_in': 12, 'opted_out': 0, 'never_asked': 1, 'no_address': 2}, Portal {'opted_in': 12, 'opted_out': 0, 'never_asked': 1, 'no_address': 2}, Voice {'opted_in': 0, 'opted_out': 2, 'never_asked': 11, 'no_address': 3} members

Check it yourselfFor any channel, count the OPTED IN, OPTED OUT and NEVER ASKED badges in that column of the Members table; they match the row, and the three add up to the member count.

app/api/engagement.py · preferences()app/static/app.js · engPreferences()app/services/engagement.py · consent_state()

Transitions of care

9 values

The care manager's list of recent hospital discharges, each with its follow-up clocks (contact within 48 hours, a visit within 7 days, readmission watch for 30 days) and a transition plan whose every step is read from the work beneath it.

Discharges (and the 'Recent discharges' count)

KPI card 1; list title

Formula

inpatient encounters discharged 0 to 120 days ago

Why this way120 days is how long a discharge stays actionable (Part I, Transition-of-care windows). Only inpatient stays count, and the Command Center's 'Discharges (30d)' card counts this same list's last 30 days. For a provider login the list and the cards above it both count the provider's panel only.

In the demo1 discharges

app/services/transitions.py · recent_discharges()app/services/transitions.py · summary()

Contact window open, Visit window open, Window missed

KPI cards 2–4; Status column (CONTACT DUE / VISIT DUE / WINDOW MISSED)

Formula

age = today − discharge date (days) contact window open when age ≤ 2 (48 hours) · visit window open when age ≤ 7 · window missed when age > 7 status badge: WINDOW MISSED if missed, else CONTACT DUE if the contact window is open, else VISIT DUE

Why this wayBoth clocks run from the DISCHARGE date, not from when the record reached the platform, so a late feed cannot buy time. A missed window is shown as missed, not as late success: a 48-hour call made on day three is a missed measure.

In the democontact open 1, visit open 1, missed 0

app/services/transitions.py · recent_discharges()

Contact by, Visit by

Recent discharges table

Formula

contact by = discharge date + contact days (the tenant's contact_hours, 48 by default, rounded up: 2) · visit by = discharge date + visit_days (7 by default); both from transitions.windows

Why this wayCalendar-day deadlines a person can act on. These columns, the KPI cards and each plan step's deadline all read the tenant's own task-rule settings, so a tenant that changes its rules changes all of them together.

In the demodischarged 2 days ago: contact by +2 d, visit by +7 d

app/services/transitions.py · recent_discharges()

Readmitted ≤30d (card and READMITTED badge)

KPI card 5; Status column

Formula

a discharge followed by another INPATIENT admission of the same member 1–30 days later (a same-day admission is not a readmission)

Why this wayA per-discharge flag and a count over the 120-day list; it can still rise for a discharge whose 30 days have not passed. This is a different figure from Finance's 30-day readmission RATE, which judges only discharges whose window has fully elapsed.

In the demo0 readmitted

app/services/transitions.py · recent_discharges()

Plans with a missed step

KPI card 6

Formula

discharges whose transition plan has at least one step in state 'missed' (counted in the browser over the listed plans)

Why this wayThe card that says whether anything has already fallen through, as opposed to what is still due.

In the demo0 plans

app/static/app.js · rTransitions()

Transition plan: 'X of Y actions completed' and its bar

Under each discharge

Formula

Y = the plan's steps except readmission monitoring, the closing step and steps not needed (the discharge itself is counted, as a step already done) X = those steps done or dismissed bar = round( 100 × X ÷ Y ) %; green ≥ 80, amber ≥ 40, red below

Why this wayProgress leaves out the steps nobody can act on (the 30-day watch, the close, a step not needed). A dismissed step counts as settled because someone recorded a decision on it. In the demo: discharge, contact, medication reconciliation and barriers are done, 4 of 7.

In the demo4 of 7 actions completed

app/services/transitions.py · plan()app/static/app.js · transitionPlan()

Transition plan: each step's state (done / open / overdue / missed / not needed …)

Under each discharge, the step chain

Formula

a task step takes its task's state: done, dismissed, open, or overdue (open with due date before today) no task and today past the step's deadline → missed ('window closed <date>, N days ago') step deadline, from the tenant's task rule: discharge + ⌈contact hours ÷ 24⌉ days, or + visit days, or + days Eligibility / PA: approved → done, denied → failed, pending → open, none raised → not needed Visit completion: read from the appointment (completed → done, booked → open)

Why this wayNo step is ticked by hand: each is read from the task, authorization or appointment it depends on, so the plan cannot claim more than has happened.

In the demoDischarge done, Contact done, Medication reconciliation done, Clinical assessment open, Follow-up appointment open, Barriers / transport done, Eligibility / PA not_needed, Visit completion open, Readmission monitoring watching, Transition closed open

app/services/transitions.py · plan()app/services/transitions.py · _step_deadline()

Transition plan: 'N missed' and 'N overdue' badges

Transition plan header

Formula

missed = actionable steps in state missed · overdue = steps whose task is open past its due date

Why this wayTwo different failures: overdue can still be done, missed cannot.

In the demomissed 0, overdue 0

app/services/transitions.py · plan()

Readmission monitoring ('N days left') and the plan's close state

Transition plan, last steps

Formula

watch ends at discharge + 30 days; 'N days left' = watch end − today; done once today ≥ watch end plan close state: failed if readmitted · open if any step is open, overdue or not raised · watching while the 30-day watch runs · otherwise done

Why this wayA transition is not closed at the follow-up visit; it is closed when the member has stayed out of hospital for 30 days, which is what the plan is for.

In the demo28 days left in the window

app/services/transitions.py · plan()

Population

14 values

The VBC manager's population view: every measure's denominator and numerator, saved cohorts, attribution (who the contract holds you responsible for), where cost and opportunity concentrate, and how each provider group performs.

Denominator (per measure)

Quality measures table

Formula

members whose age today is within the measure's age range AND who have an active condition from the measure's denominator code list, minus excluded members

Why this wayEligibility is read from the clinical record at request time, never from a stored flag, so a new diagnosis puts a member into the denominator on the next read. Every member is evaluated (so every member's gaps open and close), but the rate counts the members attributed for the current period — the contract's population, the same one PMPM and settlement divide by. See Part I, One member, one measure.

In the demoDiabetes: HbA1c Control (<8.0%) 6, Diabetes: Eye Exam Performed 6, Statin Adherence (PDC >= 80%) 8, Controlling High Blood Pressure (<140 systolic) 10, Annual Wellness Visit 8

app/services/quality.py · evaluate_tenant()app/services/quality.py · in_denominator()

Numerator (per measure)

Quality measures table

Formula

denominator members who comply: lab measures: latest qualifying result within the look-back (365 days by default) is within the measure's threshold, compared the way the package states it: strictly below for a '<' measure (max_exclusive: HbA1c <8.0, systolic <140), at or below for a '<=' one procedure measures: a qualifying procedure within the look-back medication measures: the best active qualifying medication's adherence (PDC) ≥ 80%

Why this wayThree kinds of evidence, one rule each, all read from the record. The comparison is data on the versioned package, not a guess in code: a result EXACTLY at the threshold (HbA1c 8.0, systolic 140) is NOT controlled under a '<8.0%' or '<140' title. The outcomes 'at target' test, the attention panel and care-plan goals use the same comparison (quality.within_limit).

In the demoDiabetes: HbA1c Control (<8.0%) 2, Diabetes: Eye Exam Performed 0, Statin Adherence (PDC >= 80%) 2, Controlling High Blood Pressure (<140 systolic) 4, Annual Wellness Visit 2

app/services/quality.py · meets_numerator()

Excluded (per measure)

Quality measures table

Formula

denominator members with an active condition from the measure's exclusion code list

Why this wayExcluded members leave the denominator entirely rather than counting as failures, as measure specifications require.

In the demo0 excluded across all measures

app/services/quality.py · excluded()

Open gaps and Rate % (per measure) and its bar colour

Quality measures table

Formula

open gaps = denominator − numerator rate % = round( 100 × numerator ÷ denominator, 1 ); blank with no denominator bar: green when rate ≥ the measure's target · amber within 15 points below it · red further below · blue with no target

Why this wayThe same rate as the Quality screen, from the same function, coloured by the same rule against the same target (the measure package's), so a rate is one colour everywhere it appears.

In the demoDiabetes: HbA1c Control (<8.0%): 4 open, 33.3%, Diabetes: Eye Exam Performed: 6 open, 0.0%, Statin Adherence (PDC >= 80%): 6 open, 25.0%, Controlling High Blood Pressure (<140 systolic): 6 open, 40.0%, Annual Wellness Visit: 6 open, 25.0%

app/services/quality.py · evaluate_tenant()app/static/app.js · rPopulation()

Cohorts: Members (size)

Cohorts table

Formula

members matching EVERY filter of the saved definition: risk tier in the list · PCP equals · an active condition in the code list · enrolled in a listed programme · has (or has no) open gap, optionally on named measures

Why this wayA cohort stores its definition, not its members, so its size is re-evaluated on every read and a member enters or leaves as their record changes.

In the demoRising and high risk 6, Diabetes population 6, Diabetes with open gaps 6, Uncontrolled hypertension 6

app/services/population.py · cohort_members()

Cohort members: Priority (the bold number)

Cohorts table, a cohort opened: Priority column

Formula

the member's My Panel score (panel.panel with the cohort's members as patient_ids): risk tier + unworked and in-flight gaps + discharge windows + adherence + authorization on the clock + stalled outreach + no active coverage + high risk with no programme (see Part I, My Panel); '—' for a member who scores 0 rows sorted by that score, highest first

Why this wayThe same engine as My Panel, on purpose: a member cannot rank high on one screen and low on the other. Scoring 0 on My Panel means 'not listed'; here the member is still a cohort member, so the row stays and shows '—'.

In the demo('John Doe', 80), ('Maria Gonzales', 80), ('Linda Tran', 62)

Check it yourselfOpen the same member on My Panel: the Priority there is the same number.

app/api/population.py · cohort_members()app/static/app.js · cohortMembersPanel()app/services/panel.py · panel()

Cohort members: 'U of S not yet anybody's work'

Cohorts table, a cohort opened: the badge above the member list

Formula

S = members matching the cohort's definition (and, for a provider, on their panel) U = those members with NO open task that has an owner, of ANY kind (has_owner false)

Why this wayIt says how much of the cohort is still nobody's work. 'Unworked' here is about the MEMBER: any owned open task counts, whatever it is for. It is not My Panel's 'unworked gap', which is about a gap with no live intervention, so a member can be 'assigned' here and still carry unworked gaps there.

In the demo0 of 6

Check it yourselfCount the amber 'unworked' badges in the Owner column: they are U.

app/api/population.py · cohort_members()app/static/app.js · cohortMembersPanel()

Cohort members: Owner badge ('assigned' / 'unworked')

Cohorts table, a cohort opened: Owner column

Formula

assigned when the member has at least one open task (status open) with an owner_id, of any rule, campaign or hand; else unworked

Why this wayOwnership, not activity: a task owned by somebody who has not touched it still reads 'assigned'. Read with My Work's Team view for what that owner holds.

In the demo6 assigned

Check it yourselfAssigned + unworked badges = S, the cohort size in the badge above.

app/api/population.py · cohort_members()app/static/app.js · cohortMembersPanel()

Attribution: 'N members attributed'

Attribution panel

Formula

attribution rows in the current open period (the period containing today)

Why this wayAttribution decides accountability, so each member's method and rationale are shown beside the count. The count is the period's LIVE attributions (no end date, or one after today) — the same number as the Attribution summary's 'Total attributed' (population.attributions). An ended row stays in the table, marked 'ended'.

In the demo12 members

app/services/population.py · current_period()

Attribution summary: Total, Added this period, Removed this period, per provider

Attribution summary panel

Formula

total = current-period attributions with no end date · added = those dated on or after the period start · removed = those with an end date · per provider = count of current attributions naming that provider

Why this wayRemoved is a MEASURED zero, stated as such: no de-attribution path writes an end date yet. 'Added this period' counts members attributed AFTER the opening roster: the period's first attribution run is the roster and is dated from the period start (member-months accrue from it); a member added by a later run is dated the business day it ran.

In the demototal 12, added 0, removed 0; Dr. P. Osei 5, Dr. Robert Jones 3, Dr. Sarah Smith 2, Dr. Karen White 2

app/services/vbc_manager.py · attribution_summary()

Unattributed members (and the reason)

Attribution summary panel

Formula

members with no current attribution; reason 'no declared PCP' or 'PCP on record but not attributed'

Why this wayThe members nobody is accountable for are the ones most likely to be missed, so they are listed by name with the reason.

In the demoJon Doe (PCP on record but not attributed)

app/services/vbc_manager.py · attribution_summary()

Highest-opportunity members: Cost, Primary driver, Open gaps (unworked), In flight, ranking

Highest-opportunity members panel

Formula

cost = the member's allowed $ in the period · primary driver = the place of service with the most allowed $ unworked = open gaps with no live intervention (models.iv_works_gap, the same test My Panel uses) opportunity score = round( (cost ÷ 1000) × (1 + unworked × risk weight), 1 ), risk weight HIGH 1.6 · RISING 1.3 · MEDIUM 1.0 · LOW 0.8 top 8 by score (the score itself is not shown)

Why this waySpend alone ranks members nobody can help; open gaps alone ignore cost. Multiplying them puts first the members where money is being spent AND something can still be done. A member with no unworked gaps still ranks, on cost alone.

In the demoJohn Doe: $19262.5, Professional / office, 5 unworked, score 173.4, Maria Gonzales: $19262.5, Professional / office, 5 unworked, score 173.4, Linda Tran: $10707.5, Professional / office, 4 unworked, score 66.4, Fatima Hassan: $10707.5, Professional / office, 4 unworked, score 66.4

Check it yourselfJohn Doe: 17,135 ÷ 1000 × (1 + 5 × 1.6) = 154.2.

app/services/vbc_manager.py · concentration()

'Why ranked here'

Highest-opportunity members panel

Formula

'$<spend> spend, N gap(s) with no intervention running, <tier> risk' (or the in-flight / no-gap variant)

Why this wayBuilt from the same three inputs as the ranking, so the sentence is the ranking's own reason.

In the demo$19,262 spend, 5 gap(s) with no intervention running, high risk

app/services/vbc_manager.py · _concentration_why()

Provider performance: Attributed, High / rising, Open gaps, Gap closure %, PMPM

Provider performance panel

Formula

grouped by attributed provider (else declared PCP, else 'Unattributed') gap closure % = round( 100 × closed ÷ (closed + open), 1 ) PMPM = round( the group's period allowed $ ÷ the group's member months, 2 ) rows sorted by open gaps, most first

Why this wayThe same PMPM arithmetic as the contract, narrowed to one group's members, so group PMPMs are comparable with the contract's. Closed gaps are counted all-time, so a new period does not reset closure credit.

In the demoDr. P. Osei: 5 attributed, 3 high/rising, 16 open, 0.0% closed, $1158.62 PMPM, Dr. Robert Jones: 3 attributed, 2 high/rising, 6 open, 0.0% closed, $1579.52 PMPM, Dr. Sarah Smith: 2 attributed, 1 high/rising, 5 open, 0.0% closed, $2917.76 PMPM, Dr. Karen White: 2 attributed, 0 high/rising, 1 open, 0.0% closed, $1296.42 PMPM

app/services/vbc_manager.py · provider_performance()

Quality

16 values

The VBC manager's quality screen: every measure's rate against its target, how many composite points each open gap is worth, and a what-if model of closing more gaps before committing a team to the work.

Quality opportunity (measure count)

Quality opportunity panel, title count

Formula

number of measures in the tenant's active measure package, rated or not

Why this wayEvery active measure is listed, including one with no eligible members yet, so nothing disappears from the table because it is empty.

In the demo5 measures

app/services/vbc_manager.py · quality_opportunity()

Performance (rate %) and its bar colour

Quality opportunity table, Performance column

Formula

rate % = round( 100 × N ÷ D, 1 ) D = members in the measure's denominator and not excluded, N = those meeting the numerator bar: green when rate ≥ target · amber when within 15 points below target · red further below · blue when there is no target

Why this wayThe rate is recomputed from the members every time the screen is read, never stored, so it cannot go stale. See Part I, A measure across a population. The 15-point amber band is a display convention only; it changes no number.

In the demoDiabetes: Eye Exam Performed 0.0% (0 of 6), Annual Wellness Visit 25.0% (2 of 8), Statin Adherence (PDC >= 80%) 25.0% (2 of 8), Diabetes: HbA1c Control (<8.0%) 33.3% (2 of 6), Controlling High Blood Pressure (<140 systolic) 40.0% (4 of 10)

Check it yourselfClick 'Members' on a row: the drill lists the D members and marks the N who meet it.

app/services/quality.py · evaluate_tenant()app/static/app.js · rateBarHtml()

Target

Quality opportunity table, Target column

Formula

the measure package's configured target (stored, versioned with the package); 'no target set' when none

Why this wayTargets come from the versioned measure package, not from this report. A measure with no target still shows its rate and gaps, but no shortfall: comparing a rate with a target nobody agreed would manufacture one.

In the demoDiabetes: Eye Exam Performed 70.0%, Annual Wellness Visit 60.0%, Statin Adherence (PDC >= 80%) 80.0%, Diabetes: HbA1c Control (<8.0%) 75.0%, Controlling High Blood Pressure (<140 systolic) 75.0%

app/services/vbc_manager.py · quality_opportunity()

Gap to target ('N pts short' / 'met')

Quality opportunity table, Gap to target column

Formula

gap to target = round( target % − rate %, 1 ) percentage points 'N pts short' when above 0, 'met' otherwise; blank when there is no target or no rate

Why this wayPoints here are percentage points of THAT measure's rate, not composite points; the next column converts to composite points.

In the demoDiabetes: Eye Exam Performed 70.0 pts short, Annual Wellness Visit 35.0 pts short, Statin Adherence (PDC >= 80%) 55.0 pts short, Diabetes: HbA1c Control (<8.0%) 41.7 pts short, Controlling High Blood Pressure (<140 systolic) 35.0 pts short

app/services/vbc_manager.py · quality_opportunity()

'N member(s) would close it'

Quality opportunity table, under Gap to target

Formula

members to target = max( ⌈ D × target ÷ 100 ⌉ − N, 0 )

Why this wayThe smallest whole number of additional members who must meet the measure for the rate to reach the target. Rounded UP, because a fraction of a member cannot close a gap.

In the demoDiabetes: Eye Exam Performed 5, Annual Wellness Visit 3, Statin Adherence (PDC >= 80%) 5, Diabetes: HbA1c Control (<8.0%) 3, Controlling High Blood Pressure (<140 systolic) 4

Check it yourselfEye exam: D = 6 and target 70%: ⌈6 × 0.70⌉ = 5 members; N = 0, so 5.

app/services/vbc_manager.py · quality_opportunity()

Open gaps (per measure)

Quality opportunity table, Open gaps column

Formula

open gaps = D − N (members in the denominator who do not meet the numerator)

Why this wayCounted by the measure engine from members, not from stored gap records, so it is what the rate implies. The drill behind 'Members' lists the same members, and a guard fails if the two ever disagree.

In the demoDiabetes: Eye Exam Performed 6, Annual Wellness Visit 6, Statin Adherence (PDC >= 80%) 6, Diabetes: HbA1c Control (<8.0%) 4, Controlling High Blood Pressure (<140 systolic) 6

app/services/quality.py · evaluate_tenant()

'each closure adds N composite points' (points per gap)

What if we close more gaps? panel, each row

Formula

points per gap = 100 ÷ D ÷ n, shown to 2 decimals D = that measure's denominator, n = number of RATED measures (those with a rate)

Why this wayTwo facts in sequence: closing one gap moves that measure's own rate by 100 ÷ D points, and the composite is the plain average of n measures, so it moves by that ÷ n. Small denominators move the composite fastest per gap, which is a property of the contract's scoring method, not a statement about which members matter most. See Part I, Quality opportunity and points per gap.

In the demoDiabetes: Eye Exam Performed 3.33, Annual Wellness Visit 2.5, Statin Adherence (PDC >= 80%) 2.5, Diabetes: HbA1c Control (<8.0%) 3.33, Controlling High Blood Pressure (<140 systolic) 2.0

Check it yourselfEye exam: 100 ÷ 6 ÷ 5 = 3.33.

app/services/vbc_manager.py · quality_opportunity()

'N composite pts' (points available)

Quality opportunity table, under the Opportunity band

Formula

points available = round( (100 ÷ D ÷ n) × open gaps, 2 ) multiplied from the EXACT per-gap value and rounded once, at the end rows are sorted by points available (largest first), then title

Why this wayThe composite points the score would gain if every open gap in that measure closed. Rounded once at the end, so it equals what the scenario engine answers for closing the same gaps (a guard compares the two).

In the demoDiabetes: Eye Exam Performed 20.0, Annual Wellness Visit 15.0, Statin Adherence (PDC >= 80%) 15.0, Diabetes: HbA1c Control (<8.0%) 13.33, Controlling High Blood Pressure (<140 systolic) 12.0

Check it yourselfEye exam: 100 ÷ 6 ÷ 5 × 6 = 20.0 (not 3.33 × 6 = 19.98).

app/services/vbc_manager.py · quality_opportunity()

Opportunity (High / Medium / Low)

Quality opportunity table, Opportunity column

Formula

Low when the measure is already at or above its target otherwise High when points available ≥ 3.0 · Medium when ≥ 1.0 · Low below 1.0 (a measure with no target is banded on points alone)

Why this wayStated thresholds rather than a feeling: a band is a reading aid over the points-available number, and says nothing the number does not.

In the demoDiabetes: Eye Exam Performed High, Annual Wellness Visit High, Statin Adherence (PDC >= 80%) High, Diabetes: HbA1c Control (<8.0%) High, Controlling High Blood Pressure (<140 systolic) High

app/services/vbc_manager.py · _opportunity_band()

Quality now (composite)

What if we close more gaps? result card; also Contract Health

Formula

composite % = round( mean( rate % of every rated measure ), 1 )

Why this wayThe single number the contract settles on: an unweighted mean of the rated measures, computed in one place (quality.composite) and read by every screen that shows it. A measure with no eligible members has no rate and is left out of the mean rather than counted as zero. See Part I, The quality composite.

In the demo24.7 %

app/services/quality.py · composite()app/services/vbc_manager.py · scenario()

Quality after (scenario)

What if we close more gaps? result card

Formula

for each rated measure: closures = min( requested, open gaps ); rate after = round( 100 × (N + closures) ÷ D, 1 ) quality after = round( mean( rate after ), 1 ) (each measure's rate is rounded to 1 decimal before the mean, exactly as the live composite is computed)

Why this wayThe contract engine's own arithmetic with the numerators moved; nothing is stored and nothing is predicted. Denominators do not move and closures are capped at the gaps actually open. The demo value models closing every open gap in the top-opportunity measure.

In the democlose all 6 Diabetes: Eye Exam Performed gaps: 24.7% → 44.7%

Check it yourselfEnter the same closures in the panel and press Model this.

app/services/vbc_manager.py · scenario()

'Composite moves ±N points'

What if we close more gaps? result sentence

Formula

moves = quality after − quality now, both already rounded to 1 decimal, shown to 1 decimal with its sign (computed in the browser)

Why this wayIt is the difference of the two cards above it, so it always matches them, even where it differs by 0.1 from (points per gap × closures) because each side was rounded first.

In the democlose one Diabetes: Eye Exam Performed gap: +3.3 points

app/static/app.js · scenarioResult()

Gate, and 'This clears / still does not clear the quality gate'

What if we close more gaps? result card and sentence

Formula

gate = the active contract's minimum quality % (a contract term) clears the gate when quality after ≥ gate

Why this wayThe gate is read from the contract, never set on this screen. Clearing it is what turns earned savings into a payment; see Part I, Contract performance and settlement.

In the demogate 60.0%; the gate plan (11 closures) gives 60.5%, clears: yes

app/services/vbc_manager.py · scenario()app/services/finance.py · contract_performance()

Settlement change

What if we close more gaps? result card

Formula

settlement after = round( gross savings × shared-savings rate × (1 − quality withhold), 2 ) when quality after ≥ gate and gross savings > 0, otherwise 0 settlement change = settlement after − settlement now cost performance is held at its current value

Why this wayIt models quality only: the savings already generated are held constant and the question is whether the gate now releases them. It is an estimate; the payer reconciles against run-out claims months later.

In the demowith the gate plan: $0.0 → $9069.84 (change $9069.84)

app/services/vbc_manager.py · scenario()app/services/vbc_manager.py · _rate()

Closures modelled, Rate before, Rate after (capped)

What if we close more gaps? result table

Formula

closures modelled = min( requested, open gaps ), '(capped at gaps open)' when more were requested rate before = the measure's rate now; rate after = round( 100 × (N + closures) ÷ D, 1 )

Why this wayYou cannot close more gaps than are open, so a larger request is capped and says so rather than producing a rate above 100%.

In the demoDiabetes: Eye Exam Performed: 6 modelled (capped), 0.0% → 100.0%

app/services/vbc_manager.py · scenario()

'Fill with the gate plan' (pre-filled closures)

What if we close more gaps? panel button

Formula

the gate plan: take measures in order of points per gap (highest first); from each close min( open gaps, ⌈ remaining shortfall ÷ points per gap ⌉ ) until the gate is reached

Why this wayGreedy is optimal here, not a heuristic: every gap in a measure is worth the same number of composite points, so the highest-yield gaps first is the fewest closures that clear the gate. See Part I, Path to the quality gate.

In the demoDiabetes: Eye Exam Performed 6, Diabetes: HbA1c Control (<8.0%) 4, Annual Wellness Visit 1

app/services/vbc_manager.py · gate_path()

Contract Health

16 values

The VBC manager's (and executive's) view of the active value-based contract: is it earning, what is binding it, the path to the quality gate, how each measure is trending, and today's priorities.

N attributed members

Contract banner

Formula

members attributed to the contract in the current contract period (if no period is open: every member in the tenant)

Why this wayA shared-savings contract pays on the members attributed to it, so cost and savings are computed over this population. The Command Center's 'Members' card counts everyone on file, so the two numbers differ by design.

In the demo12 members

app/services/vbc_manager.py · contract_health()app/services/population.py · attributed_patients()

Headline sentence (the binding constraint)

Contract banner, bold sentence

Formula

cost is favourable when actual PMPM ≤ benchmark; quality is met when composite ≥ gate one of four sentences: quality binding (cost ok, quality not) · both met (states the settlement) · both missed · cost binding (quality ok, cost not)

Why this wayA leader's first question is 'which of the two gates is stopping us'. The sentence answers it from the same numbers as the cards below it, so it can never contradict them.

In the demoCost performance is favourable — $19,094 of savings generated — but none of it is earned, because quality is 24.7% against a 60.0% gate. Quality is the binding constraint, not cost.

app/services/vbc_manager.py · _headline()

Actual PMPM (vs benchmark, favourable / unfavourable)

Contract Health card 1

Formula

actual PMPM = round( allowed $ of attributed members' claims with a service date in [period start, min(period end, today)] ÷ member-months, 2 ) member-months = Σ over the period's attribution rows of months between max(attributed, start) and min(end, today); a part month = days ÷ 30.44 favourable when benchmark − actual ≥ 0

Why this wayAllowed dollars, not paid, because the contract benchmark is set on allowed cost. Member-months counts partial months, so a member attributed mid-period costs only their share of the denominator. See Part I, Contract performance and settlement.

In the demo$1,580.00 against a $1,750.00 benchmark — under by $170.00

Check it yourselfFinance → Cost: the same PMPM and member-months are shown with the claims behind them.

app/services/finance.py · cost_summary()app/services/finance.py · contract_performance()

Gross savings

Contract Health card 2

Formula

gross savings = round( round(benchmark − actual PMPM, 2) × member-months, 2 )

Why this waySavings the population generated against the benchmark, before the contract decides how much of it is earned. The per-member variance is rounded to cents before it is multiplied, exactly as it is shown, so anyone can reproduce it from the card: 195.68 × 105.12.

In the demo$19094.4 (Variance against benchmark across 112.32 member-months)

app/services/finance.py · contract_performance()

Quality score (vs gate, on track / at risk)

Contract Health card 3

Formula

composite = round( mean( rate % of every rated measure ), 1 ), each rate already rounded to 1 decimal on track when composite ≥ the contract's minimum quality %, otherwise at risk

Why this wayOne definition read by every screen (quality.composite). The measure rates count the same population as the cost cards on this screen: the members attributed for the contract's period (population.scoring_population; owner decision, 2026-09-24). An unattributed member's gaps are still evaluated and worked, but are not in the score the contract settles on. See Part I, The quality composite.

In the demo24.7% against a 60.0% minimum to earn any savings

app/services/quality.py · composite()app/services/finance.py · contract_performance()

Estimated settlement

Contract Health card 4

Formula

0 when gross savings ≤ 0 or the quality gate is not met otherwise round( gross savings × shared-savings rate × (1 − quality withhold), 2 )

Why this wayThe gate is binary: below it, savings generated earn nothing. That is why the demo shows about $20,570 generated and $0 settled. It is an estimate; the payer reconciles against run-out claims after the period closes.

In the demo$0.0 — Savings generated but the quality gate is not met (24.7% against a 60.0% minimum), so none of it is earned.

app/services/finance.py · contract_performance()

Open care gaps (attributed) ('X of Y have no intervention started')

Contract Health card 5

Formula

Y = care gaps of ATTRIBUTED members whose status is an open state X = those still in OPEN or REOPENED (no work has moved them on)

Why this wayCounted over the contract's attributed members, and labelled '(attributed)' so it is not read as the Command Center's 'Open care gaps', which counts every member's gaps (28 against 29 in the demo). 'No intervention started' is read from the gap's own state: a gap an intervention has touched has moved to in progress, scheduled or awaiting authorization.

In the demo28 open; 27 of 28 have no intervention started

app/services/vbc_manager.py · contract_health()

High / rising risk members (attributed)

Contract Health card 6

Formula

attributed members whose risk tier is HIGH or RISING

Why this wayThe members most likely to drive next quarter's cost. Always 'monitor': a count of risk is not by itself good or bad. Counted over attributed members and labelled so, unlike the Command Center's 'High / rising risk', which counts every member. Tier bands are in Part I, Clinical risk score.

In the demo2 high, 4 rising, of 12 attributed

app/services/vbc_manager.py · contract_health()

Card status chips

Every Contract Health card

Formula

Actual PMPM: favourable when benchmark − actual ≥ 0, else unfavourable · Gross savings: positive when > 0, else attention · Quality score: on track when the gate is met, else at risk · Estimated settlement: positive when > 0, else at risk · Open care gaps: attention when any · High / rising: always monitor

Why this wayEach chip is a stated test on the card's own value, not a judgement typed by a person.

In the demoActual PMPM favorable, Gross savings positive, Quality score at_risk, Estimated settlement at_risk, Open care gaps (attributed) attention, High / rising risk members (attributed) monitor

app/services/vbc_manager.py · contract_health()

Path to the quality gate: Current, Required, Shortfall

Path to the quality gate panel

Formula

Current = the quality composite (same as card 3) Required = the contract's minimum quality % Shortfall = max( round(Required − Current, 1), 0 ) points

Why this wayThe distance to the only threshold that decides whether savings are paid. See Part I, Path to the quality gate.

In the democurrent 24.7%, required 60.0%, shortfall 35.3 pts

app/services/vbc_manager.py · gate_path()

Close these gaps first: Gaps (N of M), Rate after, Composite points

Path to the quality gate, plan table

Formula

measures in order of composite points per gap (highest first); from each take N = min( open gaps M, ⌈ remaining shortfall ÷ points per gap ⌉ ) until the gate is reached points per gap = 100 ÷ denominator ÷ rated measures, EXACT (not the 2-decimal figure shown) rate after = round( 100 × (numerator + N) ÷ denominator, 1 ) composite points = round( N × points per gap, 2 )

Why this wayGreedy is the fewest closures here because every gap in a measure is worth the same number of points. The arithmetic uses the exact per-gap value and rounds once at the end, so closing all six eye-exam gaps is worth +20.0 here and 20.0 on the Quality screen: one quantity, one number (a guard compares the two).

In the demoDiabetes: Eye Exam Performed: close 6 of 6 → 100.0%, +20.0, Diabetes: HbA1c Control (<8.0%): close 4 of 4 → 100.0%, +13.33, Annual Wellness Visit: close 1 of 6 → 37.5%, +2.5

app/services/vbc_manager.py · gate_path()

Projected if the plan runs, and 'N gap(s) would take the score to P%'

Path to the quality gate panel

Formula

projected = round( Current + Σ composite points of the plan, 1 ) N = Σ gaps in the plan

Why this wayWhat the arithmetic gives if exactly those gaps close and nothing else moves: a projection, not a forecast. 'Model this' on the Quality screen, run with the same plan, gives the same answer to within 0.1 point (the scenario engine re-computes each rate instead of adding points).

In the demo11 gaps → 60.5% against a 60.0% gate

app/services/vbc_manager.py · gate_path()

Trend: First and Latest

Trend table

Formula

each metric is captured as a dated point: measure rates, the quality composite, open gaps of attributed members, high/rising members, attributed members, PMPM First = the earliest point · Latest = today's point (re-captured on every read; one point per day, the day's last value wins)

Why this wayA trend is only as honest as its points, so today's is captured from the live engines rather than typed. The early points in the demo are a seeded synthetic backfill and the screen says so.

In the demoAttributed members 12.0 → 12.0, PMPM (allowed) 1664.0 → 1580.0, Open care gaps 36.0 → 28.0, Annual Wellness Visit 12.4 → 25.0, Controlling High Blood Pressure (<140 systolic) 27.4 → 40.0, Diabetes: HbA1c Control (<8.0%) 20.7 → 33.3

app/services/trends.py · capture()app/services/trends.py · catalogue()

Trend: Change and Direction (improving / deteriorating / flat)

Trend table

Formula

change = round( latest − first, 2 ) improving when change > 0 for a metric where up is good (rates, composite, attributed members), or change < 0 where down is good (open gaps, high/rising risk, PMPM); flat when change = 0

Why this wayDirection is judged against each metric's own sense of good: open gaps falling is improvement. Colouring every rise green would teach people to stop reading the column.

In the demoAttributed members flat, PMPM (allowed) improving, Open care gaps improving, Annual Wellness Visit improving, Controlling High Blood Pressure (<140 systolic) improving, Diabetes: HbA1c Control (<8.0%) improving, Diabetes: Eye Exam Performed flat, Statin Adherence (PDC >= 80%) improving, Quality composite improving, High or rising risk members improving

app/services/trends.py · catalogue()

Trend: 'N point(s) over D day(s)'

Trend table, History column

Formula

N = the number of captured points · D = days between the first and the latest point

Why this wayShown so a reader can tell a trend from a single reading: a metric with one point has no trend, and the projection engine refuses to project it (Part I, Trends and projection).

In the demoAttributed members 1 over 0 d, PMPM (allowed) 7 over 42 d, Open care gaps 7 over 42 d, Annual Wellness Visit 7 over 42 d, Controlling High Blood Pressure (<140 systolic) 7 over 42 d, Diabetes: HbA1c Control (<8.0%) 7 over 42 d, Diabetes: Eye Exam Performed 7 over 42 d, Statin Adherence (PDC >= 80%) 7 over 42 d, Quality composite 7 over 42 d, High or rising risk members 7 over 42 d

app/services/trends.py · catalogue()

Today's priorities (count, order, why, due)

Today's priorities panel

Formula

a row is raised when: the quality gate is not met (severity 100, due the contract end) · a discharge is 3–7 days old (90, due today) · up to 2 cost categories over benchmark (80) · a campaign active ≥ 3 days with every task still open (70) · up to 2 measures below target (60 + that measure's composite points available) rows sorted by severity, highest first

Why this waySeverity is a fixed, published scale rather than a hidden score: the gate outranks everything because below it no savings are paid at all. Each row's 'why' quotes the numbers that raised it.

In the demoQuality gate is not met (100), Diabetes: Eye Exam Performed below target (80.0), Annual Wellness Visit below target (75.0)

app/services/vbc_manager.py · priorities()app/services/vbc_manager.py · _stalled_campaigns()

Finance

24 values

The VBC manager's and executive's money view: cost per member per month and what drives it, utilisation, the contract's settlement arithmetic, the outcomes the contract is judged on, and observed clinical change.

Attributed members

Cost KPI cards

Formula

members attributed in the current attribution period that contains today (no open period: every member in the tenant)

Why this wayCost is only meaningful over the population a contract holds you responsible for, so every cost figure on this screen divides by this population's member-months.

In the demo12 members

app/services/finance.py · cost_summary()app/services/population.py · attributed_patients()

Member months

Cost KPI cards; also behind every PMPM

Formula

Σ over the period's attributions of months between max(attributed date, period start) and min(period end, today) months between = whole months + remaining days ÷ 30.44, rounded to 2 decimals (no open period: members × months elapsed since 1 January; no members, no member-months)

Why this wayIt counts months ELAPSED to date, so a cost-to-date is divided by the months it was incurred over, not by the year. A part month counts as its share (30.44 = the average days in a month). Known inconsistency: this period total does not stop a member at an attribution end date, while the monthly PMPM chart below does.

In the demo112.32 member-months

app/services/finance.py · member_months()app/services/finance.py · months_between()

Allowed, Paid, Member liability

Cost KPI cards

Formula

sums of allowed, paid and member_liability over claims of attributed members with a service date between the period start and min(period end, today)

Why this wayAllowed = what the plan recognises for the service (paid + member liability). Cost performance is judged on allowed, because that is what the benchmark is set on; paid alone would reward shifting cost to the member.

In the demoallowed $177466.0 = paid $140628.88 + member liability $36837.12

app/services/finance.py · cost_summary()

PMPM allowed (actual PMPM)

Cost KPI cards; Contract card; Contract Health

Formula

PMPM = round( allowed ÷ member months, 2 ); blank when member months is 0

Why this wayThe single cost figure the contract is scored on, computed in one place and read by Finance, Contract Health and the trend. See Part I, Contract performance and settlement.

In the demo$1580.0 = 177466.0 ÷ 112.32

app/services/finance.py · cost_summary()

ED visits and Inpatient admissions (count and per 1,000/yr)

Cost panel, utilisation lines

Formula

n = encounters of that kind for attributed members, admitted in the period per 1,000/yr = round( n ÷ (member months ÷ 12) × 1000, 1 )

Why this wayPer 1,000 members per year is the unit payer benchmarks are published in, so the figure can be compared with a benchmark without conversion. The same two figures appear in the Utilisation panel, computed by a second function with the same rule.

In the demoED visits: 3 → 320.5 per 1,000/yr, Inpatient admissions: 4 → 427.4 per 1,000/yr

app/services/finance.py · cost_summary()

Contract card: Benchmark PMPM

Contracts panel

Formula

the contract's benchmark PMPM (a stored contract term, not computed)

Why this wayThe number actual cost is measured against. It is a negotiated term, so it is read from the contract, never derived.

In the demo1750 $ PMPM

app/services/finance.py · contract_performance()

Contract card: Actual PMPM and Variance PMPM

Contracts panel

Formula

actual = the period PMPM above variance = round( benchmark − actual, 2 ); positive = under benchmark (good)

Why this waySign convention chosen so a positive variance is money saved. Known limit: every active contract card shows the tenant's current-period PMPM; the contract's own period is printed but does not narrow the cost.

In the demo1750.0 − 1580.0 = 170.0

app/services/finance.py · contract_performance()

Contract card: Gross savings

Contracts panel

Formula

gross savings = round( variance PMPM × member months, 2 ); negative when over benchmark

Why this wayThe variance is the displayed, cent-rounded figure, so the product can be checked on a calculator from the card itself.

In the demo170.0 × 112.32 = $19094.4

app/services/finance.py · contract_performance()

Contract card: Quality score and 'Quality gate met / NOT met'

Contracts panel

Formula

quality score = the quality composite (Part I, The quality composite) gate met = composite exists and composite ≥ the contract's minimum quality %

Why this wayRead from the one composite function every screen uses; the minimum is the contract's term.

In the demo24.7% against 60.0% → NOT met

app/services/finance.py · contract_performance()app/services/quality.py · composite()

Contract card: Est. settlement

Contracts panel

Formula

0 when gross savings ≤ 0, or when the quality gate is not met otherwise round( gross savings × shared-savings rate × (1 − quality withhold), 2 )

Why this wayThe gate is binary: savings below it are generated but not earned. Labelled an estimate, because the payer reconciles against run-out claims after the period.

In the demo$0.0 — Savings generated but the quality gate is not met (24.7% against a 60.0% minimum), so none of it is earned.

app/services/finance.py · contract_performance()

Cost drivers: claims, category PMPM, share %

What is driving PMPM panel

Formula

per place of service: claims = count; allowed = Σ allowed category PMPM = round( category allowed ÷ TOTAL period member months, 2 ) share % = round( 100 × category allowed ÷ total allowed, 1 )

Why this wayEvery category divides by the same member-months, so the category PMPMs add up to the total PMPM: a driver is a slice of the one number, not a separate average.

In the demoProfessional / office: 72 claims, $646.37 PMPM, 40.9%, Inpatient: 4 claims, $505.7 PMPM, 32.0%, Pharmacy: 48 claims, $370.51 PMPM, 23.5%, Emergency department: 3 claims, $57.43 PMPM, 3.6%, Outpatient: 0 claims, $0.0 PMPM, 0.0%

Check it yourselfThe category PMPMs add up to the total PMPM on the Cost cards (to the cent, allowing for rounding).

app/services/finance.py · cost_drivers()

Cost drivers: Benchmark and Variance (over / under)

What is driving PMPM panel

Formula

benchmark = the first active contract's benchmark for that category ('not set' when there is none) variance = round( benchmark − category PMPM, 2 ); red (over benchmark) when below 0

Why this waySame sign convention as the contract: positive is under benchmark. A category with no contracted benchmark is not judged at all rather than compared with an invented one.

In the demoProfessional / office +53.63, Inpatient +34.3, Pharmacy +9.49, Emergency department +32.57, Outpatient +40.0

app/services/finance.py · cost_drivers()

Cost drivers: Month over month

What is driving PMPM panel

Formula

for the last two calendar months: each month's category allowed ÷ that month's member months change = round( current month − prior month, 2 ); the current month is partial and says so

Why this wayA month-to-date figure read as a full month nearly always looks like an improvement, so the partial month is flagged every time.

In the demoProfessional / office 16.23 → 42.82 (+26.59), Inpatient 0.0 → 3287.04 (+3287.04), Pharmacy 0.0 → 0.0 (+0.00), Emergency department 188.6 → 0.0 (-188.60), Outpatient 0.0 → 0.0 (+0.00)

app/services/finance.py · cost_drivers()app/services/finance.py · member_months_window()

Cost drivers: Members (per category)

What is driving PMPM panel

Formula

distinct members among the category's claims

Why this wayTells a reader whether a category is a few expensive members or a broad pattern, which calls for a different action.

In the demoProfessional / office 12, Inpatient 3, Pharmacy 12, Emergency department 3, Outpatient 0

app/services/finance.py · cost_drivers()

Cost drivers: headline sentence and small-population note

What is driving PMPM panel

Formula

if any category is over its benchmark: name the one furthest over and its $ PMPM over otherwise: name the largest category by allowed dollars and its share the volatility note appears when attributed members < 100

Why this wayOne sentence answering 'what should I look at first'. Under 100 members a single admission moves PMPM visibly, so the screen warns before anyone reads a trend into it.

In the demoProfessional / office is the largest cost driver at 40.9% of allowed dollars. No category benchmark is configured on the contract, so this is the largest driver, not a measured opportunity.

app/services/finance.py · cost_drivers()

Utilisation: Inpatient admissions, ED visits, Ambulatory / PCP visits per 1,000/yr

Utilisation panel

Formula

n = encounters of attributed members admitted in the period (inpatient; emergency; ambulatory + virtual) per 1,000/yr = round( n ÷ (member months ÷ 12) × 1000, 1 )

Why this wayThe payer-standard unit. Ambulatory counts virtual visits too, because a telehealth primary-care visit is the utilisation a value-based contract wants more of.

In the demoInpatient admissions 427.4 (n=4), ED visits 320.5 (n=3), Ambulatory / PCP visits 1068.4 (n=10)

app/services/finance.py · utilization()

Utilisation: Average length of stay

Utilisation panel

Formula

round( Σ (discharge date − admission date) days ÷ number of stays, 1 ) over attributed members' inpatient stays in the period that have ended

Why this wayStays still in progress are left out, because a stay counted before it ends would make length of stay look shorter than it is.

In the demo4 days

app/services/finance.py · utilization()

30-day readmission rate (Utilisation panel and Patient outcomes)

Utilisation panel; Patient outcomes cards

Formula

index discharges = inpatient discharges whose 30-day window has fully passed readmissions = index discharges followed by another inpatient admission 1–30 days later (at most one each) rate = round( 100 × readmissions ÷ index, 1 ); not available with no index discharge 'window still open' = discharges less than 30 days ago, excluded

Why this wayA discharge whose window has not closed is excluded rather than counted as 'not readmitted', which would flatter the rate. Over the same population and window as the other utilisation rows: attributed members' index discharges inside the period to date (population.scoring_population). A readmission is looked for in any later stay of the same member.

In the demo33.3% = 1 of 3 index discharges; 1 still inside the window

app/services/outcomes.py · readmission_rate()

Post-discharge follow-up within 7 days

Utilisation panel

Formula

index = inpatient discharges at least 7 days ago followed = those with an ambulatory or virtual visit 1–7 days after discharge rate = round( 100 × followed ÷ index, 1 ); not available with no index discharge

Why this wayThe same windowing rule as readmissions: only discharges whose 7 days have passed are judged. Like readmissions it is computed over attributed members' discharges inside the period.

In the demo0.0%

app/services/finance.py · post_discharge_followup()

Specialist visits and Avoidable ED ('NOT AVAILABLE')

Utilisation panel

Formula

not computed; shown with the reason it cannot be

Why this wayThe synthetic data has no specialty on encounters and no avoidable-ED classification (such as the NYU algorithm). A metric that cannot be measured says so in place instead of showing a zero, because a zero is an answer.

In the demoSpecialist visits, Avoidable ED utilisation

app/services/finance.py · utilization()

PMPM by month (member months, allowed, PMPM, 'partial')

PMPM by month panel

Formula

for each of the last 6 calendar months: member months = Σ over attributions clipped to the month (and to any attribution end date); allowed = that month's claims PMPM = round( allowed ÷ member months, 2 ); partial while today is inside the month (finance.month_is_partial, the same rule the cost drivers' month-over-month uses)

Why this wayEach month is divided by its own member-months, so months are comparable. The current month is flagged partial because its PMPM is month-to-date.

In the demo2026-05 $3255.89, 2026-06 $2496.58, 2026-07 $1119.02, 2026-08 $1428.11, 2026-09 $204.82, 2026-10 $3329.86 (partial)

app/services/finance.py · pmpm_by_month()app/services/finance.py · member_months_window()

Outcomes the contract is judged on (quality, gap closure, ED per 1,000, PMPM, readmissions, clinical improvement)

Outcomes panel

Formula

quality = the quality composite · gap closure = round( 100 × Σ numerators ÷ Σ denominators over rated measures, 1 ) (pooled compliance) · ED per 1,000 and PMPM = the cost summary's figures · readmissions = the 30-day rate · clinical improvement = round( 100 × members improved ÷ members evaluated, 1 ) across the tracked analytes

Why this wayEach row re-uses the engine that owns the number, so this table cannot disagree with the panels above it. A value the engine cannot compute yet (for example, no discharge whose 30-day window has closed) shows as not available with the reason, never as 0.0, and is not frozen into the trend: zero is a score somebody earned.

In the demoImprove quality (composite measure rate) 24.7 %, Close care gaps 26.3 %, Reduce avoidable utilization (ED visits) 320.5 per 1,000/yr, Lower total cost of care 1580.0 $ PMPM, Reduce avoidable utilization (30-day readmissions) 33.3 %, Patient outcomes (members whose tracked values improved) 71.4 %

app/services/finance.py · compute_outcomes()

Member experience and 'Succeed in value-based contracts' (NOT MEASURED)

Outcomes panel

Formula

not computed; a NOT MEASURED badge

Why this wayThere is no survey (CAHPS) or grievance source in the platform, so these are declared unmeasured rather than estimated.

In the demoImprove member experience, Succeed in value-based contracts

app/services/finance.py · compute_outcomes()

Patient outcomes: evaluated, improved (%), worsened, mean change, at target, not evaluable

Patient outcomes table

Formula

four tracked results, all lower-is-better: HbA1c (target 8.0%), systolic BP (140 mmHg), total cholesterol (200 mg/dL), creatinine (1.3 mg/dL) per member: change = latest − earliest reading on file; improved when change < 0, worsened when > 0; not evaluable with only one reading improved % = round( 100 × improved ÷ evaluated, 1 ); mean change = round( mean of changes, 2 ); at target = latest ≤ target

Why this wayAn observed change between readings, not a claim that any intervention caused it. Computed over every member with readings, with no attribution or date window, and a member with a single reading is counted as not evaluable instead of unchanged.

In the demoHbA1c: 3 of 5 improved (60.0%), mean -0.24 %, Systolic BP: 4 of 5 improved (80.0%), mean -6.4 mmHg, Total cholesterol: 2 of 3 improved (66.7%), mean +1 mg/dL, Creatinine: 1 of 1 improved (100.0%), mean -0.1 mg/dL

app/services/outcomes.py · population_change()app/services/outcomes.py · patient_trajectory()

Campaigns, actions and outcomes

53 values

The VBC manager's (and the executive's) Actions & Outcomes screen: the operational KPI board, population-level next best actions, how providers are answering the opportunities sent to them, how each campaign's members are progressing, and what the care team has delivered.

Operational KPIs: 'N of M measurable here'

Operational KPIs panel, header

Formula

M = every KPI the business case names (16, in four groups) N = those with a value; a card with no value reads 'not measured here' and states why

Why this waySix KPIs (average case resolution, first contact resolution, CAHPS, grievances, member satisfaction, research time) have no source in this platform and are always shown with the system that owns them, never as 0 and never dropped. A rate over an empty denominator also shows as 'not measured here', so N can move as data arrives. The panel shows no targets by design: none has been agreed.

In the demo10 of 16

Check it yourselfCount the cards that show a number: they equal N; the rest say 'not measured here'.

app/services/opskpi.py · board()app/static/app.js · opsKpiPanel()

Referral completion (%, 'over N')

Operational KPIs, Operational group

Formula

closed = internal referrals at OUTCOME_CONFIRMED + vendor referrals at CONFIRMED N = settled referrals: internal OUTCOME_CONFIRMED, DECLINED, CANCELLED, UNABLE_TO_CONTACT + vendor CONFIRMED, DECLINED, FAILED, CANCELLED rate = round( 100 × closed ÷ N, 1 ); 'not measured here' when N = 0

Why this wayIn-flight referrals are left out of the denominator rather than counted as failures. Internal COMPLETED ('team reports it done') is the doer's claim, not a settled state, so it is in neither the numerator nor N until the outcome is confirmed. Closed is counted inside settled, so the rate cannot pass 100%, and it is the same calculation (referrals.loop_closed) as the per-person 'Referrals (loop closed)' column further down this screen. Tenant-wide, all time. See Part I, Referral SLA, for what settles a referral.

In the demo100% over 1

Check it yourselfIn the demo the one settled referral is a confirmed vendor referral: 1 ÷ 1 = 100%.

app/services/opskpi.py · board()app/services/referrals.py · loop_closed()

Cross-functional handoffs

Operational KPIs, Operational group

Formula

count of internal referrals in the tenant, any status, all time

Why this wayA count, so 0 is shown as 0 (a measurement), not as 'not measured'. Vendor referrals are not counted. The card also prints 'over N' where N is the same count, which adds nothing.

In the demo0 handoffs

app/services/opskpi.py · board()

Case backlog (mirrored)

Operational KPIs, Operational group

Formula

count of mirrored service cases with no closed_on date; 'over N' = all mirrored cases 'not measured here' when no case has been mirrored at all

Why this waySalesforce holds the real backlog; this counts only the cases this platform has been told about, and the card says so. With nothing mirrored the card refuses rather than printing 0, because 'no view' is not 'empty'.

In the demo1 over 1

app/services/opskpi.py · board()

Complaints (mirrored)

Operational KPIs, Member experience group

Formula

count of mirrored service cases whose case_type, lower-cased, is complaint; 'over N' = all mirrored cases 'not measured here' when no case has been mirrored

Why this waySame limit as the backlog: only complaints mirrored from Salesforce. CAHPS, grievances and member satisfaction beside it are never computed here and say which system owns them.

In the demo1 over 1

app/services/opskpi.py · board()

Outreach response (%, 'over N')

Operational KPIs, Engagement group

Formula

N = outreach ladders with at least one attempt numerator = those of the N where the member responded: the last recorded outcome is one where contact was made (anything the member said, or 'Member reached'), or the ladder is at REACHED or beyond rate = round( 100 × numerator ÷ N, 1 )

Why this wayCounted per ladder, not per message. 'No answer', 'Voicemail left' and 'Wrong number' are outcomes of an attempt nobody answered, so they are not responses. The numerator is drawn from the attempted ladders it is divided by, so the rate is bounded by 100%. The care-team 'Response' column on this screen is a different figure (members with an inbound message, per person).

In the demo100% over 2

Check it yourselfIn the demo both attempted ladders are at REACHED (needs appointment; transportation barrier): 2 ÷ 2.

app/services/opskpi.py · board()app/services/engagement.py · responded()

HRA completion (%, 'over N')

Operational KPIs, Engagement group

Formula

the latest HRA whose window has opened, per ENROLLED member; N = every ENROLLED member rate = round( 100 × members whose HRA is COMPLETED on or before its due date ÷ N )

Why this wayThe same figure GET /population/kpis/assessments serves, from the one function (A-060). An HRA completed late is a completed assessment and a missed obligation, so it does not count; one nobody attempted counts against the plan exactly as much as one the member refused, so the plan cannot choose a friendlier denominator.

In the demo62% over 8

app/services/assessments.py · kpis()app/services/opskpi.py · board()

Care-management enrolment (%, 'over N')

Operational KPIs, Engagement group

Formula

N = programme enrolments at OFFERED, ACCEPTED, ENROLLED or DECLINED rate = round( 100 × those at ENROLLED ÷ N )

Why this wayEnrolled over everyone offered, not over enrolments, so declines count against it; an offer the member accepted is an offer. The same figure GET /population/kpis/assessments serves, from the one function (A-060). Known limit: an enrolment that later moved to DISENROLLED (or is still ELIGIBLE) is in neither count, so a member who enrolled and left drops out of the denominator.

In the demo67% over 12

app/services/assessments.py · kpis()app/services/opskpi.py · board()

Members with any engagement (%, 'over N')

Operational KPIs, Engagement group

Formula

N = every member in the tenant numerator = distinct members with any engagement message (either direction) or an outreach ladder with at least one attempt rate = round( 100 × numerator ÷ N, 1 )

Why this wayThe card's basis reads 'members this platform has attempted to reach', but an inbound-only message also counts, so it is 'any two-way contact recorded'. N is the whole tenant, not the attributed population.

In the demo15.4% over 13

app/services/opskpi.py · board()

Duplicate outreach prevented

Operational KPIs, Productivity & coordination group

Formula

number of refused-duplicate and coordination-gate audit events in the last 30 days (measured back from the current time), excluding routine sweep idempotency

Why this wayA duplicate that never happens looks like nothing happening unless it is counted. The same function feeds the Duplicate activity prevented screen. No denominator is shown: there is no meaningful 'out of'.

In the demo0 prevented

app/services/prevention.py · summary()app/services/opskpi.py · board()

Completed work per coordinator ('over N')

Operational KPIs, Productivity & coordination group

Formula

N = distinct people who own at least one task (any status) value = round( tasks at done owned by one of those N ÷ N, 1 )

Why this wayA workload figure, not a score, and not ranked per person. The numerator is drawn from the same people as N, so a completed task nobody owned is no coordinator's work and does not inflate it. All time.

In the demo3 over 1

Check it yourselfDemo: 3 done tasks, all owned by Jane Smith, 1 owner: 3 ÷ 1 = 3.0.

app/services/opskpi.py · board()

Population next best actions (count)

Population next best actions panel, title count

Formula

number of recommendations after sorting by score (highest first) and keeping the first 8

Why this wayAt most 4 quality pushes plus one each for transitions, adherence, cost and enrolment can be raised, which is 8, so the limit never drops a row today. The whole panel is advisory: nothing is created until a person launches it.

In the demo7 recommendations

app/services/vbc_manager.py · population_actions()

Order of the recommendations

Population next best actions table, row order

Formula

sorted by a hidden score, highest first (ties keep this order): quality push = composite points available × 10 · post-discharge = 60 + discharges due · adherence = 55 · cost = 50 + |PMPM over benchmark| ÷ 10 · enrolment = 48

Why this wayThe score is an ordering rule, not a measurement, and it mixes units (composite points against dollars), so the exchange rate between them is a policy choice. A cost row needs more than $50 PMPM over benchmark to outrank adherence, and more than $100 plus $10 per discharge due to outrank the post-discharge queue; a quality push outranks that queue once it is worth more than 6 composite points (plus 0.1 per discharge due). In the demo all four quality pushes (13.33 to 20 points) rank first. The score is not shown.

In the demoquality_push 200.0, quality_push 150.0, quality_push 150.0, quality_push 133.3, transitions 61.0, adherence 55.0, enrollment 48.0

app/services/vbc_manager.py · population_actions()

Quality push rows: Members and 'N open gaps, P points below target — worth X composite points'

Population next best actions table, 'Prioritise … outreach' rows

Formula

the first 4 measures in the Quality opportunity order (points available, largest first), skipping any with 0 open gaps Members = open gaps = D − N · points below target = round( target − rate, 1 ), shown only when above 0 · worth = composite points available

Why this wayOnly the top 4 are offered, so a measure ranked fifth (in the demo, blood pressure control) is not proposed even with open gaps. Open gaps equal members because a member has at most one open gap per measure. Every figure is the Quality screen's own; see Part I, Quality opportunity and points per gap.

In the demoEED-RETINAL: 6 members, AWV: 6 members, SUPD-STATIN: 6 members, CDC-HBA1C: 4 members

Check it yourselfThe same four measures lead the Quality screen with the same open gaps and composite points; 'Open' lists exactly that many members.

app/services/vbc_manager.py · population_actions()app/services/vbc_manager.py · quality_opportunity()

Post-discharge follow-up row: Members

Population next best actions table, 'Work the post-discharge follow-up queue'

Formula

number of distinct MEMBERS with an inpatient discharge 0 to 120 days old that is still inside the visit window (today − discharge date ≤ the tenant's visit window, 7 days by default) and whose follow-up has not been attended (transitions.open_followup)

Why this wayThe window closes whether or not anyone acts, so it is the one event-driven row. Members, as the column says: a member with two owed discharges counts once. The count is the length of the list its Open button shows (vbc_manager.action_members), each member with the discharge and the date the visit is due by. See Part I, Transition-of-care windows.

In the demo1 discharges

Check it yourselfThe Transitions screen's 'Visit window open' card shows the same number.

app/services/vbc_manager.py · population_actions()app/services/vbc_manager.py · action_members()app/services/transitions.py · recent_discharges()

Medication adherence row: Members ('N member(s) below the 80% PDC threshold')

Population next best actions table, 'Assign outreach for medication adherence'

Formula

distinct members with an active medication whose PDC is below 0.80 (attention.below_pdc: a PDC of 0 is below; a PDC never measured counts neither way)

Why this wayCounts the whole tenant, not only attributed members. It uses the same test as the member's own risk and attention panels and My Panel, so a member recorded at 0% (never filled) is counted everywhere. The Open button lists exactly these members, each with the medications below threshold (vbc_manager.action_members).

In the demo3 members

app/services/vbc_manager.py · population_actions()app/services/vbc_manager.py · action_members()

Cost row: '$X PMPM over benchmark across N member(s)'

Population next best actions table, 'Investigate … utilisation'

Formula

among cost categories whose variance (benchmark − category PMPM) is below 0, the most negative $X = |variance|, 2 decimals · Members = distinct attributed members with a claim in that category in the period

Why this wayA financial action, not a clinical one, and raised only when the contract sets a benchmark for the category; with none there is nothing to be over. The figures are the Financial screen's cost-driver row for that category.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfFinancial → Cost drivers: the same category shows the same variance and Members; 'Open' lists that many members.

app/services/vbc_manager.py · population_actions()app/services/finance.py · cost_drivers()

Cost row 'Open': Claims and Allowed per member

Drill modal behind the cost row

Formula

per attributed member with a claim in the category, service date from the period start to min(period end, today): Claims = count · Allowed = Σ allowed, rounded to cents as it is added rows sorted by Allowed, largest first

Why this wayThe members behind the dollar figure. The row count equals the recommendation's Members. The allowed total is summed per member and rounded as it goes, while the category row rounds once at the end, so the two can differ by a cent if a claim amount carries sub-cent precision.

In the demo— nothing to show in the fresh demo tenant

app/services/finance.py · category_members()

Enrolment row: Members ('N high-risk member(s) are in no programme')

Population next best actions table, 'Offer care management to unenrolled high-risk members'

Formula

number of members in the tenant who are HIGH risk and enrolled in no care-management programme (nba.unenrolled_high_risk, the test the member-level engine uses), counted over every member

Why this wayA high-risk member outside every programme is surfaced by no standing measure, so it is raised here. It reads the fact directly rather than counting 'offer enrolment' items in a ranked list, so no list limit can cut it, and a member whose offer was deferred on their own page is still a member outside every programme. The Open button lists exactly these members (vbc_manager.action_members). See Part I, Next best action.

In the demo1 members

app/services/vbc_manager.py · population_actions()app/services/vbc_manager.py · action_members()app/services/nba.py · unenrolled_high_risk()

Post-discharge, adherence and enrolment rows 'Open': the member list

Drill modal behind those three Population next best actions rows

Formula

one row per member the recommendation counted, by the same function (vbc_manager.action_members): Member · Risk · Why counted (the discharge and its visit due date; each medication below the PDC threshold with its PDC; 'high risk, in no care-management programme') sorted by member name; the number of rows equals the row's Members

Why this wayA count that cannot open to the members inside it is a poster. The list and the count come from one function, so they cannot disagree, and each row says why the member is on it.

In the demotransitions: 1 members, first Sam Okafor (discharged 2026-10-10; follow-up visit due by 2026-10-17)

Check it yourselfClick Open on the post-discharge, adherence or enrolment row: the modal lists as many members as the row's Members column.

app/services/vbc_manager.py · action_members()app/api/population.py · population_action_members()

Quality push 'Open': the member list (count, Risk, Barrier, In flight, Recommended action)

Drill modal behind each quality push row (also behind 'Members' on the Quality screen)

Formula

one row per live care gap on the measure (statuses OPEN, IN_PROGRESS, SCHEDULED, AWAITING_AUTH, SERVICE_COMPLETED, REOPENED) sorted HIGH, RISING, MEDIUM, LOW, then name · In flight = the gap's current intervention, else 'nothing started', and beneath it the intervention's pending authorization ('auth <ref> · <status>') when one is blocking · Recommended action = that gap's member-level next best action

Why this wayThe recommendation is the same one the member's care manager sees, so the two screens cannot disagree. Barrier is the measure engine's own reason the gap is open. The list is the attributed members with a live gap — the population the measure's rate and its open-gap count are over (services/population.scoring_population). A blocking authorization is found through the intervention that raised it, so it is shown beside the intervention's status rather than instead of it.

In the demoEED-RETINAL: 6 members, first John Doe (HIGH)

Check it yourselfThe number of rows equals the row's Members (6 for the eye exam in the demo).

app/services/vbc_manager.py · measure_members()app/static/app.js · drillBody()

Provider responses (count)

Provider responses panel, title count

Formula

count of the opportunities in every provider's own set (opportunities.for_provider: member still on that provider's panel), any status, all time

Why this wayIt is the sum of the Sent column. Opportunities whose member has left the provider's panel are counted apart in a line under the table ('no longer on their provider's panel'), each provider's with its own drill. The panel is hidden entirely when no opportunity has been sent.

In the demo2 opportunities

Check it yourselfAdd up the Sent column.

app/services/opportunities.py · summary()

Sent (per provider)

Provider responses table, Sent column

Formula

count of the provider's own opportunities (member still on the provider's panel), any status, all time — the same rows the provider's screen lists

Why this wayThe denominator for the Evidence returned rate (the y% in that column). Settled and withdrawn opportunities stay in it. See Part I, Provider response metrics.

In the demoDr. P. Osei 2

Check it yourself'view' on the row lists exactly this many opportunities.

app/services/opportunities.py · summary()

Awaiting (per provider)

Provider responses table, Awaiting column

Formula

count of the provider's opportunities at stage answer (NEW or VIEWED, models.OPP_STAGE); MORE_INFO_NEEDED is the care team's turn and is returned apart as waiting_on_care_team

Why this wayWork waiting on the provider — the same stage the provider's own 'Need your answer' card counts.

In the demoDr. P. Osei 2

app/services/opportunities.py · summary()

Accepted, Disputed N (x%), Evidence returned N (y%) (per provider)

Provider responses table, Accepted, Disputed and Evidence returned columns

Formula

Accepted / Disputed = count of the provider's opportunities whose recorded response is ACCEPTED / DISPUTED, whatever their status now x = disputed_rate = round( 100 × disputed ÷ responded ); 'no responses yet' when responded = 0 Evidence returned = count at EVIDENCE_RETURNED or CLOSED with an evidence reference; y = round( 100 × evidence returned ÷ sent )

Why this wayThe recorded answer, not the current status, so an accepted opportunity later withdrawn still counts as accepted. The two rates are Part I's Provider response metrics, printed beside their counts (A-149): disputed is over what was RESPONDED (silence is not a dispute), evidence returned over everything SENT. A rate with no denominator says why rather than printing 0%.

In the demoDr. P. Osei 0 accepted, 0 disputed, 0 evidence returned (0%)

app/services/opportunities.py · summary()

Acceptance (%)

Provider responses table, Acceptance column

Formula

responded = the provider's opportunities with a response date (any of the five answers, including 'more information needed') acceptance = round( 100 × accepted ÷ responded ), a whole number; 'no responses yet' when responded = 0

Why this wayJudged only against answers: silence is not a refusal. See Part I, Provider response metrics, whose worked example quotes the same words. The responded count is not on screen, so the % cannot be rebuilt from the visible columns. Rounding is half-to-even (1 of 8 = 12.5 shows 12).

In the demoDr. P. Osei no responses yet

app/services/opportunities.py · summary()

Median days

Provider responses table, Median days column

Formula

response time = response date − the business date the opportunity was created, whole days value = the median of those times (the middle value, or the mean of the two middle values for an even count), rounded once to 1 decimal; '—' when nothing has been answered

Why this wayA true median — the product's one median (app/core/stats.py), the same function every median on every screen uses, the care-team medians on this screen included: response times 1 and 3 show 2. A median rather than a mean so one opportunity left over a holiday cannot dominate. See Part I, Provider response metrics.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfOnce a provider answers, 'view' lists each opportunity; the business-day gap between creation and response gives the list to sort.

app/services/opportunities.py · summary()app/services/opportunities.py · business_date_of()app/core/stats.py · median()

Overdue (column, 'N overdue' title badge, OVERDUE badge and Due in the drill)

Provider responses table and title; 'view' drill modal

Formula

Due = business date at creation + response days (tenant setting provider.response_days, default 7), fixed at creation overdue = the provider still owes an answer (NEW or VIEWED) and today is past Due by more than the 'Provider has not responded' rule's grace (grace_days, 0 by default) rows sorted by Overdue, then Sent, highest first; the title badge is the tenant total, hidden at 0

Why this wayOne answer, opportunities.deadline_state, shared with the provider's screen and with the rule that raises the chase task, so the badge and the task agree on the same day. The response clock stops at the first answer.

In the demo0 overdue

Check it yourselfIn the demo both of Dr. P. Osei's opportunities were created on the reset day and fall due 7 days later, so none is overdue; 'view' shows the Due date on each.

app/services/opportunities.py · row()app/services/opportunities.py · response_days()app/services/opportunities.py · summary()app/services/task_rules.py · _r_opportunity_unanswered()

Intervention performance (campaign count)

Intervention performance panel, title count

Formula

count of every campaign in the tenant, active or closed, newest first

Why this wayRe-running a push tops up the live campaign for that target instead of creating a second one, so this counts distinct pushes. With none the panel says 'No campaigns yet'.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfLaunch a push from a quality row (or the Quality screen); the count goes to 1, and launching the same measure again leaves it at 1.

app/services/campaigns.py · performance()

Eligible

Intervention performance table; funnel stage 'Eligible population'

Formula

number of members stored on the campaign: at first launch, members with a live gap on the measure (evaluated then) or the cohort's members; each re-run appends newly eligible members and removes nobody

Why this wayStored rather than recomputed, so the denominator cannot drift under the result. The cost: a member whose gap has since closed stays in it, and the denominator grows when a manager re-runs the push.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · launch()app/services/campaigns.py · funnel()

Work generated

Intervention performance table; funnel stage 'Work generated'

Formula

number of tasks carrying this campaign's id, any status

Why this wayA TASK count, not a member count, and the funnel says so: the stage carries the unit 'tasks' and the modal prints it beside the count. Members already holding this campaign's open work are skipped, and a member folded into another campaign's open task gets no new task, so it can be below Eligible; a re-run after a task is done can add a second task for the same member, so it can also exceed it.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()

Started

Intervention performance table; funnel stage 'Intervention created'

Formula

distinct eligible members with an intervention STARTED on or after the launch (its intervention.create audit row is dated at or after the campaign's creation), any status, any measure

Why this wayA campaign creates tasks, never interventions, so there is no link to count by; the launch date bounds it instead. Every creation path writes the dated create row, and an intervention with no dated start cannot be claimed for the campaign period. The cost: an intervention for another measure, or one later cancelled, still counts as started.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()

Reached

Intervention performance table; funnel stage 'Member reached'

Formula

distinct eligible members with an intervention started since the launch at ENGAGED or later in the one funnel order (AWAITING_AUTH, AUTH_APPROVED, SCHEDULED, IN_PROGRESS, COMPLETED, GAP_CLOSED, and REOPENED), or with an inbound message logged since the launch

Why this wayA sent message is not a reached member; a reply or an engaged intervention is. Unreachable, declined and cancelled interventions never count, and nothing from before the launch counts.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()app/models.py · iv_reached_at_least()

Completed

Intervention performance table; funnel stage 'Service completed'

Formula

distinct eligible members with an intervention started since the launch at COMPLETED or GAP_CLOSED

Why this wayAttendance, not authorisation: the service happened. Any measure; only work started in the campaign period.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()

Gaps closed

Intervention performance table; funnel stage 'Gap closed'

Formula

distinct eligible members with a care gap at CLOSED on the campaign's measure (any measure for a cohort campaign), closed on or after the launch's business date

Why this wayOnly the measure engine closes a gap, on clinical evidence. A closure from before the launch is not the campaign's, and the screen's own caveat applies: observed progression, not attributed effect.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()

Measure rate ('B% → C%', '±N pts')

Intervention performance table, Measure rate column

Formula

B = the measure's rate when the campaign was FIRST launched (never moved by a re-run) C = the same measure's rate now · change = round( C − B, 1 ) points '—' for a cohort campaign

Why this wayBoth rates are the Quality screen's rate (see Part I, A measure across a population) over the whole population, so the change is a before-and-after, not a campaign effect; the screen says so under the table.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfC equals the measure's Performance on the Quality screen.

app/services/campaigns.py · launch()app/services/campaigns.py · performance()app/services/quality.py · evaluate_tenant()

Funnel: Outreach attempted and Appointment scheduled (stage counts)

Campaign funnel modal ('Funnel' button), Members column

Formula

Outreach attempted = distinct eligible members with an intervention started since the launch at OUTREACH or later, or an outbound message logged since the launch Appointment scheduled = those at SCHEDULED or later, or with an appointment created since the launch at BOOKED, CONFIRMED, ARRIVED or COMPLETED each stage carries its unit: Work generated counts tasks, every other stage members (the other six stages are the table columns above)

Why this wayEight stages from eligible to closed so the drop between them is visible; the drop is the finding. Every stage is bounded to the campaign period: interventions started, messages logged, appointments made and gaps closed on or after the launch. Cancelled and no-show appointments do not count as scheduled (models.APPT_BOOKED_STATES).

In the demo— nothing to show in the fresh demo tenant

Check it yourselfLaunch a push and click 'Funnel': eight stages, each with its count; Eligible, Work generated, Started, Reached, Completed and Gaps closed equal the table row.

app/services/campaigns.py · funnel()

Funnel: % of eligible

Campaign funnel modal, % of eligible column

Formula

member stages: round( 100 × stage count ÷ Eligible, 1 ); '—' when Eligible is 0, and for Work generated (a task count is not a share of members)

Why this wayEvery member stage against the stored population.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()

Funnel: % of previous

Campaign funnel modal, % of previous column

Formula

round( 100 × stage count ÷ the previous stage of the SAME unit, 1 ) '—' when that previous stage is 0, and for the first stage of a unit (Eligible population, Work generated)

Why this wayShows where members stop moving. Intervention created is compared with Eligible population, not with the task count between them, and a stage after an empty stage reads '—': there is nothing to be a share of.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · funnel()

Funnel: 'Where it leaks' (N member(s) do not progress from A to B)

Campaign funnel modal, note under the table

Formula

among consecutive MEMBER stages, the pair with the largest drop (count A − count B); on a tie the earlier pair; no note when no stage drops

Why this wayThe one sentence a manager acts on. Work generated (a task count) is left out of the comparison, so the drop is always a number of members, and the note names its unit.

In the demo— nothing to show in the fresh demo tenant

app/services/campaigns.py · _biggest_drop()

Funnel: 'Stopped: N member(s) ended unreachable, declined or cancelled'

Campaign funnel modal, note under the table

Formula

number of eligible MEMBERS with an intervention started since the launch at UNREACHABLE, DECLINED or CANCELLED (a member with two stopped interventions counts once)

Why this wayCounted apart from 'not yet reached' because a channel that is not working should not hide inside a queue. Members, as the sentence says, and only interventions started in the campaign period.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfAppears only when an eligible member has an intervention marked unreachable, declined or cancelled.

app/services/campaigns.py · funnel()

Launch message ('Campaign launched' / 'Campaign topped up': N new task(s), M already had open work, K combined, J sent to providers)

Toast after 'Generate work' in the Launch modal

Formula

eligible = members with a live gap on the measure (re-evaluated first) or the cohort's members skipped = those already holding open work from a live campaign on the same target · combined = those folded into another campaign's open task · new tasks = the rest routed to providers: one opportunity per member with an attributed provider; the others become care-manager tasks

Why this wayLaunching creates tasks (or provider opportunities) and nothing else: no message, no intervention, no gap change. A first launch's message states no counts; a top-up states new and skipped; combined, kept-separate and provider counts are added when non-zero, so a smaller task count is not read as a smaller reach.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfLaunch any quality push, then launch it again: the second message reads 0 new task(s) and N already had open work.

app/services/campaigns.py · launch()app/static/app.js · campaignCreate()

Care-team delivery (person count and order)

Care-team delivery panel, title count and rows

Formula

distinct people who own a task or an intervention, raised an internal referral, or own a vendor referral (any status, all time); sorted by name

Why this wayDeliberately no composite score and no ranking: the customer asked for progress toward outcomes, and a leaderboard over a handful of people is a statistic about nothing. Outcome columns come first; activity is labelled as context.

In the demoJane Smith

app/services/productivity.py · by_owner()

Gaps closed (per person)

Care-team delivery table, Outcomes · Gaps closed

Formula

number of the person's owned interventions whose care gap is now CLOSED

Why this wayCredited to the intervention owner, not caused by them: the measure engine closes gaps on clinical evidence that may arrive for other reasons, and the panel says so. It counts interventions, so two interventions on one closed gap count 2, and a cancelled intervention on a gap that closed still counts. All time.

In the demoJane Smith 0

app/services/productivity.py · by_owner()

Services completed ('X of Y · Z%')

Care-team delivery table, Outcomes · Services completed

Formula

X = the person's owned interventions at COMPLETED or GAP_CLOSED · Y = every intervention they own, any status Z = round( 100 × X ÷ Y, 1 ), omitted when Y = 0

Why this wayY includes interventions still in flight, so a new owner's rate starts low; the referral rate beside it uses only settled referrals. All time.

In the demoJane Smith 0 of 0

app/services/productivity.py · by_owner()

Referrals (loop closed): 'Z% · X of Y settled · R raised'

Care-team delivery table, Outcomes · Referrals

Formula

R = internal referrals the person raised + vendor referrals they own, any status Y = those settled (internal OUTCOME_CONFIRMED, DECLINED, CANCELLED, UNABLE_TO_CONTACT; vendor CONFIRMED, DECLINED, FAILED, CANCELLED) X = internal OUTCOME_CONFIRMED + vendor CONFIRMED · Z = round( 100 × X ÷ Y, 1 ); 'none settled' when Y = 0

Why this wayCredited to whoever RAISED the handoff: the question is whether the referral a coordinator started produced an outcome for the member. In-flight referrals are neither success nor failure. Computed by the same function as the Operational KPIs 'Referral completion' card (referrals.loop_closed), so the two agree over the same referrals. See Part I, Referral SLA.

In the demoJane Smith 100% · 1 of 1 settled · 3 raised

app/services/productivity.py · by_owner()app/services/referrals.py · loop_closed()

Tasks open ('N of M')

Care-team delivery table, Flow · Tasks open

Formula

N = the person's tasks at open · M = every task they own, any status (open, done, dismissed), all time

Why this wayM is a lifetime total, not current workload. Completed and dismissed counts are computed but not shown.

In the demoJane Smith 30 of 33

app/services/productivity.py · by_owner()

Overdue (per person) and the OVERDUE badge in the 'Work' drill

Care-team delivery table, Flow · Overdue; 'Work' drill modal, task Status

Formula

a task is overdue when it is open, has a due date, and that date is before today (business date)

Why this wayA task due today is not overdue and a task with no due date never is. The drill applies the same rule row by row, so the badges in it add up to the column.

In the demoJane Smith: 8 (drill badges: 8)

Check it yourselfOpen 'Work' for the person and count the red OVERDUE badges.

app/services/productivity.py · by_owner()app/services/productivity.py · owner_work()

Median hours to first touch (and 'Hours to first touch' in the drill)

Care-team delivery table, Flow · Median hours; 'Work' drill, interventions table

Formula

per owned intervention: hours from its intervention.create audit event to the first later intervention.* audit event written by a member of staff (dropped unless strictly later) table value = median of the exact durations, rounded once to 1 decimal; '—' when none · the drill shows each duration to 2 decimals

Why this wayMeasured from the append-only audit trail, not a mutable column, and a median so one item left over a weekend cannot dominate. Every way an intervention is created (a care manager starting one, a provider accepting an opportunity) writes the intervention.create row, so every intervention is anchored. A touch is a staff action: a member's own reply and system writes move the intervention but are not somebody picking the work up.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfStart an intervention and move it once; the drill shows its hours and the table the median.

app/services/productivity.py · by_owner()app/services/productivity.py · _intervention_timings()app/services/productivity.py · owner_work()app/services/workflow.py · create_intervention()

Median hours to close a task

Care-team delivery table, Flow · Median hours (second line)

Formula

per owned task with a task.done audit event: hours from the task's creation to its FIRST task.done value = median of the exact durations, rounded once to 1 decimal; the line is hidden when there is none

Why this wayFor a task the first action is also the last (open → done), so this is the task's 'time to first action' under its accurate name. Dismissed tasks, open tasks, and tasks closed without an audit row (e.g. seeded as done) are left out. Rounded once, at the end, like first touch.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfIn the demo Jane Smith's 3 done tasks were seeded done, with no task.done event, so the line is hidden until a task is closed on screen.

app/services/productivity.py · by_owner()app/services/productivity.py · _task_close_times()

Response ('Z% replied · X of Y contacted')

Care-team delivery table, Activity (context)

Formula

Y = distinct members the person sent an outbound message to · X = those members with any inbound message Z = round( 100 × X ÷ Y, 1 ); 'no contacts made' when Y = 0

Why this wayContext, never the headline. Known limit: 'replied' has no timing or thread check, so an inbound message sent before the contact, or answering a colleague, counts, and two people who contacted the same member both get the reply. The Operational KPIs 'Outreach response' is a different figure (per ladder).

In the demoJane Smith no contacts made

app/services/productivity.py · by_owner()

Appointments ('X of Y appts kept')

Care-team delivery table, Activity (context), second line

Formula

Y = appointments at BOOKED, CONFIRMED, ARRIVED or COMPLETED of members on whom the person owns an intervention · X = those at ARRIVED or COMPLETED 'no appointments booked' when Y = 0

Why this wayNot 'appointments this person booked': it follows the members they own work for. Cancelled and no-show appointments are not booked, by the same definition (models.APPT_BOOKED_STATES) as the funnel's 'Appointment scheduled' on this screen.

In the demoJane Smith no appointments booked

app/services/productivity.py · by_owner()

'Unassigned: N task(s) and M intervention(s) belong to nobody'

Care-team delivery panel, note under the table

Formula

N = open tasks with no owner · M = live interventions (not cancelled, completed, gap closed, declined or unreachable) with no owner; the note shows only when either is above 0

Why this wayWork owned by nobody appears in no one's numbers, so it is surfaced. Only work still to be done counts: done, dismissed, completed and cancelled items are not 'going unworked'. N is the same set (worklists.unowned_open) as My Work's unassigned count.

In the demo1 task(s), 1 intervention(s)

app/services/productivity.py · by_owner()app/services/worklists.py · unowned_open()

Analytics and operational KPIs

32 values

The VBC manager's and executive's Analytics screen (prior-authorization volume, approval, turnaround, SLA and appeals; care gaps; the intervention funnel and risk mix), plus three panels any authorized role opens over other screens: the documentation readiness check, the eligibility (271) response and the Ask mdvin answers about authorizations.

PA Volume

Analytics, KPI card 1

Formula

every prior authorization in the tenant, whatever its status (draft, pending, decided, withdrawn, expired)

Why this wayA volume count, not a workload count: finished and withdrawn requests stay in it. It equals the sum of the PA Pipeline table beside it and the length of the Authorizations list for the same role, because all three read the same rows at request time.

In the demo1 authorizations

Check it yourselfAdd up the Count column of the PA Pipeline table; it is the same number.

app/api/analytics.py · pa_analytics()

Approval Rate

Analytics, KPI card 2

Formula

approval % = round( 100 × A ÷ ( A + D ), 1 ); '—' when A + D is 0 A = requests whose DETERMINATION was an approval · D = requests whose determination was a denial determination = the first approve or deny in the request's decision trace (its review records) the card's second line: 'A approved of A + D determinations (D denied)'

Why this wayCounted on what the plan determined, not where each request stands now: a later appeal outcome, a closure or an expiry does not change the first determination, so an approved request that is later closed stays an approval. Pending, pended and withdrawn requests have no determination and are in neither A nor D. Nothing to divide by reads as a dash, like the three cards beside it.

In the demo100%

Check it yourselfIn the PA Pipeline table: Approved ÷ (Approved + Denied + Closed).

app/api/analytics.py · pa_analytics()app/services/workflow.py · determinations()

Avg TAT (average turnaround, hours)

Analytics, KPI card 3

Formula

mean over requests with a submitted_at and a determination (the first approve or deny in the decision trace) of ( determination time − submitted_at ), in hours, rounded to 2 decimals; the card's second line says how many were determined '—' when there are none

Why this wayMeasured from submission, the same start as the decision clock (see Part I, Prior-authorization decision clock), to the plan's determination. A pend asks for information and determines nothing; a withdrawal ends the request without a determination; an appeal outcome is the appeal's decision. None of them moves this figure, because it reads the decision trace, which nothing rewrites, rather than the request's last-reviewer-action timestamp.

In the demo72h

Check it yourselfThe demo request PA-2026-40188 was submitted 4 days before the reset and decided 1 day before it: 3 days = 72 hours.

app/api/analytics.py · pa_analytics()app/services/workflow.py · determinations()app/services/workflow.py · decide_pa()

SLA Compliance, and 'SLA measured on N, met on M'

Analytics, KPI card 4, and the note under PA Pipeline

Formula

N = requests with submitted_at, a determination (first approve or deny in the decision trace) and a decision_due M = those of N determined at or before decision_due SLA % = round( 100 × M ÷ N, 1 ); '—' when N is 0

Why this wayOnly determined requests are judged, so a request that is late and still undecided is not in N yet; it shows on the Authorizations list's red SLA chip instead. The determination is compared with the request's own decision due; a later appeal decision does not turn an on-time determination into a miss. A pend or a withdrawal is not a determination and is not judged.

In the demo100% (met on 1 of 1 measured)

Check it yourselfThe note under PA Pipeline prints M and N; M ÷ N is the card.

app/api/analytics.py · pa_analytics()

Appeal Overturn, and 'N decided · M overturned'

Analytics, KPI card 5, and the note under Denial Reasons

Formula

decided = appeals with status UPHELD, OVERTURNED or PARTIALLY_OVERTURNED overturned = decided appeals that are not UPHELD overturn % = round( 100 × overturned ÷ decided, 1 ); '—' when none is decided

Why this wayA partial overturn counts as overturned, because the member got some of the service after the plan first said no. Level 1 and Level 2 appeals are counted alike. As the screen says, a high rate usually points to documentation missing at submission rather than to reviewer error; the readiness check further down this chapter exists to catch that before submission.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo appeal exists in the fresh demo, so the card shows '—'. File and decide one on the Authorizations screen and the card becomes overturned ÷ decided.

app/api/analytics.py · pa_analytics()

Appeals filed

Analytics, note under Denial Reasons

Formula

every appeal in the tenant, any level, any status

Why this wayFiled minus decided is the number of appeals still waiting for a decision.

In the demo0 appeals

app/api/analytics.py · pa_analytics()

Gaps Closed

Analytics, KPI card 6

Formula

care gaps whose status is exactly CLOSED, all time

Why this wayOnly a gap closed by evidence counts. EXCLUDED and NOT_APPLICABLE also take a gap off the open list but are not closures, so they are left out. Other screens show a same-named number computed differently: the Dashboard's 'Gaps closed this month' counts only closures dated this month, My Panel's 'Gaps closed' counts only gaps behind the signed-in care manager's own interventions, and Actions & Outcomes counts members, not gaps. The intervention state GAP_CLOSED in the funnel below is a different record and can differ from this count.

In the demo0 gaps

Check it yourselfEquals the 'closed' entry in the 'care gaps … total (…)' line at the foot of the screen (absent when 0).

app/api/analytics.py · vbc_analytics()

PA Pipeline: count per status

Analytics, PA Pipeline table

Formula

for each status, the number of requests currently in it; only statuses with at least one request are listed

Why this wayA snapshot of where requests stand now, not how many ever passed through a state. The rows add up to PA Volume.

In the demoAPPROVED 1

Check it yourselfFilter the Authorizations list by status; each count matches.

app/api/analytics.py · pa_analytics()app/static/app.js · rAnalytics()

Expedited

Analytics, note under PA Pipeline

Formula

requests whose urgency is expedited, any status

Why this wayExpedited requests run on the 72-hour clock rather than 7 days (see Part I, Prior-authorization decision clock). This counts all of them, open and decided.

In the demo0 requests

app/api/analytics.py · pa_analytics()

Denial Reasons (count per reason code)

Analytics, Denial Reasons panel

Formula

for each denial reason code, the number of requests that carry it, whatever their current status 'No denials recorded.' when none carries a code

Why this wayThe code is kept when a denial is overturned on appeal, so an overturned request still counts under its original reason: the panel answers 'why did we deny', not 'why are requests denied now'. A withdrawal clears the code.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo request is denied in the fresh demo. Deny one as the medical director (a reason code is mandatory) and it appears here under that code.

app/api/analytics.py · pa_analytics()

Funnel: Interventions created

Analytics, Intervention Funnel, row 1

Formula

every intervention in the tenant, in any state, including cancelled, declined and unreachable ones

Why this wayThe top of the funnel and the 100% width every other bar is drawn against. The stages, their labels and their order are one table (models.IV_FUNNEL_STAGES), which the Dashboard's Engagement funnel draws too.

In the demo1 interventions

app/api/analytics.py · vbc_analytics()app/models.py · iv_funnel()

Funnel: Outreach or beyond

Analytics, Intervention Funnel, row 2

Formula

interventions whose CURRENT state is at or past OUTREACH in the one funnel order (models.IV_STAGE): OUTREACH, ENGAGED, AWAITING_AUTH, AUTH_APPROVED, SCHEDULED, IN_PROGRESS, COMPLETED, GAP_CLOSED, and REOPENED (which re-enters at ENGAGED)

Why this wayCurrent state, not 'ever reached': an intervention that went UNREACHABLE, DECLINED or CANCELLED drops out even if outreach happened. A REOPENED intervention had already reached the member, so it counts. The name overstates it slightly: a member who phoned in can go straight from identified to engaged, or to awaiting authorization, without any outreach, and still counts here. The Dashboard's Engagement funnel draws the same stage from the same table (models.iv_funnel), and the campaign funnel reads the same order.

In the demo1 interventions

app/api/analytics.py · vbc_analytics()

Funnel: Reached (engaged or beyond)

Analytics, Intervention Funnel, row 3

Formula

interventions whose current state is at or past ENGAGED in the funnel order: ENGAGED, AWAITING_AUTH, AUTH_APPROVED, SCHEDULED, IN_PROGRESS, COMPLETED, GAP_CLOSED or REOPENED

Why this wayThe Outreach set without OUTREACH itself: the member responded. The same current-state limit applies: a DECLINED member leaves the count. REOPENED counts, because the member was reached before the service had to happen again. The demo's one intervention (Derrick Boateng's retinal exam, authorization approved) counts here although it was created directly at AUTH_APPROVED. The Dashboard labels this stage the same way.

In the demo1 interventions

app/api/analytics.py · vbc_analytics()

Funnel: Scheduled or beyond

Analytics, Intervention Funnel, row 4

Formula

interventions whose current state is SCHEDULED, IN_PROGRESS, COMPLETED or GAP_CLOSED

Why this wayAWAITING_AUTH and AUTH_APPROVED are left out because they come before booking: an approved authorization unlocks scheduling. The count can go DOWN: a no-show sends a scheduled intervention back to outreach, and a completed one can be reopened.

In the demo0 interventions

app/api/analytics.py · vbc_analytics()

Funnel: Completed or gap closed

Analytics, Intervention Funnel, row 5

Formula

interventions whose current state is COMPLETED or GAP_CLOSED

Why this wayCOMPLETED means the service happened; GAP_CLOSED means the evidence also closed the care gap. This is an intervention count, so it can differ from the Gaps Closed card, which counts care-gap records.

In the demo0 interventions

app/api/analytics.py · vbc_analytics()

Funnel bar length

Analytics, Intervention Funnel, bar beside each count

Formula

width % = Math.round( 100 × stage count ÷ Interventions created ); 0 when nothing was created

Why this wayEach bar is that stage's share of ALL interventions created, not the conversion from the stage before it, and it is rounded to a whole percent. The Dashboard's Engagement funnel draws the same stages against the same first stage, so the same counts give the same bar lengths on both screens.

In the demoInterventions created 100%, Outreach or beyond 100%, Reached (engaged or beyond) 100%, Scheduled or beyond 0%, Completed or gap closed 0%

app/static/app.js · rAnalytics()

Members, and risk mix (members per tier)

Analytics, note under Intervention Funnel

Formula

members = every member in the tenant · for each risk tier, the number of members whose STORED tier is that tier (HIGH ≥ 60 · RISING ≥ 35 · MEDIUM ≥ 15 · LOW)

Why this wayIt counts the tier stored on each member record, the same one the Patient 360 summary and the Dashboard's 'High / rising risk' card read, not a fresh score. See Part I, Clinical risk score and tier. A tier typed in by hand is counted as typed. Tiers with no members are not listed.

In the demo13 members; HIGH 2, MEDIUM 5, RISING 4, LOW 2

Check it yourselfThe four numbers add up to the member count on the Patients list (13 in the demo).

app/api/analytics.py · vbc_analytics()app/services/risk.py · rescore()

Care gaps total

Analytics, note under Intervention Funnel

Formula

every care-gap record in the tenant, open and closed, any status

Why this wayA count of stored gap records. It is not the Quality screen's 'open gaps' (D − N per measure), which the measure engine recomputes from members; the two agree in the fresh demo (29) because every stored gap is still open, and drift apart as gaps close, are excluded, or when a member has two records for one measure.

In the demo29 gaps

app/api/analytics.py · vbc_analytics()

Care gaps by status ('open 28, in progress 1', …)

Analytics, note under Intervention Funnel, in brackets

Formula

for each stored status, the number of care-gap records in it; the screen lower-cases the status and replaces underscores with spaces

Why this wayRaw statuses, not grouped into open and closed. Open side: OPEN, IN_PROGRESS, SCHEDULED, AWAITING_AUTH, SERVICE_COMPLETED, REOPENED. Closed side: CLOSED, EXCLUDED, NOT_APPLICABLE (and three statuses nothing writes today). Only CLOSED feeds the Gaps Closed card.

In the demoopen 28, in progress 1

app/api/analytics.py · vbc_analytics()app/static/app.js · rAnalytics()

Documentation readiness: each criterion (Evidenced / In the chart, not attached / Not evidenced)

Launch PA modal (Patient 360) and the Review / Detail modals on Authorizations

Formula

for each criterion of the policy, first match wins: 1 the criterion's own matcher (stored on the policy) finds the record it names — an active condition in its ICD-10 codes, the LATEST result for its LOINC code meeting its threshold within its window, a procedure in its CPT codes, or the measure engine's open gap on its measure → Evidenced, citing that record 2 a document of an allowed type already attached to this request → Evidenced 3 such a document in the chart but not attached → In the chart, not attached · otherwise → Not evidenced a criterion with no matcher can only be evidenced by a document (or by a reviewer)

Why this wayDeterministic: it points at records and never asserts a clinical fact. What satisfies a criterion is written on the policy itself, so a record of the right TYPE is not enough: on PA-2026-40188, C3 'Elevated HbA1c or insulin-dependent' is satisfied only by an HbA1c above 8.0 in the last year or a long-term insulin code, never by a cholesterol result (it used to be). Structured evidence outranks documents, so a criterion can be Evidenced with nothing attached to the request; the cited item says which record it is. Publishing a policy refuses a malformed matcher, or a criterion that neither a matcher nor any document type could ever evidence.

In the demoC1 SATISFIED (Type 2 diabetes mellitus without complications (E11.9)), C2 SATISFIED (Diabetic Retinal Exam: open (no qualifying procedure in the last 365 days)), C3 MISSING (nothing in the record), C4 MISSING (nothing in the record)

Check it yourselfEach Evidenced line names the record it used and its source system; open the member's Clinical tab and find it.

app/services/readiness.py · assess()app/services/readiness.py · _match_criterion()app/services/readiness.py · _match_document()app/services/readiness.py · validate_criteria()app/static/app.js · readinessHtml()

Documentation readiness: green or amber box, and the sentence ('N requirement(s) …', 'All N criteria …')

Readiness panel, box colour and first line

Formula

ready (green) = no criterion 'not attached' and none 'not evidenced'; otherwise amber sentence, first that applies: N not attached (naming the first file and its upload date) → N not evidenced (listing their ids) → 'All N criteria in KEY vN are evidenced'

Why this wayAn unattached document is named before a missing one because it is the cheaper fix: one click, versus obtaining new documentation. 'All N criteria are evidenced' means every criterion's own matcher found its record or a document of an allowed type is attached.

In the demonot ready: 2 requirements (C3, C4) cannot be evidenced from the record. Obtain and attach the documentation, or expect the payer to request more information.

app/services/readiness.py · assess()app/static/app.js · readinessHtml()

'Attach these N documents to the request when I submit'

Launch PA modal, checkbox under the readiness rows (before a request exists)

Formula

N = criteria in 'In the chart, not attached'; the checkbox appears only when N > 0 and no request exists yet

Why this wayTicked by default: the documents are already in the chart, so attaching them at submission avoids a pend for missing information. Only documents the check found are attached; nothing is generated.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo seeded chart holds an unattached document of a required type, so the checkbox does not appear in the fresh demo. Upload a clinical note for a member, then launch a retinal-exam request for them: the note appears as 'In the chart, not attached' and N counts it.

app/services/readiness.py · assess()app/static/app.js · readinessHtml()app/static/app.js · loadReadiness()

Prior authorization required? (required / 'No authorization required' / 'We cannot say')

Readiness panel (Launch PA modal and Authorizations)

Formula

no active coverage → cannot say policy: the one pinned on the request if any, else the newest active policy for the coverage's payer and line of business that lists the code; none → cannot say policy says pa_required false → not required · otherwise required

Why this waySilence is never read as 'no': a 'not required' answer must come from a policy that says so (the demo loads one for routine services). 'Cannot say' costs a phone call; a wrong 'no' costs a denied claim. One resolver (services/policy) answers for this panel, the booking gate, next best action and the care-gap chain, with one pin rule: a request is judged under the version it was submitted under, even after that version is retired; a question with no request reads the newest active version.

In the demoDerrick Boateng 92250: required · John Doe 99213: not required · Ahmed Ali 92250: cannot say

Check it yourselfAhmed Ali's plan (BCBS Texas, commercial) has no policy on file in the demo, so the panel says it cannot tell and asks you to confirm with the payer.

app/services/readiness.py · assess()app/services/policy.py · resolve_policy()app/services/policy.py · policy_answer()

Eligibility: which coverage is reported (status, payer, plan, dates)

Eligibility modal (Patient 360, 'Check eligibility'), Coverage and Effective lines

Formula

the one coverage rule (services/coverage.current_coverage): the coverage whose status is active and whose dates contain today (the most recently started, if several); with none current, the member's latest coverage row (largest effective_to), whose status is then reported as it is; the check is saved with a new ELG-… transaction number

Why this wayReports what the benefit record on file says; it quotes no price. Every screen that names 'the' coverage asks the same function — the header, the summary strip, the readiness check, the care-gap chain, the Dashboard and My Panel — so a terminated plan that ends later no longer wins over the active one. With no current coverage, the modal shows the latest row with its status (terminated or lapsed) rather than nothing.

In the demoACTIVE · UnitedHealthcare MA Choice · 2026-01-01 to 2026-12-31

Check it yourselfThe modal's transaction number is saved; the Coverage tab's 'Last verified with the payer' shows it afterwards.

app/services/eligibility.py · verify()app/api/eligibility.py · check()app/services/coverage.py · current_coverage()

Eligibility: Deductible bar ('$met of $deductible')

Eligibility modal, first progress bar

Formula

width % = min( 100, Math.round( 100 × deductible met ÷ deductible ) ), a missing 'met' read as 0; no deductible on file → the line reads 'not on file' with no bar

Why this wayBoth dollar figures are read from the coverage's benefit record as filed; only the bar is computed, in the browser, and capped at 100%. A figure nobody entered is stored as null and shown as 'not on file', never as '$null' or a derived number.

In the demo$875 of $1500 → 58%

Check it yourselfJohn Doe: 875 ÷ 1,500 = 58.3%, drawn as 58%.

app/static/app.js · eligibilityModal()app/services/eligibility.py · verify()

Eligibility: Out-of-pocket bar ('$met of $max')

Eligibility modal, second progress bar

Formula

width % = min( 100, Math.round( 100 × out-of-pocket met ÷ out-of-pocket maximum ) ), a missing 'met' read as 0; no maximum on file → 'not on file' with no bar · Copays: each filed copay, or 'not on file'

Why this waySame arithmetic as the deductible bar. The 'Add coverage' form has fields for the deductible and the amount met only, and it sends only what was typed: the server stores every other benefit figure as null (services/coverage.normalize_benefits), so for coverage added there the out-of-pocket line and the copays read 'not on file' rather than figures the payer never filed. Seeded coverage carries benefit figures. Coverage added before this rule keeps whatever it stored.

In the demo$2200 of $5000 → 44%

Check it yourselfJohn Doe: 2,200 ÷ 5,000 = 44%.

app/static/app.js · eligibilityModal()app/static/app.js · coverageSave()app/services/coverage.py · normalize_benefits()

Eligibility: 'Prior authorization required for' (codes and policy)

Eligibility modal, amber box

Formula

every policy in the tenant with the reported coverage's exact payer name and line of business, status active and pa_required true; one line per policy with its service codes

Why this wayThe list of services this plan says need authorization, so nobody drafts a request that eligibility would have refused. The payer match is an exact text match, and the 'Add coverage' form takes the payer as free text: coverage typed as 'United Healthcare' matches no policy filed as 'UnitedHealthcare', and the box silently disappears.

In the demoUHC-MA-EYE-014 v3: 92250

Check it yourselfSign in as the policy administrator: the Policy Packages list shows the same active, authorization-required package for UnitedHealthcare Medicare Advantage.

app/services/eligibility.py · verify()app/api/eligibility.py · check()

Eligibility: 'Active authorizations on file' (ref, CPT, auth number, 'to <date>')

Eligibility modal, green box

Formula

the member's authorizations whose status is APPROVED; 'to' = the authorization's valid_to a new approval, or an overturned appeal, is valid from the decision's business date to that date + 180 days

Why this wayValidity is not re-checked here. An authorization past its end date is moved to EXPIRED by the automation sweep (every 15 minutes by default), and until then it still lists. PARTIALLY_APPROVED requests receive no authorization number and never list. Known inconsistency in the demo: the seeded authorization PA-2026-40188 was written with a 90-day window, not the 180 days every real approval receives.

In the demo179 days after the reset

Check it yourselfCheck eligibility on Derrick Boateng, the member with the demo's approved authorization: the box lists PA-2026-40188 · CPT 92250 · AUTH-88213417, valid from the day before the reset to the date shown here.

app/services/eligibility.py · verify()app/services/workflow.py · grant_authorization()app/services/workflow.py · expire_authorizations()

Ask mdvin: 'Which requests are close to their SLA?' (N past the deadline, N due inside 48 hours, 'due in Xd Yh')

Ask panel, opener on the Authorizations screen

Formula

live = pending requests (status SUBMITTED, IN_REVIEW or PENDED, the one definition in models.pa_is_pending) with a decision_due, soonest first over = live with due < now · soon = the rest due within 48 hours facts: over + soon (or all live when both are empty), at most 6; 'in 2d 3h' / '5h' / '40m' / '1d 4h overdue', whole units, never rounded up

Why this way48 hours is the assistant's own cut; the SLA chip on the Authorizations list goes red under 24 hours and amber under 72, so the two can describe the same request differently. Pending is read from the status, so a pended or resumed request stays on this answer while its clock keeps running.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfIn the fresh demo the only request is already decided, so the answer is 'No authorization has a clock running right now.' Submit a request and ask again: it is listed with its time to deadline.

app/services/assistant.py · _h_at_risk()app/services/assistant.py · _rel()

Ask mdvin: 'What is blocking my authorizations?'

Ask panel, opener on the Authorizations screen

Formula

the request named in the question; else the named or on-screen member's request with the highest reference number runs the readiness check on it; answer, first that applies: a document to attach → N requirements not evidenced → 'Nothing is blocking'; one fact per criterion

Why this wayThe same readiness engine as the panel above, so the two cannot disagree about a request. Known limit: with no request or member named, 'highest reference' is not 'newest', because reference numbers are random; and it answers even for a request that is already decided, as the demo shows.

In the demoPA-2026-40188: 2 requirement(s) that nothing in the record satisfies

app/services/assistant.py · _h_pa_blocked()app/services/assistant.py · _newest_pa()app/services/readiness.py · assess()

Ask mdvin: authorization status facts (Status, 'Urgency (72 hours / 7 days)', Decision due)

Ask panel, any screen, e.g. 'What is the status of PA-2026-40188?'

Formula

Decision due = time to or past decision_due, in whole units as above 'past the deadline' when due < now and no decision is recorded Urgency label = '72 hours' when expedited, else '7 days'

Why this wayThe due time is the decision clock of Part I (see Prior-authorization decision clock). Two limits: the urgency label is fixed text, so a deployment that configures a different clock would see the old numbers here; and the 'Decision due' fact is shown even after the decision is made.

In the demoPA-2026-40188 · APPROVED · standard (7 days)

Check it yourselfThe decision-due fact moves with the clock; compare it with the SLA due date on the Authorizations list.

app/services/assistant.py · _h_pa_status()app/services/assistant.py · _rel()

Ask mdvin: 'N authorizations in total, N still open and N closed out'

Ask panel, any screen, e.g. 'What is the total PA volume?'

Formula

total = every request in the tenant; open = SUBMITTED, IN_REVIEW, PENDED or DRAFT; closed out = total − open facts: one per status, largest first

Why this wayTotal equals PA Volume on the Analytics screen. 'Open' here is every undecided request, so it includes DRAFT, which the Dashboard's 'PA blocking care' and the at-risk answer leave out (nothing has been sent to the payer). An APPEALED request is still undecided but counts as closed out. Phrasing matters: 'How many authorizations are there?' is answered with one request's status instead.

In the demo1 in total, 0 still open and 1 closed out

app/services/assistant.py · _h_counts()

Provider workspace

8 values

The attributed clinician's home screen (sidebar: My Opportunities), seen by a provider login such as the demo's Dr. P. Osei: the gap-closure opportunities the payer has routed to this provider, grouped by what they need, plus the size of the provider's attributed panel.

Need your answer (KPI card and section count)

KPI card 1, and the count beside the 'Need your answer' section title

Formula

count of the listed opportunities at stage answer — status NEW or VIEWED (models.OPP_STAGE, the one table) counted in the browser from the stage field of GET /population/opportunities

Why this wayAn opportunity needs the provider while nobody has answered it (NEW, VIEWED). MORE_INFO_NEEDED is the provider's own question waiting on the care team: it is the care team's turn, so it is listed in its own 'Waiting on the care team' section and is not counted here. It comes back as VIEWED when the care manager answers. The VBC manager's Provider responses panel counts the same stage as 'Awaiting'. The list is only this provider's opportunities (see 'Which opportunities are listed' below).

In the demo2 opportunities

Check it yourselfThe section below shows the same count and that many rows. As the VBC manager, Actions & Outcomes > Provider responses > Awaiting for Dr. P. Osei shows the same number.

app/static/app.js · rProvider()app/api/population.py · list_opportunities()

In progress (KPI card and section count)

KPI card 2, and the count beside the 'In progress' section title

Formula

count of the listed opportunities at stage work — status ACCEPTED, IN_PROGRESS, COMPLETED or EVIDENCE_RETURNED (models.OPP_STAGE)

Why this wayIN_PROGRESS means the provider accepted and the intervention was created in their name; COMPLETED means they marked the service done and the evidence is still to come. EVIDENCE_RETURNED is listed but never stays: both paths that reach it (already completed with evidence, return evidence) move the opportunity on to CLOSED in the same request, and evidence that does not match the measure is refused before it gets there. In practice this card is IN_PROGRESS + COMPLETED.

In the demo0 opportunities

Check it yourselfAccept an opportunity in 'Need your answer': it moves to this section, and both counts change by one.

app/static/app.js · rProvider()app/services/opportunities.py · return_evidence()

Overdue (KPI card) and the red 'overdue <date>' badge

KPI card 3; the badge replaces the date in a row's Due column

Formula

a row is overdue when the provider still owes an ANSWER (stage answer: NEW or VIEWED) and today is past its due date by more than the grace of the 'Provider has not responded' rule (grace_days, 0 by default) KPI = number of listed rows flagged overdue

Why this wayComputed by the server on every read, never stored; with no grace the due day itself is not overdue. One answer, opportunities.deadline_state, read by this screen, the VBC manager's Provider responses panel and the rule that raises the chase task, so a badge and a task cannot disagree about the same opportunity. The response clock stops at the first answer: an accepted opportunity, one marked done, or one waiting on the care team for information is not 'unanswered past the response deadline'. See Part I, Provider response metrics.

In the demo0 opportunities

Check it yourselfThe KPI equals the number of red badges in the three sections. The VBC manager's Provider responses > Overdue for Dr. P. Osei shows the same. A freshly reset tenant has none, because the seeded opportunities are due 7 days after the reset.

app/services/opportunities.py · row()app/static/app.js · _oppTable()app/services/task_rules.py · _r_opportunity_unanswered()

My panel

KPI card 4

Formula

number of members GET /vbc/patients returns for this login: members with a live PatientAttribution (no end date, or one after today) to this provider (provider_id, not the name) in the CURRENT period (open and containing today) the request failed → '—' with 'could not load the panel', never 0

Why this wayThe same list and count as the provider's My Panel screen (see the Patients chapter, 'My Panel (N), provider sign-in'). 'This period' is the current period — open and containing today — the same rule as the Patient 360 attribution line and the recipient of a provider opportunity (population.provider_panel / current_attribution); a period left open after it ended counts for nobody. A failed request is said as a failure (getOr): a 0 here is a real empty panel.

In the demo5 members

Check it yourselfClick My Panel in the sidebar: its count badge shows the same number.

app/core/scope.py · _provider_panel()app/api/vbc.py · list_patients()app/static/app.js · rProvider()

Settled (section count)

Count beside the 'Settled' section title

Formula

count of the listed opportunities at stage settled: CLOSED, WITHDRAWN, DISPUTED or NOT_CLINICALLY_APPROPRIATE

Why this waySettled means the provider's part is on the record, not that the gap is resolved. DISPUTED and NOT_CLINICALLY_APPROPRIATE land here, but each one raises a High-priority task for the VBC manager that is still open, and the gap itself is not changed: settled for the provider, unresolved for the payer. WITHDRAWN is the payer taking an opportunity back. The sections (Need your answer, Waiting on the care team, In progress, Settled) split the list by the server's stage with no overlap and no gaps: every status belongs to exactly one of them.

In the demo0 opportunities

Check it yourselfDispute an opportunity with a reason: it moves from 'Need your answer' to 'Settled', and a High-priority 'Reconcile the disputed gap' task appears in the VBC manager's queue.

app/static/app.js · rProvider()app/services/opportunities.py · row()

Due (response deadline)

Due column of every section

Formula

business date on which the opportunity was created + response_days calendar days response_days = tenant setting provider.response_days, default 7; a negative value counts as 0, and a value that is not a number falls back to 7 set once at creation, never moved

Why this wayThe deadline counts calendar days, not business days, so it can fall on a weekend. It is not restarted when the care team answers a question, so a provider can get back an opportunity that is already past due. Sending a gap again while this provider already has a live opportunity on it returns the existing opportunity, with its original deadline.

In the demo7 days after the reset

Check it yourselfSend a gap to the provider as the care manager: the new row's Due date is the reset day plus 7.

app/services/opportunities.py · create()app/services/opportunities.py · response_days()

Which opportunities are listed, and their order

Rows of all three sections

Formula

THE provider's opportunity set, opportunities.for_provider: the tenant's opportunities whose provider_id is the signed-in provider's AND whose member is on the provider's current panel (the My panel rule) sorted: unsettled first, then by due date, earliest first (no due date last)

Why this wayThe provider comes from the login's binding, never from a query parameter. A provider login bound to no provider sees nothing, not the whole tenant. When a member leaves the panel, that member's opportunities leave this list — and leave the VBC manager's per-provider counts with it, because both read the same set; the VBC manager sees them counted apart as 'no longer on their provider's panel', with a drill to withdraw or reassign them, so nothing disappears silently. Staff logins get the whole tenant from the same endpoint.

In the demoJohn Doe, CDC-HBA1C, Sarah Martinez, EED-RETINAL

Check it yourselfAs the VBC manager, Provider responses > Sent for Dr. P. Osei equals the total number of rows in the three sections here.

app/api/population.py · list_opportunities()app/services/opportunities.py · list_rows()app/core/scope.py · filter_patient_rows()

Do column: which actions a row offers

Do column of every section

Formula

NEW or VIEWED → Accept · Already completed · Need info · Not appropriate · Dispute IN_PROGRESS → Mark service done · COMPLETED → Return evidence MORE_INFO_NEEDED → 'waiting on the care team' · settled → 'settled' · anything else → '—'

Why this wayThe screen only draws the actions the server accepts at that status, and the server still checks each one: 'Already completed' needs evidence that matches the measure, and 'Not appropriate' and 'Dispute' need a reason. One difference from the server: it also accepts evidence while an opportunity is IN_PROGRESS, but the screen offers Return evidence only at COMPLETED. Opening the member from a NEW opportunity (the member button) records that the provider viewed it: NEW becomes VIEWED, and 'Viewed' means the provider opened it.

In the demoJohn Doe: Accept, Already completed, Need info, Not appropriate, Dispute, Sarah Martinez: Accept, Already completed, Need info, Not appropriate, Dispute

Check it yourselfClick Already completed and leave the evidence empty: nothing is sent. Enter evidence that does not match the measure: the server refuses it and the gap stays open.

app/static/app.js · _oppActions()app/services/opportunities.py · respond()

Exception Center

11 values

The VBC manager's (and executive's) view of what went wrong: open rule-raised work that is an exception rather than routine, grouped by kind with its age, escalation and lateness, plus the two integration failures that are not per-member work. Providers and auditors cannot open it (the API answers 403). A care manager opens My Exceptions instead: the same computation narrowed to the exception tasks they own (D-F), with no integration panel.

My Exceptions: Open exceptions (care manager)

My Exceptions (care manager sidebar), KPI card 1

Formula

the Exception Center's Open count, computed by the same exceptions.summary(), over the open exception tasks whose owner is the signed-in user no integration panel: dead letters and failed batches are platform work, not a member's

Why this wayOne computation, two audiences (D-F): the manager's count of a category and a care manager's share of it cannot be computed two ways. The page is gated by task.read, the permission its endpoint enforces; the team view stays analytics.read. A task owned by nobody is in the team view only.

In the demo5

Check it yourselfSign in as the care manager and open My Exceptions; open queue on a category lands on My Work (Mine) filtered to that exception, with the same rows.

app/services/exceptions.py · summary()app/api/population.py · my_exceptions()

Open exceptions (and the Open column per category)

KPI card 1, top row; Open column of the By category table

Formula

per category: open tasks (status open) raised by the rule engine (source = 'rule') whose rule declares kind: exception in the rule catalogue (EXCEPTION_RULES is derived from it) card = the sum of the Open column over every category

Why this wayThe Exception Center is a view over tasks the rule engine already raised, not a separate store, so an exception is resolved exactly when its task is done or dismissed. Which rules count is declared by each rule itself: every catalogue entry must say kind: exception or kind: routine, and a test fails on an entry that says neither, so no rule is routine by omission. As shipped 15 rules are exceptions (pa.sla_at_risk, pa.denied_appealable, vendor.sla_breach, referral.sla_breach, referral.awaiting_acceptance, engagement.exhausted, engagement.stalled, hra.overdue, coverage.lapsing, sdoh.barrier_unaddressed, opportunity.unanswered, gap.evidence_missing, appointment.not_rebooked, opportunity.disputed, opportunity.review). 'Abnormal result with no follow-up', 'Enrolled member with no care manager or no care plan' and 'Referral follow-up due' are declared routine, so their tasks sit in the team queue and are not counted here; whether they should be exceptions is an owner decision. The integration failures below are not included in this card.

In the demo5 = 2 HRA past its contractual window + 3 Social barrier with nothing done about it

Check it yourselfthe Open column adds up to the card. Click 'open queue' on a row: the team queue opens filtered to that rule and lists the same number of tasks.

app/services/exceptions.py · summary()app/static/app.js · rExceptions()

Critical (per category)

Critical column of the By category table (red when above 0)

Formula

per category: open exception tasks whose current priority is Critical

Why this wayThe one per-category severity signal. It counts the task's priority as it stands now, so a task escalated from High counts here after its escalation. It does not change the row order, which is Escalated, then Overdue, then Open.

In the demo0

Check it yourselfclick 'open queue' on a row: that many of its rows carry a Critical priority badge.

app/services/exceptions.py · summary()

Aged ('waiting 7+ days') and the Aged column

KPI card 2; Aged column of the By category table

Formula

an open exception task is aged when business today − waiting_since(task) ≥ 7 calendar days (AGED_DAYS = 7) waiting_since = the task's due date, else the business date it was created · card = the sum of the column

Why this wayAged counts from the same date escalation counts from (task_rules.waiting_since), so the two cannot disagree about how long a task has waited. A task that carries a due date ages from that date, so an HRA task whose assessment was due 60 days ago is aged on the day it is raised, although the nightly demo reset re-creates every task that day. A task with no due date ages from the day it was raised. The 7 days is a constant in the code, not a tenant setting; the card caption is read from the same constant (aged_days).

In the demo2 tasks

Check it yourselfa category whose 'Waiting since' date is 7 or more days ago has at least one aged task; in the fresh demo tenant the overdue HRA tasks are aged from their due dates.

app/services/exceptions.py · summary()

Escalated (card caption from the server) and the Escalated column

KPI card 3; Escalated column (amber badge when above 0)

Formula

an open exception task is escalated when a task.escalated audit row exists for it card = the sum of the column

Why this wayEscalation is read from the audit trail, never stored on the task, so it cannot be set by hand. The rule that writes it: an open rule task whose rule sets escalate_after_days and escalate_to_role, once today − waiting_since(task) ≥ escalate_after_days (its due date, else the business date it was created), is raised one priority band and gets '[escalated to <role>]' added to its reason, once; the task.escalated audit row is what makes it once. As shipped only gap.evidence_missing and appointment.not_rebooked set these (7 days, vbc_manager), so the other 13 categories show 0 unless a tenant adds the parameters. The card caption is the server's own statement of what escalation does (ESCALATION_MEANING): priority raised one band and flagged for the escalation role; the owner keeps the task. Nothing is reassigned and nobody is notified. See the My Work chapter, 'Escalated priority'.

In the demo0 tasks

Check it yourselfnone in a fresh tenant. After a rule sweep, an 'evidence missing' or 'missed visit not rebooked' task 7 days old is counted here, and in the queue its priority is one band higher and its reason ends '[escalated to vbc_manager]'.

app/services/exceptions.py · summary()app/services/task_rules.py · _escalate_overdue()

Overdue ('past their due date') and the Overdue column

KPI card 4; Overdue column (red badge when above 0)

Formula

an open exception task is overdue when its due date (first 10 characters, read as an ISO date) is strictly before business today no due date, or one that cannot be read → never overdue · card = the sum of the column

Why this wayLateness is computed from the task on every read, never stored. Due today is not overdue. This counts exception tasks only; the Command Center's 'Tasks overdue' card uses the same test over EVERY open task, so it is normally the larger number. The due date is the one the rule copied from the triggering record (for 'HRA past its contractual window', the assessment's own due date).

In the demo2 (2 HRA past its contractual window)

Check it yourselfclick 'open queue' on a category with a red badge: that many of its rows carry the red OVERDUE badge (the queue applies the same test to the same ten characters of the due date).

app/services/exceptions.py · summary()

By category N (number of categories)

Title of the By category table

Formula

number of distinct rule_key values with at least one open exception task each category's title and grey 'why' line come from the rule catalogue (the 'why' cut to 120 characters)

Why this wayCategories are rules, not a separate taxonomy, so each one maps to exactly one filter on the team queue. A rule with no open task is not listed at all (there are no zero rows); when no category is open the table is replaced by 'No open exceptions', while the four cards still show 0.

In the demo2 categories

app/services/exceptions.py · summary()

Row order of the By category table

By category table, top to bottom

Formula

sorted by Escalated (most first), then Overdue (most first), then Open (most first); a full tie keeps the order the tasks were read

Why this wayEscalation is the strongest signal because a rule already decided the task had waited too long; lateness comes next; volume last, so a large pile of on-time work does not bury one late item. The order is the only output; no rank is shown.

In the demoHRA past its contractual window (esc 0, overdue 2, open 2), Social barrier with nothing done about it (esc 0, overdue 0, open 3)

Check it yourselfthe first row has the most escalated tasks; among equal escalated counts, the most overdue.

app/services/exceptions.py · summary()

Waiting since (per category)

Waiting since column of the By category table

Formula

the earliest waiting_since date among the category's open exception tasks (the due date, else the business date the task was created), as an ISO date '—' when none of its tasks has either

Why this wayIt is the same clock as Aged and as escalation, so a category whose date is 7 or more days ago has at least one aged task. Where a task carries a due date copied from its record (an HRA's due date), this is when the obligation fell due, not when the task was raised.

In the demo60 days before the reset

Check it yourselfa date 7 or more days before today means the Aged column of that row is at least 1.

app/services/exceptions.py · summary()

Dead-letter events ('webhook deliveries that gave up')

Integration failures panel, card 1

Formula

number of the tenant's outbox events that were never published and have used all MAX_ATTEMPTS = 8 delivery attempts all-time: no date window

Why this wayAfter MAX_ATTEMPTS failed attempts the relay stops retrying an event and leaves it unpublished, so it stays in this count for as long as it stays unpublished; there is no age limit and no dismissed state. The test is one function in the relay (relay.dead_letters), which the administrator's Webhooks screen also calls, so the two screens always show the same events; the Webhooks screen reads the attempt limit from the server rather than typing it.

In the demo0 events

Check it yourselfzero in the fresh tenant (the demo relay is off). The administrator's Webhooks screen shows the same number.

app/services/exceptions.py · summary()app/services/relay.py · drain()

Failed ingestion batches ('files with rows that did not load')

Integration failures panel, card 2

Formula

number of the tenant's ingestion batches with rows_failed > 0 that nothing has resolved (ingest.unresolved_failed) resolved = a LATER batch of the same source and file name loaded with 0 failed rows

Why this wayA failure stays here until the corrected file is re-sent and loads clean, so the card can return to 0 instead of counting every failure ever. The list behind it is GET /ingest/batches?failed_only=1, the same function with no row cap, so the card always equals that list. Known limit: a failure that will never be re-sent (the rows were fixed by hand, or the file was obsolete) has no way to be marked handled yet; an explicit acknowledge-with-a-note is an owner decision.

In the demo0 batches

Check it yourselfzero in the fresh tenant. Load a file with a bad row: the card reads 1; re-send the corrected file under the same name: it returns to 0.

app/services/exceptions.py · summary()app/services/ingest.py · unresolved_failed()

Automation

12 values

The VBC manager's (and the care manager's) check on the task rules: which rules are on, how much work each has raised, what happened to that work, and what the rules would raise if they ran now. Figures are shown as the VBC manager Alex sees them; the care manager sees the same numbers.

Rules on (X/Y), and the Rules table count

KPI card 1 ('watching for work'); the count next to the Rules panel title

Formula

X = catalogue rules whose tenant setting has enabled = true Y = number of rules in the product catalogue (CATALOGUE), on or off the Rules table has one row per catalogue rule, so its count is also Y

Why this wayThe catalogue is fixed in the product: a tenant can turn a rule off or change its priority, owner role and thresholds, but cannot add a rule with no implementation behind it. Every rule starts on. Known limit: three rules (opportunity.review, opportunity.disputed, opportunity.more_info) are raised by a provider's response, not by the sweep. They count as 'on' and 'watching for work' although the sweep never proposes anything for them.

In the demo29/29 (table: 29 rules)

Check it yourselfCount the rows in the Rules table: there are Y. As the VBC manager, each row's button says 'Turn off' when the rule is on; the care manager sees an ON badge instead.

app/services/task_rules.py · outcomes()app/services/task_rules.py · ensure_rules()

Raised

KPI card 2 ('tasks created by rule'); Rules table, Raised column

Formula

per rule: number of tasks carrying that rule_key, in any status (open, done or dismissed), all time card = sum of the Raised column over the catalogue rules

Why this wayAll time, with no date window, so the number only grows. Each rule raises at most one task per triggering record (the store enforces one task per tenant, rule and trigger), so a rule's Raised is the number of distinct records that ever triggered it. Known limit: two features write rule-keyed tasks under keys that are not in the catalogue (hra.<finding> from an HRA finding, engagement.evidence_retrieval from an Engagement outcome). They appear in no row and in no card on this screen, the Unassigned card included (every card is the sum of its column). There are none in the fresh demo tenant.

In the demo28 (transition.contact 1, transition.visit 1, transition.med_rec 1, transition.assessment 1, transition.barriers 1, gap.due_soon 1, lab.critical_result 6, sdoh.barrier_unaddressed 3, hra.overdue 2, enrollment.offer 1, enrollment.started_without_owner 8, engagement.outcome_untriggered 2) tasks

Check it yourselfAdd up the Raised column: it equals the card.

app/services/task_rules.py · outcomes()

Open

KPI card 3; Rules table, Open column

Formula

per rule: tasks with that rule_key whose status is exactly open (a started task is still open) card = sum of the Open column

Why this wayOpen means not yet settled, whether or not someone has started it. It can differ from My Work's 'raised by a rule' count, which is based on the task's origin (rule or the legacy transitions sweep), not on its rule key. In the demo the two agree because every rule-origin open task was raised by a catalogue rule. Known limit: the status is compared case-sensitively here, while the sweep lower-cases it; a task stored as 'Open' would count in Raised and in no other column. None exist in the demo tenant.

In the demo25 tasks

Check it yourselfAdd up the Open column: it equals the card. My Work, Team view, shows the same tasks with an AUTO badge.

app/services/task_rules.py · outcomes()

'N overdue' (card subtitle) and the red 'N overdue' badge

Under the Open card; Rules table, next to a rule's Open count

Formula

an open rule task is overdue when its due date (first 10 characters, read as a date) is before today's business date (America/New_York) per rule badge: shown only when N > 0 · card subtitle = sum over rules

Why this wayThe same boundary as My Work's OVERDUE badge: a task due today is not overdue. Two rules (gap.evidence_missing, appointment.not_rebooked) raise tasks with no due date, so their tasks can never be overdue here; those two rules escalate by age instead (see My Work, 'Escalated priority').

In the demo7 (gap.due_soon 1, lab.critical_result 4, hra.overdue 2) tasks

Check it yourselfThe red badges in the Open column add up to the card subtitle.

app/services/task_rules.py · outcomes()app/static/app.js · rAutomation()

Completed (card)

KPI card 4 ('settled as done')

Formula

number of catalogue rule tasks with status done, all time

Why this wayA task is done when a person completes it, or when the platform completes it because the care gap it was raised for closed on evidence. The Rules table's 'Completed %' column is a percentage of settled work (next row), not this count.

In the demo3 tasks

Check it yourselfIn the demo the three done tasks are Sam Okafor's post-discharge contact, medication reconciliation and barriers steps: those three rules show 100% in the Completed column, and the Transitions screen shows the same steps done.

app/services/task_rules.py · outcomes()

Completed % and its badge colour

Rules table, Completed % column

Formula

completed % = round( 100 × done ÷ (done + dismissed) ); '—' when done + dismissed = 0 badge: red when dismissed % > 50 · else green when completed % ≥ 60 · else grey

Why this wayThe denominator is settled work only. Counting open tasks as failures would make a rule look worse the more recently it fired. The cost: a rule with one settled task shows 100% or 0% on that one task, so the percentage says nothing about volume; read it with Raised. Rounding is Python's, half to even, so 1 of 8 (12.5) prints 12 where a spreadsheet prints 13.

In the demotransition.contact 100%, transition.med_rec 100%, transition.barriers 100%

Check it yourselftransition.contact: 1 done, 0 dismissed, so 100% and a green badge. A rule with nothing settled shows '—' in grey.

app/services/task_rules.py · outcomes()app/static/app.js · rAutomation()

Dismissed %

Rules table, Dismissed % column

Formula

dismissed % = round( 100 × dismissed ÷ (done + dismissed) ); '—' when nothing is settled withdrawn tasks are in neither the numerator nor the denominator completed % + dismissed % = 100 (±1 from rounding)

Why this wayThe screen reads a high dismissal rate as a rule raising work the team does not believe in, and only a person's dismissal counts here. For seven rules (gap.due_soon, sdoh.barrier_unaddressed, engagement.exhausted, engagement.escalate, coverage.lapsing, pa.sla_at_risk, pa.denied_appealable), an open task nobody has started is WITHDRAWN when its trigger stops being true, for example because the authorization was decided, coverage was renewed or a referral for the social barrier started. It is stored as its own status (withdrawn), shown in the Withdrawn column, and counted in neither rate, so a rule whose work mostly resolves itself does not show red. Its reason still ends '— withdrawn <date>: no longer applies'. (A care gap closed on evidence is different: its tasks are marked done.)

In the demotransition.contact 0%, transition.med_rec 0%, transition.barriers 0%

Check it yourselfNo task is dismissed in the fresh tenant, so every settled rule shows 0%. Dismiss a rule-raised task in My Work and reload: that rule's Dismissed column moves and its Completed column falls by the same amount.

app/services/task_rules.py · outcomes()app/services/task_rules.py · retire_stale()

Withdrawn

Rules table, Withdrawn column

Formula

per rule: tasks with that rule_key whose status is withdrawn, all time

Why this wayThe platform retracting its own task because the trigger stopped being true (see Dismissed %). It is not a person's judgment, so it is kept out of both rates; a rule with many withdrawals is one whose work often resolves itself before anybody acts. Tasks retracted before this column existed were moved over from 'dismissed' by a data migration that recognises the '— withdrawn' the engine wrote on their reason.

In the demo0

Check it yourselfDecide an authorization that has an open 'approaching its decision deadline' task and press Run now: that rule's Withdrawn goes up by one and its Dismissed % does not move.

app/services/task_rules.py · outcomes()app/services/task_rules.py · retire_stale()

Unassigned

KPI card 5 ('rule tasks with no owner')

Formula

per rule: catalogue rule tasks whose status is open and that have no owner_id card = the sum of that per-rule count over the catalogue rules

Why this wayA rule gives a task to the member's current owner first, then to the holder of the rule's role with the fewest open tasks, and leaves it unowned only when nobody holds that role. Like every card on this screen it is limited to the catalogue rules, so it can never exceed Open. It differs from My Work's 'unassigned', which counts every open task with no owner, typed-in and event-raised ones included. In the demo tenant this card reads 0 while My Work reads 1, because the one unowned task was typed in by hand.

In the demo0 (My Work unassigned: 1) tasks

Check it yourselfMy Work, Team view: count the UNASSIGNED badges on rows with an AUTO badge and a rule; that is this card.

app/services/task_rules.py · outcomes()app/services/task_rules.py · _assign()

Would raise now, and 'N already raised'

KPI card 6; the count next to 'What would be raised right now'

Formula

candidates = one evaluation pass of every ENABLED rule, today would raise = candidates whose (rule, trigger) has no task yet, in ANY status already raised = candidates − would raise '—' and 'preview unavailable' when the preview call fails

Why this wayIt shows the size of a change to somebody else's queue before it happens. Three things to know. (1) A trigger whose task was completed or dismissed counts as 'already raised' and is never raised again, so a recurring problem on the same record does not come back as new work. (2) 'Run now' repeats the pass up to 4 times so rules that feed each other settle, so a run can create more than this card predicted. (3) The preview evaluates the rules and then rolls back everything the evaluation did (task_rules.preview): some rules act as they evaluate (a stalled ladder moves to its next channel), and opening this screen must not do that. 'Already raised' can be lower than Raised: in the demo the trigger of one 'enrolled but no active care plan' task (enrollment.started_without_owner) no longer holds, and that rule does not withdraw its tasks, so one raised task is no longer live.

In the demo0 would raise, 27 already raised

Check it yourselfPress Run now as a user who may run it: the toast's 'N task(s) created' is at least the card's number, and the card then reads 0.

app/api/population.py · task_rule_preview()app/services/task_rules.py · evaluate()

What would be raised right now: the rows, their priority and due date

Preview panel under the Rules table (hidden when nothing would be raised)

Formula

the first 10 new candidates, in the order the rules are evaluated (not sorted by priority or due date) priority = the candidate's own grade where the rule sets one (e.g. abnormal lab), else the rule's configured priority due = the date the rule computes (see My Work, 'Due date of a rule-raised task'); '—' when none

Why this wayA sample, not the list: the panel's count is the full number, and only 10 rows are shown. Because the order is evaluation order, the most urgent candidate is not guaranteed to be among the 10.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNothing would be raised in the fresh tenant, so the panel is hidden. It appears when a rule finds a trigger with no task yet, for example after a rule is turned back on or new data is loaded, and it lists what Run now would create.

app/api/population.py · task_rule_preview()app/services/task_rules.py · evaluate()app/static/app.js · rAutomation()

'N task(s) created' / 'Nothing new' (Run now)

Toast after pressing Run now

Formula

N = tasks created across up to 4 evaluation passes, stopping at the first pass that creates nothing; a (rule, trigger) that already has a task is skipped after the passes, open unstarted tasks of the seven withdrawable rules whose trigger is gone are withdrawn (status withdrawn), and overdue tasks of rules with an escalation setting move up one priority band

Why this wayRunning repeatedly is safe: each trigger raises at most one task, so a second press says 'Nothing new'. The run result also reports how many tasks were withdrawn and escalated and whether the passes settled, but the toast shows only N. The same run happens on a timer and on every ingestion, so the button is for watching it happen.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfProduced only by pressing Run now, which writes. The button is shown only to a user holding the permission its endpoint requires (vbc.admin: the VBC manager in the demo); the page reads that permission from one table (ACTION_NEEDS) that a test compares with the endpoint.

app/api/population.py · task_rule_run()app/services/task_rules.py · run()

Prior authorizations

19 values

The UM nurse's and medical director's Review Queue, and the Authorizations screen that the care manager, PA coordinator, provider (own panel only), VBC manager, auditor and administrator open from the same menu item: every prior-authorization request with its status, decision clock, policy and authorization number, the Review and Detail windows behind each row, and the Appeals panel. This chapter also covers the messages the Launch PA flow on Patient 360 prints, and the notifications bell those decisions feed. The documentation readiness panel, the eligibility window and the Ask panel have their own rows in the Analytics chapter.

Review Queue / Authorizations (count in the heading)

Panel heading. UM nurse and medical director see 'Review Queue'; every other role sees 'Authorizations'

Formula

count = every prior-authorization request in the tenant, in EVERY status, after the provider-panel filter (a provider sees only members attributed to them) rows are listed newest submission first

Why this wayThis is the whole register, not open work. A reviewer's 'Review Queue' count includes approved, denied, withdrawn, closed and expired requests, and only rows in IN_REVIEW get a Review button; all the others get Detail. In the fresh demo the medical director's Review Queue shows 1 and has nothing to review: the one seeded request is already approved. The screen also counts the appeals still in review, and never shows that count.

In the demo1 requests

Check it yourselfCount the table rows: they match the heading, whatever their status. For work actually waiting on a reviewer, count only the rows with a Review button.

app/api/pa.py · list_pas()app/static/app.js · rPAs()

Status (request status badge)

Status column; also the Detail window's title and the determination message

Formula

on submit → IN_REVIEW (it passes through SUBMITTED inside the same save) · approve → APPROVED · deny → DENIED · pend → PENDED · provider responds to a pend → IN_REVIEW · withdraw → WITHDRAWN appeal filed → APPEALED; appeal overturned or partially overturned → APPROVED, upheld → CLOSED APPROVED whose valid-to date is before today (US Eastern business date) → EXPIRED

Why this wayMoves follow one transition table, and the decision paths refuse a move it does not allow. Two paths, the provider's response and the appeal decision, set the status directly, but only to moves the table allows. Only a medical director with an MD credential can deny, and only with a reason code and text. Expiry is applied by the automation sweep (every 15 minutes by default), so an authorization can read APPROVED for up to one sweep after its last valid day. PARTIALLY_APPROVED is a legal state that nothing writes: a partially overturned appeal leaves the request APPROVED, with a full authorization number.

In the demoPA-2026-40188 APPROVED

Check it yourselfApprove a request in the Review window: the toast and the badge both read APPROVED. The Audit Trail has one event for each move.

app/services/workflow.py · submit_pa()app/services/workflow.py · decide_pa()app/services/workflow.py · respond_to_pend()app/services/workflow.py · withdraw_pa()app/services/appeals.py · file_appeal()app/services/appeals.py · decide_appeal()app/services/workflow.py · expire_authorizations()

SLA due clock ('in 6d 22h' / 'overdue 3h 10m')

SLA due column; the chip in the Review window title; 'SLA due <date> (in …)' in the message after Submit PA on Patient 360

Formula

decision due = submitted at + 72 h (expedited) or + 7 days (standard); see Part I, Prior-authorization decision clock remaining = due − now, floored: 'Xd Yh' when 1 day or more, 'Hh Mm' under a day, 'Mm' under an hour; 'overdue …' once past the column shows it only while the status is IN_REVIEW; every other status shows '—'

Why this wayThe clock keeps running while a request is PENDED: answering the pend does not restart it, and the column shows '—' for the whole pend (My Work's at-risk task does watch PENDED requests; see My Work, 'decision due in Hh'). Known defect: the list and the Review window receive the due time without a time zone, and a browser reads a time with no zone as its own local time. The countdown is off by the viewer's offset from UTC: a viewer on US Eastern time sees about 4 hours more than really remains, and a viewer in Pakistan about 5 hours less. The message after Submit PA receives the time with its zone, so it is correct, and for the same request it can disagree with the column by that offset. The 72 h and 7 days come from deployment settings; the labels 'Expedited — 72h SLA' and '7 calendar days' are fixed text.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfThe one seeded request is approved, so the fresh demo shows '—'. Launch a PA for John Doe's retinal exam: for a viewer on UTC the new row's chip reads 'in 6d 23h' (standard), and on US Eastern time about 'in 7d 3h' because of the defect above. Hover the chip to see the due time: it is UTC, printed without a zone.

app/services/workflow.py · submit_pa()app/api/pa.py · list_pas()app/api/pa.py · get_pa()app/static/app.js · relTime()app/static/app.js · slaChip()app/static/app.js · submitPA()

Clock colour (red / amber / green)

Every SLA chip on this screen: the request column, the Review window, the Appeals panel and the Independent appeal review window

Formula

red when overdue or less than 24 h remain · amber when less than 72 h remain · green otherwise

Why this wayFixed thresholds in the browser, the same for request and appeal clocks. An expedited clock is 72 hours long, so it is amber from the moment it starts. These bands only colour the chip. They are separate from the points an approaching deadline adds to a task's rank on My Work (see Part I, Work-queue ranking). The same time-zone defect as the clock applies, so for a viewer far from UTC a chip can turn red hours early or late.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNothing is on a clock in the fresh demo. Submit an expedited request: its chip is amber at once, while a standard request's chip is green.

app/static/app.js · relTime()

Policy and version ('UHC-MA-EYE-014 v3')

Policy column; 'policy … v…' in the Review window; 'Policy snapshot' in the Detail window

Formula

at submission, from the member's active coverage (payer, line of business): among ACTIVE policies for that payer and line of business, newest version first, the first whose service codes list the request's CPT the key and version are frozen on the request; blank when no policy lists the code

Why this wayFrozen so that publishing a new policy version never rewrites the rules an open case is judged under; the decision trace records the same version. When nothing matched, the column prints a bare 'v' with no key: the request was still accepted, with no policy to review it against.

In the demoPA-2026-40188: UHC-MA-EYE-014 v3

Check it yourselfSign in as the policy administrator: Policy Packages lists UHC-MA-EYE-014 v3 as active for UnitedHealthcare Medicare Advantage, with 92250 among its codes.

app/services/policy.py · resolve_policy()app/api/pa.py · create_pa()

PA reference ('PA-2026-40188')

PA column of both tables; window titles; toasts

Formula

'PA-2026-' + a random whole number from 10000 to 99999

Why this wayAn identifier, not a sequence. A gap between numbers is not a missing case, and the numbers carry no order or count. '2026' is fixed text, not the year of submission. Known limit: the space is 90,000 numbers shared by every tenant, and a repeated number fails the submission with a server error instead of drawing again.

In the demoPA-2026-40188

app/api/pa.py · create_pa()

Auth # (authorization number) and its validity window

Auth # column; Detail window; determination message; 'Auth … · valid to <date>' in the approval notification

Formula

on approval, or on an overturned or partially overturned appeal: AUTH- + 8 random upper-case hex characters valid from = the decision's business date (US Eastern); valid to = the business date 180 days later '—' when the request was never approved

Why this wayOne function mints both the number and the window, so a first approval and an overturned appeal cannot disagree on how long an authorization lasts. An overturn starts a new 180 days from the overturn date. The Detail window shows the number but not the dates, although the server returns them; they appear in the approval notification and the eligibility window. Known inconsistency in the demo: the seeded authorization was written directly, not through an approval, so its window is 90 days (valid from the day before the reset to 89 days after it) and not the 180 every real approval receives.

In the demoPA-2026-40188: AUTH-88213417

Check it yourselfApprove a request: the message ends '· AUTH-…', and the care manager's bell shows 'valid to' a date 180 days after today.

app/services/workflow.py · grant_authorization()app/services/workflow.py · decide_pa()app/services/appeals.py · decide_appeal()

AI advisory (model label and note)

Review window, 'AI advisory (…), cannot decide' box, after 'Run criteria analysis'

Formula

criterion outcomes = the readiness rules on the record: evidenced → MET; in the chart but not attached, or not evidenced → UNKNOWN; when the check returns no criteria (no policy found), the browser's fixed C1–C3 MET are stored instead model label = the fixed text apex-um v1.8 note stored = the text the browser sends, and the readiness narrative only when it sends none

Why this wayNo model is involved: the advisory is the deterministic readiness check (see the Analytics chapter's readiness rows), which is why it can cite evidence and never invent it. It records a trace row and cannot change the case. Read the box with care. The label names a model version that does not exist. The browser always sends the same fixed sentence, '3/3 MET on available evidence. Approval probability high', so that sentence is what is stored, even for a four-criterion policy and even when the check found criteria it could not evidence. No approval probability is computed anywhere. On screen the note line is blank, because the window's data does not include the stored note.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo advisory exists in the fresh demo. As the UM nurse, open Review on a request and click Run criteria analysis: the box appears with the label apex-um v1.8, and the Detail window's trace gains an 'ai_advisory' row.

app/api/pa.py · ai_advisory()app/services/readiness.py · to_outcomes()app/static/app.js · runAI()app/static/app.js · reviewPA()

Determination message ('PA-… → APPROVED · AUTH-…') and the criterion outcomes it records

Review window, after Approve, Pend or Deny

Formula

status = approve → APPROVED, deny → DENIED, pend → PENDED; the authorization number is added only on approval criterion outcomes recorded = one of two fixed templates chosen in the window: 'ALL MET' (C1 C2 C3 all MET) or 'C2 UNKNOWN' (C1 C3 MET, C2 UNKNOWN)

Why this wayThe status and number come back from the server, so the message cannot claim what was not saved. The outcomes do not come from the policy. The window offers two fixed three-criterion templates, whatever the policy says. The demo's retinal-exam policy has four criteria (C1 to C4), so a human decision records C4 as neither met nor unknown: it is absent. Read the trace's outcomes as the reviewer's selection, not as an evaluation of each criterion.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfAppears only after a decision. In the demo, sign in as md@demo.payer, Review the care manager's new request and Approve it. The message reads '<ref> → APPROVED · AUTH-…'.

app/services/workflow.py · decide_pa()app/api/pa.py · decision()app/static/app.js · decide()

Decision trace (count)

Detail window, 'Decision trace' heading

Formula

count = every review row on the request, of every kind: human decisions, withdrawals and appeal decisions (all recorded as human), ai_advisory, peer_to_peer and provider_response

Why this wayA count of recorded events, not of decisions: an advisory followed by an approval counts 2, and a peer-to-peer request counts as well. Known defect, visible in the fresh demo: the seeded approved authorization PA-2026-40188 shows a trace of 0 and the word 'none'. It was written into the database directly, not through the approval path that records the reviewer, so an APPROVED case has no recorded decision behind it.

In the demo1 rows

Check it yourselfOpen Detail on PA-2026-40188: 'Decision trace 0', then 'none'. Approve a new request and its Detail window shows 1 human row.

app/api/pa.py · get_pa()app/static/app.js · viewPA()

Decision trace row ('human · MD… | approve · criteria v3')

Detail window, one line per trace row

Formula

left: kind · model (advisory only) · reviewer credential right: disposition (approve deny pend withdrawn info_provided p2p_requested appeal_…; 'advisory' when none) · criteria v = the request's frozen policy version, 0 when it has none

Why this wayThe criteria version is copied from the request, not looked up again, so the trace proves which version of the rules the case was judged under even after the policy moves on. The credential is the reviewer's own, from their login, which is how an adverse decision shows it came from a physician.

In the demohuman · approve · criteria v3

Check it yourselfEmpty in the fresh demo (see the count above). After an approval, the row reads 'human · MD-… | approve · criteria v3'.

app/services/workflow.py · decide_pa()app/api/pa.py · ai_advisory()app/api/pa.py · peer_to_peer()app/static/app.js · viewPA()

Appeals (count in the panel heading)

Appeals panel under the request table; the panel is hidden when there are none

Formula

count = every appeal in the tenant, any status and any level, after the provider-panel filter rows are listed newest filing first

Why this wayOpen and decided appeals are counted together, like the request count above. The screen also computes how many are still in review, and never shows that figure. Level-2 external reviews are counted when they exist, but no screen can create one: the endpoint has no button.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo appeal exists in the fresh demo, so the panel is hidden. Deny a request (as the medical director, with a reason), then file an appeal from its Detail window: the panel appears with 1.

app/api/appeals.py · list_appeals()app/static/app.js · rPAs()

Appeal status, and the 'Appeal … — PA …' message

Appeals panel, Status column; message after 'Submit determination' in the Independent appeal review window

Formula

filed → IN_REVIEW · decided: upheld → UPHELD, overturned → OVERTURNED, partially overturned → PARTIALLY_OVERTURNED effect on the request: overturned or partially overturned → APPROVED with a new authorization number · upheld → CLOSED

Why this wayOnly a denied request can be appealed, and only one appeal at a time. The decision is refused unless the reviewer is a medical director with an MD credential who did not issue the original denial, and an uphold needs a reasoned note of at least 10 characters. A partial overturn is treated exactly like a full one: the request becomes APPROVED, not partially approved.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNeeds a denied, appealed request (see the appeals count). Decide it as a different medical director: the message reads 'Appeal overturned — PA APPROVED · AUTH-…'.

app/services/appeals.py · file_appeal()app/services/appeals.py · decide_appeal()app/static/app.js · decideAppeal()

Appeal type ('EXPEDITED 72h' / 'Standard')

Appeals panel, Type column

Formula

'EXPEDITED 72h' when the filer ticked Expedited, otherwise 'Standard'

Why this wayThe '72h' is fixed text in the browser, and so is the '(72h)' in the medical director's notification. The clock actually applied is the tenant's appeal rule (72 hours unless the tenant changes it), so a tenant that sets 24 hours still sees '72h'. A Level-2 external review is never expedited.

In the demo— nothing to show in the fresh demo tenant

app/static/app.js · rPAs()app/services/appeals.py · appeal_rules()

Appeal decision due clock, and 'independent review due in …'

Appeals panel, Decision due column; Independent appeal review window title (expedited appeals only); message after filing an appeal

Formula

decision due = filing time + 72 h (expedited) or + 30 days (standard), from the tenant's appeal rules Level-2 external review: escalation time + 45 days, fixed shown as the same 'in … / overdue …' chip and colours as the request clock, only while the appeal is IN_REVIEW

Why this wayAppeal clocks are a tenant rule, while the request clock is a deployment setting: the two are configured in different places. The review window shows the chip only for expedited appeals. The same time-zone defect as the request clock applies to the panel. The message printed right after filing receives the time with its zone and is correct, so the two can differ for the same appeal by the viewer's offset from UTC.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfFile a standard appeal: the message reads 'independent review due in 29d 23h' or close to it. The panel's chip shows the same due time, shifted by the viewer's offset from UTC.

app/services/appeals.py · file_appeal()app/api/appeals.py · escalate_external()app/api/appeals.py · list_appeals()app/static/app.js · submitAppeal()app/static/app.js · appealReviewModal()

Appeal filing window ('Appeal window closed <date> (N days for this line of business)')

File appeal window, error message; the fixed notes under both panels

Formula

window = the tenant rule's days for the coverage's line of business (Medicare Advantage 60), else its default (180) deadline = the denial's decision time + window days; filing after it is refused, and the message prints the deadline's US Eastern business date

Why this wayLine-of-business rules, not one universal clock, as the panel notes say. The deadline appears here only once it has passed. My Work's denied-authorization task and the member attention panel read this same window (appeals.appeal_window), so they name the same closing date. One known limit: a tenant that sets its own per-line-of-business table replaces the default table entirely, so unless it restates Medicare Advantage's 60 days, Medicare Advantage falls back to the tenant's default window.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfThe demo has no denial, so the window never shows. A Medicare Advantage denial decided on 1 July (US Eastern) can be appealed until the same time of day on 30 August; after that, filing is refused with 'Appeal window closed 2026-08-30 (60 days for this line of business)'.

app/services/appeals.py · appeal_rules()app/services/appeals.py · file_appeal()

'PA REQUIRED under <policy> vN' / 'No PA required' / 'PA requirement UNKNOWN' / 'not checked'

Patient 360, message after starting an intervention on a care gap

Formula

no service code on the gap → 'not checked' otherwise, the member's active coverage and the policy resolver (see Policy and version above): a policy lists the code and requires authorization → REQUIRED · lists it and says not required → 'No PA required' · no policy lists it → UNKNOWN

Why this way'Not required' has to come from a policy that says so. A code that no policy on file lists is 'we do not know', never 'no': an unknown shown as unknown costs a phone call, and an unknown shown as 'no' costs a denied claim. The readiness panel answers the same question with the same resolver (see the Analytics chapter, 'Prior authorization required?').

In the demoPA REQUIRED under UHC-MA-EYE-014 v3

Check it yourselfThe screen asks through POST /pa/requirement-check, which writes an audit row, so this value is read through the readiness pre-check, which uses the same resolver. On John Doe's retinal exam (92250) the demo message reads 'PA REQUIRED under UHC-MA-EYE-014 v3' (runbook step 10).

app/api/pa.py · policy_answer()app/api/pa.py · requirement_check()app/static/app.js · startIntervention()

'N of M found documents attached'

Patient 360, message after Submit PA in the Launch PA window

Formula

M = the documents the readiness check found in the chart but not attached, when 'attach' is ticked (0 otherwise) N = those the browser then attached, plus any already attached to this request the message turns red when N < M

Why this wayAttaching runs after the request is saved, one document at a time, so a failure to attach never loses the request; the message says how many made it. A document already attached counts as attached, because a repeat submission under the same idempotency key finds the request already bound to it, which is the intended end state.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfNo seeded chart holds an unattached document of a required type, so the fresh demo prints no 'N of M'. Upload a clinical note for the member first, then Submit PA: the message ends '1 of 1 found document attached'.

app/static/app.js · submitPA()

Notifications bell (unread count) and its items

Header bell, every screen

Formula

unread = notifications not marked read, among the newest 30 addressed to this user or to this user's role hidden at 0, '9+' above 9 items: a decision → the care manager role; a pend answered → the UM nurse role; an appeal filed or a peer-to-peer request → the medical director role; an appeal decided → the person who filed it; a referral update → the person who created the referral

Why this wayEmitted in the same save as the event, so a notification can never announce something that did not happen. Known limits. Read is one flag on the notification, not per person, so when one care manager opens a role-wide notification it disappears for every care manager. Unread items older than the newest 30 are never counted. A decision notifies the care manager role only, so the PA coordinator or provider who submitted the request is not told.

In the demo— nothing to show in the fresh demo tenant

Check it yourselfThe fresh demo has no notifications. After the medical director approves the care manager's request (runbook step 13), the care manager's bell shows 1: '<ref> approved', 'Auth AUTH-… · valid to <date>'.

app/api/appeals.py · my_notifications()app/api/appeals.py · mark_read()app/services/appeals.py · notify()app/static/app.js · refreshBell()

Member portal

6 values

The member's own view, signed in as the member (the demonstration member John Doe): what care is due, their visits, their prior authorizations, their messages and how they want to be contacted. It deliberately shows no risk tier, cost, contract or internal review detail.

Plan and effective dates (header line)

Header, under the member's name

Formula

the one coverage rule (services/coverage.current_coverage): the coverage whose status is active and whose dates contain today (the most recently started, if several — a mid-year change not yet closed out); payer, plan, effective from and to, then 'verified with the plan on <date>' or 'not yet verified with the plan' (eligibility.coverage_state) no current coverage → the line is not shown

Why this wayThe member sees the plan the care team's screens name, by the same function, and the same verification caveat the care team's authorizations panel prints (eligibility.coverage_state): the record saying 'active' is not the payer saying so today, and the member is told which it is.

In the demoUnitedHealthcare MA Choice, 2026-01-01 to 2026-12-31, not yet verified

Check it yourselfThe care team's Patient 360 for the same member shows the same payer and plan.

app/api/member.py · profile()

What is due (count and list)

What is due panel, title count and table

Formula

the member's care gaps whose status is OPEN, IN_PROGRESS, SCHEDULED, AWAITING_AUTH or REOPENED (the open-gap states GAP_OPEN_STATES minus SERVICE_COMPLETED); count = number of rows, no limit sorted by due date, soonest first (no date last); Due = the gap's due date, with 'overdue' when the business day is past it (clock.is_past_due, the care team's lateness test) 'Why' = the reason the gap engine recorded when it opened the gap

Why this wayDeliberately narrower than every staff gap count: a gap in SERVICE_COMPLETED means the member has attended and the evidence is still on its way, so there is nothing left for the member to do and listing it would ask them to go again. The cost: this count can be LOWER than 'Open gaps' on the care team's Patient 360 summary strip for the same person, by the number of gaps awaiting evidence. The due date is shown, so an overdue item does not look the same as one due at year end.

In the demo5 due: HbA1c Control — Glycemic Assessment; Annual Wellness Visit; Blood Pressure Control; Diabetic Retinal Exam; Statin Adherence (PDC >= 80%)

Check it yourselfCount the table rows; open John Doe's Patient 360 as the care manager and compare with the Open gaps card (they differ only by gaps in SERVICE COMPLETED).

app/api/member.py · care()app/static/app.js · rMember()

Your appointments (count and list)

Your appointments panel, title count and table

Formula

upcoming = the member's appointments that are BOOKED or CONFIRMED on today's business date or later (scheduling.split_upcoming, the definition My Panel's next appointment uses), soonest first; title count = upcoming past and cancelled visits (every other appointment) are listed under them, newest first, and not counted

Why this wayThe title promises booked visits, so it counts only visits the member is still expected at: a cancelled or missed visit, or one already attended, is history and is shown as such. The fresh demo member has no booked visit, so the panel shows 'No upcoming appointments'.

In the demo0 appointments

Check it yourselfWhen the care team books a visit for John Doe, sign in as the member: the count rises by one; cancelling the visit returns the count to where it was, and the visit moves to 'Past and cancelled visits'.

app/api/member.py · appointments()

Your authorizations (count, status and reason)

Your authorizations panel, title count and table

Formula

every prior authorization request for the member, any status, in database order; no limit 'Valid' = valid from … to … when an approval set them, otherwise '—' 'Reason' = the recorded denial reason text only

Why this wayThe member sees status, dates and the specific reason they are entitled to for an adverse determination (CMS-0057-F), and nothing from the review itself: no reviewer notes, criteria evaluation or AI advisory, because exposing those would make the portal a discovery surface. The server also returns the submitted and decided dates and the authorization number, which the table does not show. Empty in the fresh demo tenant.

In the demo0 authorizations

Check it yourselfSubmit a prior authorization for John Doe as the provider or coordinator; it appears here with the same reference and status the PA queue shows.

app/api/member.py · authorizations()

Messages (count and list, '(you)')

Messages panel, title count and table

Formula

the care team's contact history for the member (engagement.history, the same ledger, one row per contact), member audience: a send that FAILED and a call still in progress are left out newest first; the newest 25 are listed; count = every such row (total), with 'Showing the newest 25 of N' when there are more · '(you)' = the row's direction is inbound

Why this wayOne list across SMS, email, portal and voice, so a reply the member typed sits next to the message it answers, and the same rows the care team's contact history lists — a finished call is one row with its transcript. A message that never reached the member is not shown to them as received.

In the demo0 messages

Check it yourselfSend an outreach message to John Doe as the care manager, then sign in as the member: it appears at the top. Replying from the portal adds a row marked '(you)'.

app/api/member.py · messages()app/services/engagement.py · history()

How we contact you (SMS / EMAIL / PHONE ticked)

How we contact you panel, checkboxes

Formula

one box per channel on the engagement ladder (SMS, Email, Portal, Voice — the labels staff screens use), from the member's consent record (engagement.consent_state, the one consent authority): ticked = opted in · unticked and marked 'opted out' or 'never asked' Save sends only the channels the member changed

Why this wayWhat is ticked is what is recorded: the profile carries the consent record the care team's consent panel reads. 'Never asked' is neither a yes nor a no, so it is shown unticked and a Save that does not touch it leaves it never asked; the server accepts only ladder channels and yes/no values. The consent rules themselves are the one consent authority described in Part I, Engagement ladder, wait times and the contact ceiling; the care team's consent table is in the Patient 360: coordination chapter.

In the demoSMS opted in, Email opted in, Portal opted in, Voice never asked

Check it yourselfUntick a ticked channel, Save, reload: it shows 'opted out'. The care manager's Patient 360 Engagement tab shows the same consent per channel.

app/static/app.js · rMember()app/api/member.py · profile()app/services/engagement.py · consent_state()

Admin, integrations, policies and audit

6 values

The platform administrator's and auditor's screens: the tamper-evident audit trail and its verification, inbound data batches, payer policy packages, users and webhooks. Mostly records rather than calculations; the values below are the ones the product computes.

Audit chain verification ('chain intact', events checked, first break)

Audit screen, Verify

Formula

for every audit event in sequence order: hash = SHA-256( previous event's hash + the event's canonical content ); the first event chains from 'GENESIS' intact when every stored hash equals the recomputed one; otherwise the sequence number of the first event that does not match

Why this wayEach event commits to the one before it, so changing, deleting or inserting any past event breaks every hash after it and the check names the first break. Nothing is trusted from storage: every hash is recomputed on each verification.

In the demointact

Check it yourselfPress Verify on the Audit screen: it recomputes the whole chain and reports the count checked.

app/core/audit.py · verify_chain()app/core/audit.py · _digest()

Audit events (total, and the count per action in the filter)

Audit screen, header and action filter

Formula

total = audit events matching the filters (tenant, action, member, date): the number beside 'Audit Trail', read as total from /audit, not the 50 rows one page lists; per-action counts = events grouped by action

Why this wayThe audit trail is append-only; these are plain counts of what it holds, so an auditor can see what kinds of activity exist before filtering to them.

In the demomoves with the clock — read it on screen

app/api/audit.py · list_events()app/api/audit.py · action_vocabulary()

Inbound batches: rows total, promoted, failed, and the batch status

Integrations screen, batches table

Formula

rows total = rows in the file; promoted = rows written into the clinical record; failed = rejected (unparseable value) + unmatched (no member found) status: PROMOTED when any row promoted · FAILED when none promoted and some failed · STAGED otherwise derived from the rows' own statuses every time, so a re-run cannot rewrite the record

Why this wayEvery inbound row is accounted for, one outcome each, with the reason and the raw payload kept for a failed row: in the demo file one row names a member we do not hold and one carries the value 'pending' where a number is required. Known defect being fixed: the batch header's stored promoted/failed counters can lag the rows' own statuses (the header reads 1 promoted, 0 failed for this file); the row list is the authority.

In the demoquest_labs_2026-10-09.csv: 4 rows — 2 promoted, 1 rejected, 1 unmatched

app/services/ingest.py · promote()app/api/integration.py · batches()

Policy packages (count, and which require prior authorization)

Administration → Policy packages

Formula

one row per versioned payer policy package: payer, line of business, state, service codes, whether authorization is required, criteria, status and effective dates

Why this wayPolicy is data, not code: the requirement check, the readiness criteria and the authorization screens all read these rows, so changing a policy changes every screen at once and the version is recorded on each decision.

In the demo3 packages, 2 require authorization

app/static/app.js · rAdmin()

Users (count, by role, MFA enabled)

Administration → Users

Formula

the tenant's user accounts, active or not, with role and whether multi-factor authentication is enabled

Why this wayA plain count of accounts; permissions come from the role (see the manual's permission matrix), never from the user row.

In the demo12 users, 0 with MFA

app/static/app.js · rAdmin()

Webhooks (subscriptions, and the topics you can subscribe to)

Administration → Webhooks

Formula

subscriptions = the tenant's registered endpoints; topics = every event the platform emits to its outbox (authorization, appeal, care gap and intervention events)

Why this wayOnly emitted events are offered, so a subscription can never be to a topic nothing publishes.

In the demo0 subscriptions, 25 topics

app/static/app.js · rAdmin()
mdvin · US healthcare prior authorization and value-based care orchestration · User manual · Architecture · Security · Home
Educational and analytics platform running on synthetic data. Not medical advice, not a coverage determination, and not a substitute for a licensed clinician's judgment.