Process automation
EDI naar Exact Online: één Inngest workflow, drie magazijnen
Op een dinsdag in maart stonden drie orderpickers in Mechelen tweeënveertig minuten bij de magazijnprinter te wachten op één PDF. In juni duurde hetzelfde klusje achtenzeventig seconden.

Op een dinsdag in maart stonden drie orderpickers in Mechelen tweeënveertig minuten bij de magazijnprinter. De EDI-order was om 07:12 binnengekomen. Een heftruck stond stationair te draaien bij het laaddok. De trailer naar Antwerpen zou om 09:00 vertrekken, met of zonder die order erbij. Niemand bij de distributeur kon iets zinnigs doen totdat de picklijst uit de printer rolde, en die picklijst zat ergens vast tussen een EDIFACT-bestand op een SFTP drop, zes Power Automate flows, en een concept-verkooporder in Exact Online die wachtte op een statusupdate die nooit zou komen.
Dit is het verhaal van hoe een industriële coatingsdistributeur met 33 mensen die flow terugbracht tot één durable workflow, en wat ze ervoor moesten opgeven.
De veertien stappen, eerlijk opgeschreven
Toen wij binnenkwamen, kon niemand van begin tot eind uitleggen wat er met een order gebeurde. Iedereen kende zijn eigen stuk. De operations manager kende de EDI-kant. Een freelance consultant onderhield de Power Automate flows. De boekhouder hield de Exact Online integratie in de lucht met twee handmatige correcties per dag. Dus deden we eerst het saaie werk en schreven de stappen op. Het waren er veertien.
- De AS2-ontvanger schrijft het EDIFACT ORDERS bericht naar een SFTP-map.
- Een Logic App pollt de SFTP-map elke vijf minuten.
- Een kleine Azure Function parst het EDIFACT-bestand naar JSON.
- De JSON komt op een Service Bus queue terecht.
- Een Power Automate flow pakt het op en zoekt de klant op in Exact Online.
- Een tweede flow mapt GTIN naar interne SKU via een SharePoint-lijst.
- Een derde flow controleert voorraad per magazijn via de Exact Online REST API.
- Een vierde flow schrijft een concept-verkooporder weg in Exact Online.
- Een vijfde flow wacht tien minuten op een bevestiging in een Teams-kanaal dat niemand leest.
- Een zesde flow genereert de picklijst-HTML uit een Word-template in SharePoint.
- Een print-agent op een Windows VM zet de HTML om naar PDF.
- De PDF komt op de netwerkshare van elk magazijn.
- Er wordt een EDI-bevestigingsbericht opgesteld en teruggestuurd naar de AS2-partner.
- De boekhouder controleert de volgende ochtend of de order in het juiste grootboek terechtkwam.
Die tweeënveertig minuten waren niet de schuld van de EDI-leverancier. Ook niet van Exact Online. Het was de cumulatieve kost van veertien aparte systemen, elk pollend op de volgende, elk met een eigen retry-schema, elk eigenaar van een stukje state dat niemand anders kon lezen.
Waar die minuten echt zaten
Op drie plekken.
De eerste was polling. Vijf minuten hier, vijf minuten daar, drie minuten voordat de print-agent een nieuw bestand op de netwerkshare opmerkte. We hebben de mediane order opgemeten en ongeveer eenentwintig van de tweeënveertig minuten waren wachttijd op de volgende tik van iets.
De tweede was de Exact Online API. De tenant van de klant zat op een rate limit van zestig requests per minuut, en de voorraadcheck liep sequentieel over de orderregels. Een order met twintig regels van een verfhandel betekende twintig round trips, en op een drukke ochtend liep de bucket leeg en deed de integratie negentig seconden niets voor hij verder ging. Dat kostte op slechte dagen nog eens tien minuten.
De derde was een human-in-the-loop stap die niet menselijk was en ook niet in de loop. Stap 9 was oorspronkelijk een echte goedkeuring. Daarna werd het een audit log. Daarna werd het een timeout omdat niemand iets goedkeurde. De flow wachtte nog steeds tien minuten op een bevestiging die nooit kwam, en ging dan door. Dat was de makkelijkste minuut om terug te winnen.
De meeste "trage" automatisering is geen trage code. Het is snelle code die wacht op het volgende polling-interval, het volgende rate-limit window, of een handmatige goedkeuring die niemand daadwerkelijk doet.
Eén workflow in plaats van veertien
We hebben het hele orderpad herschreven als één Inngest functie. Inngest is een durable workflow engine die als gewone Node service draait en de state van elke step buiten je code opslaat. Een functie kan dus halverwege crashen en bij de volgende deploy verder zonder context te verliezen. Voor een EDI-flow die een ERP raakt, telt durability zwaarder dan ruwe doorvoer. Een order kwijtraken is een echte klant aan de telefoon.
De architectonische omslag was deze. Elke stap die eerder een eigen gepollde flow was, werd een step.run binnen één functie. Waar we echt parallellisme nodig hadden (drie magazijnen, picklijsten parallel genereren), stuurden we events en lieten we aparte functies het werk doen. Waar we retries nodig hadden, regelde Inngest dat. Waar we observability nodig hadden, kregen we het gratis, want elke step wordt gelogd met input, output, en timing.
Babelway bleef voor het EDI-transport. Exact Online bleef het grootboek. Iedereen in het magazijn en op kantoor bleef hetzelfde werk doen, alleen met scherper gereedschap.
De order workflow, in code
import { inngest } from "./client";
export const processOrder = inngest.createFunction(
{ id: "edi-order-to-exact", retries: 5 },
{ event: "edi/orders.received" },
async ({ event, step }) => {
const order = await step.run("parse-edifact", async () => {
return parseEdifact(event.data.raw);
});
const dedupeKey = `${order.buyerVat}:${order.controlNumber}`;
const customer = await step.run("resolve-customer", async () => {
return exactOnline.contacts.lookup({ vat: order.buyerVat });
});
const allocations = await step.run("allocate-stock", async () => {
return allocateAcrossWarehouses(order.lines, ["MEC", "ANT", "GHE"]);
});
await step.run("create-sales-order", async () => {
return exactOnline.salesOrders.create(
{ contact: customer.id, lines: order.lines, allocation: allocations },
{ idempotencyKey: dedupeKey }
);
});
await Promise.all(
allocations.map((a) =>
step.sendEvent(`fanout-${a.warehouse}`, {
name: "picking/list.requested",
data: { warehouse: a.warehouse, lines: a.lines, orderId: order.id },
})
)
);
return { orderId: order.id, lines: order.lines.length };
}
);
De picklijst-fanout
export const generatePickingList = inngest.createFunction(
{
id: "picking-list-pdf",
concurrency: { limit: 3, key: "event.data.warehouse" },
retries: 4,
},
{ event: "picking/list.requested" },
async ({ event, step, runId }) => {
const sequence = await step.run("reserve-sequence", async () => {
return reservePickingSequence(event.data.warehouse);
});
const pdf = await step.run("render-pdf", async () => {
return renderPickingPdf({
warehouse: event.data.warehouse,
sequence,
lines: event.data.lines,
orderId: event.data.orderId,
jobId: runId, // deterministic across retries
});
});
await step.run("send-to-printer", async () => {
return printQueue.push(event.data.warehouse, pdf);
});
await step.run("ack-to-edi", async () => {
return edi.sendOrderResponse({
orderId: event.data.orderId,
status: "ACCEPTED",
});
});
}
);
Drie details die het noemen waard zijn. Ten eerste: de concurrency key is de magazijncode. We staan maximaal drie gelijktijdige renders per magazijn toe, omdat de print-agent op elke netwerkshare single-threaded is en de PDF-library niet snel. Ten tweede: de sequence-reservering draait als eigen step. Als de PDF-rendering bij een retry faalt, blijft de sequence behouden en dringt de order niet voor in de wachtrij. Ten derde: de EDI-bevestiging is de laatste step. Als er eerder iets misgaat, krijgt de partner geen valse OK.
Wat er in productie kapotging
Drie dingen, voorspelbaar.
Het eerste was idempotentie. Exact Online accepteert dubbele verkooporders zonder waarschuwing. Het eerste weekend na de cutover veroorzaakte een korte netwerkstoring een step-retry die twee identieke orders in hun grootboek aanmaakte. We losten het op door het BTW-nummer van de koper en het EDIFACT controlenummer te hashen tot een deduplication key op de create call. De Exact Online REST API ondersteunt een idempotency header op de meeste write-endpoints, maar alleen als support het op jouw tenant aanzet. We hebben het gevraagd. Ze deden het.
Het tweede was tijd. EDIFACT-tijdstempels zijn ambigu over de tijdzone, tenzij de partner een profiel afspreekt. Twee van de drie partners van de klant sturen tijdstempels in lokale Brusselse tijd. Eén stuurt UTC. Wij hadden overal UTC aangenomen. Een week lang werden picklijsten van een Poolse leverancier met één uur verschil afgedrukt, wat uitmaakte voor de ploegplanning in het magazijn. De fix was een per-partner config in de parse-edifact step.
Het derde was een verrassing. Picklijst-PDF's waren niet deterministisch. De renderer bouwde een job ID en een generation timestamp in het bestand, waardoor dezelfde input bij een retry een andere output opleverde, waardoor de print queue elke retry als nieuwe job behandelde. We maakten de PDF deterministisch door de run ID van de Inngest step als ingebakken job ID mee te geven in plaats van een nieuwe te genereren. Dat is de regel die je in het code-fragment hierboven ziet.
Drie maanden later
Een picklijst aanmaken duurt nu mediaan achtenzeventig seconden end-to-end over de drie magazijnen, vanaf het moment dat een EDIFACT op de SFTP landt tot de PDF in de printerlade. De traagste order van de afgelopen zestig dagen was drie minuten en elf seconden, en dat was een order van achtenzestig regels van hun grootste klant, met een voorraadcheck die de API calls echt nodig had.
De trailer naar Antwerpen heeft sinds de herschrijving niet één keer de vertrektijd van 09:00 gemist.
De bezetting is gelijk gebleven. De boekhouder blijft eigenaar van de relatie met Exact Online. De operations manager blijft eigenaar van de EDI-partners. De freelance consultant die de Power Automate flows onderhield, schrijft nu met ons de volgende reeks Inngest functies. Dat soort herverdeling van aandacht is het verschil tussen automatisering die een team helpt en automatisering die een team uitholt. De orderpickers kregen hun ochtendritueel terug als een echt uur. Dat besteden ze nu aan tellingen waar niemand tijd voor had. Die tellingen brachten een voorraadverschil aan het licht dat het hele project in het eerste kwartaal dubbel terugverdiende.
Wat we niet hebben gedaan
We hebben Exact Online niet vervangen. We hebben Babelway niet vervangen. We hebben geen nieuw ERP of een nieuwe EDI-leverancier binnengehaald. De verleiding bij zo'n herschrijving is om elke beslissing van het team van de afgelopen tien jaar opnieuw ter discussie te stellen. Doe het niet. De flow met veertien stappen was traag omdat er veertien stappen waren, niet omdat één van de tools fout zat. De juiste scope was de orchestratie, niet de stack.
We hebben ook geen AI-agent in het orderpad gezet. Er is een plek voor agents in operations, en we leveren er veel, maar een deterministische EDI-naar-ERP flow is daar de verkeerde plek voor. De orderdata is gestructureerd, de regels zijn stabiel, en de faalmodi zijn auditbaar. Een workflow engine met retries is het juiste gereedschap. Een language model in het midden is een risico dat je niet nodig hebt.
Als jouw flow er ongeveer zo uitziet
Toen we deze procesautomatisering bouwden voor de distributeur in Mechelen, was het zwaarste werk niet het EDI-parsen of de Exact Online integratie. Het was veertien aparte systemen lang genoeg uit de weg krijgen om te zien wat de workflow eigenlijk was.
Als jouw orderflow langer duurt dan zou moeten, is het kleinste dat je vandaag kunt doen het saaie ding dat wij als eerste deden. Open een gedeeld document, schrijf elke stap op vanaf het moment dat een order je systemen binnenkomt tot het moment dat een bevestiging vertrekt, en klok elke stap op basis van een echte order van vorige week. Het aantal stappen en het totaal aan minuten zijn meestal een verrassing voor degene die de flow beheert. Dat document is de briefing voor alles wat daarna komt.
Kern
Veertien systemen die elkaar pollen leverden tweeënveertig minuten latency op. Eén durable workflow met goede fan-out leverde achtenzeventig seconden op.
FAQ
Waarom Inngest en niet Temporal of AWS Step Functions?
Inngest draait als Node service in je eigen deploy, heeft TypeScript-ergonomie die een klein team in zijn hoofd kan houden, en bewaart step-state buiten je code zodat retries een redeploy overleven. Temporal en Step Functions werken prima; de stack van dit team paste bij Inngest.
Moest de klant Exact Online verlaten?
Nee. Exact Online bleef het officiële grootboek. De herschrijving veranderde alleen de orchestratie eromheen. De boekhoudworkflow, de BTW-logica en bestaande rapportages bleven onaangeroerd.
Hoe lang duurde de herschrijving van kickoff tot go-live?
Zes weken. Twee weken om de bestaande flow in kaart te brengen en testcases te schrijven tegen de orders van vorig kwartaal, drie weken om de Inngest functies parallel te bouwen en te testen, één week cutover met beide flows naast elkaar.
Wat is er gebeurd met de mensen die de oude flow onderhielden?
Niets. De bezetting bleef gelijk. De freelance consultant die de Power Automate flows beheerde, schrijft nu Inngest functies. De boekhouder blijft eigenaar van de relatie met Exact Online. Het magazijnteam kreeg een uur terug dat ze nu besteden aan tellingen.