NetSuite · SuiteScript 2.1 · 1 November 2026

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:

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.

A subscriber isn’t a faster afterSubmit. It’s a different contract: later, with less information, and possibly more than once. Logic that assumed “now, once, with the old record” needs rewriting, not copying.

How we moved production logic safely

  1. 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.
  2. 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.
  3. 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.

New England Systems Group

If there's a process everyone works around instead of through, that's where we start.

SuiteScript development and back-office automation for manufacturers, distributors, producers and retailers. Danbury, Connecticut.

hello@nesystemsgroup.com