Email automation
E-mailautomatisering tegen verouderd SAP EDI: het vier-ogen playbook
23:00 in Eindhoven, 740 ongelezen leveranciersbevestigingen in de inkoop-inbox. De 14 jaar oude SAP eronder kan ze niet lezen. Dit is het playbook waarmee we de agent bouwden die dat wel kan.

Het is 23:00 in Eindhoven en de inkoop-inbox heeft 740 ongelezen berichten. Stuk voor stuk leveranciers die een inkooporder bevestigen: ja we leveren, dit is onze datum, dit is onze prijs. Sommige zijn PDF's. Andere zijn Word-bijlagen waarin de klantreferentie ergens in de voettekst zit. Een paar zijn platte tekst in het Duits. Achter die inbox draait een 14 jaar oude SAP ECC 6.0, een in 2014 vastgeschroefde EDI-broker en een AS2-gateway waar niemand meer aan heeft gezeten sinds de engineer die hem bouwde in 2019 vertrok. Vrijdag is dit volume 3.640 bevestigingen. Maandagochtend wil de operations lead weten welke door de vingers zijn geglipt.
Dit is het playbook waarmee we tegen exact die stack een e-mail-agent live brachten, voor een semicon-toeleverancier van 32 mensen vlakbij de High Tech Campus. Geen rip-and-replace, geen SAP-upgrade, geen ingreep op AS2. Zes weken van kickoff tot eerste productie-run.
De randvoorwaarde die het project definieert
Je mag de EDI-997 functionele acknowledgement niet terugsturen naar de klant voordat een mens elke schuif in leverdatum van meer dan vijf werkdagen heeft goedgekeurd. Dat is de regel. De rest (de parser, de SAP-write, de UX van de wachtrij) volgt uit die ene randvoorwaarde.
De reden is contractueel. Zodra de 997 de AS2-gateway verlaat, heeft de leverancier de order formeel geaccepteerd op de bevestigde datum. Schuift die datum daarna en heeft niemand het gemeld, dan kan de afnemer line-down-uren terugfactureren. In de semicon kost een uur line-down vijf cijfers. Het inkoop-team deed die check dus met de hand: e-mail openen, datum zoeken, vergelijken met de regel in SAP, beoordelen of de slip acceptabel is, op verzenden klikken.
Dat 3.640 keer per week doen, dat was wat we mochten vervangen. Niet het beslissen. Wel het openen, zoeken, vergelijken, en de tachtig procent van de bevestigingen waarbij het antwoord voor de hand ligt.
Wat we bouwden, in één zin
Een e-mail-agent die leveranciersbevestigingen inleest tot een gestructureerd record, deze via de bestaande EDI-broker vergelijkt met de open inkooporder in SAP, de bevestigde datum terugschrijft via een IDoc die de broker al begreep, en de 997 in een wachtrij vasthoudt wanneer (en alleen wanneer) de slip groter is dan vijf werkdagen.

De bevestiging parsen
Tweederde van het inkomende verkeer was PDF, een kwart was Excel per e-mail, de rest vrije tekst. We bouwden geen parser. We bouwden een router en drie specialisten:
- Een layout-anchored PDF-extractor voor de acht leveranciers die samen 71% van het weekvolume leveren. Elk heeft een stabiel template; we mikken eerst op bekende coördinaten en vallen alleen bij een mismatch terug op een LLM.
- Een spreadsheetlezer voor twee leveranciers die Excel-bijlagen sturen. Schema-on-read tegen een YAML hint-bestand per leverancier.
- Een algemene LLM-extractor voor de long tail, met een strikt JSON-schema en een refusal path als de confidence onder een drempel zakt.
Het schema is klein: customer_po_number, line_item, confirmed_quantity, confirmed_delivery_date, supplier_reference, currency, unit_price. Al het andere gaat als ruwe context mee naar de vier-ogen reviewer, maar weegt niet mee in de routing. Lukt het de parser niet om die zeven velden te vullen boven een drempel die we tegen drie weken handmatig gelabelde mail hebben gekalibreerd, dan gaat de bevestiging naar de wachtrij, met de ruwe e-mail naast de gedeeltelijke extractie.
Als je parser op basis van een LLM-extractie iets naar SAP schrijft zonder deterministische check tegen de open PO, dan boek je vroeg of laat een prijsmutatie op de verkeerde regel. Dat leerden we op een zaterdag in week twee, toen een LLM het interne referentienummer van de leverancier oppikte in plaats van ons PO-nummer en het matchte tegen de enige open regel die toevallig twee cijfers deelde. De diff ving het op omdat de stuksprijs buiten tolerantie viel. We bouwden die tolerantiecheck in voordat we ook maar één IDoc naar productie schreven.
Praten met een 14 jaar oude SAP
SAP ECC 6.0 zit in extended mainstream maintenance tot eind 2027, dus de klant ging geen S/4HANA-migratie financieren als onderdeel van dit project. Mooi. Hoefde ook niet.
De EDI-broker accepteerde al ORDERS-, ORDRSP- en DELFOR-IDocs. De agent logt helemaal niet in SAP in. Hij bouwt een ORDRSP.ORDERS05 IDoc, ondertekent die voor de broker, en zet hem in dezelfde inbound directory die de broker sinds 2014 in de gaten houdt. De broker roept vervolgens dezelfde BAPI aan als altijd. SAP kan ons IDoc niet onderscheiden van eentje die een mens via ME22N heeft geschreven.
Dit is veruit de belangrijkste architectuurkeuze in het playbook: behandel het bestaande integratievlak als de API. Verzin er geen nieuwe. Vraag geen nieuwe RFC-user aan. Vraag het SAP-team om niets, behalve leestoegang tot één MARA/EKKO/EKPO-view, die we binnen 40 minuten kregen omdat we verder nergens om vroegen.
def build_ordrsp_idoc(po: PurchaseOrder, conf: SupplierConfirmation) -> bytes:
seg = IDocBuilder("ORDRSP", basictype="ORDERS05")
seg.control(sender=AGENT_LS, receiver=SAP_LS, mestyp="ORDRSP")
seg.e1edk01(belnr=po.number, currency=conf.currency)
for line in conf.lines:
seg.e1edp01(
posex=line.po_line,
menge=line.confirmed_qty,
preis=line.unit_price,
)
seg.e1edp20(edatu=line.confirmed_date.strftime("%Y%m%d"))
return seg.serialize()
De vier-ogen wachtrij
Nu de randvoorwaarde. Voor elke bevestigde regel berekenen we slip_days = working_days_between(po.requested_date, conf.confirmed_date). Werkdagen, geen kalenderdagen, tegen een Nederlandse feestdagenkalender plus een leveranciersland-kalender voor de top acht. Als slip_days > 5, dan wordt de regel geparkeerd. De 997 gaat niet de deur uit.
Geparkeerde bevestigingen komen in een wachtrij die twee mensen zien: de inkoop-lead en één van drie supply-chain planners. Beiden moeten binnen hun rol op goedkeuren of afkeuren klikken. De UI is één scherm. Het toont de oorspronkelijke e-mail, het record dat de parser eruit haalde, de open PO, de slip in werkdagen, en een Slack-achtige commentaarthread.
Wat we expliciet níet deden: de vier-ogen wachtrij via e-mail laten lopen. We hebben er lang over gediscussieerd. Goedkeuring per e-mail was sneller te bouwen, maar je verliest het audit trail en je nodigt reply-all-chaos uit. We kozen voor een kleine Next.js-app op hetzelfde domein als de agent, met magic-link auth tegen de Entra ID van de klant. De zes weken liggen inclusief die app.
De EDI-997 timen
De functionele acknowledgement is het lastigste stukje. AS2-partners verwachten doorgaans een 997 binnen een afgesproken SLA. Voor de meeste afnemers van deze leverancier is dat twee uur. Vasthouden voor een vier-ogen review kan daar overheen schieten.
We splitsten de 997 in twee getimede events. De technische 997 (we hebben het bericht ontvangen en geparset) gaat direct en onvoorwaardelijk de deur uit. De zakelijke 997 (we accepteren de bevestigde datums) gaat pas weg als de vier-ogen wachtrij is afgewerkt. De meeste EDI-implementaties van afnemers accepteren die splitsing omdat het past binnen de X12-spec; een handvol niet, en voor die hebben we het parkeervenster verkort tot vier uur en de escalatie strakker getrokken als een planner het liet lopen.
Als je agent moet beslissen over het verzenden van een onomkeerbaar bericht, splits het bericht dan in een technische ontvangstbevestiging en een zakelijke toezegging. Stuur de ontvangst; zet de toezegging in de wachtrij.
Idempotency, replay en de saaie middenlaag
Leveranciers-inboxen zijn rommelig. Dezelfde bevestiging komt twee keer binnen, soms vanaf twee verschillende adressen bij dezelfde leverancier. We sleutelen elke inbound op een tuple van (supplier_id, customer_po, line, confirmed_date_hash). Een tweede binnenkomst met dezelfde tuple wordt gelogd en weggegooid. Een tweede binnenkomst met een andere datum behandelen we als revisie en draait de slip-check opnieuw.
De replay-log staat in Postgres, niet in het runtime-geheugen van de agent. Klapt de agent eruit, dan pakt de volgende instantie verder bij de laatste niet-bevestigde e-mail. Keurt een planner een bevestiging goed en blijkt daarna dat de parser de datum verkeerd las, dan rollen we vooruit door een corrigerende ORDRSP te schrijven. Nooit terugrollen, want SAP kent geen "undo my last IDoc".
Shadow mode vóór cutover
We zetten de schrijfacties naar de broker-directory niet meteen op dag één aan. De eerste twee weken draaide de agent in shadow mode: elke inkomende e-mail ging door de volledige pipeline, maar het gegenereerde IDoc landde in een staging-directory die de broker niet bekeek. Elke ochtend om 08:30 opende de inkoop-lead een diff op één scherm: elke bevestiging die de agent in de laatste 24 uur had geparset, het IDoc dat hij zou hebben geschreven, en het IDoc dat een mens daadwerkelijk schreef. Overeenstemming telde; een verschil kreeg met één klik een reden-code.
De shadow-run bracht zeventien edge cases boven water die we in de ontwerpgesprekken niet hadden gezien. Een leverancier in Singapore die in lokale tijd bevestigt, maar de PDF in UTC stempelt, met een verschuiving van zeven uur over een middernachtgrens. Twee opeenvolgende PO-nummers die alleen verschilden door een zero-width space die de PDF-generator van een leverancier ertussen had gegooid. Een planner die routinematig een slip van één dag accepteert op lithografie-spares zonder die vast te leggen, wat onze wachtrij zou hebben overspoeld met bevestigingen waar het team in de praktijk niets om geeft. Elk werd een parser-regel, een normalizer of een per-leverancier override voordat we de agent ook maar één live IDoc lieten schrijven.
Wat we bewust niet bouwden
We bouwden geen leveranciersportaal. Het team wilde dat. Het instinct is logisch: als leveranciers een gestructureerd formulier invullen, verdwijnt de parser. Maar de leveranciers die er het meest toe doen zijn tier-one fabs en EMS-shops met hun eigen EDI-stack en nul interesse om in een klantenportaal in te loggen om data over te tikken die ze al gestuurd hebben. Een portaal bouwen had het integratieprobleem naar de verkeerde kant van de relatie verplaatst.
We hebben ook niet automatisch geëscaleerd naar de afnemer als een leverancier een SLA miste. Dat wilde het team ook. We hielden af, omdat de agent niet weet welke klantrelaties een vriendelijk zetje kunnen hebben en welke niet. Dat blijft bij de planners, die een dagelijkse digest van naderende slips krijgen.
Hoe het er in productie uitziet
Op het moment van schrijven draait de agent elf weken live. Het weekvolume kwam uit op gemiddeld 3.640 bevestigingen met pieken boven de 4.200 rond kwartaaleinde. 81% van de bevestigingen rolt door de agent zonder ooit de vier-ogen wachtrij aan te raken. De resterende 19% (slips groter dan vijf werkdagen, lage parser-confidence of nieuwe leveranciers) doet gemiddeld 11 minuten van binnenkomst tot beslissing door de planner, tegen een eerdere mediaan van "de volgende ochtend". Het inkoop-team ging van drie FTE op inbox-triage naar één FTE op wachtrij-review en uitzonderingsafhandeling.
De eerste interessante failure mode, die we niet hadden voorzien, waren leveranciers die op de oorspronkelijke PO-mail antwoordden met een eenregelige bevestiging in de body. De router liet die de eerste week schieten omdat we op de aanwezigheid van een bijlage sleutelden. We voegden een body-parser fallback toe en de recovery rate op de long tail sprong van 78% naar 96%.
De tweede was een leverancier wiens mailserver PDF-bijlagen herschreef via een third-party scan-and-rebuild proxy. De herbouwde PDF's braken onze layout-anchored extractor omdat elke coördinaat met twee punten was verschoven. We bouwden een per-leverancier sentinel die de bounding box van een bekend veld vergelijkt met het template; drijft die meer dan een halve millimeter af, dan vallen we terug op de LLM-extractor en loggen het voor de volgende template-refresh. Diagnose: één dag. Fix: een uur. Omdat shadow mode ons al had geleerd de diff meer te vertrouwen dan de parser.
Het kleinste wat je vandaag kunt doen
Draai je een EDI-flow tegen een verouderd ERP, open dan de inbound directory die je broker in de gaten houdt. Kijk naar de IDocs van de afgelopen week. Tel hoeveel ervan door een mens zijn ingetikt vanuit een e-mail. Dat getal is je business case.
Toen we de e-mail-agent bouwden voor deze Eindhovense toeleverancier, was het lastigste niet de LLM of de SAP-integratie. Het was het recht verdienen om een EDI-997 voor een mens vast te houden. Dat verdienden we door de acknowledgement te splitsen in een technische ontvangst en een zakelijke toezegging, en door de vier-ogen reviewers een scherm te geven dat hun vijf seconden aandacht respecteerde.
Kern
Splits de EDI-acknowledgement in een technische ontvangst en een zakelijke toezegging, en zet de toezegging in de wachtrij tot een mens de slip heeft goedgekeurd.
FAQ
Waarom niet meteen upgraden naar S/4HANA in dit project?
De klant ging geen ERP-upgrade financieren naast een automatiseringsproject, en dat hoefde ook niet. Mainstream maintenance op SAP ECC 6.0 loopt tot eind 2027, dus we hadden meerdere jaren runway op de bestaande stack.
Wat houdt de LLM tegen om de verkeerde datum naar SAP te schrijven?
Een deterministische diff tegen de open PO. De agent controleert PO-nummer, regel, aantal en stuksprijs binnen tolerantie voordat er een IDoc wordt geschreven. Alles wat de diff niet haalt, gaat met de ruwe e-mail naar de vier-ogen wachtrij.
Waarom de EDI-997 vasthouden in plaats van later een uitzondering te sturen?
De meeste AS2-partners zien de 997 als toezegging. Door hem te splitsen in een technische 997 (direct verzonden) en een zakelijke 997 (na het leegmaken van de wachtrij) hou je de SLA in stand zonder iets toe te zeggen wat de leverancier nog niet heeft goedgekeurd.
Werkt hetzelfde patroon voor EDIFACT in plaats van X12?
Ja. Het EDIFACT-equivalent van de 997 is CONTRL. Dezelfde splitsing geldt: stuur de technische CONTRL bij ontvangst, hou de zakelijke ORDRSP vast tot de vier-ogen wachtrij is afgewerkt.
Hoe lang duurde de build bij een klant van 32 mensen?
Zes weken van kickoff tot eerste productie-run, inclusief de Next.js wachtrij-UI, de IDoc-builder, de parser-router en de AS2-timingsplit. Eén ABN-engineer plus deeltijdtoegang tot de SAP- en EDI-leads van de klant.