How to graft AI onto a warehouse that already runs, with direct writes excluded by architecture, a threshold that decides on its own what gets applied and what goes to review, and the log that makes every suggestion auditable after the fact.
by Nadia Maar ·
The question that comes up most is not “can we use AI for the warehouse”, it is “can we use it without it breaking what already works”. They are two different questions, and the second is the right one: a warehouse system that has run for years carries constraints nobody wrote in a manual — a supplier that only accepts orders on Tuesdays, a volume discount that kicks in at precise thresholds, a minimum margin below which a product never gets reordered. A language model knows none of this, and the temptation to give it direct database access “because it understands context” is the same temptation that has already caused incidents in other systems, just under a newer name.
The fix is not avoiding AI, it is putting it in the right spot in the chain: it proposes, it does not decide. The model reads structured data through functions with a strict schema, never a free-form query; every proposal passes through a risk threshold that decides on its own whether to apply it or send it to a person; and everything — proposal, decision, outcome — stays written in a log that can be checked months later.
This piece is that architecture: the code of the function the model can call and the one it cannot, the threshold that routes automatic from manual, and why the log is not a formality but the difference between a system you can trust and one you can only hope works.
A language model does not fail the way traditional code fails — with the same error repeating until you fix it. It fails differently each time, in a way that depends on how the question was phrased, on the context provided, on a model update nobody asked for. Giving it access to a direct `UPDATE` on a warehouse table means accepting that, once every few thousand times, that `UPDATE` will be wrong in a way no test predicted, because no test can predict every possible phrasing of a natural-language request.
“What happens if the model gets it wrong?” If the answer describes a log to check afterwards, the write is already happening before the check. The right answer describes a step that MUST happen before anything changes — not a safety net under a mistake that has already occurred.
The schema below is what gets passed to the model as an “available function”. It is not documentation: it is a contract — the model can only populate these fields, with these types, and the function receiving the call rejects anything that does not match the schema before it even looks at the content.
const proposeReorder = {
name: "propose_reorder",
description: "Proposes reordering a product below minimum stock. " +
"Executes nothing: the proposal enters an approval queue.",
parameters: {
type: "object",
properties: {
productCode: { type: "string", pattern: "^[A-Z]{2}-[0-9]{4,6}quot; }, quantity: { type: "integer", minimum: 1, maximum: 5000 }, preferredSupplier: { type: "string" }, reasoning: { type: "string", maxLength: 400 }, }, required: ["productCode", "quantity", "reasoning"], additionalProperties: false, // the model cannot add fields // it was not offered, not even "to help" }, } as const async function executeProposeReorder(args: unknown) { // First check: the schema. Second check: the domain rules // no JSON schema can express. const data = ReorderSchema.parse(args) // throws if invalid const product = await getProduct(data.productCode) if (!product) throw new Error("Unknown product: proposal discarded") const estimatedCost = data.quantity * product.unitCost if (estimatedCost > product.maxReorderBudget) { // The model can propose above budget: the function does not execute // it, it demotes it to manual review instead of silently refusing — // section 3 explains why that difference matters. return queueForReview(data, "exceeds reorder budget") } return evaluateThreshold(data, product) }
additionalProperties: false is the line that stops the model from “being helpful” by adding a field nobody asked for — a discount it decides to apply on its own initiative, for instance. Without that line, a JSON schema still accepts extra fields by default.
Not every proposal carries the same risk. Reordering 40 units of a product that sells steadily, within the expected budget, from a regular supplier, is a decision a warehouse clerk would approve in three seconds without a second thought — automating it does not remove oversight, it removes repetitive work. Reordering 3,000 units of something that has not moved in two months is a decision that deserves a person's eyes, whatever the model says.
23 reorder proposals in one day
20 stay under threshold; 3 manual review.
Three proposals exceed the reorder budget or the maximum quantity for that product. They are not rejected: they are demoted to review, with the model's reasoning alongside. The difference matters — a silent rejection would have thrown away the analysis the model did anyway.
18 stay under threshold; 2 manual review.
Two proposals involve a supplier never used before, or a product that has not moved in weeks. These are exactly the cases where the model has the least historical context to work from, and where a person decides better in thirty seconds than any automatic threshold can.
Around 78%: routine reorders meeting every constraint, generated in the ERP with the AI's proposal logged as the reasoning, and reversible for as long as the supplier accepts changes. A person's time concentrates on the five that actually matter, instead of spreading evenly across all twenty-three.
Six months from now, somebody will ask why the warehouse has 400 units of a product that does not sell. If the answer is “we don't know, the system decided”, the AI stops being a tool and becomes a permanent suspect. The log exists to answer precisely: who proposed what, with what reasoning, who approved or corrected it, and when.
| 08:14 | System | Proposed reorder EK-4471 · 120 units · under budget threshold | auto |
| 08:14 | System | Applied automatically — order generated with the regular supplier | auto |
| 09:02 | System | Proposed reorder TK-1182 · 2,400 units · exceeds quantity threshold | coda |
| 10:41 | M. Ferretti | Corrected to 800 units, reasoning: last week's promotion has ended | revisione |
| 11:15 | M. Ferretti | Approved with correction — order generated for 800 units | manuale |
| 14:30 | System | Proposed reorder QZ-0093 · new supplier, never ordered from before | coda |
| 16:02 | M. Ferretti | Rejected — supplier not yet approved by procurement | revisione |
23 proposals generated, 18 applied automatically, 5 sent for review
It is not an error rate to minimise: it is proof the threshold works. If it dropped to zero, that would mean the threshold is letting everything through automatically — not that the model has become perfect. A system that is honest with itself sends a share of its own work to review every day, by design.
If you have a repetitive process you'd like to lighten with AI — reorders, checks, routing — bring a real example of ten past decisions. In half an hour we see how many would fall under an automatic threshold and how many would still deserve a person.
If your process rarely changes and has fewer than a dozen exceptional cases a year, I will tell you: you probably do not need a language model, hand-written rules will do — more predictable, cheaper to maintain, and just as automatic.
Not with direct write access. The safe architecture limits the model to a closed list of typed functions — “propose a reorder”, never “run this query” — each of which validates its own arguments against domain rules before doing anything. The model generates arguments for functions somebody else wrote, not executable code.
With a risk threshold across several criteria at once — quantity, cost, supplier reliability, product sales history. On a warehouse with stable sales, the majority of routine proposals clear the threshold and apply automatically; any criterion out of threshold sends the proposal to a review queue with the model's reasoning attached.
With a log that records every proposal, applied or not: who generated it, with what reasoning, who approved or corrected it, and when. Without that log, any question about a past decision has no verifiable answer — and the AI stops being a tool and becomes a permanent suspect instead.
No, and that is the main difference from traditional software: a language model fails differently depending on how a request is phrased and its context, not with a repeatable error. That is why the control cannot be “fix the bug once found”: it has to be a threshold that blocks execution before the mistake, whatever shape it takes, touches real data.