Email automation
E-mailagent voor apotheek: 2.340 herhaalvragen per week
Om 08:00 in een Arnhemse online apotheek wachten al 312 mails tot de uitgiftebalie opengaat. De PostNL-cut-off is 11:30. Dit is wat we bouwden.

Acht uur 's ochtends, Arnhem. De uitgiftebalie opent om negen uur. De nachtdienst bij de groothandel liep vanaf 04:00. Tussen die twee klokken zit een inbox met 312 ongelezen berichten, bijna allemaal met dezelfde vraag in net iets andere bewoordingen: Kan mijn herhaalrecept vandaag de deur uit?
Zo zag de dagelijkse praktijk eruit bij een online apotheek met 21 mensen ten oosten van de rivier, voordat we de e-mailagent bouwden. Tegen de tijd dat de apotheker de wachtrij handmatig had getrieerd, miste de helft van de zendingen de PostNL-cut-off van 11:30. Het team was in drie jaar gegroeid van zes naar 21. Het Pharmacom-AIS eronder was vijftien jaar oud. Niemand had tijd om het te migreren, en niemand wilde degene zijn die een systeem brak dat 1,4 miljoen recepten had verwerkt.
Wat er werkelijk in de wachtrij zat
Voordat we ook maar één regel code aanraakten, lazen we 2.340 berichten. Eén week aan mail. We sorteerden ze op wat de afzender daadwerkelijk nodig had:
- 1.612 herhaalrecept-vragen met een schone AIS-match (patiënt-ID, geneesmiddel en laatste uitgiftedatum klopten allemaal)
- 409 herhaalrecept-vragen waarbij iets niet klopte: gewijzigde dosering, verlopen voorschrift, of de huisarts had de herhaling nog niet via het LSP doorgezet
- 211 vragen over levertijd omdat CB Online een voorraadtekort liet zien op een UR-geneesmiddel
- 74 berichten met een verzoek om een ander merk van dezelfde werkzame stof
- 34 berichten met wat we BIG-twijfel zijn gaan noemen: ophalen via machtiging, een onbekende voorschrijver, of een BIG-nummer dat niet schoon resolved
Die 34 kun je niet automatiseren. Nu niet, volgend jaar niet. Daarom staat de apotheker op de vloer. De andere 2.306 zagen er behapbaar uit.
De vorm van de integratie
Pharmacom heeft geen REST API zoals een SaaS uit 2025. Het levert een HL7v2-interface op een vaste interne poort, plus een flat-file-export die het AIS elk kwartier wegschrijft naar een gedeeld volume. We kozen voor de flat file. HL7 was sneller en mooier geweest, maar vroeg om een vendor change request, en de apotheek wachtte al elf maanden op een andere change request. De leverancier (PharmaPartners) reageert keurig, maar de queue is de queue.
CB Online is een ander beest. De voorraadfeed komt binnen als een nachtelijk CSV-bestand plus een kleine JSON-delta-endpoint die de groothandel openzet voor geverifieerde accounts. De refresh-cadans is ongeveer elke twintig minuten tijdens kantooruren. We pollen de delta, niet de volledige CSV, en houden een lokale SQLite-spiegel bij zodat de agent nooit blokkeert op een netwerkcall.
incoming-mail (IMAP, info@apotheek-...)
-> classifier (LLM, prompt v3)
-> AIS lookup (Pharmacom flat file, patient match)
-> stock check (CB Online SQLite mirror)
-> decision tree
-> auto-reply (SMTP relay) [85% of traffic]
-> apotheker queue (webhook -> board) [15%]
Dat is de hele vorm. Twee leesoperaties, één schrijfoperatie, één menselijke queue.
Waar de agent de grens trekt
De triage-prompt is kort. Hij vertelt het model drie dingen, in deze volgorde: wie er schrijft (gematchte patiënt of onbekend), waar de vraag over gaat (herhaling, voorraad, klacht, merkwissel, overig), en of een van zeven harde signalen aanwezig is. Die zeven signalen zijn niet onderhandelbaar. Eén ervan en het bericht wipt naar de apotheker-queue. Het model krijgt de instructie nooit een antwoord op te stellen voor zulke berichten.
- Werkzame stof op de BIG-1-watchlist (opioïden, benzodiazepines, ADHD-medicatie)
- BIG-nummer van de voorschrijver resolved niet in onze CIBG-spiegel
- Dosering in het bericht wijkt af van de laatste AIS-uitgifte
- Patiënt schrijft over een bijwerking of een contra-indicatie
- Machtigingstaal (een derde die voor iemand anders ophaalt)
- Bericht is in een andere taal dan Nederlands of Engels
- Laatste AIS-contact van de patiënt is meer dan 90 dagen geleden
Een model dat één keer besluit een hard signaal te omzeilen, doet het opnieuw. We laten het LLM geen ernst beoordelen. De signalen worden geëvalueerd door deterministische code, en alleen berichten die alle zeven gates passeren bereiken ooit de drafting-prompt.
Die scheiding is het deel van het systeem dat de apotheker elke vrijdagmiddag controleert. Ze leest tien willekeurige auto-replies en tien willekeurige queue-items. Als iets in de queue auto-replied had kunnen worden, of iets in de auto-reply-set in de queue had gemoeten, draaien we de gates strakker aan. Zes keer aangedraaid in acht weken, daarna was het stabiel.
De drafting-prompt, in gewone tekst
Mensen vragen vaak hoe de prompt eruitziet. Korter dan ze verwachten. Het interessante werk zit niet in de prompt; dat zit in de gestructureerde input die we eraan meegeven.
You are drafting a single reply on behalf of {pharmacy_name}.
Facts you may use (do not invent any others):
- Patient: {patient_first_name}, last issue {last_issue_date}
- Medicine requested: {medicine_name} {strength}
- Last issued dosage: {last_dose}
- Current stock: {stock_status}
- Earliest dispatch: {dispatch_eta}
- House signature: {signature_block}
Write in Dutch unless the original message was in English.
Do not promise a delivery date later than {dispatch_eta}.
Do not mention price.
Do not give medical advice.
Close with the house signature, exactly as provided.
Het model ziet nooit het BIG-nummer, de SKU van de voorraad, of de volledige AIS-regel. Het ziet wat een junior-assistent op een geprint briefje zou zien. Dat houdt het oppervlak voor hallucinatie klein.
Het moment van de verzendbevestiging
Een auto-reply is niet het einde van de keten. De patiënt verwacht nog een verzendbevestiging als het pakket daadwerkelijk de apotheek verlaat. In de oude workflow was dat een handmatige mail merge die iemand twee keer per dag draaide na de PostNL-ophaling. De agent schrijft de bevestiging nu wel, maar verstuurt hem niet. Pharmacom schrijft een regel naar zijn uitgiftelog op het moment dat het medicijn het pand verlaat. Een kleine worker tailt die log en triggert pas dan de SMTP-relay.
Die volgorde is essentieel. Een verzendbevestiging sturen voordat het pakket bestaat, is dé klacht waarmee een apotheek voor de IGJ komt te staan. Dus knoopten we de SMTP-send vast aan een echt, fysiek event: de regel in het AIS-log waarin staat dat het medicijn de kast uitging.
def on_dispense_logged(row):
if row["state"] != "DISPATCHED":
return
draft = redis.get(f"reply-draft:{row['rx_id']}")
if not draft:
return # apotheker handled this one manually
smtp.send(
to=row["patient_email"],
subject=f"Verzonden: {row['medicine']}",
body=draft,
from_addr="info@apotheek-...nl",
reply_to="info@apotheek-...nl",
)
redis.delete(f"reply-draft:{row['rx_id']}")
Cijfers na acht weken
We noemen geen percentages zonder de absolute getallen. Dus:
- 2.340 mails per week in scope (de slice van herhaalrecepten en voorraadvragen, niet de hele apotheek-inbox)
- 1.989 auto-replied binnen 12 minuten na ontvangst, mediaan 4 minuten
- 351 doorgezet naar de apotheker-queue, waarvan 34 de harde BIG-twijfel-cases waren waarvoor het systeem was ontworpen
- 0 verzendbevestigingen verstuurd voor pakketten die het pand nog niet uit waren (geverifieerd tegen het PostNL-scanlog)
- 11:30-cut-off van PostNL gehaald op 19 van de 20 werkdagen in de pilot
De vrijgekomen tijd is de regel die voor de oprichter telt. Het team besteedde voorheen 14 uur per week, verdeeld over drie assistenten, aan het lezen en triëren van deze inbox. Nu zijn ze daar 2 uur mee bezig: 90 minuten review van de queue, 30 minuten audit op een sample van de auto-replies. Twaalf uur per week terug, voor een team van 21. Dat is de enige ROI-zin in het hele projectdocument.
De dingen die we eerst fout deden
Drie dingen die we zouden overslaan als we het opnieuw deden.
We begonnen met een lokaal model op een Mac Studio in het achterkantoor, omdat het privacy-argument zo voor de hand lag. We hadden het mis over de bottleneck. Het model was niet het trage stuk; het pollen van de flat file was dat. We verhuisden de classifier naar een hosted API en hingen alle PII-redactie aan een lokale pre-processor. Dat was de architectuur die we op dag één hadden moeten shippen. Het eerlijke antwoord op de vraag lokaal-versus-hosted is 'hangt af van de bottleneck', en de bottleneck is zelden het model.
We probeerden de agent ook merkwisselingen te laten beantwoorden. Die categorie ziet er onschuldig uit en is dat niet. Een verzoek om 'een ander merk van dezelfde werkzame stof' verbergt vaak een allergie of een preferentiebeleid-issue met de verzekeraar, en in beide gevallen moet de apotheker het zien. We trokken dat draft-pad er na drie weken weer uit.
We gingen ervan uit dat BIG-nummers stabiele identifiers waren. Dat zijn ze, totdat een voorschrijver met pensioen gaat of van praktijk verandert en het CIBG-record achterloopt. We refreshen de CIBG-spiegel nu wekelijks en zetten elk onopgelost nummer in de queue in plaats van te gokken. Het BIG-register is de bron; we spiegelen het lokaal om hun endpoint niet stuk te bellen, en we kruisverwijzen onduidelijke gevallen tegen de KNMP-richtlijnen waar de apotheker toch al op leunt.
De architecturale les
Het model is het goedkoopste onderdeel van een e-mailagent voor een apotheek. De dure onderdelen zijn de zeven onverhandelbare gates, de trigger op het verzendevent, en de wekelijkse audit door de apotheker. Bouw die eerst.
Als je één diagram uit dit stuk meeneemt, neem dan deze. Het LLM zit in het midden van de stack, niet aan de randen. De randen zijn deterministisch: de IMAP-listener, de AIS-reader, de voorraadspiegel, de gate-evaluator, de SMTP-relay, de listener op het verzendevent. Het model stelt alleen op. En een opgesteld antwoord gaat alleen weg als een fysiek event in het pand zegt dat het zover is.
Dat geldt voor elke gereguleerde sector, niet alleen apotheken. De auditor moet een papieren spoor kunnen volgen van het verzonden pakket terug naar het oorspronkelijke bericht, en vooruit naar de verstuurde bevestiging. Een model midden in een deterministische loop geeft je dat spoor. Een model aan de randen niet.
Wat je vanmiddag kunt doen
Run je een organisatie van 10 tot 50 mensen met een zware inbox en een verouderd line-of-business-systeem, dan is het kleinste nuttige wat je vandaag kunt doen: lees één week van je eigen mail met een markeerstift. Markeer elk bericht waarvan je had gewild dat een junior het zonder jou had kunnen beantwoorden, en elk bericht dat je persoonlijk wil zien. De ratio tussen die twee stapels is het enige datapunt dat je nodig hebt om te beslissen of een e-mailagent het scopen waard is. We hebben die exercitie gedaan met alles van een Pharmacom-AIS tot een Joomla-webshop uit 2009 met een custom facturatie-module, en de ratio is bijna nooit wat de oprichter voorspelt.
Toen we deze e-mailagent voor de Arnhemse apotheek bouwden, hadden we de audit-loop onderschat. We eindigden met een one-screen-dashboard voor de apotheker dat elke vrijdag tien willekeurige antwoorden en tien willekeurige queue-items toont, en dat ene scherm hield de uitrol veilig. Hetzelfde patroon past op factuurherinneringen, kennisbanken en onboarding-inboxes; ons werk met AI-agents loopt langs de andere live deployments waar we het hebben toegepast.
Kern
Het model is het goedkoopste onderdeel van een e-mailagent voor een apotheek. Het werk zit in de deterministische gates, de trigger op het verzendevent, en de wekelijkse audit door de apotheker.
FAQ
Waarom een flat-file-export en geen HL7v2?
HL7 was sneller en schoner geweest, maar vroeg om een vendor change request die al elf maanden in een wachtrij zat. De flat file landde elke vijftien minuten op een gedeeld volume en dat was goed genoeg om mee te shippen.
Hoe voorkom je dat het model risicovolle gevallen automatisch beantwoordt?
Voor de drafting-prompt draaien zeven deterministische gates. Vuurt er één (BIG-1-watchlist, doseringsverschil, onopgelost BIG-nummer, taal over bijwerkingen, en zo verder), dan gaat het bericht naar de apotheker-queue en ziet het model het nooit.
Waarom wachten op een AIS-event voor je de verzendbevestiging stuurt?
Een verzending bevestigen die nog niet heeft plaatsgevonden, is precies de klacht waarmee de IGJ op de stoep staat. We tailen het uitgiftelog van Pharmacom en sturen de vooraf geschreven bevestiging pas weg als het medicijn echt de kast uit is.
Hoeveel uur per week heeft de agent vrijgemaakt?
Twaalf. Drie assistenten besteedden voorheen samen zo'n veertien uur per week aan de herhaalrecept-inbox. Na acht weken zat het team daar op twee uur: anderhalf uur op de queue, een half uur op de vrijdagaudit.
Draaiden jullie het model lokaal voor data residency?
We hebben het geprobeerd, daarna zijn we overgestapt op een hosted API met een lokale PII-redactiestap ervoor. Het model was nooit de bottleneck; het pollen van de flat file wel. Residency losten we op in de redactielaag, niet in de modellaag.