What Shopify actually reports
Shopify's inventory model is more detailed than most people use. Stock at a location sits in one of eight named states — available, committed, reserved, damaged, safety stock, quality control, incoming, and the on-hand total — and any of them can be read per inventory item per location. It is a good model. It answers a question about a product at a place.
Two properties of it shape everything downstream. Committed stock is Shopify's to write and not yours: the documentation says plainly that you cannot adjust or move quantities in the committed state through the Admin API, because Shopify manages it when orders are created and fulfilled, when draft orders reserve stock, and when transfers are marked ready to ship. And incoming stock is real and queryable, but it is not part of the on-hand total — it is not in the building. Whether the numbers you are reading are the right numbers in the first place is a separate problem with its own failure modes, and worth settling before you build anything on top of them.
So: eight states, all readable, all per SKU. Nothing in that model has an opinion about an order.
The object that should answer this, and doesn't
The obvious place to look is the FulfillmentOrder object, which exists precisely to represent the work to be done on an order. It carries no inventory state at all — no sufficiency flag, no available quantity per line, no backorder indicator. Its status moves through OPEN, SCHEDULED, ON_HOLD, IN_PROGRESS, CLOSED and CANCELLED, which is workflow, not stock. Shopify's own wording is that fulfilment orders represent the work which is intended to be done. Intended. Nobody has checked.
The incoming figure has a related gap that is worth knowing about because it is moving. Until recently a scheduled change could attach an expected date to inbound stock, which is the closest the platform came to answering when. In API version 2026-07 the mutation that created them, inventorySetScheduledChanges, is gone, and the scheduledChanges field on InventoryLevel is deprecated — you can read a scheduled change you can no longer create. Shopify's stated migration is to have no replacement and adjust quantities when the change actually happens. The 2026-04 version is still supported, so this is not yet a hard wall, but the direction is clear: the platform is getting further from being able to tell you when stock arrives, not closer.
Splitting a line three ways
Which leaves the calculation to you, and the unit of it is the line, not the product.
A line ordering ten units with three on the shelf is neither in stock nor out of stock. It is three units of ship it now and seven units of something else, and the something else divides again: quantities covered by stock that is genuinely inbound, and quantities covered by nothing at all. Those two are not the same conversation. One is a date to tell the customer. The other is a phone call.
So every ordered quantity splits three ways — available, incoming, unavailable — and the split is ordered rather than proportional. On-hand stock is claimed first because it is the only stock that can actually be picked, then inbound, and the remainder is the problem.
const SHIPPABLE_THRESHOLD = 350;
const PRIORITY_THRESHOLD = 500;
/**
* Split one ordered quantity three ways. The split is ordered, not proportional:
* on-hand stock is claimed first, then inbound, and whatever is left is unavailable.
*/
export function allocateLine({ ordered, available, incoming }) {
if (!Number.isInteger(ordered) || ordered < 0) throw new Error(`bad ordered: ${ordered}`);
const fromAvailable = Math.min(ordered, Math.max(0, available));
const fromIncoming = Math.min(ordered - fromAvailable, Math.max(0, incoming));
const unavailable = ordered - fromAvailable - fromIncoming;
return { fromAvailable, fromIncoming, unavailable };
}
/**
* Derive the order's state from all of its open lines.
*
* Read in this order — the first match wins — because the states are not a scale.
* "Something is unavailable" is a different kind of fact from "some of it is coming",
* and an order carrying both needs the escalation, not the reassurance.
*/
export function classifyOrder(lines) {
const allocated = lines.map((line) => ({ ...line, ...allocateLine(line) }));
const shippableValue = allocated.reduce((sum, l) => sum + l.fromAvailable * l.unitPrice, 0);
const anyUnavailable = allocated.some((l) => l.unavailable > 0);
const anyAvailable = allocated.some((l) => l.fromAvailable > 0);
const allAvailable = allocated.every((l) => l.fromAvailable === l.ordered);
let state;
if (anyUnavailable) state = "unavailable_item";
else if (allAvailable) state = "fully_available";
else if (anyAvailable) state = "partial";
else state = "incoming_only";
return {
state,
shippableValue,
lines: allocated,
// Worth shipping short, or worth waiting? The inventory answer and the commercial
// answer disagree often enough that the number has to travel with the state.
shipNow: shippableValue >= SHIPPABLE_THRESHOLD,
priority: shippableValue >= PRIORITY_THRESHOLD,
};
}An order's state is derived, not stored
The aggregation is where the real work is, and where the intuitive implementation is wrong.
An order is not a line, and its state cannot be read from any single field — not from Shopify's fulfilment status, which describes what has already been done, and not from the status field on the sales order in the ERP, which describes where it is in a workflow. It has to be derived from every open line, every time, because the inputs move independently of the order. Another customer's order commits stock; a delivery lands; a line is amended. An allocation stored this morning is a fact about this morning.
Four states are enough to run on: everything available, partially available, nothing available but inbound, and at least one line nothing can cover. The fourth outranks the rest, and the ordering of the checks is the part that has to be right. Total the quantities across an order and a line with forty spare units will net against a line with five missing, reporting an order as shippable when it cannot be completed. The states are not a scale and they do not average.
Money is half the decision
Here is where the inventory answer and the operational answer come apart, and it is the part most dashboards leave out.
Knowing an order is partially available does not tell you to ship it. A partial shipment costs a pick, a box and a carrier movement, and it is worth making at nine hundred euros of available goods and not worth making at forty. On the Weldaad dashboard that judgement is two thresholds on the value of what is currently pickable — 350 euros to mark an order worth shipping short, 500 euros to flag it as priority — attached to each order alongside its state. The thresholds are the merchant's, not ours, and they are the kind of number that changes with carrier pricing.
Inbound stock is deliberately excluded from that value. Money you cannot pick today is not a reason to raise a pick today.
What this does not do
It does not reserve anything. A calculation that recomputes on every read tells you what is true now; it does not stop the next order committing the stock it just counted. Two people looking at the same dashboard can both be told the same units are available, and for a wholesale operation picking in batches that is usually acceptable — but it is a real limitation and it is the point at which a reservation layer becomes the next piece of work rather than a feature of this one.
It does not know about substitutions, part-shipping agreements held in a sales rep's head, or customers who would rather wait than receive two parcels. It does not improve the underlying data: an allocation computed from a stale sync is a confident wrong answer, delivered faster than the manual process it replaced. And it produces a recommendation, not a decision — the thresholds are a prompt for a human, and the moment they are wired directly into automatic fulfilment they will ship something they should not have.
When this needs an engineer
Often it does not. A store fulfilling from Shopify's own inventory, with no ERP and no meaningful backorder volume, already has this: the admin shows what is unfulfilled and stock is one number in one place. If partial shipments are rare, a spreadsheet updated twice a week is a proportionate answer, and building software to replace it is a way of spending money to formalise a problem you do not have.
It becomes engineering when orders arrive through more than one channel so Shopify is not the complete picture, when the stock that matters lives in an ERP, and when partial fulfilment is routine enough that the sequencing of picks is a commercial decision rather than an administrative one. That was the case for a wholesale business running four Exact Online integrations, where the order availability dashboard replaced opening orders one at a time. It is Shopify ERP and CRM integration work rather than a Shopify configuration, because the answer is assembled from two systems and owned by neither.
Send us the store and the symptom.
