← Blog

Process automation

LIMS-reconciliatie-agent: een vriezerinventaris-playbook

Een Leuvense biotech van 19 mensen reconcilieerde wekelijks 1.420 vriezersamples met de hand tussen LabVantage en Access. Hier is de agent die we bouwden, en de GxP-queue die hem audit-veilig hield.

Jacob Molkenboer· Oprichter · A Brand New Company· 15 jun 2026· 9 min
Messing rek met matglazen flesjes, flesje met groen labeltje, leren logboek en lakzegel op ivoren vloeiblad bij raam.

De wet-lab manager pingt ons op vrijdag om 16:47. "Vriezer #7 zegt dat we 38 vials CHO-K1-lysaat hebben. LabVantage zegt 41. De Access-sheet zegt 39. Welke vertrouw ik voor het weekend?"

Vermenigvuldig dat ene bericht met 1.420 sample-rijen per week en je hebt waar elke vrijdag bij deze Leuvense biotech van 19 mensen op uitkwam voordat wij erbij betrokken werden. Twee wetenschappers bleven standaard tot 21:00 om vriezeraantallen te reconciliëren tussen een 16 jaar oud LabVantage LIMS, een zelfgebouwde Microsoft Access-database uit 2011, en een iPad-log die de bench-technicus met de hand bijwerkte als ze eraan dacht.

Deze post is het playbook voor de agent die we hebben opgeleverd om die reconciliatie af te handelen. Het interessante zit niet in de matching-code. Het interessante zit in de queue die we hebben gebouwd om te voorkomen dat de agent ooit naar de GxP-gevalideerde laag schrijft zonder mens in de lus.

Het reconciliatieprobleem bij een biotech van 19 mensen

Even een kort overzicht van de systemen die in het spel zijn, want zonder die context werkt het verhaal niet.

  • LabVantage LIMS, on-prem, Oracle-backend, laatste grote upgrade in 2018. Bevat het canonieke sample-record voor alles wat ooit door de clinical-grade workflow gaat.
  • Access-database, in 2011 gebouwd door een postdoc die in 2018 vertrok, met 14 jaar afgeleide-assay-data en een woud van VBA-macro's dat niemand volledig in kaart heeft.
  • Vriezerlog, een gedeelde iPad-app gebouwd in FileMaker die de bench-wetenschappers bijwerken na elke pull of deposit.

Het lab kan de Access-database niet laten vallen. LabVantage eruit rukken kan ook niet. Alleen de LabVantage-licentie kost al zo'n €38k per jaar, en de migratieschatting van de leverancier kwam uit op 18 maanden en zeven cijfers. Onze opdracht was om de drie systemen het op een cron van 15 minuten met elkaar eens te laten worden, niet om iets te vervangen.

1.420 wekelijkse entries was het gemiddelde over de voorgaande 12 maanden. Twee FTE's waren elk ongeveer zes uur per week aan reconciliatie kwijt. Dat is 12 manuren per week, of grofweg €31k per jaar aan fully-loaded salaris, opgegeten door het diffen van spreadsheets.

Waarom twee systemen nog steeds bestaan

Het is verleidelijk om te schrijven "het juiste antwoord is alles op één systeem consolideren" en verder te gaan. We hebben het geprobeerd. De Access-database is een schatkamer aan institutionele kennis. Er zitten macro's in die een assay-export van 12 kolommen pakken, daar zeven QC-metrics uit afleiden, en die terugvoeren in een worksheet die de wetenschappelijk directeur maandagochtend doorneemt. Iedereen die ooit Access VBA naar een moderne stack heeft proberen te porten weet dat dit een project van maanden is, waarin je in week acht een ongedocumenteerde regel ontdekt die elke aanname uit week één onderuit haalt.

Het LIMS is ondertussen waar elk sample leeft dat bestemd is voor een clinical-grade bereiding. De compliance-laag doet ertoe. FDA 21 CFR Part 11 en EMA Annex 11 eisen allebei dat elektronische records in deze laag een audit trail dragen die attribueerbaar, contemporain, origineel en accuraat is. Een geautomatiseerd proces mag in die laag schrijven, maar bij elke write moet een verifieerbare menselijke goedkeuring zijn vastgelegd.

De opdracht was dus smaller dan "fix de data". Hij was: reconcilieer overal waar het mag, en route al het andere naar een menselijke queue.

De vorm van de pipeline

We zijn uitgekomen op een pipeline met vier stappen. Hij draait elke 15 minuten. Het geheel is Python, ongeveer 1.800 regels, gedeployed als één container op de bestaande on-prem Proxmox-host van het lab. De data verlaat het gebouw niet.

  1. Pull. Een read-only Oracle-connectie scrapet de LabVantage SAMPLE- en STORAGE_LOCATION-tabellen. Een aparte ODBC-connectie leest de Access-tabellen. De iPad-vriezerlog synct via een 5-minuten-pull naar een SQLite-mirror op dezelfde host.
  2. Normaliseren. Sample-ID's worden in de drie systemen op drie verschillende manieren geschreven. We canoniseren ze in een Postgres staging-schema.
  3. Matchen. De agent groepeert rijen die naar dezelfde fysieke vial verwijzen en produceert een diff per vial.
  4. Routen. Elke diff gaat naar één van drie buckets: auto-apply, human-review, of clinical-quarantine. Alleen de eerste schrijft terug zonder menselijke handtekening.

De sample-ID's normaliseren

LabVantage slaat samples op als CHO-K1-001. De Access-database gebruikt CHOK1_0001. De vriezerlog is wat de bench-wetenschapper ook getypt heeft, soms dus cho k1 1 om 18:00 op een vrijdag. Voordat er gematched kan worden, moeten de ID's in één vorm landen.

import re

CELL_LINE = re.compile(r"^([A-Za-z0-9]+?)[\-_\s]*(K?\d+)[\-_\s]*0*(\d+)$")

def canonical_id(raw: str) -> str | None:
    s = raw.strip().upper().replace(" ", "")
    m = CELL_LINE.match(s)
    if not m:
        return None
    line, variant, serial = m.groups()
    return f"{line}-{variant}-{int(serial):04d}"

assert canonical_id("CHO-K1-001")  == "CHO-K1-0001"
assert canonical_id("CHOK1_0001")  == "CHO-K1-0001"
assert canonical_id("cho k1 1")    == "CHO-K1-0001"

Voor alles wat niet op de regex matcht (legacy-ID's van voor de naamgevingsconventie van 2014) vallen we terug op een fuzzy match met de pg_trgm-extensie in Postgres. Drempelwaarde 0,78, gekozen na tests tegen 800 hand-gelabelde voorbeelden. Alles onder de drempel gaat direct naar de human-review bucket. We laten de agent niet gokken op legacy-ID's.

De queue voor clinical-grade reagentia

Dit is het deel van de bouw waar het ontwerp het langst duurde en de code het minst.

LabVantage draagt op elk sample een vlag REGULATORY_TIER. Waardes zijn RESEARCH, CLINICAL_GRADE, en GMP. De agent schrijft niet naar een rij met CLINICAL_GRADE of GMP totdat de wetenschappelijk directeur de diff heeft goedgekeurd.

De approval-surface is de kleinste interface die we konden opleveren. Het is één webpagina, gehost op dezelfde on-prem container, met openstaande diffs gegroepeerd per sample. Elke rij heeft een groen vinkje, een rood kruis, en een vrij tekstveld voor een audit-trail-commentaar. De vink-knop schrijft terug naar LabVantage en stempelt de rij met de e-handtekening van de directeur. Het kruis schrijft een reject-record. Beide acties zijn gelogd, onwijzigbaar, en worden 's nachts geëxporteerd naar de audit-opslag van het lab.

Let op

Laat de agent nooit zelf bepalen wat als clinical-grade telt. Lees de vlag uit het system of record. We hadden een verleidelijke shortcut waarbij we de tier afleidden uit de protocolcode (alles in de CL-XXX-serie is clinical). Het werkte voor 1.418 van de eerste 1.420 samples. Sample 1.419 was een research-tier vial die in een klinisch protocol met een oude code werd hergebruikt. Die ene misclassificatie had een batchrecord kunnen invalideren.

Wat brak in week drie

Drie dingen braken die we niet hadden voorspeld.

De Access-database ging op slot. Access opent een .ldb-lock-bestand zodra een client verbonden is. Onze ODBC-reader hield de connectie open voor de hele duur van de cron-run, ongeveer 90 seconden. In dat venster kreeg de postdoc die de QC-worksheet wilde bijwerken een sharing violation. Fix: overgestapt op een snapshot-read, connectie binnen 4 seconden gedropt, en de cron verschoven naar starts op :02, :17, :32, :47 om de handmatige opslagpatronen te ontwijken die het lab inmiddels gewend was.

LabVantage rate-limitete ons. De web-services-laag op de oude LabVantage-build heeft een ongedocumenteerde soft cap rond 60 writes per minuut. In de eerste week van volledige operatie batchte de agent op maandagochtend 312 reconciled writes tegelijk. De middleware liet de staart stilletjes vallen. We hebben aan onze kant een exponential backoff toegevoegd en een hard plafond van 50 writes per minuut, met persistent retry over cron-runs heen.

Staging-tabel bloat. We waren de Postgres staging-tabel aan het truncaten na elke succesvolle run. Na een maand hadden dead tuples en index-bloat de cron job zo traag gemaakt dat runs gingen overlappen. We hebben de staging-tabel herbouwd als partitioned table met de cron-run-timestamp als sleutel, en oude partities gedropt (niet deleted) na retentie. Zelfde resultaat, geen bloat, geen autovacuum-drama.

Operationeel beeld na 90 dagen

De agent draait nu 14 weken op moment van schrijven. Cijfers uit de laatste volle maand:

  • Gemiddeld 1.418 samples per week gereconcilieerd. Het volume bleef stabiel.
  • 87,4% auto-applied zonder menselijke review (research-tier, high-confidence match).
  • 9,1% gerouteerd naar de human-review bucket, ongeacht tier.
  • 3,5% gerouteerd naar de clinical-grade queue voor goedkeuring van de directeur.
  • Mediane tijd van diff-detectie tot goedkeuring directeur: 4 uur 12 minuten, gestuurd door werkuren in plaats van door de queue.
  • Teruggewonnen FTE-uren: 11,5 per week. De twee wetenschappers kregen hun vrijdagavonden terug.

Het ene cijfer waar we het meest om geven is de false-merge rate. Tot nu toe nul, gemeten tegen een wekelijkse spot audit van 40 gesamplede diffs. Niet omdat de matcher perfect is, maar omdat de legacy-ID-fallback en de clinical-grade queue de gevallen opvangen waarin de matcher fout had gezeten.

Wat we de volgende keer anders doen

Twee dingen, allebei over de interface, niet over de engine.

Ten eerste: de approval-queue mobile-first bouwen. De wetenschappelijk directeur bekijkt diffs vaker op haar telefoon tussen meetings door dan achter haar bureau. We hebben eerst een desktop-layout opgeleverd en die in week zes mobile-first herbouwd.

Ten tweede: het reject-commentaarveld vanaf dag één verplicht maken. De helft van de vroege rejects kwam leeg terug, wat betekende dat we de auto-apply-confidence-drempels niet konden hertrainen. Het veld is nu verplicht, en de commentaren voeden een wekelijkse review van waar de matcher zonder reden conservatief is.

Het kleinste dat je vandaag kunt doen

Als je een lab runt, een ops-team, of welk bedrijf dan ook met twee systems of record die het niet met elkaar eens zijn, begin dan niet bij de integratie. Begin bij de queue. Bouw één pagina die de diffs toont, en laat een mens een week lang elke diff goedkeuren of afwijzen. De regels die je in week twee opschrijft zijn de regels die in jaar twee de audit overleven.

Toen we de vriezer-reconciliatie-agent voor de Leuvense biotech bouwden, was het stuk dat we keer op keer herschreven niet de matcher, maar de routing-logica die bepaalde welke diffs de agent zelfstandig mocht aanraken. Dat soort process-automation-werk verdient alleen vertrouwen door vroeg en vaak het oordeel terug te leggen bij een mens.

Kern

Begin bij de menselijke approval-queue, niet bij de matcher. De routing-regels zijn wat het vertrouwen verdient dat de audit overleeft.

FAQ

Hoe lang duurde de bouw?

Elf weken van kick-off tot volledige operatie. Het grootste deel daarvan was iteratie aan de approval-queue en het ontdekken van edge cases, niet de matching-engine zelf.

Mag de agent rechtstreeks in een GxP-gevalideerd systeem schrijven?

Alleen naar research-tier-rijen. Alles met de tag clinical-grade of GMP gaat naar een menselijke approval-queue, zodat aan de e-handtekening-eisen onder 21 CFR Part 11 en EMA Annex 11 wordt voldaan.

Welke stack hebben jullie gebruikt?

Python, Postgres met pg_trgm en table partitioning, plus Oracle- en ODBC-clients voor de legacy-systemen. Gedeployed als één container op de on-prem Proxmox-host van het lab. Geen cloud.

Hoe voorkom je matching-fouten op legacy-sample-ID's?

Alles onder een trigram-similariteit van 0,78 gaat direct naar een human-review queue. De agent gokt niet op de lange staart van naamgevingsconventies van voor 2014.

process automationai agentsintegrationsworkflowcase studyarchitecture

Iets bouwen?

Start een project