Clinical rules automation
Encoding CMS care-management rules as a testable system
Medicare runs several care-management programs that a practice can enroll patients into. Each has its own eligibility, its own time thresholds, and its own rules about which of the others it can coexist with in the same month. Getting that wrong is not a bug report — it is a claim that should not have been submitted.
Scope note. This page describes how we built software. It is not billing, coding, or legal guidance, and nothing here should be relied on to determine what a practice may bill. program rules change with each annual rulemaking cycle; the authority is always the current final rule, not this write-up.
Why this is harder than it sounds
Described in a sentence, it is a lookup: patient is in program A, can they also be in program B. The difficulty is that the real answer has at least four shapes, not two. Some pairs are fine. Some are mutually exclusive. Some are permitted only under conditions that software cannot verify on its own — different providers, different conditions, separate care plans. And some are not really a pair at all: one program becomes an add-on to the other, changing which codes apply rather than whether the enrollment is allowed.
Layer on top of that: eligibility is not static. It depends on accumulated time in the month, on how many days a device transmitted, on whether a live conversation happened, on whether this is the patient’s first month in the program. The answer on the 3rd is legitimately different from the answer on the 28th.
And all of it is restated every year in a new final rule, which adds programs, retires codes, introduces eligibility conditions that did not previously exist, and caps things that were previously uncapped.
The approach: rules as cited data
A matrix, not conditionals
Every program pair has an explicit entry — compatible, excluded, conditional, or add-on. Written as a table, gaps are visible. Written as nested conditionals, the pair nobody thought about looks exactly like the pair that is allowed.
Every rule cites its source
Each entry carries a plain-language description and a reference to the rulemaking it comes from. When someone asks why the system refused, the answer is the citation, not an appeal to the developer’s memory.
Enforced at enrollment
A guard checks the matrix when a patient is enrolled, so an impossible combination is refused at the point it is created rather than discovered downstream in a billing report.
Four answers, not two
Modeling this as a boolean was the first design we threw away. The interesting behavior lives in the two middle cases.
Compatible
Both programs may run in the same month. The system enrolls and moves on.
Excluded
The pair cannot coexist. The enrollment is refused with the reason and the citation attached, so the person on the other end can evaluate it rather than just retry.
Conditional
Permitted only under circumstances the software cannot verify — different providers treating different conditions under separate care plans. The system surfaces the condition and requires an explicit acknowledgement instead of silently allowing or silently blocking. Pretending to know here would be worse than either.
Add-on
Not an exclusion at all. One program becomes a supplement to the other, which changes the applicable code set rather than the legality of the enrollment. This case is why a boolean cannot represent the domain.
Eligibility is a function of the month so far
Rather than answering yes or no, the system evaluates the month’s accumulated state — recorded time, transmission days, whether a qualifying live interaction occurred, whether this is an initial month — and reports which thresholds are met, which are not, and what is missing to reach the next one.
“Not yet eligible, needs eleven more minutes” is a far more useful thing to render in a care manager’s worklist than a disabled button. The distance to the threshold is the actionable part, and it falls out naturally once eligibility is modeled as a computation over the period rather than a flag on a record.
Tiering falls out of the same evaluation: base thresholds, additional increments, and the caps on how many increments may stack. Those caps are exactly the kind of detail that changes between rule cycles, which is why they are values in the rule set and not numbers inline in the logic.
Designed around the fact that it changes every year
A new final rule lands annually. In a recent cycle that meant a new eligibility condition on one program that had none before, a cap applied to increments that were previously unlimited, and a fallback path for partial months that did not previously exist. None of those are large changes. All of them are the kind that silently invalidate hardcoded assumptions.
Because the rules are data with citations attached, the annual update is a diff against a table that a domain expert can read and check. It is not an archaeology exercise across a codebase, which is what this becomes when the rules live inside the logic that applies them.
What we rejected
Asking a language model to decide eligibility
This is a deterministic rule set with a published source. A model would be less accurate, unable to explain itself in terms of the rule, and non-reproducible across identical inputs — for a decision that has to be defensible months later. Retrieval over regulation is the right tool for reading unstructured guidance; we built one of those too. It is the wrong tool for evaluating a matrix.
Auto-submitting whatever the rules permit
The system suggests and explains; a person decides. Not because the logic is untrusted, but because the accountable party for a submitted claim is a human being, and software that removes them from the loop has moved liability without moving competence.
Treating conditional pairs as simply allowed
The tempting simplification, because the conditions are not machine-checkable. It converts a rule the practice is responsible for into a silent default they never saw. If software cannot verify a condition, the honest move is to show it and ask.
Where this generalises
The pattern applies anywhere externally-authored rules change on a schedule you do not control and being wrong has consequences beyond a support ticket: tax and payroll, licensing and permits, insurance eligibility, benefits administration, trade compliance.
The recurring mistake is the same everywhere. The rules get embedded in the code that applies them, the person who understood the domain moves on, and each annual update becomes an increasingly nervous archaeology project. Separating the rules from the engine, and making every rule cite its source, is what keeps that from happening. It is most of what we do.
Rules that change every year
If your last regulatory update meant reading through code to find where the rules were hiding, there is a better structure for it.