← Blog

Email automation

E-mailautomatisering case: van 11 dagen naar 36 uur intake

Een intake-paralegal bij een Utrechts advocatenkantoor van 41 mensen had 137 ongelezen berichten en een partner die vroeg naar een contract van negen dagen terug. Zo hebben we het opgelost.

Jacob Molkenboer· Oprichter · A Brand New Company· 6 jun 2026· 9 min
Crèmekleurige envelop met limoengroen lint, messing paperclip, ivoren kaart, halfopen leren map op botpapier.

Het was een donderdagmiddag eind februari. De intake-paralegal bij een Utrechts advocatenkantoor van 41 mensen had 137 ongelezen berichten in de gedeelde mailbox en een partner op Teams die vroeg naar een contract dat zij negen dagen geleden had ingestuurd. Het contract zat in de wachtrij. Het was gelezen, geclassificeerd, conflict-gecheckt. Het was alleen nog niet naar een partner gerouteerd. De mailbox was het knelpunt, en iedereen op kantoor wist dat.

De wachtrij van 11 dagen

Het kantoor is gespecialiseerd in commerciële geschillen en mid-market M&A. Elke contractintake van een nieuwe prospect landt in één gedeelde mailbox op intake@[kantoor].nl. Vanaf daar leest één paralegal het bericht, opent de bijlage, draait een conflict-check tegen de matter-database van het kantoor, beslist bij welke practice group het hoort, schrijft een samenvatting van één alinea en stuurt het door naar de juiste partner.

Toen we de wachtrij in januari 2026 maten, was de mediane tijd van 'mail komt binnen' tot 'partner heeft het op zijn bureau' 11,2 dagen. Het 95e percentiel zat op 19 dagen. Twee van de grootste deals van het kantoor in de afgelopen twaalf maanden hingen aan een zijden draad in die wachtrij. Eén sneuvelde.

De oorzaak was niet de paralegal. Ze was uitstekend. De oorzaak was dat één persoon zeven stappen uitvoerde namens eenenveertig mensen, en die zeven stappen hadden geen gedeelde state.

Waarom we geen SaaS hebben gekocht

Het kantoor had in 2025 al twee kant-en-klare legal intake producten getest. Beide werden door de partners afgewezen om dezelfde drie redenen.

De classifier van het eerste product was een black box. Partners konden niet zien waarom een contract was gelabeld als 'vastgoed' in plaats van 'corporate'. Als de classifier het bij het verkeerde eind had, kon niemand op kantoor het inspecteren of corrigeren.

De audit trail van het tweede product leefde in de database van de leverancier. De compliance officer van het kantoor wilde elke routing-beslissing in hun eigen systemen, op hun eigen back-ups, queryable met hun eigen SQL. 'In iemand anders zijn cloud' was geen optie.

Beide producten dwongen de prospect ook om een intake-wizard in te vullen voordat een mens op kantoor reageerde. Prospects haatten die wizards. Het kantoor verloor minstens één engagement aan een concurrent die simpelweg een e-mailadres publiceerde. Wij werden binnengehaald om dezelfde workflow opnieuw te bouwen binnen de eigen Microsoft 365-tenant van het kantoor, met hun eigen Postgres database, en met de prospect die nog steeds een normaal adres mailt.

De Microsoft Graph subscription

Het kantoor draait op Microsoft 365. De eerste technische beslissing was: pollen op de mailbox, of subscriben?

Pollen is het voor de hand liggende. Draai elke minuut een job, list berichten sinds de laatste cursor, verwerk ze. Het werkt. Het is ook verspillend, en je krijgt er een ondergrens van één minuut op intake-latency mee.

Microsoft Graph ondersteunt webhook-achtige change notifications op een mailbox. Je registreert een subscription, Graph post naar jouw endpoint zodra er een nieuw bericht binnenkomt, en jij haalt het bericht op via ID. End-to-end latency in productie ligt rond de twee seconden. De Microsoft Graph change notifications documentatie beschrijft de validatie-handshake. De meest voorkomende reden dat teams Graph webhooks opgeven is dat ze niet binnen tien seconden reageren op de eerste validation token. Maak dat endpoint dus synchroon en snel.

import { Client } from '@microsoft/microsoft-graph-client'

const graph = Client.initWithMiddleware({ authProvider })

const subscription = await graph.api('/subscriptions').post({
  changeType: 'created',
  notificationUrl: 'https://intake.firm.nl/graph/webhook',
  lifecycleNotificationUrl: 'https://intake.firm.nl/graph/lifecycle',
  resource: "users/intake@firm.nl/mailFolders('Inbox')/messages",
  expirationDateTime: new Date(Date.now() + 1000 * 60 * 60 * 24 * 2).toISOString(),
  clientState: process.env.GRAPH_CLIENT_STATE,
})

Mailbox-subscriptions lopen na ongeveer drie dagen af, dus we vernieuwen op een cron van twaalf uur. De clientState is een gedeeld geheim dat we bij elke inkomende webhook verifiëren, want de notification URL is bereikbaar vanaf het publieke internet.

De classifier en de confidence-ondergrens

Zodra er een bericht binnenkomt, halen we het door een classifier die drie labels produceert met een confidence-score: een practice group (corporate, vastgoed, arbeidsrecht, litigation, IP, overig), een client type (nieuwe prospect, bestaande klant, wederpartij, ruis), en een urgentieniveau (standaard, tijdsgevoelig, na kantooruren).

De classifier is een klein language model, fine-tuned op achttien maanden historische intake-mail van het kantoor, met de daadwerkelijke routing-beslissingen van de partners als labels. We hebben geen generiek chatmodel in de loop gezet. Overkill voor de taak, langzamer per call, en de partners vertrouwden het niet. Een fine-tuned classifier op gelabelde in-domain data is het saaie, juiste antwoord.

De interessante beslissing was de confidence-drempel. Het model heeft het in 94% van de gevallen goed op practice group. Als je elk bericht automatisch routeert, krijg je één foute routing per twintig contracten. Onaanvaardbaar in een litigation-praktijk, waar een verkeerd gerouteerd contract een conflict-check kan opblazen voordat het kantoor weet dat de zaak bestaat.

Dus de classifier routeert niet. Hij stelt voor. Elke classificatie met een confidence onder de 0,85 landt op het review board. Op een normale dag is dat ongeveer 30% van de intake. De paralegal accepteert of corrigeert in één klik. De correcties voeden de volgende fine-tune.

Takeaway

De classifier beslist nooit. Hij stelt voor. Een mens accepteert de routing, en de audit trail legt vast wie wat wanneer accepteerde.

Het Postgres review board

Het review board is één Postgres-tabel en een kleine Next.js pagina. Dat is het hele ding.

create table intake_review (
  id                      uuid primary key default gen_random_uuid(),
  graph_message_id        text not null unique,
  received_at             timestamptz not null,
  from_address            text not null,
  subject                 text not null,
  body_preview            text not null,
  attachment_count        int  not null default 0,
  proposed_practice_group text not null,
  proposed_client_type    text not null,
  proposed_urgency        text not null,
  confidence              numeric(4,3) not null,
  status                  text not null default 'pending'
    check (status in ('pending', 'accepted', 'corrected', 'rejected')),
  final_practice_group    text,
  final_partner_id        uuid references partners(id),
  reviewed_by             uuid references staff(id),
  reviewed_at             timestamptz,
  routed_at               timestamptz
);

create index intake_review_pending_idx
  on intake_review (received_at)
  where status = 'pending';

Elke classificatie-suggestie wordt een rij. De partial index op pending-rijen houdt de board-pagina snel, ook wanneer de tabel honderdduizend historie-rijen telt.

Postgres draait ook de realtime layer. Zodra een rij op pending komt, vuurt een trigger NOTIFY intake_review_new. De Next.js pagina houdt een open LISTEN-connectie aan en pusht de nieuwe rij via een websocket. Het hele 'realtime dashboard' is ongeveer veertig regels code. We waren in de verleiding om Redis of een queue toe te voegen. Dat hoefde niet. De Postgres LISTEN/NOTIFY primitive doet dit werk sinds 2001 en doet het nog steeds prima.

De audit trail die de compliance officer vroeg

Elke state-transitie op een rij schrijft een record naar intake_review_audit: de message ID, de vorige state, de nieuwe state, de actor, de reden. Elke classifier-suggestie, elke paralegal-correctie, elke partner-herschikking.

We hebben dit op de harde manier geleerd. De eerste versie van het systeem sloeg alleen de eindstaat op. Twee weken later vroeg een managing partner waarom een vastgoedcontract was gerouteerd naar litigation. We hadden geen antwoord. De classifier had het goed gehad, de paralegal had overruled, en we hadden de overrule-reden niet opgeslagen. We shipten die week de audit-tabel. De compliance officer beantwoordt nu 'waarom is dit contract bij deze partner terechtgekomen' met één SQL-query, en dat is het hele punt.

Wat er in 36 uur veranderde

Na acht weken in productie zakte de mediane tijd van 'mail komt binnen' tot 'partner heeft het op zijn bureau' van 11,2 dagen naar 36 uur. Het 95e percentiel zakte van 19 dagen naar 4 dagen. Paralegal-uren aan intake-triage gingen van ongeveer 22 uur per week naar 6. Verkeerd gerouteerde contracten gingen van 4 in het vorige kwartaal naar 1 in de acht weken sinds de launch, en die ene was een edge case die de classifier nog nooit in training had gezien.

De mediaan van 36 uur is geen technische limiet. Het systeem zelf routeert in seconden. De 36 uur is het kantoor dat besluit dat elke classificatie tijdens kantooruren door een mens gereviewed wordt, ongeacht confidence. Ze zouden de mediaan naar twee uur kunnen drukken door intake met hoge confidence buiten kantooruren automatisch te routeren. Ze kozen ervoor dat niet te doen. Advocaten hebben, voorspelbaar, liever een mens in de loop.

Die voorkeur is niet alleen advocatenbrein. Mensen die voor hun beroep systemen bouwen, wantrouwen meestal systemen die ze niet kunnen inspecteren. De classifier die dit kantoor vertrouwt, is degene waarvan ze de beslissingen kunnen reviewen, overrulen en in SQL kunnen queryen. Dat is ook de classifier die de partnervergadering en de compliance-review overleefde.

Drie dingen die we anders zouden doen

Begin met het review board, niet met de classifier

We staken de eerste twee weken in het tunen van de classifier en bouwden het review board als laatste. Dat was andersom. Het review board was nuttig geweest zelfs met een classifier van nul procent, want de paralegal triagede al met de hand. We hadden het lege board in week één moeten shippen en de classifier zijn plek moeten laten verdienen tegen een werkende baseline.

Behandel de Graph subscription als een fragiele dependency

Microsoft vernieuwt subscription-tokens meestal stilletjes, en dan een keer niet. De lifecycleNotificationUrl is niet optioneel. Als Graph vertelt dat de subscription bijna verloopt of is verwijderd, moet je direct opnieuw subscriben en een mens alarmeren bij een mislukking. We hadden één zaterdag in maart waarop we elf uur aan intake misten omdat we het lifecycle-endpoint hadden behandeld als nice-to-have. De fix is vijftien regels. Schrijf ze op dag één.

Stop de prospect nooit in een formulier

De grootste niet-technische winst voor het kantoor was dat er voor de prospect niets veranderde. Ze sturen nog steeds een mail naar een leesbaar adres. Ze krijgen nog steeds binnen een paar uur antwoord van een echte partner. De classifier en het board zijn voor hen onzichtbaar. Elke legal intake SaaS die we bekeken probeerde een formulier tussen de prospect en het kantoor te schuiven. Dat is de verkeerde plek voor frictie. Zet de frictie in de workflow van je eigen team, waar je erop kunt itereren, niet voor de neus van de klant.

Het vijf-minuten-ding dat je vandaag kunt doen

Toen we de intake-agent voor dit kantoor bouwden, liepen we steeds aan tegen het gat tussen wat een classifier alleen kon en wat een partner zou accepteren. We losten het uiteindelijk op met een confidence-ondergrens, een Postgres review board en een audit-tabel die de mens centraal stelde en elke beslissing queryable maakte. Zit jouw team in een vergelijkbare wachtrij, dan is het kleinste nuttige ding dat je vandaag kunt doen: de gedeelde mailbox openen, de berichten ouder dan 72 uur tellen, en opschrijven waar elk bericht op wacht. Die spreadsheet is de spec voor de AI-agent die je uiteindelijk bouwt.

Kern

De classifier beslist nooit. Hij stelt voor. Een mens accepteert de routing, en de audit trail legt vast wie wat wanneer accepteerde.

FAQ

Waarom Microsoft Graph en niet IMAP?

Graph biedt webhook-achtige change notifications met ongeveer twee seconden latency, dus je hoeft niet te pollen. Het sluit ook aan op het bestaande Microsoft 365 admin- en consent-model van het kantoor, wat het credentialbeheer van IMAP niet doet.

Waarom Postgres en geen queue of pub/sub broker?

Het review board is fundamenteel state, geen events. Postgres LISTEN/NOTIFY geeft realtime updates zonder aparte broker, en dezelfde database houdt de audit trail die de compliance officer wilde.

Hoe hebben jullie de audit trail gebouwd?

Elke state-transitie schrijft een rij naar een aparte audit-tabel met de message ID, de vorige state, de nieuwe state, de actor, de timestamp en de reden. De compliance officer queryt het direct met SQL zodra een routing-beslissing wordt aangevochten.

Kan de classifier automatisch routeren boven een confidence-drempel?

Technisch wel. Het kantoor koos ervoor dat niet te doen. Elke classificatie wordt tijdens kantooruren door een mens gereviewed, en daarom is de mediaan 36 uur in plaats van minuten. Ze hebben liever een iets langzamer systeem dat ze volledig kunnen auditen.

email automationai agentscase studyintegrationsworkflowprocess automation

Iets bouwen?

Start een project