Event Subscriber scripts: what to move out of afterSubmit, and what to leave
NetSuite’s new script type runs your post-save logic after the user has moved on. That’s a real speed-up for slow records — and a different contract from afterSubmit, with its own traps. Here is what we learned moving production logic onto it.
The 2026.2 cycle added a server-side script type called Event Subscriber. It runs after a supported record event — a record being created, updated or deleted — and it runs after the originating operation has finished. The user who clicked Save doesn’t wait for it.
If you have afterSubmit user events that create related records, send emails or write logs, that sounds like free performance. Some of it is. But a subscriber isn’t a faster afterSubmit. It receives less information, runs later, and can run more than once, and logic written for afterSubmit can quietly misbehave if it’s moved across unchanged.
What it looks like
SuiteScript 2.1 only, one entry point. You get the record type and ID — not the record:
/**
* @NApiVersion 2.1
* @NScriptType EventSubscriber
*/
define(['N/record', 'N/log'], (record, log) => {
async function handle(options) {
const type = String(options.context.recordType || '').toLowerCase();
const id = options.payload && options.payload.recordId;
if (!id) return;
const rec = record.load({ type, id }); // the record as it is now
// ...follow-up work...
}
return { handle };
});
Which events it listens to isn’t set on a deployment record. It’s an SDF object with criteria:
<eventsubscriber scriptid="custevsub_example_followup">
<scriptfilepath>[/SuiteScripts/example/followup_es.js]</scriptfilepath>
<criteria>
<criterion>
<domain>RECORD</domain><type>CREATED</type>
<parameters><parameter>
<name>RECORDTYPE</name><operator>EQ</operator><value>SALESORDER</value>
</parameter></parameters>
</criterion>
</criteria>
</eventsubscriber>
Oracle documents a limit of 1,000 usage units and 300 seconds per execution. Anything heavier should hand off to a Map/Reduce.
The decision rule
After going through every afterSubmit on our slowest record types, one rule sorted almost all of them:
- Move it if it touches other records or sends something, and nobody is waiting on the result: creating a related transaction, a notification email, a follow-up task, a history or audit log.
- Leave it in a user event if it sets fields on the record being saved. Those belong in beforeSubmit anyway, where the record is already in memory and there’s no extra save.
- Leave it in a user event if it has to block the save, warn the user, or show a result on the page that loads after the save.
- Leave it in a user event if it compares old and new values. A subscriber has no old record.
That last one disqualified more scripts than expected. Anything that asks “did this field just change?” can’t move unless it stores the previous value itself.
Five things that behave differently
1. It runs minutes later, not milliseconds
In the account we moved first, runs landed roughly five minutes after the save. For a notification that’s irrelevant. For anything a downstream process depends on — a transfer that has to exist before someone picks the order, a status the billing run reads — ask what can happen in those five minutes before you move it.
2. You see the record as it is now, not as it was saved
Because you load the record yourself, minutes later, you may see edits made after the triggering save. Usually that’s what you want. It does mean a subscriber can’t answer “what did this save change?”
3. The same event can arrive more than once
In shadow mode we regularly saw the same order processed two or three times within the same second. Some of that is integrations saving an order several times; some looked like duplicate delivery. If your handler creates something, two copies running at once will create it twice. Every subscriber that writes needs to be idempotent:
function alreadyRunning(type, id) {
const c = cache.getCache({ name: 'EXAMPLE_ES_LOCK', scope: cache.Scope.PROTECTED });
const key = type + '_' + id;
if (c.get({ key })) return true;
c.put({ key, value: '1', ttl: 60 });
return false;
}
A cache lock catches the simultaneous runs. It isn’t a guarantee, so the handler should also check whether the work already exists — an existing linked transfer, an email already logged — before doing it.
4. It fires on every save, including its own
A user event can skip saves made by scripts by checking the execution context. A subscriber gets every update, including the one it causes if it saves the record it was triggered by. If yours re-saves, write a short-lived marker before the save and exit when the next run finds it. Better still, don’t re-save the triggering record at all.
5. No deployment parameters, and nobody sees the errors
There’s no deployment record to hang a test-mode checkbox on, so switches live in code or in a configuration record. And because no user is waiting, a failure appears in the execution log and nowhere else. Anything that matters should write failures to a custom record someone actually reviews.
How we moved production logic safely
- Shadow first. The subscriber ran the full logic but only logged what it would do, while the existing user event kept doing the real work.
- Compare the logs. For a day of real orders, every “would create” was checked against what the user event actually created. The logs are where the duplicate runs and the five-minute delay showed up — before either could do damage.
- Fix, then cut over. Add the idempotency guard, flip the switch, undeploy the old user event on the same record type in the same step, so the two never both run live.
The result is the same work done, with the user no longer waiting for it. On records that people save hundreds of times a day, that’s the difference people notice.
Related: collapsing eight workflows into one set of rules, why your ERP feels slow, and finding the scripts that slow every save.