Stopping the work order cascade
A special-order item generated work orders all the way down the bill of material — for subassemblies already sitting on the shelf. Here is what fixed it, and the three traps on the way.
The problem, in operations terms
An order comes in for an item flagged as special order. NetSuite creates a work order to build it. That work order's components are themselves assemblies, so NetSuite creates work orders for those. Those have components too. The tree keeps expanding.
Which would be correct, if you didn't already have the subassemblies in stock.
That's the whole problem. The components were on the shelf, at the right location, in sufficient quantity — and the system built more anyway, because the sourcing decision was made from the BOM structure rather than from what was actually available. Every one of those unnecessary work orders is a release, a pick, a build, a completion, and a transaction someone has to touch. And the material consumed to build a part you already had is material that isn't there for the next order.
Nobody logged this as a defect. Production just quietly did more work than it needed to, and inventory drifted in ways that were hard to attribute.
The fix in one sentence
On work order create and edit, look at every non-stock component line, check real availability at the work order's location, and flip the ones you can actually pull to STOCK. A line sourced from stock does not generate a downstream work order — so the cascade stops at the first level where inventory exists.
The evaluation is deliberately narrow:
// Only consider non-stock Assembly/InvtPart items
if (source !== 'STOCK' && (type === 'Assembly' || type === 'InvtPart')) {
eligibleLines.push({ lineIndex: i, itemId, quantity, assemblyLevel });
}
There's also an override. Some items should always be built rather than pulled, regardless of what's on hand — a custom item checkbox forces those to STOCK-check differently, and a matching flag lets you exempt an item from the automation entirely. Any rule that touches production needs a manual escape hatch, because there will always be a part that breaks the rule for reasons the system can't see.
Trap 1: enableSourcing: true will undo your changes
This is the one that cost the most time. The script would run, log every flip correctly, report success — and the work order would come back with components still phantom.
Not intermittently. Not on some lines. The values were being set, committed, saved, and then reverted, with nothing in the log to say so.
The cause was the save call:
// before
dyn.save({ enableSourcing: true, ignoreMandatoryFields: true });
// after
dyn.save({ ignoreMandatoryFields: true });
enableSourcing: true asks NetSuite to run its sourcing and defaulting logic as part of the save. On a work order, part of that logic is deciding how each component line should be sourced — which is precisely the decision you just overrode. NetSuite re-derives it from the BOM, finds the component is a phantom, and helpfully puts it back.
So the script and the platform were fighting over the same field, and the platform went last.
enableSourcing is usually harmless and often necessary — it's what populates dependent fields you didn't set. But whenever you are deliberately overriding a value NetSuite would otherwise derive, sourcing on save is your adversary. If a change vanishes with no error, suspect it before suspecting your own logic.
When the platform derives a value and you override it, assume it will try to derive it again.
Trap 2: line indexes shift after BOM expansion
The script verifies its own work — reload the record after save, confirm the lines are actually STOCK. The first version matched by line index, and reported failures that hadn't happened.
When component sourcing changes, NetSuite may re-expand or collapse the BOM structure on the record. Lines move. Index 7 before the save is not necessarily index 7 after it, so a line that persisted perfectly reads as missing.
The fix is to verify by item rather than position — and to handle the same item legitimately appearing on more than one line:
const postSaveByItem = {};
for (let i = 0; i < verificationLineCount; i++) {
const itemId = verification.getSublistValue({ sublistId:'item', fieldId:'item', line:i });
const src = verification.getSublistValue({ sublistId:'item', fieldId:'itemsource', line:i });
if (!postSaveByItem[itemId]) postSaveByItem[itemId] = [];
postSaveByItem[itemId].push({ line: i, source: src });
}
The same instinct applies to the write side. Eligible lines are processed deepest-first, sorted by assembly level descending, so that changes to nested components don't invalidate the positions of lines not yet processed:
eligibleLines.sort((a, b) => b.assemblyLevel - a.assemblyLevel);
General rule for any multi-level BOM: treat the line index as valid only for the current read. If anything between reading and writing can restructure the sublist, key on the item.
Trap 3: dynamic mode selects a line that isn't the one you asked for
In dynamic mode, selectLine can land somewhere other than where you expected once the sublist has been restructured underneath you. Since a wrong selection means writing a sourcing change to the wrong component — a real inventory consequence, not a cosmetic one — it's worth an explicit check rather than a trusting assumption:
dyn.selectLine({ sublistId: 'item', line: line.lineIndex });
const selectedLine = dyn.getCurrentSublistIndex({ sublistId: 'item' });
if (selectedLine !== line.lineIndex) {
log.error('Line Selection Failed',
`Expected line ${line.lineIndex}, got ${selectedLine}`);
continue;
}
Skipping a line is a missed optimisation. Flipping the wrong line is a production error. When those are the two failure modes, bias hard toward skipping.
Check availability immediately before the flip
Availability is read inside the loop, right before each decision — not gathered up front for all lines and then applied afterwards. That ordering matters: a save can commit stock, and any gap between "I checked" and "I decided" is a window where the answer changes. It's a small structural choice that eliminates a whole class of intermittent bug.
What changed
Work orders now stop cascading at the first level where inventory actually exists. The subassemblies on the shelf get consumed instead of duplicated, production stops releasing builds for parts it already has, and the material stays available for the orders that genuinely need it.
The part worth noting for anyone weighing this kind of automation: the logic is not complicated. Availability against requirement, one field, one flip. What made it hard was that NetSuite silently disagreed with the answer on save — and the three traps above are all variations of the same lesson. When the platform derives a value and you override it, assume it will try to derive it again.