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.
- 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. - 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.
- 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.
| Method | Builder behavior |
|---|---|
| Straight-Line | Amortizes each member line over its own dates, summed per month, edge months day-weighted |
| Even Spread | Flat monthly across the POB term; last month absorbs rounding |
| Delivery / Fulfillment | One 100% row in the actual-date period, only once every line carries the signal |
| Event-Based | One row per Obligation Detail, in its transaction's period (as-billed) |
| % Complete | Catch-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.
- The HTTP response is a single plain-text line starting with
DISABLED:DISABLED Continuous Revenue Engine is off in Continuous Revenue Settings — Update POBs still runs; revenue plans, schedules and journal entries are not produced.A UI caller sees the full sentence. A headless caller, like the Salesforce engine queueable or the run-engine tooling, reads only the first word. - The exported cores the scheduled close calls,
buildPlansForContractin the plan builder andrecognizeContractin the recognizer, return{disabled:true}instead of doing work. The Map/Reduce then records the stage Update POBs (engine off) and moves on. - The switch never touches
cc_pob_update. It runs regardless.
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.
- Read at point of use, never at module scope. NetSuite caches a loaded module across executions, so a read at module load would freeze a switch at its first-seen value. Every request and every Map/Reduce stage calls
read()fresh. The settings row's internal id is found by search and cached, since ids never change, rather than assumed to be 1. If there's no settings row at all, both switches read as enabled, and Auto-Recognize reads as off. - The checkboxes are stored inverted. The record carries Disable Continuous Revenue Engine and Disable Native Sales Orders, both unchecked by default. A checkbox NetSuite has never stored on an existing row reads as false through
lookupFields, so a positive "enabled" flag would read as off until an admin saved the row. An unset "disable" flag reads as false, which means enabled. Salesforce'sDisable_Revenue_Engine__cis stored the same way, for the same reason.
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.
- Collects unposted schedule rows through the as-of period, buckets them by source period, and creates one summary journal entry per period. The header memo names the contract and period. Each POB gets a Dr Deferred / Cr Revenue line pair. Each line carries the customer entity and the POB reference. A per-line memo shows the detail, for example "<Item> · Jun 2026 · Straight-Line, month 6 of 36". A negative planned row posts the reversed pair instead. It's flagged as a contract-reduction catch-down.
- A row whose source period is closed posts as a catch-up into the chosen open period, named as such in the memo.
- Each period posts in three steps: its rows are marked as posting, the journal entry is saved with a matching id, then the rows are stamped Recognized. An already-posted row never re-posts.
- If a run stops partway, the next run finishes the job. It links the rows to the journal entry that was saved, or posts them if none was. The same revenue never posts twice.
- A period another run marked as posting moments ago is left alone rather than posted twice: this run reports it and asks you to run again in a few minutes.
- Reverse walks posted journals from a chosen period, one at a time. It deletes the entry outright when the posting period is open. Otherwise it posts a mirror-image reversing entry into the current open period. That entry's rows return to Planned before the next entry.
- A rollup pass then recomputes Recognized and Deferred on plans, POBs, and the contract.
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.
- Update Revenue Plans builds only the plans whose inputs changed, oldest first; a plan stays current through any save that doesn't touch its rule, dates, amount or lines. Each run builds what's left, so the next run finishes.
- Recognize Revenue posts one period at a time and never leaves a period half-posted.
- Update POBs never removes an Obligation Detail until it has read every billed line.
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)
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.