Skip to main content
AI Architecture Healthcare
View as Markdown Suggest changes

Documented but Never Coded: Designing a System for Lost Hospital Revenue

· Reading time: 20 min
Documented but Never Coded: Designing a System for Lost Hospital Revenue

TL;DR

German hospitals are paid per case, by codes, so care that was given and documented but never coded is never paid for. Malnutrition is the classic example: the nurses screen for it, a dietitian treats it, and because no physician ever writes the diagnosis down, nobody codes it. In a system that finds such gaps, no model decides anything on its own: cheap signals in code raise candidate codes, a self-hosted decision model that cannot write text confirms each one by picking the passage that proves it, a certified grouper prices it, and a coder decides. It has to favour precision over recall, because a wrong code can fail an audit, and a coder who sees a few bad suggestions stops trusting the queue. At a few hundred discharges a day even in a large hospital, throughput is not the problem; integrating with each hospital’s systems, earning coders’ trust and evaluating without ground truth are.

How a German hospital gets paid

You can’t design a system for a domain you can’t draw, so first the chain the system has to fit into.

Documentin the KISCodeICD-10-GM, OPSGroupto a DRGInvoiceweight x base rateKrankenkassepays or auditsMD auditcuts unsupportedGap

The six stations of an inpatient case’s billing. Grey belongs to the hospital, teal is the coders, orange the payer. The gap this post is about sits between documenting and coding.

From the record to a DRG

A hospital is paid per inpatient case with a flat rate, the DRG (diagnosis-related group). Physicians and nurses document the treatment in the hospital information system, the KIS (Krankenhausinformationssystem). Coders turn that record into codes: ICD-10-GM for diagnoses, one main diagnosis and any number of secondary ones, and OPS (Operationen- und Prozedurenschlüssel) for procedures.

The rulebook for coding is the DKR (Deutsche Kodierrichtlinien), the German coding guidelines. A certified grouper, deterministic software built on the catalog that the InEK (Institut für das Entgeltsystem im Krankenhaus) publishes every year, then maps the codes plus age, length of stay and a few more fields to a DRG. The DRG part of the invoice is roughly

DRG revenue≈cost weight×base rate,\text{DRG revenue} \approx \text{cost weight} \times \text{base rate},

with a state base rate of about 4,570 EUR in 2026; nursing staff costs are paid separately, per day.

Why one secondary diagnosis matters

Secondary diagnoses matter more than you’d think. They feed a severity score, the PCCL (patient clinical complexity level, 0 to 6), which often decides the DRG’s suffix letter: F62B, heart failure with higher severity, pays more than F62C. But under DKR rule D003, a secondary diagnosis only counts if it caused treatment, diagnostics, or extra care or monitoring. What a counted code is worth follows from grouping the case twice:

price of a code=(wwith−wwithout)×base rate,\text{price of a code} = \big(w_{\text{with}} - w_{\text{without}}\big) \times \text{base rate},

where ww is the cost weight of the DRG the case lands in with and without the code.

groupergrouperOne case versionas documentedWithoutN17.93F62Clower severityWithN17.93F62Bhigher severityPrice of the codeweight gap x base rate

The price of one code: group the same case without it and with it. Here acute kidney failure raises the severity enough to move the case from F62C to F62B, and the formula above prices the move.

Audits

The invoice goes to the patient’s Krankenkasse, the statutory health insurer, which can send it to the MD (Medizinischer Dienst) for audit. Since 2021 the MD has been formally independent of the insurers, though they still fund it. The MD cuts whatever the record doesn’t support. How many of a hospital’s invoices a payer may send to audit depends on its track record. With ss the share of its audited invoices the MD left uncut last time, the cap until the end of 2026 is

audit quota≤{5 %if s≥60 %10 %if 40 %≤s<60 %15 %if s<40 %.\text{audit quota} \le \begin{cases} 5\,\% & \text{if } s \ge 60\,\% \\ 10\,\% & \text{if } 40\,\% \le s < 60\,\% \\ 15\,\% & \text{if } s < 40\,\%. \end{cases}

A law passed in July 2026 raises the cap to 40 percent from 2027, and removes it entirely for the hospitals with the worst track record. With two thirds of hospitals posting a loss in 2024, two things matter at once: finding the revenue that was earned but not coded, and never claiming a code the record can’t defend.

That second half is the one this design keeps coming back to. A system that only maximises revenue is an upcoding machine, and stricter audits punish exactly that.

What the system must do

Documentation?CoderUnknownHospitalPlatformPerson

All we know so far: documentation goes in, and a coder should get something to act on.

The running example

The running example is invented; the coding rules around it are real. A 74-year-old is admitted with heart failure, and two things happen that nobody codes:

  • Acute kidney failure. On day three, creatinine is 4.3 mg/dl against 1.2 on admission, 4.3/1.2≈3.64.3 / 1.2 \approx 3.6 times the baseline, so at least three times: stage 3. The ward pauses the ACE inhibitor and checks creatinine daily. That is treatment under D003, so N17.93 counts, and it moves the case from F62C to F62B, worth roughly a thousand euros in this invented case.
  • Malnutrition. The nursing admission screening, the NRS 2002 (Nutritional Risk Screening), scores 4, where 3 already means the patient is at risk. A dietitian’s note on day four records a 10 percent weight loss in three months and prescribes supplement drinks twice a day. No physician mentions it. The coding guidelines make diagnoses the treating physician’s responsibility, so a coder won’t code what only a dietitian wrote down, and E44.0, moderate malnutrition, stays uncoded.
Kidneya number crosses a lineMalnutritionfacts spread over paperbaseline 1.23× baseline4.3 mg/dlDay 12345drug pausedNursing formNRS 2002: 4Scanned note−10% in 3 modrinks 2×/dayMedicationSupplementPhysicianno diagnosisTreated (D003)Physician involvedSuggest N17.93F62C → F62BAsk the physicianabout E44.0

The two misses are different kinds of evidence. The kidney failure is a number crossing a line, which a rule can watch. The malnutrition is spread over documents that have to be read, and the physician’s is missing. Both were treated; only one has a physician behind it.

The first is easy to find with a lab threshold. The second is the kind a rule can’t find: scattered across scanned nursing notes, worded differently every time, and blocked on a physician’s confirmation. A design that handles both is the goal.

Boundaries

Before drawing anything, the problem needs boundaries. These are the ones I’d settle on:

  • Coders act on the output. They own the codes, so they get the suggestions. Physicians are the source of missing facts, and controlling only sees totals.
  • Review runs while the patient is admitted and settles by discharge. Earlier leaves time to ask a physician while they still remember the case; later gives a more complete record. Concurrent review gets both.
  • A wrong code costs more than a missed one. A missed code costs its revenue delta once. A wrong code can fail an audit, and a bad audit record raises the audit rate for every case. So the system optimises for precision.
  • The data is heterogeneous. Structured data from the KIS arrives as HL7 v2 messages, the decades-old workhorse of hospital data exchange, or through FHIR, the newer web-API standard from the same organisation. Alongside it come discharge letters, lab PDFs, scanned nursing notes, in a different mix at every hospital.
  • Patient data stays in Germany. German law keeps cloud processing of health data in the EU or in countries with an adequacy decision, and hospitals’ own rules go further, so in practice that means German data centers, and a model that runs there too.
  • Scale is small. Even a large university hospital discharges only a few hundred inpatients a day, around 150,000 a year; the average German hospital handles about 10,000 cases a year. To size the model’s load, take a large hospital and two guesses of mine, not measurements: each case’s record changes about five times before discharge (new lab results, new documents), and each change leaves about three possible missing codes for the model to check. For 300 discharges a day, that is 300×5×3=4,500300 \times 5 \times 3 = 4{,}500 model calls a day, each over about 20,000 tokens of documentation: 4,500×20,000≈904{,}500 \times 20{,}000 \approx 90 million tokens a day. For a generative LLM that is real money. For a small decision model on one GPU, it is not, so cost is not what limits the design; precision is. For a database, a few dozen hospitals are nothing.

The last point shapes the design. The hard part is not requests per second. It is getting data out of a dozen KIS installations, keeping the view of a case current as the record changes, and making coders trust suggestions enough to act on them.

The simplest design that works

LabelsHospital systemsCase context storeGap detectorsGrouperRanked worklistWrite-back, labelsHospitalPlatformPerson

Six steps, still technology-agnostic. Blue boxes are the platform, the teal one is a person, the grey one belongs to the hospital.

The simplest design that works end to end:

  1. Hospital systems send what happens to a case.
  2. A case context store keeps one record per case: every code, document and lab value, with the time it arrived.
  3. Gap detectors read a case and propose codes it is missing, each with the evidence for it.
  4. A grouper prices each proposed code: it groups the case with and without it and reports the revenue delta.
  5. A ranked worklist shows the coder the suggestions worth their time, best first. They accept or reject.
  6. Write-back and labels: accepted codes go back into the KIS, and every decision goes back to the detectors as a label.

Two things already fall out of this. Each suggestion points at a case version, the record as it stood when the suggestion was made, so when the record changes we know which suggestions to re-check. And the grouper sits after the detectors, not inside them: the model confirms, deterministic certified software does the money math. I never want a model to tell a coder what a code is worth.

What runs when

Nothing in the pipeline waits for a person, and no person waits for the pipeline. The suggestion queue sits between them.

Background, asyncminutes behind the recordNew versionDetectorsGrouperUpdate queuePhysician answers: days later, as new eventsNightly: reconcile with the KIS, re-run on a new catalogSuggestion queueper case versionForeground, syncmillisecondsOpen queueAccept / rejectWrite-backPeople read and decide; nobody waits on a model

The background keeps the queue current; the foreground only reads and decides.

  • In the background, asynchronously: every new case version, debounced, runs detectors and grouper and updates that case’s suggestions in the queue, minutes behind the record. Physician answers come back as new events, often days later. A nightly sweep reconciles the case store with the KIS and re-runs cases when a catalog changes.
  • In the foreground, synchronously: a coder opening the queue gets suggestions that are already computed, in milliseconds. Accept and reject are saved at once; writing a code back to the KIS is queued.
  • The deadline is discharge. A case’s suggestions should settle before it is billed, so cases close to discharge rank first and a final run happens at discharge.

The components

LabelsHospital systemsHL7 v2, FHIRLetters, PDFs, scansCase context storeAdapter per KISEvent log per caseVersions, audit logGap detectorsCheap signalsEvidence selectorDKR rule checksGrouperCertified grouperWith and withoutCache per versionRanked worklistCoder: accept, rejectPhysician queryWrite-back, labelsCodes into the KISDecisions into evalsHospitalPlatformPerson

The same six steps, each with the components that make it concrete. Each part below highlights its step.

Ingestion and case store

LabelsHospital systemsHL7 v2, FHIRLetters, PDFs, scansCase context storeAdapter per KISEvent log per caseVersions, audit logGap detectorsGrouperRanked worklistWrite-back, labelsMost effortHospitalPlatformPerson
  • Adapters: one per KIS, turning HL7 v2 messages, FHIR resources and file drops into events on one timeline per case: admission, transfer, lab result, document, discharge. Scans go through OCR. Most KIS installations offer no API to query; the hospital’s interface engine routes a copy of their message feeds to us. Lab results, for example, arrive as HL7 v2 result messages from the lab system, with local test codes and units (creatinine in mg/dl or µmol/l) that are mapped once per hospital.
  • Where the effort goes: messages arrive out of order and get corrected or cancelled afterwards, so an adapter applies corrections instead of appending. And every hospital’s installation is configured differently.
  • Store: Postgres, an append-only event log per case plus a materialised current view. The log gives versions and an audit log for free, and a few hundred thousand cases a year need no sharding.
  • Orchestration: each new case version starts a durable workflow, for example in Temporal, keyed by case and version. A message delivered twice runs once, and a question to a physician can wait days without a process waiting with it. Ten lab results in an hour cause one re-run, and every result is cached under hash(inputs,catalog year,detector version)\text{hash}(\text{inputs}, \text{catalog year}, \text{detector version}), so a re-run only pays for what changed.
Case 4711: event logappend-onlyv1AdmissionMon 08:10 · ADT messagev2Creatinine 1.2 mg/dlMon 09:02 · lab resultv3Creatinine 4.3 mg/dlWed 07:45 · lab resultv4ACE inhibitor pausedWed 11:20 · medication orderN17.93v5Dietitian noteThu 14:05 · scanned documentv6v3 corrected to 4.1Thu 16:30 · lab correctionRe-checkreplayed intoCurrent viewcreatinine 4.1, drug paused, 1 scan

The running example’s case in the store. Every message is a new version; nothing is overwritten. The suggestion points at the version it was made on, so when the lab corrects a value two versions later, the store knows which suggestion to re-check. 4.1 is still over three times the baseline, so it stands.

10 lab results in one hour08:0009:2015 min quietOne re-runon the newest versionInside the run:Creatinine ruleRe-runEvidence selector, kidneyRe-runNutrition signalsCachedEvidence selector, nutritionCachedGrouperCached

Orchestration for one burst of lab results: one run after the burst, and inside it only the steps whose inputs changed. The 15 minutes are an example.

Gap detectors

LabelsHospital systemsCase context storeGap detectorsCheap signalsEvidence selectorDKR rule checksGrouperRanked worklistWrite-back, labelsPrecision firstHospitalPlatformPerson

A detector is code, versioned and tested like any other: a cheap signal that raises a candidate code, the questions the evidence selector has to answer about it, and the DKR rules it has to pass. A few examples, with illustrative thresholds; the real ones come from the coding guidelines and each hospital’s coders:

SignalCandidate codeTreatment that would satisfy D003
Creatinine at least 3 × the admission valueN17.93, acute kidney failure, stage 3a drug paused, fluids, daily checks
NRS 2002 score of 3 or moreE43 / E44.0 / E44.1, malnutrition by severitya dietitian, supplement drinks
Potassium below 3.5 mmol/l twiceE87.6, hypokalaemiapotassium substitution
A procedure code without its usual diagnosisthe diagnosis the procedure impliesthe procedure itself

Signals and the evidence selector

  • Cheap signals, for recall: thresholds on lab values, screening scores, the medication list, procedure codes. Code, fast, run on every version.
  • Evidence selector, for precision: the documents are split into numbered passages after OCR, a search on the candidate’s terms and the signal’s values keeps the twenty most relevant, and a decision model answers a few fixed questions.
  • Rule checks: the mechanical DKR checks. Is this code combination valid, and does D003 hold given what the evidence says was done?
140passages20kept by search3questionsKidneyN17.93MalnutritionE44.0Which passage proves it?passage 7passage 3Was it treated?0.910.93Did a physician diagnose it?0.860.95SuggestAskLeads to

The selector’s questions for the running example’s two candidates, with invented probabilities. The answers, not a free-text verdict, decide what happens next: the kidney failure becomes a suggestion, the malnutrition a question for the physician.

A non-generative decision model, the kind TypeSafe introduced with Jev in September 2026, cannot write text at all: given a question and a fixed set of options, it returns a choice or a probability its maker calls calibrated, typed like any other step. A citation chosen from numbered passages can’t point at a quote that isn’t there, which is a better story for an audit than checking generated quotes afterwards. And the probabilities set the thresholds for suggesting, asking and dropping.

Was it treated?candidate: E44.01Mobilised with help2Weight down 10% in 3 months3Supplement drinks twice a day4Wound dressing changed5Sleeps poorlyNone of theseDecision modelpicks 3, p = 0.93No text it could invent

The evidence selector on the malnutrition case. The model’s only possible answers are the passage numbers and “none of these”, so whatever it cites is in the record.

Traps

A keyword finds malnutrition in many passages that don’t prove it. Clinical text is full of negations, earlier stays, plans and text copied forward from old letters, and these are exactly the cases the selector has to answer with “none of these”:

No signs of malnutrition.NegatedWeight loss after surgery, 2023.Earlier stayDietitian consult if weight drops.Only a planWeight −10% (copied from 2023).Copied forwardSupplement drinks since day 4.Current, treated

Five passages a search for malnutrition returns. Only the last proves current, treated malnutrition. The traps are what the labelled test set for a detector has to cover.

Shipping a detector

  • Tests: a few hundred coder-labelled candidates, traps included, run on every change.
  • Versions: each detector carries a version and a catalog year, so results stay reproducible and cache correctly.
  • Shadow mode first: a new detector or version logs what it would suggest on live cases before any coder sees it (see the evaluation below).

Grouper

LabelsHospital systemsCase context storeGap detectorsGrouperCertified grouperWith and withoutCache per versionRanked worklistWrite-back, labelsDeterministicHospitalPlatformPerson

The certified grouper as a service: case in, DRG and revenue out, deterministic, milliseconds per run. It prices a suggestion the way the pricing figure above shows, by grouping the case with and without the code.

Worklist

LabelsHospital systemsCase context storeGap detectorsGrouperRanked worklistCoder: accept, rejectPhysician queryWrite-back, labelsHospitalPlatformPerson

Suggestions are ranked by expected value:

score=Δrevenue×P(coder accepts)−λ×audit risk\text{score} = \Delta\text{revenue} \times P(\text{coder accepts}) - \lambda \times \text{audit risk}

A code with Δrevenue=0\Delta\text{revenue} = 0 still appears, ranked last, since the guidelines ask for every diagnosis that meets their definition.

Δ revenue× P(accept)− audit risk1Code B modest, well documented€805€900 × 0.95 − €502Code A valuable, thin evidence€450€3,000 × 0.25 − €3003Code C worth nothing, still listed€0€0 × 0.90 − €0

Three invented suggestions. The most valuable code ranks second: its evidence is thin, so the coder is unlikely to accept it and an audit likely to cut it.

  • Evidence on screen: the quote highlighted on its page, the DRG before and after, one-click reject reasons.
  • Questions bundled: a suggestion that needs a fact only the physician can supply becomes a question, one yes or no per case, not six interruptions.
N17.93Suggestion 1 of 4Acute kidney failure, stage 3Creatinine 4.3 mg/dlLab report, day 3, p. 2ACE inhibitor pausedWard note, day 3, p. 1DRGF62C → F62BAcceptRejectNot treatedWrong codeAlready codedAsk the physicianModerate malnutrition, relevant to treatment?Dietitian note attachedYesNo

What the coder sees for the running example: the kidney failure as a suggestion, with the evidence highlighted and the DRG before and after, and the malnutrition as a question for the physician.

One click on a quote opens its page, so the coder checks the evidence where it was written, not a copy of it:

← SuggestionDietitian note, scanned, p. 112Weight down 10% in 3 months.3Supplement drinks twice a day.45Passage 3 answers "Was it treated?"p = 0.93

The malnutrition evidence on its scanned page. The numbered passages are the ones the selector chose from; the outlined one is its answer.

Write-back and labels

LabelsHospital systemsCase context storeGap detectorsGrouperRanked worklistWrite-back, labelsCodes into the KISDecisions into evalsHospitalPlatformPerson
  • Write-back: accepted codes go into the KIS through the same adapter, so the hospital’s system stays the record of truth. Many installations don’t allow writing codes back; there the first version is a coder copying the code, and that is fine.
  • Labels: every decision goes to a labels table the evaluation reads.

The life of one suggestion

LabelsHospital systemsCase context storeGap detectorsCheap signalsEvidence selectorDKR rule checksGrouperCertified grouperWith and withoutCache per versionRanked worklistCoder: accept, rejectPhysician queryWrite-back, labelsPrecision firstHospitalPlatformPerson

Highlighted: the part where the model, the rules and the money meet.

The kidney failure takes the easy path: the creatinine threshold fires, the selector picks the lab report and the note pausing the ACE inhibitor, D003 holds, and the grouper prices the move to F62B. It becomes a suggestion in the cardiology coder’s queue. The malnutrition is harder, and more typical:

Cheap signalNRS 2002 score 4Evidence selectorDietitian note, p. 1DKR rule checkTreated: D003 holdsGrouperWith and without the codeSuggestto the coderAskthe physicianDropnot codableThis case

The malnutrition case, from the signal that raised it to a question for the physician. Dropping is dashed: it is the outcome nobody sees.

The screening score of 4 is the cheap signal. The evidence selector reads the passages from the scanned nursing notes and picks the dietitian’s note on page 1, with the weight loss and the supplement drinks. The rule check says D003 holds, since the malnutrition was treated, but finds no physician’s diagnosis, which a coder needs. So it doesn’t become a suggestion. It becomes one bundled question to the physician:

Questions from codingWard 4B, 3 casesCase 4711, discharge FridayModerate malnutrition (E44.0),relevant to treatment?Dietitian note, day 4−10% in 3 months, supplement drinksYesNoNot sureCase 46981 questionCase 47022 questions

The physician’s side: questions from coding, one card per case, each with the evidence attached and a one-click answer.

If they confirm, it re-enters the queue as a suggestion, priced like any other. Had the signal been a single low potassium value, normal on the recheck and never treated, D003 would fail and the candidate would be dropped.

Tradeoffs

  • Rules, a model, or both. Rules alone are precise but brittle: the creatinine rule works, but nobody writes a rule for every way a nurse records weight loss. A model alone needs a question to answer, and asking it about every possible code on every case version buries the coder in low-confidence guesses. The hybrid uses rules to say where to look and the model to read what’s there. That matches a support automation I built: code in charge of the sequence and the model confined to interpretation made each piece testable on its own.
  • Which model. An open-weight decision model, self-hosted in a German data center; Jev itself runs only on its maker’s US-hosted service, which rules it out for patient data. Models of this kind are new and their benchmarks are their makers’ own, so I’d measure one on a few hundred coder-labelled candidates before trusting it. The fallback is a generative open-weight LLM in the same data center, slower and costlier but well understood. The selector’s typed interface makes the model a swap, not a redesign.
  • Precision over recall. A coder who sees three bad suggestions in a row stops trusting the queue, and then the good ones go unread too. I’d start with a few detectors that are nearly always right, and add more only when the acceptance rate holds.
  • Workflow or agent. For this pipeline, a workflow: every suggestion goes through the same checks in the same order, and the order is what makes it defensible in an audit. An agent might earn a place later, for open-ended work like drafting an answer to an MD query.

When to upgrade: let the decision model ask about codes no signal raised only once the signals’ misses are measured and coders still trust the queue.

Running it: evaluation, rollout, security

LabelsHospital systemsCase context storeGap detectorsGrouperRanked worklistCoder: accept, rejectPhysician queryWrite-back, labelsCodes into the KISDecisions into evalsEvalsHospitalPlatformPerson

Highlighted: the decisions, which close the loop into the evaluation.

Evaluation

  • Labels: coder decisions. Cheap, fast and plentiful, but biased by whatever the queue showed them.
  • Ground truth: MD audit outcomes, months later and only for the fewer than one in ten cases that get audited.
  • The metric: not the acceptance rate but ∑acceptedΔrevenue−euros cut in audits\sum_{\text{accepted}} \Delta\text{revenue} - \text{euros cut in audits}, the same gap as between an approval rate and savings.
  • Reviewers: coders disagree with each other, so I’d measure each one’s agreement with the others before trusting their labels.
  • Shadow mode, not replay: offline replay is weaker than it looks, because it is hard to recreate what the record looked like when a coder would have seen the suggestion. The honest test runs a new detector on live cases and logs what it would have suggested. The Missing Test Suite makes the longer case for that, and for why the experts’ review throughput is the real bottleneck.
Malnutrition detector4 weeks on live casesv1 livev2 shadowWould suggest per week140212Agreement with coders78%74%Revenue at stake per week€41k€58kAgreement at least 70%200 cases checked by a coderPromote v2audit outcomes follow in months

A new detector version in shadow mode next to the live one. The numbers are invented. It suggests more and agrees with coders a little less, which is the trade to decide on before promoting it; the audit outcomes that settle it arrive months later.

Adding a hospital

A new adapter, or a configuration of an existing one, a per-hospital overlay for house rules on top of the shared DKR rules, and a backfill of open cases. The yearly catalog updates for ICD-10-GM, OPS and the DRG catalog mean every rule, signal and grouper run is tied to a catalog year, versioned and tested like code. A case admitted in December and discharged in January is the edge case to test first.

Security

German hosting only, a model that runs there too, encryption, per-hospital isolation, and the case store’s audit log, which shows which data the model saw for each suggestion.

Contracts and growth

Three small records pass between the parts: a case version (the record as of one change), a suggestion (the proposed code, its evidence, the rule that allows it, the revenue delta and a status), and a coder’s decision with its reason. They are the contracts between the parts, so ingestion, detectors, the grouper service and the review UI can change independently. They also carry the next products: answering MD queries from the same evidence, billing privately insured patients under the GOÄ (Gebührenordnung für Ärzte), the physicians’ fee schedule, and showing controlling the revenue at risk.

AI Chat

Messages you send are processed by the Google Gemini API to generate responses. Do not share sensitive personal data. See the privacy policy for details.