← Blog

Process automation

CMR + ADR-automatisering: escalatie-playbook van 45 sec

Dinsdag 06:47: 614 ongelezen mails, één begraven ADR klasse 3-lek en een ILT-klok die al tikt. Dit is de agent die we bouwden zodat het nooit meer gebeurt.

Jacob Molkenboer· Oprichter · A Brand New Company· 22 jun 2026· 10 min
Koperen bel, crème document met stempel ADR klasse 3, stopwatch, groen lint, rode lakzegel op ivoor papier.

Dinsdag, 06:47. De wagenparkbeheerder van een transportbedrijf met 25 trucks net buiten Amersfoort opent zijn Outlook en telt 614 ongelezen items. Elf zijn CMR-vrachtbrieven, gescand door chauffeurs die hun dienst tussen 22:00 en 04:00 afsloten. Drie zijn van de nachtplanner met de vraag te bevestigen welke trailer waar staat. Eén, op positie 387, is een doorgestuurde WhatsApp van een chauffeur die meldt dat er een traag druppellek zit onder zijn truck op de A27, vanuit een IBC met ethanolhoudend reinigingsmiddel. ADR klasse 3. De wettelijke klok om de Inspectie Leefomgeving en Transport te informeren begon te tikken op het moment dat de chauffeur het opmerkte.

Hij heeft, technisch gezien, tot vroeg in de middag om het te melden.

Hij ziet de mail om 09:18.

Deze post is het playbook dat we gebruikten om ervoor te zorgen dat dat niet meer gebeurt. De klant is een echt Amersfoorts transportbedrijf met 25 trucks die door de Benelux en het Ruhrgebied rijden, dat ongeveer 3.200 CMR-vrachtbrieven per week verwerkt, en dat gebonden is aan de Wet vervoer gevaarlijke stoffen (Wvgs) elke keer dat een ADR-zending Nederlandse wegen raakt. We noemen ze niet bij naam. De rest komt uit het build log.

De randvoorwaarde die alles bepaalt

Onder de Wvgs en het onderliggende ADR-verdrag moet een transportbedrijf een significante uitstroom van een gevaarlijke stof onverwijld melden bij de ILT. De klok is kort. De vervoerder heeft uren, geen dagen, en de ILT-inspecteur die terugbelt vraagt wanneer het bedrijf het voor het eerst wist. "Begraven in de inbox van de wagenparkbeheerder tot lunchtijd" is geen antwoord dat goed afloopt.

Die ene regelgevende klok is wat het project de moeite waard maakte. Al het andere, de achterstand in CMR-scans, de chaos in de planner-rooster, het feit dat één monteur elke vrijdag Transics-data afstemt met een geprint weekschema, is op te lossen met geduld. De meldklok los je op met software, of helemaal niet.

Twee systemen, die niet met elkaar praten

De vloot draait op Transics, het Belgische telematicaproduct dat nu eigendom is van ZF. De installatie is uit 2013. Het registreert positie, rij- en rusttijden, brandstof, en een dunne laag tripmetadata. Het weet niet wat er op de truck zit. De vrachtdocumenten, CMR-vrachtbrieven, ADR-bijlagen, weegtickets, leven op papier en daarna als PDF-scans in een gedeelde mailbox.

De planner-rooster is een zelfgebouwde Outlook + Exchange 2016-opstelling. Elke truck is een agenda; elke dienst is een afspraak; de dispatcher sleept afspraken tussen agenda's om werk opnieuw toe te wijzen. Het is het soort systeem dat je in 2014 in een weekend bouwt en twaalf jaar laat draaien omdat het werkt. We gingen het niet vervangen. Het doel van het project was om het te lezen.

De agent moest dus in dat gat leven. Inputs: de gedeelde Exchange-mailbox, de Transics REST-feed, en de calendar API. Outputs: een Slack-achtige escalatiequeue voor de wagenparkbeheerder, een stille "deze CMR is verwerkt en gematcht"-log voor het kantoor, en een gestructureerde rij in een Postgres-ledger voor de ILT-auditor die zes maanden later langskomt.

De vorm van de agent

We bouwden niet één grote agent. We bouwden er vier kleine en een router.

  • Inbox-reader polt Exchange elke 30 seconden, haalt nieuwe berichten op, draait OCR over PDF-bijlagen en geeft een genormaliseerde JSON-envelope door aan de router.
  • Classifier bepaalt wat het bericht is: routinematige CMR-scan, planningsvraag, pech onderweg, ADR-relevante melding. Twee fases: eerst een deterministische trefwoordzeef, dan de LLM.
  • Transics-correlator neemt een trucknummer en een timestamp, haalt de laatst bekende positie, de huidige chauffeur en de actieve trip uit de Transics API.
  • Escalator pakt alles wat de classifier markeert als ADR-3-lekkage, opent een ticket in een aparte queue met een SLA van 45 seconden en triggert een pagercall naar wie er dienst heeft.

De router is vijftig regels Python. Het is geen LLM. We hebben pijnlijk geleerd, op een eerder project, dat "laat het model beslissen welke tool het aanroept" een mooie demo is en een fragiel productiepatroon. Het principe dat overleeft in productie is hetzelfde principe waar ervaren bouwers steeds opnieuw op uitkomen: deterministische randen, generatieve knopen. Gebruik het model waar de ambiguïteit echt is. Gebruik code waar dat niet zo is.

ADR-3 uit de ruis filteren

Ongeveer één op de vierhonderd inkomende berichten blijkt in een gemiddelde week ADR-relevant. Daarvan is misschien één op twintig een klasse-3-lek in plaats van een papiervraag. We zoeken dus ruwweg één naald per week in een hooiberg van 3.200 items.

De goedkope zeef draait eerst. Het is een Nederlandse trefwoordclassifier die niet slim hoeft te zijn, alleen paranoïde.

# adr_triage.py - first-pass before the LLM
ADR3_SUBSTANCES = {
    "ethanol", "benzine", "diesel", "aceton", "wasbenzine",
    "spiritus", "thinner", "white spirit", "petroleum",
    "UN1170", "UN1202", "UN1090", "UN1219", "UN1263",
}
LEAK_VERBS = {"lekt", "lekken", "lekkage", "druipt", "drupt",
              "loopt leeg", "spuit", "drip", "leak", "leaking"}

def looks_like_adr3_leak(text: str) -> bool:
    t = text.lower()
    substance = any(s in t for s in ADR3_SUBSTANCES)
    leak = any(v in t for v in LEAK_VERBS)
    return substance and leak

Alles wat door de zeef glipt gaat naar de LLM, met de volledige berichtinhoud, de OCR-uitvoer van de bijlage, de recente berichten van de chauffeur en de laatste GPS-ping van de truck. Het model retourneert een strikt JSON-object: { adr_class, event_type, substance, confidence, recommended_action }. We forceren een confidence onder 0,7 om sowieso te escaleren, vanuit de aanname dat te weinig escaleren de faalmodus is die carrières beëindigt.

Waarschuwing

Laat het model de drempel niet bepalen. De drempel is een regelgevende vraag. Zet hem in code, achter een review, en zet op elke wijziging een datum. De ILT-auditor zal ernaar vragen.

De escalatiequeue van 45 seconden

Vijfenveertig seconden is geen latency-target voor het model. Het is het wall-clock-budget van "Exchange laat de nieuwe mail zien" tot "de telefoon van de wagenparkbeheerder gaat over". In dat budget zitten de polling-interval, de OCR-stap, twee classifier-calls, de Transics-lookup, het wegschrijven van het ticket en de eigen verzending van PagerDuty.

De routing is bewust saai:

# routes.yaml
queues:
  wagenpark_urgent:
    sla_seconds: 45
    pager: pagerduty:wagenparkbeheerder
    requires_ack: true
    fallback_after_seconds: 120
    fallback_to: pagerduty:operations_lead
  planning_routine:
    sla_seconds: 900
    pager: null

rules:
  - when:
      classifier.adr_class: 3
      classifier.event: leak
    then:
      enqueue: wagenpark_urgent
      with_context:
        - transics.last_position
        - transics.current_driver
        - cmr.shipper
        - cmr.consignee
        - attachments.ocr_text
  - when:
      classifier.event: cmr_scan
    then:
      enqueue: planning_routine

De regel requires_ack: true is de regel die er toe doet. Als de wagenparkbeheerder niet binnen 120 seconden op "acknowledged" tikt, escaleert het ticket naar de operations lead en daarna naar de externe veiligheidsadviseur van het bedrijf. Het systeem gaat ervan uit dat de dienstdoende persoon dood is, slaapt of zelf aan het rijden is. Het kiest standaard voor lawaai.

Drie dingen die kapot gingen in week drie

Op volgorde van ernst.

Eerst rouleerde de Transics API-key. Niemand had ons iets verteld. Transics roteert keys volgens een schema dat leeft in een PDF in de inbox van een sales engineer in Ieper. De correlator begon stilletjes te falen omdat we de errors hadden verpakt in een try/except die een warning logde en doorging. De escalator vuurde nog steeds, maar zonder de GPS-context moest de wagenparkbeheerder de chauffeur bellen om te vragen waar hij was. We hebben het opgelost met een health check die Transics elke vijf minuten pingt en piept als de call tien minuten faalt.

Daarna zat de Exchange-mailbox vol. Exchange 2016 on-prem heeft een mailboxquota die de IT-contractor in 2017 op 50 GB had gezet. Nu de OCR-pipeline elke bijlage trok, zat de mailbox op dinsdagen tegen de cap aan en weigerde inkomende mail. De fix was de gedeelde mailbox te verplaatsen naar een archief van 200 GB en de verwerkte bijlagen te tieren naar S3 met een lokale cache van dertig dagen.

Het derde, en gênantste, geval: de LLM hallucineerde een UN-nummer. Een chauffeur schreef "klein lek bij de IBC, denk dat het schoonmaakmiddel is". Het model retourneerde UN1170 ethanol, confidence 0.62. De stof was in werkelijkheid een niet-ADR alkalisch reinigingsmiddel. De escalator vuurde correct, de wagenparkbeheerder bereikte de chauffeur in zeventig seconden, geen echte schade. Maar het was een herinnering. We voegden een regel toe: als de stof niet visueel identificeerbaar is op de CMR of in een fotobijlage, moet het model substance: "unknown" teruggeven en het door een mens laten classificeren.

Cijfers na zes weken

Over de eerste zes productieweken verwerkten we 19.114 inkomende berichten. De classifier flagde 47 ADR-relevante items en escaleerde er 6 als kandidaat voor klasse-3-lekkage. Vier waren echte lekken (twee klein, twee meldplichtig bij de ILT). Twee waren false positives: één gebarsten ruitensproeierfles, één chauffeur die een eerder incident in de verleden tijd beschreef. Mediaan tijd van inkomende mail tot acknowledgement van de wagenparkbeheerder op de echte gevallen: 38 seconden. De langste was 71 seconden, op een zondag om 03:14, toen de pager moest doorvallen naar de operations lead.

De saaie metric telt zwaarder. De CMR-scan-afstemming die de monteur op vrijdag vier uur kostte, kost hem nu zeventien minuten. De agent matcht de scan met de planner-rooster-entry en de Transics-trip, en laat alleen de gevallen zien die niet netjes matchen. De gedeelde mailbox op dinsdag om 09:18 telt nu 23 ongelezen items, geen 614.

Identity en audit, kort

Het audit-verhaal weegt net zo zwaar als het escalatie-verhaal. Voor een Nederlandse transportklant die een schoon spoor moet kunnen laten zien aan de ILT moet elke modelcall gekoppeld zijn aan een eigenaar met naam, en elke externe API-call gebruikmaken van een kortlopende, scoped credential in plaats van een langlevende service-key. We roteren de credentials van de Transics-correlator nu op een venster van vier uur. Was dat patroon er vanaf het begin geweest, dan was de stille key-rotatie in week drie binnen uren als credential-uitgiftefout opgekomen, niet als trage degradatie die we dagen later opmerkten.

Kernpunt

Bouw agents rond de klok van de toezichthouder, niet rond het gemak van de gebruiker. Alles in de architectuur volgt uit dat ene getal waarmee je niet kunt onderhandelen.

Het kleinste ding dat je deze week kunt kopiëren

Als je een operationele inbox draait die bepaalde berichten wettelijk binnen een venster moet escaleren, doe dit op maandag. Schrijf een lijst met twee kolommen. Linkerkolom: de berichttypes die moeten escaleren. Rechterkolom: het werkelijke wall-clock-budget per type. Als de rechterkolom een getal bevat dat je nu meer dan één keer per kwartaal mist, heb je een process-automation-probleem dat zichzelf terugverdient. De rest is leidingwerk.

Toen we deze AI-agent bouwden voor het Amersfoortse transportbedrijf, liepen we tegen het feit aan dat de klok van de toezichthouder en de confidence-drempel van het model dezelfde soort variabele zijn, en dat ze allebei thuishoren in versie-beheerde code en niet in een prompt. We hebben uiteindelijk elke drempel in een klein YAML-bestand gezet dat de veiligheidsadviseur elk kwartaal beoordeelt en aftekent.

Kern

Bouw de agent rond de klok van de toezichthouder, niet rond het gemak van de gebruiker. Het ILT-meldvenster is de ene variabele waarmee je niet kunt onderhandelen.

FAQ

Waarom een SLA van 45 seconden en niet 30 of 60?

Vijfenveertig seconden is het wall-clock-budget waar polling, OCR, twee classifier-calls, de Transics-lookup en een PagerDuty-verzending met marge in passen. Strakker dwingt je de tweede LLM-check te skippen; ruimer begint routine te voelen voor de dienstdoende operator.

Dient de agent de ILT-melding automatisch in?

Nee. De agent escaleert, pakt de context samen en stelt een concept-melding op. Een persoon met naam binnen het bedrijf dient hem in. Wettelijke meldingen horen niet bij de verantwoordelijkheid van het model, en de ILT wil een ondertekende menselijke verantwoordelijke.

Waarom vervangen jullie de Exchange 2016 planner-rooster niet?

Omdat het werkt, het team het kent, en het vervangen van een twaalf jaar oud planningssysteem is een apart project van twaalf maanden. De agent leest in plaats daarvan de agenda's. Migraties verdienen hun kosten terug op het moment dat het oude systeem de verandering blokkeert, niet eerder.

Wat voorkomt dat de LLM opnieuw een UN-nummer hallucineert?

Als de stof niet zichtbaar is op de CMR of in een fotobijlage, moet het model substance unknown teruggeven en het item doorzetten naar een menselijke classifier. We behandelen gehallucineerde zekerheid als faalmodus en ontwerpen de prompt om standaard naar onwetendheid terug te vallen.

process automationai agentsintegrationsworkflowcase studyoperations

Iets bouwen?

Start een project