← Blog

Process automation

Procesautomatisering-incident: 940 aangiften, oude TARIC

De aangifte-queue stond op groen. Toch belde Douane om 11:47. Drieënveertig aangiften van die ochtend waren goedgekeurd met een tariefcode die de dag ervoor was vervallen.

Jacob Molkenboer· Oprichter · A Brand New Company· 20 jun 2026· 11 min
Open douaneregister, koperen stempel met rood inktkussen, doorslagformulieren onder presse-papier, groen briefje, stilstaande klok.

Wat er stuk ging

11:47 op donderdag 2 april. De operations manager van een haven-expediteur met 28 mensen in de Waalhaven krijgt een telefoontje van Douane. Drieënveertig aangiften van die ochtend bevatten een TARIC-code die de dag ervoor is vervallen. Tegen de tijd dat het bericht ons bereikt en we gaan kijken, heeft de agent sinds middernacht 940 aangiften verstuurd. Stuk voor stuk dragen ze dezelfde oude code op minstens één regel.

De agent crashte niet. De Douane-tunnel wees niets af. De controle-queue stond op groen. De facturatie van de klant draaide op dezelfde code, dus de interne reconciliatie zag er ook prima uit. Het hele punt van een autonome procesautomatisering is dat het systeem je waarschuwt als er iets niet klopt, en het systeem waarschuwde niemand. Wat we hier hadden, was een pipeline die stil-correct werkte en 's nachts stil-fout was gegaan.

Tussen 11:47 en 12:30 trokken wij en de douaneagent van de klant de bestandslijst van die ochtend op, vergeleken twaalf aangiften met de actuele TARIC en bevestigden dezelfde versie-skew op elke geraakte regel. Om 13:00 hadden we een telling: 940 aangiften verstuurd, 412 daarvan met minstens één regel op een TARIC-code die om middernacht was ingetrokken. Geen van die 412 had een exceptie veroorzaakt. De structurele validator van de agent had ze allemaal goedgekeurd, de Descartes-lookup had teruggegeven wat hij als geldige measure beschouwde, en de Douane-tunnel had het groene acknowledgement teruggestuurd. Drie verschillende systemen waren het erover eens dat de ochtend normaal was.

De opzet

De agent doet wat de meeste back-offices van een haven-expediteur doen, alleen zonder de mensen. Een boeking komt binnen via EDI of e-mail. De agent haalt de goederencode, het gewicht, het land van oorsprong, de partij-waarde en de procedurele details eruit, valideert ze tegen de actuele TARIC-measure voor die classificatie, vult de AGS-aangifte in en duwt 'm door de Douane-tunnel via de Descartes-integratie van de klant. De douaneagent beoordeelt de excepties; al het andere stroomt door.

Die validatiestap is bedoeld om een typfout in een code te vangen, een verwisseld cijfer, een classificatie die niet meer past bij de goederen. Hij is niet ontworpen voor het geval dat de code-lookup zelf uit een versie van de wereld leest die gisteren is geëindigd. De agent heeft geen manier om te zien dat de measure die hij kreeg de juiste vorm heeft voor het verkeerde moment in de tijd. Vanuit de request-loop ziet een oude cache er identiek uit aan een actuele cache.

De TARIC-lookups gaan via Descartes. Descartes houdt een lokale mirror van de EU-TARIC-database bij, die actueel wordt gehouden tegen de dagelijkse update van de Commissie. Die mirror is een van de redenen dat je voor Descartes betaalt. Het hele idee is dat je de cache vertrouwt.

Dat vertrouwen is de bug.

Het publicatievenster van 1 april

De TARIC is het geïntegreerde douanetarief van de EU. De Commissie publiceert dagelijkse delta-bestanden en een master-versie die aan het begin van elk kwartaal kantelt. 1 april is een van de vier harde cutovers in het jaar. Codes worden ingetrokken, nieuwe codes ingevoerd, rechten veranderen, en een niet-triviaal aantal measures ziet zijn geldigheidsvenster eindigen om 23:59:59 op 31 maart en een opvolger-measure beginnen om 00:00:00 op 1 april.

De delta's landen op de distributieserver van de Commissie in een venster dat de leveranciers het publicatievenster noemen. In de praktijk is dat venster rekbaar. Sommige kwartalen landen ze in de kleine uurtjes van de cutover-dag, andere kwartalen pas laat in de ochtend. Leveranciers met een eigen mirror trekken op een schema en stempelen hun cache met de nieuwe versie zodra ze die hebben.

Wat Descartes op de ochtend van 1 april deed, was de gecachte Q1-2026-versie vasthouden door het publicatievenster heen, lookups daarop bedienen, en pas in de middag overschakelen naar Q2-2026. Er is een hint van te zien in de Descartes-admin-console, een klein versie-label bovenaan het TARIC-paneel, maar onze agent las dat label niet. Hij las de measure, kreeg een geldige measure terug en ging verder.

Waarom het een werkdag lang onopgemerkt bleef

Het eerste uur of zo van 1 april was dit niet te onderscheiden van een normale ochtend. De meeste codes waarop de klant aangifte doet, waren niet geraakt door de Q2-update. De aangiften met geraakte codes valideerden structureel nog steeds: de cache gaf een measure terug, de measure was intern consistent, het systeem was tevreden. De Douane-backend accepteerde de aangifte op een TARIC-code die per entry-datum juridisch niet bestond voor die classificatie.

De reden dat de Douane-backend het accepteerde, is dezelfde reden dat dit soort bug overal overleeft: beide kanten van de transactie vertrouwden dezelfde upstream. De Douane-validatieregels voor een measure volgen dezelfde TARIC-publicatiecyclus. Als de master aan de Douane-kant toevallig een paar uur achterloopt in dezelfde richting als de mirror van je leverancier, ziet de verkeerde code er aan beide kanten goed uit. Tegen de tijd dat de achterstand is ingelopen, heb je al ingediend.

De fout komt weer boven wanneer een downstream-systeem, een controleur, een audit-batch, een post-clearance-controle, dezelfde aangifte leest tegen de nu-actuele TARIC. Dat was het telefoontje om 11:47. Als je automatisering een tarief- of regelgevings-cache leest op code en nooit op versie, is een cache die stilletjes terugvalt op de werkelijkheid van gisteren onzichtbaar totdat een mens stroomafwaarts het opmerkt. De fout zit niet in het code-pad dat het liet afweten. De fout zit in de afwezigheid van een code-pad dat vergelijkt.

De dual-version diff-gate

We gingen de Descartes-cache niet weghalen. De cache bestaat om een goede reden, en het alternatief, per lookup de TARIC-consultatie van de Commissie aanroepen, heeft zijn eigen faalmodi en rate limits. Wat we nodig hadden, was een manier voor de agent om te weigeren een aangifte in te dienen wanneer de cache en de werkelijkheid het oneens waren over wat de code vandaag betekent.

De fix is onspectaculair. Voordat een aangifte de tunnel uit gaat, haalt de agent dezelfde measure op uit twee plekken: de Descartes-cache zoals voorheen, en een tweede, onafhankelijk bijgehouden TARIC-mirror met zijn eigen publicatievenster-discipline. We trekken een minimale set velden op, measure ID, duty expression, additional codes, geldigheidsbegin en -einde, en we diffen ze.

De juiste velden kiezen kostte meer tijd dan de code. De TARIC publiceert meer dan 200 attributen per measure, waarvan de meeste tussen kwartalen nooit veranderen en waarvan een aangifte de meeste niet nodig heeft. We hebben het versmald tot waar de Douane-backend de aangifte daadwerkelijk op afrekent: measure ID, duty expression in een canonieke string-vorm, additional codes, geldigheidsbegin en -einde, en de voetnootcodes die meewegen in de berekening. De rest laten we drijven. Hoe smaller de diff, hoe lager de false-positive rate, en in een systeem als dit is een false positive een geparkeerde aangifte en een telefoontje naar de douaneagent dat we ons op ruis niet kunnen veroorloven. De middag die we besteedden aan het kiezen van de velden, was de middag die bepaalde of de gate het contact met productie zou overleven.

Als de twee bronnen het eens zijn, gaat de aangifte de deur uit. Zijn ze het oneens, dan doet de agent drie dingen: hij parkeert de aangifte in een hold-queue, hij pingt de douaneagent van de klant met de diff inline, en hij stempelt het incident met de twee bron-versies zodat de operator kan zien welke kant achterloopt. De hold-queue heeft een SLA van 20 minuten. In normaal bedrijf is hij leeg.

// libs/customs/diff-gate.ts
type TaricSnapshot = {
  source: 'descartes' | 'mirror-b';
  versie: string;           // e.g. '2026Q2-04-01'
  measureId: string;
  dutyExpr: string;         // canonicalised
  additionalCodes: string[];
  validFrom: string;        // ISO
  validTo: string | null;
};

export async function gate(code: string, entryDatum: string) {
  const [a, b] = await Promise.all([
    descartes.lookup(code, entryDatum),
    mirrorB.lookup(code, entryDatum),
  ]);

  const drift = diff(a, b);
  if (drift.fields.length === 0) return { ok: true, snapshot: a };

  return {
    ok: false,
    reason: 'taric-drift',
    drift,
    sources: { a: a.versie, b: b.versie },
  };
}

De diff-functie is bewust strikt. We proberen niet te beslissen welke kant gelijk heeft. We weigeren in te dienen. Een douaneagent beslist.

Wat we niet hebben gedaan

We hebben geen TTL aan de Descartes-cache toegevoegd. De cache heeft een TTL. De TTL werd op 1 april gerespecteerd. De cache had simpelweg de nieuwe versie nog niet ontvangen. Cache-invalidatie is hier niet de bug.

We hebben geen trust-score voor tariefbronnen geschreven. We hebben er een middag over nagedacht. De vorm van het systeem beloont een hard veto: elke diff, ongeacht de omvang, blokkeert de aangifte. Een trust-weighted vote klinkt intelligent en zou, op de dag dat dit incident plaatsvond, hebben gestemd voor het indienen van de verkeerde code.

We hebben er geen ML-anomaliedetector opgeplakt. Een model dat een ongebruikelijke duty rate zou markeren, had dit niet gevangen. De duty rate was niet ongebruikelijk; hij was simpelweg toegewezen aan een code die niet meer bestond. Het signaal dat we nodig hadden, zat niet in de data die de agent had. Het moest opgehaald worden, en wel ergens vandaan waar de cache niet bij kon. De faalklasse hier is niet statistisch, hij is bibliografisch, en je vangt een verwijzingsfout niet door de verwijzingen te clusteren.

We zijn niet van leverancier gewisseld. Descartes was op geen enkele bewijsbare manier nalatig, en de tweede mirror heeft een ochtend in het verschiet waarop hij degene is die achterloopt. Het punt van de gate is dat beide fout kunnen zitten; alleen overeenkomende antwoorden gaan de deur uit.

Wat die 940 hebben gekost

Elke geraakte aangifte moest met een verbeterverzoek via het Douane-portaal worden gecorrigeerd. De douaneagent van de klant, drie controleurs en twee engineers van ons werkten er de daaropvolgende vrijdag en maandag aan. Er werden fysiek geen goederen vastgehouden. Twee zendingen moesten qua rechten worden hergecodeerd, en het verschil in rechten was in beide richtingen klein. De klant verloor geen zending. Ze verloren een weekend, het vertrouwen van een controleur voor ongeveer drie weken, en de prijs van een kleine keukenrenovatie aan engineerstijd aan beide kanten.

De echte kosten zijn de kosten die we in het post-mortem-document bewaren: een werkdag lang stempelde het systeem juridisch betekenisvolle documenten op naam van een gereguleerde entiteit met een code die niet bestond. De rechten klopten bijna. De audit trail klopte voor geen meter.

Wat we andere klanten nu vertellen

Elke regelgevings-input naar een automatiseringsagent, TARIC, REACH, EORI-register, sanctielijst, btw-nummercontrole, alles wat door een publieke autoriteit op een schema wordt gepubliceerd, krijgt een versie-veld dat in het werkgeheugen van de agent zichtbaar wordt, en een tweede onafhankelijke bron. Kun je voor een lijst geen tweede onafhankelijke bron vinden, dan is dat op zichzelf een bevinding: het vertelt je dat de agent niet autonoom op die input zou moeten indienen.

De agent hoeft de regelgeving niet te begrijpen. De agent moet met overtuiging weigeren wanneer twee upstreams het oneens zijn over wat de regelgeving vandaag zegt.

Takeaway

Verouderde regelgevings-caches falen niet luidruchtig. De cache geeft de hele dag een perfect gevormd verkeerd antwoord terug. Diff tegen een tweede bron; weiger te handelen bij een mismatch.

Een opmerking over het AI-op-school-debat

In de week van dit incident stond het bijna-verbod van de Noorse overheid op AI in het basisonderwijs op elke voorpagina. De framing in de meeste berichtgeving ging over klaslokalen. De kern is ouder dan dat: laat geen ondoorzichtige automatisering juridisch bindend werk doen zonder een tweede paar ogen dat het er gemotiveerd mee oneens mag zijn. Scholen zijn één geval. De douane-queue van een haven-expediteur is een ander. Het patroon is hetzelfde. Bouw het meningsverschil erin.

Wat je vanmiddag kunt doen

Kies één regelgevings-feed die je automatisering leest. Maak een lijst van de velden die hij gebruikt. Zoek één andere plek, een vendor-mirror, een Commissie-portaal, een open-data-dump, waar je dezelfde velden kunt ophalen. Bouw een job van vijf minuten die ze allebei trekt, de velden waar je om geeft diffed, en je mailt als ze niet overeenkomen. Je hoeft vandaag geen gate live te zetten. Je hoeft alleen te weten of jouw feed de afgelopen 90 dagen stilletjes fout is geweest.

Toen we de douane-automatiseringsagent bouwden voor de Rotterdamse haven-expediteur, was het gat waar we tegenaan liepen precies dit: een cache waarvan zowel de leverancier als de Douane-backend dacht dat hij actueel was. We hebben het opgelost door te weigeren in te dienen op elke onopgeloste diff, en we laten die gate standaard aanstaan voor elke procesautomatisering die we nu opleveren.

Kern

Als een regelgevings-cache een perfect gevormd verkeerd antwoord teruggeeft, is diffen tegen een tweede bron het enige signaal dat je hebt dat hij heeft gelogen.

FAQ

Waarom wees de Douane-backend de aangifte niet af?

TARIC-publicatie loopt aan beide kanten op hetzelfde schema. Rondom een kwartaal-cutover kunnen de achterstand van je leverancier en de achterstand van de Douane-mirror een paar uur lang dezelfde kant op wijzen. De verkeerde code ziet er aan beide kanten geldig uit.

Was Descartes hier schuldig aan?

Nee. Descartes draaide de cache binnen zijn gedocumenteerde update-venster. De bug is dat een autonome agent juridisch betekenisvolle documenten heeft ingediend tegen één bron, zonder de versie van die bron zichtbaar te maken. Dat is een integratie-ontwerpkeuze, geen leveranciersfout.

Waarom niet rechtstreeks de EU TARIC-consultatie aanroepen?

Rate limits, latency en wisselende beschikbaarheid. De publieke consultatie is ontworpen voor menselijke lookups, niet voor automatisering op transactiesnelheid. Gebruik hem als diff-bron tegenover je vendor-cache; maak hem niet je enige bron.

Hoe groot was de codewijziging voor de diff-gate?

Ongeveer 200 regels plus een adapter voor de tweede bron en een hold-queue-UI voor de douaneagent. Het werk is operationeel, niet algoritmisch. De meeste tijd ging zitten in afspraken maken over wat 'dezelfde measure' eigenlijk betekent over twee mirrors heen.

process automationai agentscase studyintegrationsoperationsworkflow

Iets bouwen?

Start een project