Process automation
EDI to Exact Online: one Inngest workflow, three warehouses
On a Tuesday in March, three pickers in Mechelen stood around the warehouse printer for forty-two minutes waiting for one PDF. By June, the same job took seventy-eight seconds.

On a Tuesday in March, three pickers in Mechelen stood at the warehouse printer for forty-two minutes waiting for one PDF. The EDI order had landed at 07:12. A forklift was idling near the loading dock. The trailer to Antwerp was leaving at 09:00 whether the order made it on or not. Nobody at the distributor could do anything useful until the picking list printed, and the picking list was stuck somewhere between an EDIFACT file on an SFTP drop, six Power Automate flows, and a draft sales order in Exact Online that was waiting for a status update that would never arrive.
This is the story of how a 33-person industrial coatings distributor compressed that flow into a single durable workflow, and what they had to give up to do it.
The fourteen steps, written down honestly
When we first arrived, nobody could explain end-to-end what happened to an order. Each person knew their own segment. The operations manager knew the EDI side. A part-time consultant maintained the Power Automate flows. The bookkeeper kept the Exact Online integration alive with two manual reconciliations a day. So we did the boring thing first and wrote the steps down. There were fourteen.
- AS2 receiver writes the EDIFACT ORDERS message to an SFTP folder.
- A Logic App polls the SFTP folder every five minutes.
- The EDIFACT is parsed into JSON by a small Azure Function.
- The JSON is dropped onto a Service Bus queue.
- A Power Automate flow picks it up and looks up the customer in Exact Online.
- A second flow maps GTIN to internal SKU through a SharePoint list.
- A third flow checks stock per warehouse via the Exact Online REST API.
- A fourth flow writes a draft sales order into Exact Online.
- A fifth flow waits ten minutes for a confirmation in a Teams channel that nobody reads.
- A sixth flow generates the picking-list HTML from a Word template in SharePoint.
- The HTML is converted to PDF by a print agent on a Windows VM.
- The PDF is dropped onto the network share for each warehouse.
- A confirmation EDI message is composed and dropped back to the AS2 partner.
- The bookkeeper checks the next morning whether the order ended up in the right ledger.
The forty-two minutes were not the EDI vendor's fault. They were not Exact Online's fault either. They were the cumulative cost of fourteen separate systems, each polling the next, each retrying on its own schedule, each owning a small piece of state that nobody else could read.
Where the minutes actually lived
Three places.
The first was polling. Five minutes here, five minutes there, three minutes for the print agent to notice a new file on the network share. We measured the median order and roughly twenty-one of the forty-two minutes were spent waiting for something to tick.
The second was the Exact Online API. The customer's tenant was rate-limited at sixty requests per minute, and the stock check ran sequentially over the order lines. A twenty-line order from a paint trade meant twenty round trips, and on a busy morning the bucket would drain and the integration would back off for ninety seconds before resuming. That cost another ten minutes on bad days.
The third was a human-in-the-loop step that was not human and not in the loop. Step 9 was originally a real approval. Then it became an audit log. Then it became a timeout because nobody approved anything. The flow still waited ten minutes for a confirmation that never came, then carried on. That was the easiest minute to give back.
Most "slow" automation is not slow code. It is fast code waiting for the next polling interval, the next rate-limit window, or a manual approval that nobody is actually doing.
One workflow instead of fourteen
We rewrote the whole order path as one Inngest function. Inngest is a durable workflow engine that runs as a normal Node service and stores step state outside your code, which means a function can crash mid-way through and resume on the next deploy without losing context. For an EDI flow that touches an ERP, durability matters more than raw throughput. Losing an order is a real customer phone call.
The architectural pivot was this. Every step that used to be its own polled flow became a step.run inside one function. Where we genuinely needed parallelism (three warehouses, fan out the picking lists), we sent events and let separate functions handle them. Where we needed retries, Inngest handled them. Where we needed observability, we got it for free, because every step is logged with input, output, and timing.
We kept Babelway for the EDI transport. We kept Exact Online for the ledger. We kept every member of the warehouse and back-office team doing the job they were doing before, just with a sharper tool.
The 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 };
}
);
The picking-list 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",
});
});
}
);
Three details worth pointing at. First, the concurrency key is the warehouse code. We limit to three concurrent renders per warehouse because the print agent on each network share is single-threaded and the PDF library is not fast. Second, the sequence reservation runs as its own step, which means if the PDF rendering fails on retry the sequence is preserved and the order does not jump the queue. Third, the EDI acknowledgement is the last step. If anything earlier fails, the partner does not get a false OK.
What broke in production
Three things, predictably.
The first was idempotency. Exact Online accepts duplicate sales orders silently. The first weekend after cutover, a transient network blip caused a step retry that created two identical orders in their ledger. We fixed it by hashing the buyer VAT and the EDIFACT control number into a deduplication key on the create call. The Exact Online REST API supports an idempotency header on most write endpoints, but only if support enables it on your tenant. We asked. They did.
The second was time. EDIFACT timestamps are ambiguous about timezone unless the partner agrees a profile. Two of the customer's three partners send timestamps as Brussels local. One sends UTC. We had assumed UTC everywhere. For a week, picking lists from a Polish supplier printed dated one hour off, which mattered for the warehouse shift planning. The fix was a per-partner config in the parse-edifact step.
The third was a surprise. Picking-list PDFs were not deterministic. The renderer embedded a job ID and a generation timestamp, which meant the same input produced a different output on retry, which meant the print queue treated each retry as a new job. We made the PDF deterministic by passing the Inngest step's run ID as the embedded job ID instead of generating a new one. That is the one-line change in the snippet above.
Three months later
Picking-list generation is now a median of seventy-eight seconds end-to-end across the three warehouses, from EDIFACT landing on the SFTP to PDF on the printer tray. The slowest order in the last sixty days was three minutes and eleven seconds, and that was a sixty-eight-line order from their largest customer with a stock check that genuinely needed the API calls.
The trailer to Antwerp has not missed its 09:00 departure once since the rewrite.
Headcount is the same. The bookkeeper still owns the Exact Online relationship. The operations manager still owns the EDI partners. The part-time consultant who maintained the Power Automate flows is now writing the next set of Inngest functions with us, which is the kind of redeployment of attention that separates automation that helps a team from automation that hollows it out. The pickers got the morning ritual back as an actual hour. They use it for cycle counts that nobody had time for. The cycle counts surfaced a stock variance that paid for the project twice over in the first quarter.
What we did not do
We did not replace Exact Online. We did not replace Babelway. We did not introduce a new ERP or a new EDI vendor. The temptation in a rewrite like this is to relitigate every decision the team made over the last decade. Resist it. The fourteen-step flow was slow because there were fourteen steps, not because any single tool in it was wrong. The right scope was the orchestration, not the stack.
We also did not put an AI agent in the middle of the order path. There is a place for agents in operations work, and we ship a lot of them, but a deterministic EDI-to-ERP flow is the wrong place for one. The order data is structured, the rules are stable, and the failure modes are auditable. A workflow engine with retries is the correct tool. A language model in the middle is a liability you do not need.
If you have a flow like this
When we built this process automation for the Mechelen distributor, the hardest part was not the EDI parsing or the Exact Online integration. It was getting fourteen separate systems out of the way long enough to see what the workflow actually was.
If your order flow takes longer than it should, the smallest thing you can do today is the boring thing we did first. Open a shared doc, list every step from the moment an order enters your systems to the moment a confirmation leaves, and time each step against a real order from last week. The number of steps and the cumulative minutes are usually a surprise to the person who owns the flow. That document is the brief for everything that comes after.
Key takeaway
Fourteen systems each polling the next produced forty-two minutes of latency. One durable workflow with proper fan-out produced seventy-eight seconds.
FAQ
Why Inngest instead of Temporal or AWS Step Functions?
Inngest runs as a Node service in your own deploy, has TypeScript ergonomics a small team can hold in their head, and stores step state outside your code so retries survive a redeploy. Temporal and Step Functions work fine; this team's stack matched Inngest.
Did the customer have to leave Exact Online?
No. Exact Online stayed as the ledger of record. The rewrite only changed the orchestration around it. The accounting workflow, the VAT logic, and existing reports were untouched.
How long did the rewrite take from kickoff to go-live?
Six weeks. Two weeks mapping the existing flow and writing test cases against last quarter's orders, three weeks building and testing the Inngest functions in parallel, one week cutover with both flows running side by side.
What happened to the people who maintained the old flow?
Nothing. Headcount stayed flat. The part-time consultant who owned the Power Automate flows now writes Inngest functions. The bookkeeper still owns the Exact Online relationship. The warehouse team gained an hour they spend on cycle counts.