An automation reads the stock report, sees eleven units available, marks four orders as ready to ship, and moves on. Nothing checked whether anyone else touched the number in between, and nothing wrote a commitment anywhere. Three days later the fourth order is short.
This is the specific failure mode of fulfilment automation: not a broken rule, but a rule that treated a read as if it were a claim. The physical process of picking and packing is covered in pick, pack and ship. This is the layer above it, where transitions happen without a person, and where the guarantees have to be stronger because nobody is watching each one.
The one rule that governs all of it
Automation must never allocate stock it has not reserved. Every other guideline in this article is a consequence of that sentence.
A reservation is a durable record that a specific quantity is committed to a specific order. An allocation decision made from a number that was merely read is a guess about what other processes are doing. The gap between the read and the write is where two orders claim the same unit, and automation makes that gap dangerous because it runs far more often than a human does, and at machine speed.
The practical test for any automated transition: if this step advances an order towards shipping, does it hold a reservation for every unit it is advancing? If the answer is no, the step is asserting something it has not secured.
What can run automatically, and what should not
| Transition | Automatic? | Why |
|---|---|---|
| Accept an order and reserve stock | Yes | One correct outcome given availability, and it fails closed when stock is short |
| Split a short line into a backorder | Yes | Deterministic: reserve what exists, record the remainder |
| Release reservations on cancellation | Yes, always | The forgotten path. Unreleased reservations create phantom shortages |
| Advance status when a label is bought | Yes | The carrier’s response is the authority, not an inference |
| Ship an order that is short | No | Requires a decision about partial shipment, freight cost and the customer |
| Substitute a different variant | No | A commercial judgement, and frequently a returns liability |
| Override a failed reservation | Never | Converts a caught error into a customer-facing one |
The pattern separating the rows: automation is appropriate where the data fully determines the answer and where the failure mode is refusal. It is inappropriate where the correct answer depends on context the system does not hold, which is the general argument in when not to automate.
Where the reservation has to be taken
The reservation belongs at order acceptance, in the same transaction that records the acceptance. Not before, not after.
Reserving at picking is the common mistake, because picking feels like the moment stock is physically committed. It is not the moment the promise was made. Between the confirmation email and the pick list, the customer believes the units are theirs, and the system believes they are free. Every minute of that window is available for a second order to consume the same units.
Reserving at cart or quote stage is the opposite error. Unconverted demand holds real stock, and abandoned carts quietly starve orders that would have paid. If you reserve at quote stage, the reservation needs an expiry, and something has to sweep expired reservations back into availability.
The race condition, and what actually closes it
Two orders for the last unit arrive within the same second. Both read availability as one. Both pass the check. Both write a reservation. Availability is now minus one, or worse, it is zero and two orders believe they own the unit.
Shortening the gap does not fix this. Retrying does not fix this. Checking twice does not fix this. The only fix is to make the check and the write inseparable, either by locking the stock row before reading it, or by expressing the reservation as a conditional write whose condition is the availability test. A conditional update that only succeeds when on-hand minus reserved is at least the requested quantity returns zero rows when the stock is not there, and zero rows is the signal to fail the order.
Automation raises the stakes because it removes the accidental serialisation a human workflow provides. Two people rarely confirm two orders in the same millisecond. Two queue workers processing a webhook backlog do it routinely.
The failure case: one order, delivered twice
A storefront sends an order-created webhook. The receiving endpoint takes longer than the storefront’s timeout, so the storefront retries. Two deliveries, same order, same external identifier.
The import path looks up the external identifier, finds nothing on either request because both lookups run before either insert, and creates two internal orders. Both reserve stock correctly and atomically. Each reservation is individually sound. The customer is now committed to twice the goods, and the warehouse picks both because the two orders look like two orders.
Notice that the reservation logic was not at fault. The defect is one layer up: order creation was not idempotent. Webhook delivery is at-least-once by design, so the uniqueness of an external order identifier has to be enforced by a database constraint, not by a read followed by a write. The lookup-then-insert pattern has exactly the same race as check-then-reserve, and it needs exactly the same remedy.
What an automated order flow should record at each step
Automation that cannot be audited is automation you will eventually switch off in a panic. Each automatic transition should leave behind what it decided, what it read, and what it wrote: the quantity reserved per line, the quantity backordered per line, and the document that caused the movement.
A stock movement row that records quantity and value before and after gives you a history you can read backwards. A status field alone gives you a present tense with no explanation, which is unusable at the moment you most need it.
How Oneop handles the automated order path
In Oneop, the transitions that commit stock are built into the order lifecycle rather than configured as rules, which is a deliberate constraint. Converting an accepted quote runs as a single transaction that creates the sales order and reserves stock together, and it is idempotent: a unique constraint on the workspace and quote pair means a second conversion call returns the existing records instead of creating a second order.
The reservation itself is a conditional update that only succeeds when on-hand minus reserved covers the quantity. Zero rows returned means insufficient stock, which throws and rolls the whole transaction back, leaving no partial order behind. Where only part of a line can be reserved, the remainder is written as a backordered quantity on the line and the order is held in backorder rather than confirmed.
Shipping re-validates under a lock. It takes the sales-order row with a locking read, confirms the order is still shippable, and aborts if a concurrent request has already advanced it, so a duplicated ship request cannot ship twice. The same transaction decrements stock, posts cost of goods sold to the ledger, raises the invoice and advances the status.
One honest limit. Oneop’s automation engine triggers on record events in the CRM, projects and help desk, on SLA breaches and on a schedule. It is not where order-to-fulfilment stock transitions live, and we would rather say so than imply a trigger list we do not have. The stock guarantees in the inventory module come from the order service, enforced at the write.
The audit to run this week
Take your three most common automatic order transitions and, for each one, answer whether it holds a reservation for every unit it advances, and whether running it twice produces the same result as running it once. The mechanics of reservations themselves are in inventory reservations explained, and the arithmetic they protect is in available-to-promise.