Eight workflows, a handful of scripts, one set of rules
Every rule on the sales order made sense the day it was added. Years later, nobody could say which one won when two disagreed, and every save ran all of them twice. Here’s how we folded them into one place — and proved the new version matched the old one on real orders before switching anything off.
How rules sprawl
A sales order collects rules. Web orders should be prepaid. Orders shipping to a certain region need a particular carrier. A bulk ship date should copy to every line. Each line should default to the right warehouse unless someone overrode it. Repair items get a repair type. Orders get released to the warehouse when they meet certain conditions.
Each one was added by whoever needed it, in whatever tool they knew: a workflow here, a user event there, sometimes a workflow that did half the job and a script that finished it. In the account we worked on, the sales order rules were spread across eight workflows and several user event scripts.
None of them was wrong. Together they had three problems:
- Nobody knew which rule won. When two of them touched the same field, the answer was whichever ran last — and that depended on trigger types nobody remembered setting.
- Saves were doing double work. Several workflows set fields after the order was saved, which meant saving it again and running every script on the record a second time.
- Debugging meant opening ten things. “Why did this order get that ship date?” had no single place to look.
If nobody can say which rule wins, the answer is whichever one saved last.
One library, one place it runs
We rewrote every rule as a small, named function in one script library. Each rule says when it applies and what it would change, and returns that as data instead of writing directly:
const RULES = [
{
name: 'Web orders are prepaid',
when: (r, ctx) => ctx.isCreate && r.get('source') === 'web',
apply: (r) => r.set('terms', PREPAID)
},
{
name: 'Bulk ship date copies to lines',
when: (r) => !!r.get('bulkShipDate'),
apply: (r) => r.eachLine((l) => l.set('shipDate', r.get('bulkShipDate')))
}
// ...one entry per business rule, in an order you can read
];
One before-save user event runs the list. Because it runs before the save, setting fields costs nothing extra: the order is already in memory, and there’s no second save. The order of the rules is now the order in the file, which anyone can read.
Two properties we insisted on:
- A dry-run mode. The same rules can run against a record and report what they would change without changing it.
- Idempotence. Run the rules on an order they’ve already processed and they change nothing. We tested that with a second pass before deploying anything.
Proving it before switching anything off
Rewriting rules is easy. Knowing the rewrite matches years of accumulated behavior is the hard part, because the old behavior was never written down anywhere except in the workflows themselves.
So we didn’t switch anything off. We left every old workflow and script running, and deployed a compare job using NetSuite’s new Event Subscriber script type. A few minutes after each order, quote or fulfillment is saved — after every old workflow and script has finished — it loads the saved record and runs the new rules in dry-run mode.
- If the old logic produced the same result, the new rules find nothing to change, and the job logs MATCH.
- If the new rules would change something, it logs MISMATCH, field by field: what was saved, and what the rules want.
On the first afternoon it checked about 35 orders, fulfillments and quotes — more than 100 checks counting repeat saves — and every one matched. We left it running across a full business day before planning the cutover, so that rarer cases had a chance to show up.
The cutover is one step
Because the old logic never stopped running, cutover is a short checklist done in one sitting: deploy the new user event live, undeploy the old scripts, inactivate the old workflows, remove the compare job. If anything looks wrong, reactivating the workflows puts the old behavior back in minutes.
What the business gets
- Faster saves. No second save, and one script instead of a dozen triggers.
- One answer to “why.” Every rule is named, in one file, in the order it runs.
- Documentation that stays true. The rule list is the documentation, so it can’t drift from what the system does.
- Cheaper changes. Adding a rule means adding one entry and running the compare job again, not threading a new workflow between old ones.
This is most of what “cleaning up” an inherited NetSuite account means in practice. Not ripping things out, but proving what they do, and then putting it somewhere a person can read.
Related: your ERP isn’t slow, it’s carrying years of customizations, Event Subscriber scripts, and integration cause vs. symptom.