← Blog

Process automation

Procesautomatisering: vier-ogen-controle voor STL-prints

Een Bossche implantaatkliniek routeerde wekelijks 1.940 Vectra-scans handmatig door een 11 jaar oud Exquise-EPD. Het playbook om dat te automatiseren zonder het menselijke fiat te schrappen.

Jacob Molkenboer· Oprichter · A Brand New Company· 16 jun 2026· 11 min
Messing relais naast gevouwen doorslagformulier met groen label, houten stempel op inktkussen, ivoorkleurig papier.

Vrijdagmiddag in Den Bosch. De senior tandtechnicus opent de map SCANS_NIEUW op de fileserver en ziet 387 ongelezen STL-bestanden. Sommige staan er sinds maandag. Hij heeft drie uur voordat de printers uitgaan voor het weekend, en hij moet nog beslissen welke scans naar welke van de vier Formlabs-printers gaan, welke een second opinion nodig hebben omdat het implantaat boven de €1.800 ligt, en bij welke hij geen bijbehorend patiëntdossier in Exquise kan vinden. Rond 14:30 stopt hij met reageren op Teams-berichten.

Dit is het playbook waarmee we die workflow automatiseerden voor een implantaatkliniekgroep van 29 mensen, verspreid over drie locaties in Noord-Brabant. De menselijke check bleef precies op de plek waar die telde.

De uitgangssituatie

De groep had drie locaties, 29 medewerkers, vier Formlabs Form 3B printers en één 11 jaar oud Exquise-EPD dat de vorige IT-leverancier in 2014 had ingericht en daarna niet meer had aangeraakt. Vectra DT intraorale scanners op iedere stoel schreven scans weg naar een gedeelde map. Een dienstdoende tandtechnicus pakte ze op, zocht de patiënt op in Exquise, bepaalde het implantaattype, koos de printer en hernoemde het bestand zodat het matchte met het verwachte patroon van die printer.

Vrijdag was de doorvoer 387 bestanden. De mediane vertraging van scan tot printer was 71 uur. De groep verloor ongeveer zeven cases per maand door verkeerde printertoewijzing of ontbrekende patiëntdossiers. Eén daarvan kostte €1.640 aan weggegooid materiaal. Twee kostten goodwill, en daar plak je lastiger een prijskaartje aan.

Loop de swim lanes door voordat je een regel code schrijft

Voordat we iets aanraakten, brachten we twee dagen door op kantoor met een stopwatch. Niet figuurlijk. Een echte stopwatch op de werkbank. Elke overdracht werd geklokt. We zaten op een dinsdag en opnieuw op een vrijdag van 08:00 tot 17:00 naast de dienstdoende tandtechnicus en noteerden elke klik.

Het resultaat was 11 overdrachten tussen scan-op-schijf en STL-in-printerwachtrij. Vier van die overdrachten hadden dezelfde vorm: open deze PDF, zoek het zaaknummer, plak het in Exquise, kopieer de patiëntnaam terug, hernoem het bestand. Dat is 22 minuten per case bij de mediaan, en het is dezelfde persoon die hetzelfde vier keer doet per case. Het schema op het whiteboard zag er lelijk uit. We hebben het lelijk gelaten. Mooie schema's verbergen overdrachten.

Wat ons het meest verraste was de impliciete triage. De dienstdoende tandtechnicus had al een mentaal model van welke printer paste bij welke klus (resin-viscositeit per implantaatfamilie, laatste kalibratiedatum per machine, wie 's avonds welke tank moest schoonmaken), en dat model leefde alleen in zijn hoofd. We hebben het er tijdens drie koffies uitgetrokken, op de muur als tabel getekend en omgezet in de routeringsregels. Zonder die stap zou de automatisering op papier correct zijn geweest en op de werkvloer fout vanaf de tweede week.

Exquise uitlezen zonder het supportcontract te breken

Exquise draait lokaal op een Windows-server en bewaart de data in een Firebird-database. De leverancier verkoopt een HL7-exportmodule op een licentie per jaar, en biedt geen publieke API. Die module hebben we niet gekocht.

Wat we wel deden: een read-only ODBC-verbinding direct naar de Firebird-instance, met de rol RDB$READONLY. Firebird ondersteunt read-only attachments standaard, en zolang je nooit schrijft behandelt het supportcontract van de leverancier jouw proces hetzelfde als een rapportagetool. De vorige IT-leverancier van de kliniek deed jarenlang precies hetzelfde voor zorgverzekeraarsrapportages. We hebben dit vooraf schriftelijk bevestigd voordat we het in productie zetten.

import fdb, os

conn = fdb.connect(
    dsn='exquise-srv01:C:/Exquise/Data/PATIENT.FDB',
    user='SYSDBA',
    password=os.environ['EXQUISE_RO_PASS'],
    role='RDB$READONLY',
    charset='WIN1252',
)
cur = conn.cursor()
cur.execute("""
    SELECT p.patient_id, p.bsn, p.last_name, p.first_name,
           t.treatment_id, t.tooth_number, t.implant_code, t.price_eur
    FROM patient p
    JOIN treatment t ON t.patient_id = p.patient_id
    WHERE t.created_at > ?
      AND t.status = 'OPEN'
""", (since,))
Waarschuwing

Schrijf nooit naar de Exquise-database vanuit iets anders dan de officiële client. Supportcontracten van leveranciers behandelen elke schrijfactie vanuit een derde-partij-proces als grond om de overeenkomst nietig te verklaren. Read-only is prima, alles daarbuiten is de besparing niet waard.

Scans aan patiënten koppelen

De Vectra DT schrijft scans weg met een bestandsnaam waarin het stoelnummer en een timestamp staan. Geen patiënt-ID. De koppeling tussen scan en patiënt zit in de afsprakensoftware van de stoel, en dat is óók Exquise.

We hebben niet geprobeerd gezichtsherkenning op de scan toe te passen. We hebben niet geprobeerd de metadata te OCR-en. We deden het saaie ding: we matchten op het afsprakenslot. Een scan die om 14:23 op stoel 4 wordt weggeschreven hoort bij wie er die dag om 14:15 op stoel 4 stond ingepland. Als geen enkele afspraak binnen tien minuten ervoor of erna matcht, gaat de scan naar een unmatched queue en kijkt een mens ernaar. Dat gebeurt ongeveer twee keer per week, en de oorzaak is bijna altijd een stoelwissel die niemand heeft opgeschreven.

Pak eerst de saaie oplossing. We hebben een week gepuzzeld op scan-metadata en chair-vendor API's voordat we het voor de hand liggende deden. Les opnieuw geleerd.

De vier-ogen-wachtrij boven €1.800

Dit is het deel waar de eigenaar van de kliniek het meest om gaf. Implantaten boven de €1.800 aan materiaal zijn meestal complexe cases: hoekige abutments, sinuslifts, full-arch-klussen. Fouten zijn duur in geld en erger in reputatie. De eigenaar wilde dat geen enkele STL voor zulke cases ooit een printer zou bereiken zonder dat twee verschillende tandtechnici hadden afgetekend.

De state machine is klein:

scan_received
  -> matched_to_patient (auto)
      -> if price_eur < 1800: queued_for_print
      -> if price_eur >= 1800: pending_first_review
          -> first_review_signed (by tandtechnicus A)
              -> pending_second_review
                  -> second_review_signed (by B, B != A)
                      -> queued_for_print

Drie regels, geen uitzonderingen:

  • De twee beoordelaars moeten verschillende Active Directory-identiteiten zijn. Geen weergavenamen. AD object GUIDs.
  • De beoordelaar mag niet dezelfde persoon zijn die de prijs in Exquise heeft ingevoerd. We halen de editor uit de audit-tabel van Exquise en blokkeren die GUID voor beide reviewslots.
  • Beide handtekeningen krijgen een timestamp en zijn onveranderbaar. We appenden, we updaten niet.

We hebben overwogen een eigen review-UI te bouwen. Dat hebben we niet gedaan. De groep had Microsoft Teams al op elke werkplek staan. Elke openstaande case post een adaptive card in een privé Teams-kanaal voor de tandtechnici, met initialen, elementnummer, implantaatcode, prijs en een link naar de gerenderde STL-preview. Ze klikken op Goedkeuren of Terugsturen, en de bot schrijft de handtekening terug naar onze database. Twee weken werk in plaats van twee maanden. De tandtechnici hoeven geen derde frontend te leren.

We waren bang dat het zou ontaarden in afstempelen. Tot nu toe niet. We monitoren de mediane reviewtijd per beoordelaar per week. Wie onder de 30 seconden mediaan zakt, krijgt een stil gesprek met de teamleider. Die drempel is gezet nadat we een tandtechnicus een echte second review hadden zien doen en die hadden geklokt.

De STL in de printerwachtrij droppen

De Formlabs-printers bewaken via PreForm een map op de gedeelde schijf. Drop er een correct genoemde STL in en de software van de printer pikt 'm op bij de volgende poll. Simpel, en robuust als je twee extra dingen doet.

Idempotentie. De bestandsnaam codeert case-ID en revisie. Als een bestand met die naam al in de printermap of in het archief bestaat, schrijven we niet opnieuw. Dit ving in week drie één case op waarbij een netwerkhapering ervoor zorgde dat onze agent een drop opnieuw probeerde. Zonder die check hadden we twee keer geprint en één keer gefactureerd.

Backpressure. Als een printerwachtrij meer dan 12 openstaande jobs heeft, houden we vast en sturen door naar een andere printer. Als alle vier vol zitten, houden we vast en sturen een melding op Teams. We bouwen niet stilzwijgend een oneindige wachtrij op achter een trage printer.

def drop_to_printer(case, printer):
    target = printer.folder / case.printable_filename()
    if target.exists() or printer.archive_has(case.printable_filename()):
        log.info('already dropped, skipping', case=case.id)
        return
    if printer.pending_count() > 12:
        raise PrinterFull(printer.name)
    tmp = target.with_suffix('.stl.tmp')
    tmp.write_bytes(case.stl_bytes())
    tmp.rename(target)
    audit.record(case, 'dropped_to_printer', printer=printer.name)

De renametruc aan het eind zit erin zodat PreForm nooit een half geschreven bestand ziet. Watched folders op een gedeelde schijf pikken soms halve bestanden op en falen dan op verwarrende manieren. Schrijf naar een tmp-naam, doe daarna een atomic rename.

Het audit trail waar je later blij mee bent dat je 'm gebouwd hebt

Elke statusverandering schrijft een append-only rij weg: case-ID, van-status, naar-status, actor (systeem of AD-gebruiker), timestamp, SHA-256 van de payload vóór en ná. Zes weken na go-live vroeg de kwaliteitsmanager van de groep ons om alles wat met case 2026-04-1881 was gebeurd te reconstrueren. Het kostte 90 seconden.

Het schema bestaat uit drie tabellen: cases, case_events, reviewers. case_events is gepartitioneerd per maand voor querysnelheid. We hebben de neiging weerstaan om foreign keys terug te leggen naar de live operationele data. Het audit log draait op een eigen Postgres-instance zonder joins naar het hoofdsysteem, zodat het audit blijft werken als de applicatiedatabase een slechte dag heeft. De keerzijde is dat we wat patiëntmetadata dupliceren op het moment van iedere gebeurtenis, en dat is voor een gereguleerde workflow de juiste keuze.

De groep valt onder toezicht van de KNMT en de IGJ-inspectie, en elk van hun beoordelaars kan op elk moment om traceerbaarheid vragen. Een append-only log van wie wat heeft goedgekeurd, wanneer, ondertekend met AD-identiteit, is de goedkoopste verzekering die je kunt kopen. We slaan het op in Postgres met een dagelijkse logische back-up offsite. Niets exotisch.

Wat we na zes weken maten

Cijfers uit de eerste zes volle weken na de overstap, afgezet tegen de mediaan van de acht weken ervoor:

  • Mediane scan-to-printer-vertraging: van 71 uur naar 9 uur.
  • Cases die door de vier-ogen-controle werden opgevangen en anders verkeerd waren verstuurd: 3.
  • Exquise-supporttickets die de kliniek over onze integratie opende: 0.
  • Junior tandtechnicus FTE die vrijkwam van routeringswerk: 1,0. Zij is doorgegroeid naar een CAD-modelleerrol die ze al een tijd wilde.
  • STL-achterstand op vrijdag 17:00: meestal leeg, soms 6 tot 10 cases. De ergste vrijdag daarvoor was 387.
Kernpunt

Als je een gereguleerde workflow automatiseert, haal je de menselijke check niet weg. Verplaats die naar de plek waar 'ie voor de mens het goedkoopst is en het duurst om over te slaan.

Wat we anders zouden doen

Drie dingen.

We bouwden de prijscontrole tegen de prijs die in Exquise stond op het moment dat de scan binnenkwam. In twee gevallen werd de prijs naar boven aangepast nadat de scan was binnen, maar voordat hij was goedgekeurd, en gleed de case door het pad onder €1.800. Het juiste gedrag is om de prijs opnieuw te checken op het moment dat een tandtechnicus de review opent, niet op het moment dat de scan landt. Dat hebben we in week vijf gepatcht en met terugwerkende kracht hersteld.

We hebben in week één ook te weinig aandacht besteed aan de unmatched queue. We gingen ervan uit dat die leeg zou zijn. Dat was niet zo. Hij had een eigen kleine UI nodig en een dagelijkse reminder, want niemand keek ernaar. Bouw de uitzonderingsafhandeling met dezelfde zorg als het happy path.

Ten derde zetten we Teams-meldingen aan voordat we hadden uitgezocht wie elk alert eigenlijk in zijn portefeuille had. Twee weken lang pingde elke openstaande review het hele kanaal, en de senior tandtechnicus bracht zijn ochtenden door met het muten van zijn eigen laptop. We hebben het opgelost met per-reviewer-stiltevensters gekoppeld aan het dienstrooster in Exquise, maar als we het opnieuw deden bouwden we eerst de roosterkoppeling en daarna de meldingen. Meldingen zonder eigenaar zijn ruis.

Afronding

Toen we dit voor de Bossche groep bouwden, liepen we steeds tegen hetzelfde aan: het officiële pad (HL7-module, consultancy van de leverancier, een eigen review-UI) was zowel duurder als brozer dan het saaie pad (read-only ODBC, Teams adaptive cards, watched folders, append-only audit). Voor de patiënt veranderde er niets. In de back office veranderde er veel. Dat is meestal de vorm van nuttige procesautomatisering.

Heb je een vergelijkbare workflow op je bureau, dan is het kleinste wat je vandaag kunt doen: pak een stopwatch en ga één volle dienst naast wie nu het werk routeert zitten. Twee dagen elke overdracht klokken vertelt je welke automatisering de moeite waard is om te bouwen en welke theater is.

Kern

Als je een gereguleerde workflow automatiseert, haal je de menselijke check niet weg. Verplaats 'm naar de plek waar 'ie het goedkoopst is om te handhaven en het duurst om over te slaan.

FAQ

Kun je echt uit de Exquise-database lezen zonder het supportcontract te verliezen?

Ja, met een read-only ODBC-verbinding via de RDB$READONLY-rol van Firebird. Bevestig het wel vooraf schriftelijk bij de leverancier. Schrijven vanuit een derde-partij-proces is een ander gesprek en doet meestal de support vervallen.

Waarom de officiële HL7-exportmodule niet kopen?

Het is een terugkerende jaarlicentie, exporteert maar een deel van de velden en voegt een tweede bewegend onderdeel toe dat je moet bijhouden. Voor lezen alleen is een directe read-only verbinding goedkoper en stabieler.

Hoe voorkom je dat de vier-ogen-review verandert in afstempelen?

Volg de mediane reviewtijd per beoordelaar per week. Als iemand onder de 30 seconden zakt, gaat de teamleider met die persoon in gesprek. De drempel is gezet door een echte review te klokken, niet door te gissen.

Wat als Exquise een update uitbrengt die het schema verandert?

We pinnen de schemaversie in onze reader en alerten bij mismatch. Updates landen eerst in een staging-omgeving. In twee jaar vergelijkbare integraties zagen we één breaking kolomhernoeming, opgevangen in staging.

Waarom STL-bestanden in een map droppen in plaats van een print-API te gebruiken?

De watched-folder-pattern van Formlabs PreForm is gedocumenteerd, ondersteund en overleeft software-updates beter dan welke onofficiële API ook. Saai wint van slim voor productie-printerwachtrijen.

process automationautomationintegrationsworkflowarchitecturecase study

Iets bouwen?

Start een project