The engine Suitelets

Three NetSuite Suitelets sit behind the contract buttons. Each is a plain HTTP endpoint. The contract buttons call them directly, the automatic post-sync legs call them headlessly, and the scheduled close calls their shared internals.

Update POBs is data maintenance, and it always runs. The other two, Update Revenue Plans and Recognize Revenue, are the Continuous Revenue Engine, and they honor the engine switch.

This page is the implementation reference. For the operator view, see Running the revenue engine. Their script records are customscript_cc_pob_update, customscript_cc_rev_plan, and customscript_cc_rev_rec_run.

Update POBs (cc_pob_update)

Runs a three-stage pipeline per contract, no matter what the engine switch says. RC Order Line records mirror the Salesforce order lines that make up the contract.

  1. Group. Reads every RC Order Line on the contract and groups them by item. Each group finds its existing POB or creates one. Every POB carries a key built from the contract and the item, so the same pair always finds the same POB. It rolls up the per-POB amount as the sum of line amounts, plus a shape-aware quantity and the min and max dates. Once every member line carries a Delivery or Fulfillment signal, the dates anchor to that date instead. Once any member line carries a % Complete from Salesforce, it sets the POB's % Complete to the average of all its lines, weighted by amount. A line with none counts as 0. It copies the item's revenue rule onto POBs with no rule of their own, and sets Even Spread automatically on ramp-shaped POBs. It enforces the name POB-{id} — {item}, and rolls up contract Booked. It also creates Event-Based POBs for usage items that bill without an order line.
  2. Bill. Scans the customer's posted invoice and credit-memo lines. It writes one Obligation Detail per line, routed to that line's item's POB, with credit-memo amounts always negative. It deletes stale details, and rolls up Billed to POBs and the contract. It reads every line first. If a run can't finish, it deletes nothing and says how far it got.
  3. Link billing. Points billing schedules, periods, and the POB's group link at each other. The group-to-POB link is set only when a POB maps to a single group.

There's no allocation stage. A POB's Booked amount is what its plan recognizes. Allocating a contract price across obligations belongs to the system that owns that accounting, not to this pipeline.

Freshness matters mid-pipeline, so these reads use SuiteQL instead of the search API. Saved-search style reads can lag same-execution writes, and that could miss records this same run already created. That's also what makes overlapping runs safe. The button, a Salesforce sync, and the scheduled run can all call Update POBs on the same contract at once. NetSuite refuses a second POB with the same key, so only one create can win. The run that loses reads the contract's POBs again and updates the POB the other run made, instead of failing.

A POB made before this key existed gets it stamped in place, the next time Update POBs runs on its contract, with the same id and name. A duplicate an earlier release left for one item is never removed by this pipeline. The keyed POB, or the oldest one, is the POB its lines and billing use. A POB or an order line that already holds what this run would write isn't saved again, so an unchanged contract writes nothing.

The POB and each Obligation Detail row copy their Source Org stamp from the contract. The Suitelet never receives a payload of its own to stamp from, so it carries forward whatever booking already wrote onto the contract.

Update Revenue Plans (cc_rev_plan)

For each POB, this upserts one Revenue Plan and rebuilds its monthly Revenue Schedule rows by method. It spreads the POB's Booked amount. Deferred on the plan is Booked minus Recognized.

MethodBuilder behavior
Straight-LineAmortizes each member line over its own dates, summed per month, edge months day-weighted
Even SpreadFlat monthly across the POB term; last month absorbs rounding
Delivery / FulfillmentOne 100% row in the actual-date period, only once every line carries the signal
Event-BasedOne row per Obligation Detail, in its transaction's period (as-billed)
% CompleteCatch-up row in the current period bringing cumulative recognition to pct × total. The POB's value is read as points: 40 is 40%, and 0.4 is 0.4%

Schedule rows are keyed per plan and month, so a rebuild updates rows in place, and stale future rows get cleaned up as orphans. The plan and its schedule rows copy the Source Org stamp from their POB, the same borrow-from-the-parent pattern Update POBs uses.

The engine switch and cc_settings_lib.js

cc_rev_plan and cc_rev_rec_run are the Continuous Revenue Engine. Both short-circuit when Disable Continuous Revenue Engine is checked on the Continuous Revenue Settings record. That check is the first thing each request does, before it reads any parameter.

Every script reads the switches through one shared library, cc_settings_lib.js, so they can never disagree. Its read() function returns engineEnabled, nativeSoAllowed, autoRecognize, and defaultRule. It also exports the engine-off sentence, so the two Suitelets and the contract's Revenue plans by POB card quote the same text. The RESTlet's installcheck reads the same library to report engineEnabled and nativeSoAllowed to the Salesforce wizard. Two design points matter.

Posted rows are frozen

The engine's central rule: a schedule row that carries a journal entry is never modified. A rebuild spreads only the remainder, the total minus what's recognized, across unposted months. That's the prospective treatment ASC 606-10-25-13(a) requires for these modifications.

A downsize past what's already been recognized makes the remainder negative. When that happens, the plan gets a single catch-down row in the current open period, instead of a negative dribble, and non-positive future months drop out entirely. That's how a mid-month cancellation nets into its effective month, and the schedule ends there.

Recognize Revenue (cc_rev_rec_run)

Read and write paths stay strictly separate. A GET renders the form and previews. It never posts. Submission, or the headless go parameter, runs the posting path.

Accounts come from script parameters, defaulting to the configured Deferred Revenue and Revenue accounts, and can be overridden per plan.

Each posted journal entry copies the Source Org stamp from the contract. A reversal copies it from the journal entry it reverses instead, not from the contract again, so a reversing entry always names the org that posted the entry it undoes.

Governance

A Suitelet run has a limited budget, so a large contract can need more than one run. Each Suitelet checks what's left before work that could overrun it and stops cleanly, saying how far it got and asking you to run again.

The automatic engine legs that follow a sync call the Suitelet again for you, up to six times, so as an operator you only see a progress message. The scheduled close works differently: each nightly run gives a contract its own budget. A contract that doesn't finish is reported Partial in the run log, not re-fired within that same run. The next scheduled run, or another Run Portfolio Close, carries on from there. To rebuild every plan after a change that doesn't touch the records, such as a new rule value, add &rebuild=1 to the Update Revenue Plans link.

The scheduled close's own deployment (customdeploy_cc_engine_sched) ships with no recurrence. Nothing runs on its own until an admin sets one. See Close every contract at once for the steps, and why Auto-Recognize on Scheduled Run does nothing until a schedule exists.

Headless operation

Every automatic invocation is a signed GET with a parameter that makes the response machine-readable.

cc_pob_update   ?contract=<id>&headless=1      // responds "OK contract=<id>"
cc_rev_plan     ?contract=<id>&headless=1      // same; partial runs say "processed N of M"
cc_rev_rec_run  ?go=1&contract=<id>&asof=YYYYMM         // recognize
cc_rev_rec_run  ?go=1&contract=<id>&reset=1[&asof=YYYYMM] // reverse (no asof = everything)
Note the asymmetry: the first two use headless=1. Recognize uses go=1 instead, because its plain GET renders the form, so the explicit go parameter is what authorizes a headless posting run. A headless Recognize or Reverse must name a contract. Without one it answers ERROR contract is required and does nothing. To run every contract, use the Recognize Revenue form.

With the engine off, cc_rev_plan and cc_rev_rec_run answer the same headless call with the DISABLED … line described above instead of OK. Callers branch on the first word. cc_pob_update answers OK contract=<id> when it finishes, and the "processed … Run again" line when it doesn't.

Both POB-update and plan-build also accept ?pob=<id> to process a single POB. That's what the POB record's own buttons call.

Who can reach these Suitelets

Update POBs and Update Revenue Plans stay open to every role, not narrowed to Administrator and the integration role the way a later release narrows the RESTlet. Two reasons. A contract's own buttons need every role that opens a contract to reach them. Salesforce's revenue engine also calls both Suitelets directly, with the connection's own signed token, alongside the RESTlet rather than through it. Narrowing them waits for the release that moves every endpoint together. See Moving a connection to the new role.