Email automation
Email-agent voor diergeneesmiddelen: 1.640 vragen per week
Een distributeur van diergeneesmiddelen met 27 medewerkers verdronk in 1.640 ordervragen per week. Wij zetten een email-agent voor SAP Business One zonder de verouderde stack aan te raken.

Toen we de inbox voor het eerst zagen, was het 09:14 op een dinsdag en had de teamleider klantenservice 312 ongelezen berichten. Ze zat al sinds 07:30 achter haar bureau. Het Nijmeegse kantoor van een distributeur van diergeneesmiddelen (27 mensen, derde generatie, familiebedrijf) verwerkte 1.640 ordervragen per week via één gedeelde mailbox en een team van vier. Op donderdagmiddag liepen ze altijd achter. Op vrijdag stuurden ze excuusmails over excuusmails.
Wat het vreemder maakte: elke bestelling werd uiteindelijk een nette verkooporder in SAP Business One. De informatie was er. De mailbox kon haar alleen niet zien.
De stack die we niet mochten aanraken
SAP Business One, on-premise, versie 9.2, laatst geüpgraded in 2021. DocuWare voor archief van facturen en certificaten. Een eigen PHP-middleware uit 2013 die voorraden synchroniseerde tussen SAP en een Magento 1.9-webshop die het team al zes jaar beloofde uit te faseren. Een Postfix-relay voor uitgaande mail. Interne gebruikers op Outlook 365.
De briefing van de operationeel directeur was op dag één glashelder. "Wij hebben dit jaar geen zin in een SAP-project. Volgend jaar ook niet."
Prima. Email-agents hoeven niet ín SAP te draaien. Ze moeten ervóór draaien.
Waar gedeelde-inboxtools tekortschieten
De kant-en-klare tools voor gedeelde inboxen (Front, Missive, Help Scout) lossen allemaal het toewijzingsprobleem op. Geen van allen lost het apothekersprobleem op. Een distributeur van diergeneesmiddelen in Nederland heeft een specifieke regelgevingseis: elk product met UDD-status (Uitsluitend door Dierenarts) moet door een geregistreerde apotheker worden bevestigd voordat het het pand verlaat. Verkeerde vrijgave, verkeerd product, verkeerd volume, en de IGJ staat op de stoep.
De agent moest dus drie dingen doen die geen enkele kant-en-klare tool doet:
- De email lezen en herkennen welke SKU's besteld werden, ook als de klant schreef "10 doosjes van die antibiotica voor melkvee, zelfde als vorige keer".
- Elke SKU controleren tegen de UDD-status-vlag in SAP.
- Het antwoord vasthouden in een apothekerswachtrij als er een UDD-product in zat, en automatisch laten doorgaan als dat niet zo was.
Ongeveer 22% van de inkomende vragen raakte minstens één UDD-product. De overige 78% kon binnen een minuut een bevestiging versturen. Die verhouding is de hele business case.
De architectuur in één alinea
De agent zit tussen de mailbox en SAP, nooit ín één van beide. Microsoft Graph bewaakt de bestellingen@-mailbox via een webhook-abonnement. Elk nieuw bericht landt in een kleine Node-service op de bestaande Hetzner-VPS van de klant. De service roept onze agent runtime aan met de berichttekst, het klant-ID van de afzender (gematcht op emaildomein, daarna fuzzy gematcht op weergavenaam) en de orderhistorie van de afgelopen 30 dagen, opgehaald via de SAP Service Layer. De agent retourneert een gestructureerd object: gematchte SKU's, aantallen, of er UDD-producten in zitten, een conceptantwoord en een confidence score per regel. Zit er geen UDD-product in de mand en is de confidence op elke match boven 0,85, dan wordt het concept verstuurd. Anders belandt het in een wachtrij.
De SAP Service Layer is hier de stille held. Het is een REST-interface die SAP heeft toegevoegd in Business One 9.0, en de meeste teams die B1 draaien hebben hem nog nooit aangeraakt. We hoefden geen enkele SAP add-on te schrijven.
De daadwerkelijke call om een UDD-vlag op een item te controleren:
GET /b1s/v1/Items('123456')?$select=ItemCode,ItemName,U_UDD_STATUS,QuantityOnStock
Cookie: B1SESSION=...; ROUTEID=...
Prefer: odata.maxpagesize=1Het veld U_UDD_STATUS was een user-defined field dat een vroegere IT-manager in 2018 had toegevoegd voor een interne rapportage. Het werd sindsdien stilletjes onderhouden. We checkten het: 4.113 actieve SKU's, elke UDD-gevlagde correct getagd. De data klopte. De agent hoefde alleen de juiste vraag te stellen.
De apothekerswachtrij
De apothekerswachtrij is het onderdeel van deze build waar we het meest trots op zijn, en het onderdeel dat het vaakst is herschreven.
Versie één was een Slack-kanaal. De apotheker (één van twee BIG-geregistreerde medewerkers) kreeg een notificatie per concept, klikte door naar een kleine webweergave, keurde goed of paste aan, en de agent stuurde het antwoord. Het werkte. Het maakte de apotheker ook gek. Hij kreeg 70+ notificaties per dag, zijn focus brak constant, en de wachtrij was óf leeg (frustrerend wachten) óf 40 diep (frustrerende paniek).
In versie twee verhuisde de wachtrij naar een kleine eigen webapp met twee veranderingen die telden. Ten eerste: gebatchte reviewmomenten. Drie keer per dag (09:00, 13:00, 16:00) krijgt de apotheker één Slack-notificatie met een link, en bekijkt alles wat zich heeft opgehoopt. Zelfde totale werk, een twintigste van de onderbrekingskosten. Ten tweede: standaard goedkeuren met override. De agent toont nu de SKU-match, de UDD-vlag, de eerdere orderhistorie van de klant voor die SKU, en een vooraf ingevulde goedkeuring. Het werk van de apotheker is uitkijken naar de uitzondering, niet het uitschrijven van de regel.
De mediane reviewtijd daalde van 4m 12s per item naar 47s per item.
De bottleneck in een gereguleerde workflow is zelden de regelgever. Het is het onderbrekingspatroon rondom de regelgever. Batch de mens, hou de agent live.
Het audit trail
UDD-orders hebben een audit trail nodig die de toets van een inspecteur doorstaat. Elk concept, elke goedkeuring, elke verzending, met timestamps en identiteit van de beoordelaar. De Nederlandse inspectie kan deze tot vijf jaar terug opvragen.
We overwogen om in SAP te loggen. We hebben het niet gedaan. SAP Business One is geen logging-database, en onze briefing was: niet aanraken. In plaats daarvan schreven we naar een kleine Postgres-instantie met append-only constraints: geen UPDATE-rechten op de audit_events-tabel, geen DELETE-rechten, punt.
Dat is zelf ook een mening. Vorige week stond er een stuk op Hacker News dat betoogde dat de enige schaalbare delete in Postgres DROP TABLE is, en voor een auditlog is dat precies het punt. Je verwijdert geen events. Je rolt hele partities af op een vijfjaarsvenster en dropt de oudste.
CREATE TABLE audit_events_2026 (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
occurred_at timestamptz NOT NULL DEFAULT now(),
event_type text NOT NULL,
actor text NOT NULL,
sap_item text,
udd_flag boolean,
payload jsonb NOT NULL
) PARTITION BY RANGE (occurred_at);
REVOKE UPDATE, DELETE ON audit_events_2026 FROM agent_writer;
GRANT INSERT, SELECT ON audit_events_2026 TO agent_writer;Goedkoop, saai, controleerbaar. De inspecteur krijgt een CSV. De agent krijgt nooit de mogelijkheid om de geschiedenis te herschrijven.
Wat misging in de eerste maand
Drie dingen gingen kapot in de eerste maand. De moeite waard om ze te benoemen, want ze gaan kapot voor iedereen die dit doet.
De fuzzy klantenmatch. Klanten mailen vanaf gmail-adressen die ze al vijftien jaar gebruiken. De agent moest vrije tekst "Praktijk Van der Heijden" matchen tegen een SAP-klantkaart met de naam "DIERENART HEIJDEN BV". We hebben hem zo afgesteld dat hij bij de eerste onbekende match per adres bevestiging vraagt aan de teamleider klantenservice, en die match daarna voor altijd cachet. Drempel voor auto-match: cosine similarity boven 0,92 op een Nederlandse name embedding, EN een postcodematch in het ondertekeningsblok. Allebei, niet één van beide.
Stock-out-antwoorden. Toen een klant 10 dozen van een product vroeg waar er 6 op voorraad waren, schreef de vroege agent vol zelfvertrouwen "we sturen morgen 10 dozen". We voegden een harde regel toe: de agent moet voor elke bevestigde regel live QuantityOnStock uit de Service Layer lezen, en als het gevraagde aantal boven het beschikbare uitkomt, gaat het concept naar een mens, ongeacht UDD-status. Achteraf gezien voor de hand liggend. Op dag drie pijnlijk.
De DocuWare-bijlageloop. Klanten antwoordden vaak op oude facturen die in DocuWare gearchiveerd waren, en die zetten een PDF van 3MB in het antwoord. De Microsoft Graph-webhook vuurde, wij stuurden de PDF rondom door de agent context, factureerden een pijnlijk aantal tokens, en kregen er niets bruikbaars uit. Fix: bijlagen groter dan 256KB strippen voordat de agent ze ziet, en alleen opnieuw bijvoegen als de agent ze expliciet op bestandsnaam opvraagt.
Als je agent standaard bijlagen leest, wordt je tokenrekening eerder zwaarder dan je inbox lichter. Eerst strippen, dan vragen.
Cijfers na zes maanden
De teamleider klantenservice begint haar dag nu om 08:45 in plaats van 07:30. De inbox is om 10:00 leeg. Het team van vier is anders ingezet: twee doen nu proactieve klantbenadering (iets wat het bedrijf al drie jaar wilde), twee bleven op inbox-supervisie en de apothekerswachtrij.
Uit de laatste volledige maand:
- 7.219 inkomende berichten verwerkt
- 5.634 (78%) automatisch beantwoord binnen 90 seconden
- 1.585 (22%) doorgestuurd naar de apothekerswachtrij
- 1.571 daarvan goedgekeurd zonder aanpassing
- 14 aangepast (vooral aantalscorrecties)
- 0 onjuiste UDD-vrijgaven
Nul is het getal dat ertoe doet. De IGJ is niet langsgekomen. We hopen dat dat zo blijft.
Lessen uit de herbouw
We zouden de apothekerswachtrij als eerste bouwen, niet als laatste. We staken twee weken in de auto-response-pipeline in de overtuiging dat dat het moeilijke stuk was. De apothekerswachtrij is het eigenlijke product. De auto-response is een bijproduct van schone SKU-matching.
We zouden ook harder pushen tegen de "niet aan SAP zitten"-lijn. De klant had gelijk dat een SAP-project van tafel was, maar drie user-defined fields toevoegen aan de klantkaart (voorkeurs-dierenarts, standaard-verzendvenster, laatst-bekende-BIG-nummer) had de helft van onze middleware-logica geëlimineerd. We behandelden SAP als onveranderlijk; het was alleen lastig.
Toen we de email automation voor de Nijmeegse distributeur bouwden, bleef ons verbazen hoeveel werk in het vormgeven van de human-in-the-loop zat, en niet in de agent zelf. Daar zouden we de eerste twee weken van elke vergelijkbare herbouw insteken.
Wat je deze week kunt doen
Draai je een gedeelde mailbox vóór een verouderde ERP, dan is dit de kleinste nuttige audit. Pak willekeurig 100 berichten uit de laatste zeven dagen. Sorteer ze met de hand in drie bakken: (a) kan beantwoord worden door de ERP te lezen, (b) vraagt een menselijke beoordeling, (c) is het begin van een verkoopgesprek. Het percentage (a) is je plafond voor wat een email-agent van je team kan overnemen. Is (a) boven de 60%, dan verdient de build zich terug vóór Q4.
Kern
De apothekerswachtrij is het product, niet de auto-responder. Bouw eerst het gereguleerde pad, dan valt het makkelijke pad er gratis uit.
FAQ
Vergde dit aanpassingen aan SAP Business One?
Nee. We hebben de REST API van de SAP Service Layer gebruikt om items, voorraad en klantkaarten te lezen. Geen SAP add-ons, geen schemawijzigingen. De middleware draaide buiten SAP op de bestaande VPS van de klant.
Hoe bepaalt de agent of een product UDD is?
Hij leest het user-defined veld U_UDD_STATUS op elk gematcht item via de SAP Service Layer. Zit er in een concept ook maar één regel met een UDD-product, dan gaat het concept naar de apothekerswachtrij in plaats van te versturen.
En wat met antwoorden die de agent fout heeft?
Twee vangnetten. Een confidence-drempel van 0,85 per SKU-match (alles eronder gaat naar een mens), en een harde regel dat een gevraagd aantal boven de beschikbare voorraad altijd naar een mens gaat, ongeacht UDD-status.
Kan een kleinere distributeur hetzelfde patroon gebruiken?
Ja. Het patroon (mailbox-webhook plus gestructureerde agent-output plus een wachtrij voor het gereguleerde deel) werkt voor elke inbox die voor een systeem van vastlegging zit. Het auditlog-stuk telt het zwaarst in gereguleerde sectoren.