# Continuous Control > Continuous Control connects Salesforce Revenue Cloud to NetSuite. It books Revenue Cloud orders into NetSuite, syncs invoices, credit memos, payments and refunds, and runs a revenue recognition engine in NetSuite whose results flow back to Salesforce. It ships as a Salesforce unlocked package plus a NetSuite SDF release zip. Every page below is the Markdown version of a page on the docs site, written for agents and developers. It keeps the technical detail (API names, payloads, script ids, limits) that the human pages leave out. To load everything at once, fetch https://taggalongtech.github.io/continuous-control/llms-full.txt. Automating setup with Claude Code: start with https://taggalongtech.github.io/continuous-control/automate-with-claude-code.md. # Continuous Control documentation Human page: https://taggalongtech.github.io/continuous-control/index.html Continuous Control connects Salesforce Revenue Cloud to NetSuite with one authenticated call per business event. Salesforce stays the system of record for customers, orders, billing, and tax; NetSuite gets a purpose-built revenue layer — Performance Obligations, revenue plans, and recognition journals — that mirrors back to the Salesforce Contract. | Side | Holds | |---|---| | Salesforce (Revenue Cloud) | Accounts · Orders · Contracts; billing schedules · invoices; payments · tax | | NetSuite (A/R + Revenue) | Performance Obligations; revenue plans · journals; invoices · payments · GL | One signed call carries the full graph from Salesforce to NetSuite; NetSuite mirrors ids and POBs back. ## Start here - [How it works](how-it-works.md): The one-call model, end to end, on a single page. - [Quickstart](quickstart.md): From install to a booked order with recognized revenue. - [General concepts](revenue-lifecycle.md): Booked → Billed → Recognized, POBs, and the data model. ## Set it up - [Install](install-salesforce.md): The Salesforce package and the NetSuite components. - [Configure](connection-wizard.md): The nine-step Connection wizard, mappings, and settings. - [Roll out](placing-components.md): Put the sync card and Flow actions where your teams work. ## Under the hood - [The Book Order call flow](book-order-call-flow.md): Every class and hand-off from click to booked contract. - [Anatomy of the payload](payload-anatomy.md): The JSON that crosses the wire, annotated key by key. - [The revenue engine](engine-suitelets.md): How POBs group, plans amortize, and journals post. # How it works at a glance Human page: https://taggalongtech.github.io/continuous-control/how-it-works.html Salesforce owns every commercial fact — customers, orders, billing schedules, invoices, payments, tax. When a business event completes, Continuous Control sends the whole graph to NetSuite in one authenticated call. NetSuite lands it as native records, runs the revenue engine, and the results mirror back to the Salesforce Contract. ## One call, the whole graph Most integrations sync records one at a time and force you to reason about ordering, retries, and partial state. Continuous Control does the opposite: when you book an order, Salesforce assembles the *entire* order graph — account, contacts, addresses, products, contract, order, lines, billing schedule groups, schedules, and billing periods — into a single JSON document and sends it in one signed HTTPS call to a Continuous endpoint installed in your NetSuite account. The addresses in that list are worth a footnote: they ride two configurable [address objects](calls-edit-mappings.md#addresses) rather than being written into the payload by code, which is the general direction of travel here. The same pattern covers the rest of the lifecycle: posted invoices and credit memos, payments and refunds each travel as one call carrying everything NetSuite needs. Every call is recorded as a [sync run](sync-runs-idempotency.md) with the exact JSON sent and received. The lifecycle in order: 1. **Order activated**, **invoices posted**, or **payments applied** in Salesforce each send one call to the Continuous endpoint in NetSuite. 2. The endpoint lands the graph — this leg is idempotent — and POBs plus revenue plans build automatically. 3. **Recognize Revenue** (finance decides when) posts journal entries: debit Deferred Revenue, credit Revenue. 4. Results mirror back to the Salesforce Contract as Booked, Billed, and Recognized. ## What NetSuite does with it The Continuous components in NetSuite land the graph as a set of purpose-built custom records — the RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), RC Orders, RC Order Lines, and the billing mirrors. Then three processes close the loop: - **Update POBs** groups order lines into [Performance Obligations](performance-obligations.md) by contract and product, rolls up Booked, and ties every posted invoice line to its POB to compute Billed. This always runs. - **Update Revenue Plans** builds a monthly recognition schedule per POB from its [recognition method](recognition-methods.md), recognizing off the POB's Booked amount. - **Recognize Revenue** posts one summary journal entry per period — debit Deferred Revenue, credit Revenue — with a full preview before anything posts. This step stays a deliberate human decision (or a scheduled close, if you enable it). After a booking, order line sync or invoice sync completes, the first two processes run automatically in the background; recognition is left to finance. The last two are the **Continuous Revenue Engine** and can be switched off on either side, for an account that recognizes revenue elsewhere; Update POBs and the mirror keep running regardless. See [One engine, two switches](one-engine-two-options.md). ## What mirrors back to Salesforce Every call returns a map of NetSuite internal ids. Salesforce stamps them, along with a sync status and timestamp, onto each synced record — so every Account, Order, Invoice, and Payment carries a live link to its NetSuite twin. After the engine runs, the POBs themselves mirror back into Salesforce as **Performance Obligation** records, and the Contract page shows the Booked → Billed → Recognized waterfall without leaving Salesforce. See [Writebacks & the POB mirror](writebacks-pob-mirror.md). ## What you click day to day | Moment | Where | What you do | |---|---|---| | Order is activated | Order record page | Click **Book Order to NetSuite** on the sync card (or let a Flow do it) | | Billing run posts invoices | Invoice Batch Run page | Click **Sync** on the card — every posted invoice from the run lands in NetSuite | | Cash arrives | Payment page, either system | Sync the payment, or let the scheduled pull mirror NetSuite-side settlement back | | Month-end close | RC Contract (NetSuite) | Review the waterfall, click **Recognize Revenue**, confirm the preview | | Anytime | Sync Runs tab | Watch every call, its status, and its exact JSON | > **Safe by construction.** Every record matches across the two systems on its Salesforce id, so re-running any sync updates rather than duplicates. There is no ordering to get wrong and no partial state to repair — see [Sync runs & idempotency](sync-runs-idempotency.md). # Prerequisites Human page: https://taggalongtech.github.io/continuous-control/prerequisites.html Continuous Control has two halves — a Salesforce package and a NetSuite package — and each has a short list of requirements. Collect everything on this page before you start; the install itself is quick once you have it. ## Salesforce requirements - **Revenue Cloud with Billing enabled.** The package attaches its sync fields to Invoice, Billing Schedule, Credit Memo, Payment, and related Revenue Cloud objects. Because it is an org-dependent package, installation fails if those objects are not already present. - **System Administrator access** to install the package, assign the permission set, and (in the Connection wizard) run one metadata deployment for environment links. - **Order Type values (recommended, optional).** The org should carry *Net New / Amendment / Renewal* on `Order.Type`. If the field is blank, the sync derives *New* or *Amendment* from the order's action automatically. ## NetSuite requirements Enable these under **Setup → Company → Enable Features** (one-time): | Tab | Features | |---|---| | SuiteCloud | Custom Records · Client SuiteScript · Server SuiteScript · SuiteCloud Development Framework · Token-Based Authentication · REST Web Services · OAuth 2.0 | | Accounting | Accounting · A/R (Receivables) · Sales Orders. Advanced Billing is not required — Continuous Control ships its own billing schedule records and never touches NetSuite's native ones. No NetSuite revenue feature is required. | | Company | **Subsidiaries and Multiple Currencies (required)** · Departments / Classes / Locations, as your account uses them | Subsidiaries (OneWorld) and Multiple Currencies are required, not optional: every record the sync creates or updates needs a subsidiary and a currency, and the manifest declares both features `required="true"`. You will also need **Administrator access** to the NetSuite account and, for the installer, a workstation with **Node.js 18+**. ## One NetSuite account, several Salesforce orgs One NetSuite account can be the sync target for more than one Salesforce org — one production org per NetSuite account per business unit is a normal shape, and each org runs its own Connection wizard independently. **Never point a sandbox and its production org at the same NetSuite account.** The two would share NetSuite record ids for what Continuous treats as different customers, contracts and orders, and their syncs would overwrite each other's work. Each connected org creates its own NetSuite customer for a given account rather than sharing one, by design. Every record the sync writes to NetSuite carries a **Source Org** field stamped with the connection that sent it — the org's my-domain host plus its 18-character org id — so records from two orgs pointed at the same NetSuite account stay attributable to the one that created them. ## Access you will need | Person | Access | Used for | |---|---|---| | Salesforce admin | System Administrator | Package install, permission set, Connection wizard, App Builder | | NetSuite admin | Administrator (or an SDF-permissioned role) | Feature enablement, running the installer, integration record + token | | Finance lead | NetSuite user with the Continuous Control Integration role's record access, or broader | Reviewing POBs, running Recognize Revenue at close | ## Information to collect first Everything later steps consume: | Item | Where it comes from | |---|---| | Continuous Control package install link | Published on [Install the Salesforce package](install-salesforce.md) (current release 3.4.1, package version id published at release) | | NetSuite release zip | [`continuous-control-netsuite-v3.3.1.zip`](https://taggalongtech.github.io/continuous-control/downloads/continuous-control-netsuite-v3.3.1.zip) (see [Install the NetSuite components](install-netsuite.md)) | | NetSuite account id | Visible in your NetSuite URL (for example `1234567`; sandboxes look like `1234567-sb1`) | | NetSuite internal ids: Deferred Revenue account, Revenue account, selling subsidiary, currency, default sales tax item | NetSuite Setup → Chart of Accounts / Subsidiaries / Tax items — the id shows in the record URL or the Internal ID column | | Runtime credentials (consumer key/secret, token id/secret) | Created during NetSuite install, step 4 of [the NetSuite install](install-netsuite.md) | > **Nothing syncs until you connect it.** Installing either package on its own changes no data and sends nothing anywhere. Data starts moving only after the [Connection wizard](connection-wizard.md) is configured and someone triggers a sync. # Quickstart Human page: https://taggalongtech.github.io/continuous-control/quickstart.html The condensed end-to-end path: install both halves, connect them, book an order, sync an invoice, and recognize revenue. Budget about a day the first time — most of it is one-time NetSuite setup — and a few hours once you have done it before. Demoing? Follow the [demo install runbook](demo-install-runbook.md). ## 1 · Install both packages Install the Salesforce package from the [install link](install-salesforce.md) and assign the **Continuous Control** permission set to everyone who syncs, plus **Continuous Control Admin** to whoever configures the connection ([details on both](install-salesforce.md)). Have your NetSuite admin run the one-command installer from the [release zip](https://taggalongtech.github.io/continuous-control/downloads/continuous-control-netsuite-v3.3.1.zip) ([details](install-netsuite.md)), then create the runtime integration record and access token. Finish the short list of [NetSuite manual steps](netsuite-manual-setup.md). ## 2 · Connect the environments Open the **Continuous Control** app in Salesforce → **NetSuite Connection** tab, and work down the nine steps: enter credentials, add the two Remote Sites, test the connection, verify the NetSuite install and run post-install setup, deploy environment links, seed field mappings, pull reference data, review sync settings, and check health ([details](connection-wizard.md)). Every step is verifiable and safe to re-run. ## 3 · Book your first order Pick (or build) an **activated** order whose account has billing details and whose order references a Contract — the sync requires one. On the Order record page, the **Continuous: NetSuite Sync** card shows a pre-book checklist of everything that will travel. Click **Book Order to NetSuite** and watch the card walk through customer → contract → order → lines → billing schedules, then land on **Booked** with a deep link to the new RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) in NetSuite — or **Booked with warnings** when the order landed but one or more lines have no matching NetSuite item: fix the item in wizard step 7 or in NetSuite and book again ([details](booking-an-order.md)). Card labels (LWC `ccNetSuiteSyncCall`, App Builder name "Continuous: NetSuite Sync"): on an Order the card heading and button read **Book Order to NetSuite**, the working state **Booking to NetSuite…**, the done state **Booked to NetSuite**, and the link **Open RC Contract in NetSuite**. On an Invoice Batch Run: **Sync Invoices to NetSuite** / **Invoices synced to NetSuite** / **View invoices in NetSuite**. Card rows for a booking: Customer (Account · Contacts · Addresses → NetSuite Customer), Contract → RC Contract, Order → RC Order, Order Lines (OrderItem → RC Order Line), Billing Schedule Groups, Billing Schedules, Billing Periods. ![The sync card after booking Kingsbridge Digital's order: seven object rows all green, 64 records synced, the Sync Order Lines button and a deep link to the RC Contract in NetSuite](https://taggalongtech.github.io/continuous-control/assets/shots/sf-book-card.jpg) *The card after booking: every object green, with counts and a NetSuite deep link.* In NetSuite, open the RC Contract (the card links straight to it). The engine has already grouped the lines into Performance Obligations and built revenue plans — the contract shows Booked with $0 Billed and $0 Recognized. ## 4 · Sync an invoice Run billing in Salesforce as usual and post the invoices. On the Invoice Batch Run page, click **Sync** on the card — every posted invoice from the run lands in NetSuite as a native invoice with Salesforce-owned tax, so totals match to the cent ([details](invoices-credit-memos.md)). The engine follows up automatically: Billed rolls up on each POB, and the refreshed POBs mirror back to the Salesforce Contract. ## 5 · Recognize revenue In NetSuite, open the RC Contract and click **Recognize Revenue**. The form previews exactly what will post — period by period, POB by POB, with debit and credit accounts — before you confirm. On submit, one summary journal entry per period posts Dr Deferred Revenue / Cr Revenue ([details](revenue-engine.md)). ## 6 · See it in both systems Back in Salesforce, the Contract's **Revenue Summary** card shows the live waterfall — Booked, Billed, Recognized, Deferred — with a per-POB breakdown, refreshed from NetSuite. ![The Revenue Summary card on the Salesforce Contract showing Booked, Billed, Recognized bars and per-POB progress](https://taggalongtech.github.io/continuous-control/assets/shots/sf-revenue-summary-card.jpg) *The Salesforce Contract after recognition: the full waterfall without leaving Salesforce.* > **Where to look when something fails.** Every sync is a run on the **Sync Runs** tab with its status (`Completed`, `Completed with warnings`, or `Failed`), duration, error, and the exact JSON exchanged — start there. See [Monitoring & troubleshooting](monitoring-troubleshooting.md). ## Implementer runbook (terse) For an engineer standing up a fresh environment: empty Salesforce org + empty NetSuite account → a booked order and recognized revenue. The steps above are the narrative; this is the same work as an ordered checklist with exact commands. Budget about a day the first time, hours once practiced. ### NetSuite 1. **Manual prerequisites** — enable features, create the Integration record, assign the shipped **Continuous Control Integration** role, and create the Access Token ([NetSuite manual setup](netsuite-manual-setup.md) covers what has no deploy API; [Install the NetSuite components](install-netsuite.md) covers the installer, the role, and runtime credentials). Capture the five values into `.env` at the repo root (copy `.env.example`). On the Integration record, uncheck **Authorization Code Grant** under OAuth 2.0 — NetSuite pre-checks it, and leaving it on fails the connection with *Invalid Redirect URI*. 2. **Deploy.** Customer path: the release zip plus `./install.sh` ([details](install-netsuite.md)). From source, for Continuous engineers or demo accounts: ```bash cd netsuite suitecloud account:setup # one-time: bind your local authid to the target account suitecloud project:validate --server suitecloud project:deploy ``` 3. **Set the Continuous Revenue Settings record** (`customrecord_cc_settings`) to the account's real ids — subsidiary, currency, GL accounts, tax, refund account and payment method. From ns-v3.1.0, choose them in wizard step 4 (**NetSuite account settings**), which writes the record in one save. Field table in [NetSuite manual setup](netsuite-manual-setup.md). Defaults match the Continuous demo account; every fresh account sets its own. 4. **Saved searches + settings record**: the Salesforce wizard's **Run post-install setup** (NetSuite Connection → step 4) builds every `customsearch_cc_*` search and creates the settings record over the connection. Headless alternative: `python3 tools/run-engine.py search`. 5. **Manual finish** — [NetSuite manual setup](netsuite-manual-setup.md): attach the five saved-search Custom Sublists (the GL Impact tab on transactions fills itself), create and prefer the RC POBs form (and the other nine RC record forms), item/tax prep. ### Salesforce 1. **Install the package** (single, org-dependent — no dependency, no key): ```bash sf package install --package <3.4.1 package version id> -o --wait 20 ``` Or use the browser install link. 3.4.1 pairs with NetSuite ns-v3.3.1. See [Install the Salesforce package](install-salesforce.md). 2. **Assign the permission sets**: ```bash sf org assign permset --name Continuous_Control -o sf org assign permset --name Continuous_Control_Admin -o ``` **Continuous_Control_Admin** grants mapping/call configuration and the connection wizard; it's additive to the base **Continuous_Control** set — assign it only to admins who configure the integration. 3. **Run the post-install configuration** (reads `.env`; each takes seconds): ```bash SF_ORG= tools/create-remote-sites.sh # the two NetSuite callout endpoints SF_ORG= tools/load-ns-creds.sh # TBA credentials → CC_NetSuite_Cred__c SF_ORG= tools/configure-links.sh # record-type ids + search id + base URL ``` Reference data (NetSuite subsidiaries → Legal Entities) and the field-mapping rows are seeded in-app from the **NetSuite Connection** tab — no scripts. 4. **Page layouts** (~15 min in Setup / App Builder): drop **Continuous — NetSuite Sync** (`ccNetSuiteSyncCall`) on the Order, Invoice, Invoice Batch Run, and Payment record pages (callType Auto — the Credit Memo page ships in the package); drop **Continuous Control – Revenue Summary** (`ccContractRevenue`) on the Contract page; add the **Performance Obligations** related list to the Contract page layout. 5. **Wire the Flow triggers** the customer wants automated (all optional — the card works standalone): record-triggered Flows calling **Book Order to NetSuite** (Order → Activated), **Sync Invoice to NetSuite** (Invoice → Posted), **Sync Credit Memo to NetSuite** (CreditMemo → Posted), **Sync Payment to NetSuite** (Payment → Processed), a Flow mapping the fulfillment source into `CC_Delivered__c` / `CC_Fulfilled__c` / `CC_Percent_Complete__c` on order products, and a record-triggered Flow on Order Product calling **Sync Order Lines to NetSuite** when those change ([recipe](placing-components.md#sync-order-lines)). ### Smoke test 1. Activate an order with a contract in Salesforce → click **Book Order to NetSuite** → card completes with counts + a deep link to the RC Contract. 2. In NetSuite, open the RC Contract: waterfall banner shows Booked; lines grouped into POBs (the engine ran automatically after booking). 3. Post + sync an invoice → Billed appears; open a POB → Obligation Details present. 4. Click **Recognize Revenue** on the contract → preview → confirm → JE posts; Recognized rolls up; the Salesforce Contract revenue card shows the same numbers after the mirror. ### Upgrades Within 3.x, upgrades are ordinary re-runs. Salesforce: install the newer package version in place. NetSuite: re-run `./install.sh` from the newer release zip (or `suitecloud project:deploy` from the matching `ns-v*` git tag). SDF upserts and never deletes, so a re-run cannot damage existing data or configuration. The Salesforce wizard's **Verify installation** shows the installed package version and flags when it is older than the app requires. # Automate with Claude Code Human page: https://taggalongtech.github.io/continuous-control/automate-with-claude-code.html How an agent (Claude Code or another) installs and configures Continuous Control. ## Machine-readable docs | Resource | URL | Use | |---|---|---| | Index | https://taggalongtech.github.io/continuous-control/llms.txt | Every page's `.md` URL with a one-line summary, grouped like the site nav | | Everything | https://taggalongtech.github.io/continuous-control/llms-full.txt | All `.md` pages concatenated in nav order | | One page | `https://taggalongtech.github.io/continuous-control/.md` | Same name as the HTML page | The `.md` pages are the technical versions: API names, script ids, Apex methods, payloads, limits. The `.html` pages are shortened for people. When the two differ in detail, the `.md` is complete. ## Setup skill Download: https://taggalongtech.github.io/continuous-control/downloads/continuous-control-setup-skill.zip ```bash mkdir -p .claude/skills curl -LO https://taggalongtech.github.io/continuous-control/downloads/continuous-control-setup-skill.zip unzip continuous-control-setup-skill.zip -d .claude/skills ``` Contents: `continuous-control-setup/SKILL.md` (workflow and ground rules), `README.md`, `assets/cc-setup.sh`. `cc-setup.sh ` runs each Connection wizard step by calling the same `@AuraEnabled` methods on `CcNsConnectionSetupController` through anonymous Apex (`sf apex run`), and prints the method's result as JSON followed by `RESULT: ok|failed`. "ok" means the Apex ran; judge success from the JSON using the "Done when" line of that step in `connection-wizard.md`. | Command | Wizard step | Apex | Writes? | |---|---|---|---| | `install [04t id]` | (install) | `sf package install` + `sf org assign permset` for `Continuous_Control`, `Continuous_Control_Admin` | yes | | `creds ` | 1 | `saveCredentials(credsJson)` | yes (`CC_NetSuite_Cred__c` org defaults) | | `remote-sites ` | 2 | Metadata deploy of `CC_NetSuite_App`, `CC_NetSuite_Restlets` Remote Site Settings | yes | | `test` | 3 | `testConnection()` | no | | `verify` | 4 | `checkNsInstall()` | stamps `Ns_Version__c` | | `postinstall` | 4 | `runNsPostInstall()` | yes (NetSuite settings row, searches) | | `env` | 5 | `discoverEnvironment()` then `deployEnvironment(json)` (async Metadata API) | yes (`CC_NetSuite_Env__mdt`, `CC_NetSuite_Link__mdt`) | | `deploy-calls` | 6 | `deployShippedCalls()` | yes (calls, nodes, mappings; activates never-activated calls) | | `refdata` | 7 | `fetchReferenceData()` | no | | `health` | 9 | `getHealth()` + `validateAllMappings()` | no | | `status` | all | `getSetupStatus()` | no | Env file keys for `creds`: `NS_ACCOUNT`, `NS_CONSUMER_KEY`, `NS_CONSUMER_SECRET`, `NS_TOKEN_ID`, `NS_TOKEN_SECRET`, optional `NS_RESTLET_SCRIPT_ID` / `NS_RESTLET_DEPLOY_ID` (blank = `customscript_cc_control_rl` / `customdeploy_cc_control_rl`). Secrets go into a temp Apex file deleted after the run; they are never echoed. Not automated (needs a person): creating the NetSuite Integration record, role and access token (`install-netsuite.md`); running `install.sh` with the admin's certificate; applying product matches (step 7) and changing sync settings (step 8) — both are judgment calls; the NetSuite UI steps in `netsuite-manual-setup.md`. The skill never runs Reset to shipped defaults. Only step 6 (`deploy-calls`) creates the prebuilt calls; opening Calls & Mappings or a record page never does. Before it runs, every sync start is refused with `CcNsCallRegistry.notDeployedMessage(callKey)`: '"Order Booking" isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then try again.' A call that exists but was never published fails with `CcNsPayloadBuilder.NOT_SET_UP_MESSAGE`: "Continuous Control isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then try again." Running `deploy-calls` fixes both. ## AGENTS.md template Download: https://taggalongtech.github.io/continuous-control/downloads/AGENTS.md. Put it at the root of a customer repository (rename to `CLAUDE.md` for Claude Code). It records the org alias, NetSuite account and versions, and rules: no committed credentials or internal ids; SDF never deletes; don't edit the package's own `CC_` / `CcNs` metadata; change calls in the app (draft → Validate → Activate); ask before acting on production. ## Page actions Under every page title: **Copy page as Markdown** (fetches `.md` to the clipboard), **View .md**, **Open in Claude** (`https://claude.ai/new?q=` with a prompt naming the page's `.md` URL and `llms.txt`), and a link to `llms.txt`. Implemented in `docs/assets/nav.js`. # Demo install runbook Human page: https://taggalongtech.github.io/continuous-control/demo-install-runbook.html Set up a Revenue Cloud org for the Continuous Control demo: install, connect to the shared demo NetSuite account, check the cards, and prepare the QuantumBit catalog and customers. The demo itself is [Demo the recognition methods](demo-recognition-methods.md). > **This is the demo path, not the customer path.** It connects your org to Continuous's **shared demo NetSuite account**, already on ns-v3.3.1 and already holding the QuantumBit items, so there is nothing to install in NetSuite. For a real customer install, use [Install the Salesforce package](install-salesforce.md) and [The Connection wizard](connection-wizard.md). ## Transactions only, no NetSuite account For a demo of the Invoice, Payment, Refund and Credit Memo cards and Flow actions with no Order Booking, skip the shared demo account and use **Simulated NetSuite** (package 3.4.0 and later): `CC_NetSuite_Settings__c.Simulate_NetSuite__c` makes Salesforce answer every NetSuite request itself with made-up ids, so no connection, token or Remote Site Settings are needed. Install, assign both permission sets, turn on **Simulate NetSuite** in wizard step 1, click **Deploy prebuilt calls & mappings** in step 6 and **Stamp every account and product** in step 7 (or run `tools/demo/simulated-setup.apex` with `sf apex run -f simulated-setup.apex -o `), place the sync card on the Invoice, Payment and Refund pages, and activate the packaged `Credit_Memo_Record_Page`. Full recipe and limits: [Demo without a NetSuite account](simulated-netsuite.md#transactions-only-demo-recipe). The rest of this page doesn't apply. ## Before you start - An org with **Revenue Cloud and Billing** enabled: a Developer or trial org from Salesforce, or a scratch org built with the [rlm-base flow](build-test-org.md). You need **System Administrator**. - **The Continuous team connects your org to the demo NetSuite account.** Ask them for a connection and give them your org's My Domain. They set up the connection and create an access token for your org; no NetSuite credentials are handed out. Each org gets its own token, revoked when the org is retired. ## Install 1. Open the install link: `https://login.salesforce.com/packaging/installPackage.apexp?p0=04tPc000000zaubIAA` (Continuous Control 3.4.1, no installation key; choose **Install for Admins Only** or all users). In a scratch org built with the [rlm-scratch-org skill](build-test-org.md): `./install-cc.sh --package 04tPc000000zaubIAA`, which also assigns the permission sets and smoke-checks the install. 2. Assign **Continuous_Control_Admin** and **Continuous_Control** to yourself if you installed from the link. 3. Open **NetSuite Connection** (App Launcher → Continuous Control). ## Connect: the wizard, top to bottom Full detail on every step: [The Connection wizard](connection-wizard.md). 1. **Credentials.** Done with the Continuous team: they type in the account, the consumer key and secret and your org's token. Nothing is preset, so the credentials are entered on every install. The RESTlet ids stay blank. 2. **Remote Site Settings.** Add the two URLs the page lists (Setup → Remote Site Settings). 3. **Test the connection.** Expect "Connected to ", RESTlet `cc_control_rl`, NetSuite package **ns-v3.3.1**, and "Records from this org are stamped as ()". That stamp keeps your org's records apart from every other org's records in the shared account. 4. **Verify installation.** Expect **3.3.1** and **12 record types / 11 scripts / 18 saved searches**. A note may name the connection's role; that one is for the Continuous team. 5. **Environment.** Click **Discover & configure**. It stores the RC Contract / RC Order / RC POB record-type ids and the invoice-run search in the org's link settings. Needs System Administrator. 6. **Deploy prebuilt calls & mappings.** Click it. This step creates and publishes the nine prebuilt calls (Order Booking, Invoice, Credit Memo, Payment, Refund, Product, Customer, Order Line Sync, POB Refresh), and nothing else does. Until you run it, Calls & Mappings is empty and the sync cards point back here. 7. **Reference data.** - **Subsidiary:** match your org's legal entity to the NetSuite subsidiary (the US entity to **Continuous US**). - **Products → NetSuite items:** click **Fetch from NetSuite**, review the matches (the shared account holds the QuantumBit items; exact product codes are proposed), **Apply product matches**, then **Create N products in NetSuite** for anything your orders use that is still unmatched. A product another demo org already created is matched, never created twice. 8. **Sync settings.** The defaults are right for a demo. 9. **Health.** Run it once. Mapping validation should report no problems. ## The cards on your pages The package ships the cards, not the record pages. In a scratch org built with the [rlm-scratch-org skill](build-test-org.md), `install-cc.sh` runs `assets/place-cards.py` right after the install (skip it with `--no-cards`; it's idempotent, so run it again at any time). For each object the sync card serves: | Object | Page used | Card | |---|---|---| | Order | `CC_Order_Record_Page` ("Continuous Order Record Page"): a Header and Right Sidebar copy of the launchpad's `RLM_Order_Record_Page` (Header and One Region), same header and main column | Continuous: NetSuite Sync, top of the right sidebar | | Product2 | `CC_Product2_Record_Page`: the same kind of copy of `RLM_Product_Record_Page` | NetSuite Sync, sidebar | | Invoice, InvoiceBatchRun, Payment | the launchpad's `RLM_*_Record_Page` (already Header and Right Sidebar) | NetSuite Sync, sidebar | | Account | `RLM_Account_Record_Page`, plus a **Payments** related list (`force:relatedListSingleContainer`, `Payment.AccountId`) right after the launchpad's Invoices list on the Billing tab, so invoices and cash show together without the Receivables app | NetSuite Sync, sidebar | | CreditMemo | `Credit_Memo_Record_Page` | NetSuite Sync, sidebar | | Refund | `CC_Refund_Record_Page`, created: highlights panel, Details and Related tabs, Activity in the sidebar | NetSuite Sync, sidebar | | Contract | `CC_Contract_Record_Page`: a Header and Right Sidebar copy of the launchpad's `RLM_Contract_Record_Page` (Header and One Region), same header and tabs | Continuous: Revenue Summary, top of the right sidebar (title Revenue Summary, hide zero-booked POBs) | | PaymentBatchRun | none: Salesforce offers no Lightning record page for it | no card | - Every sync card is on **Sync Mode: Auto** (`callType = Auto`) and moves to the top of the right sidebar if the page carried it elsewhere. - Salesforce won't change an existing page's template through metadata ("You can't change a Lightning page's associated template"), which is why Order, Product and Contract get Header and Right Sidebar copies. The copy stands on its own (no `parentFlexiPage`), because the original's tabs were written against its own parent's regions. The launchpad's pages are never changed except to add the card. - Each page becomes the org default (`CustomObject` View action override, desktop) and the page in the `Continuous_Control`, `RLM_Revenue_Cloud` and `RLM_Receivables_Management` apps (`CustomApplication` action overrides, desktop only; phone assignments are left alone). An app's own assignment wins over the org default inside that app, and the launchpad assigns its RLM pages in `RLM_Revenue_Cloud`. Payment and Refund refuse the org-default override through metadata ("Invalid Type Exception"), so they get their page per app only. - Page layouts, one deploy each: every `OrderItem` layout gets a **Delivery and Progress** section with the five `CC_` fields below (only the ones it lacks), and every `Contract` layout gets the **Performance Obligations** related list (`CC_POB__c.Contract__c`: Name, Product, Revenue Rule, Booked, Billed, Recognized, recognition start and end dates) first among its related lists. A layout that refuses the deploy is reported with the manual step. - The internal `tools/place-demo-cards.py ` runs the same script against any org (a PDE, a sandbox). - By hand: open a record, gear → **Edit Page**, card at the top of the right sidebar, **Save**, **Activation** → **Org Default** (Payment and Refund: **App Default** per app). Hard-reload the browser after activating. The same script turns on Revenue Cloud's **Hide Price Refresh Notification** (Setup → Revenue Settings) and checks the Analytics setting below. Booking writes each order line's NetSuite id back onto the line, and Revenue Cloud marks any order whose lines change after activation as not configured and priced (`Order.ValidationResult = TransactionIncomplete`). The order then shows "Prices aren't up to date. Refresh Prices." although no price changed. Don't click **Refresh Prices** on a booked order: repricing can change its amounts from what NetSuite holds. The setting also hides the banner on quotes; with instant pricing on in the line editor, quote prices refresh as you edit. ## Usage overage bills at $0 until one Analytics setting is on Turn on **Setup → Analytics → Settings → Allow null measure handling in datasets** before any usage is orchestrated. Without it, the usage orchestration's liable-summary job (the Data Processing Engine definition `Create_Liable_Summary_v3`) computes the overage amount as `OverageUnits × IF(ISNULL(commitment rate), NetUnitRate, commitment rate)`; a grant product has no commitment rate, the unmatched left-outer-join measure comes through as 0 instead of null, and `UsageBillingPeriodItem.OverageAmount` is written as 0. Rating itself is right (`UsageRatableSummary.TotalAmount` carries the money); the 0 is copied onto `BillingPeriodItem.Amount` and the invoice line, which Continuous Control then skips as a $0 usage line. Salesforce's consumption-management considerations name the symptom ("the overage amount shown is zero") and this fix. The setting can't be set through metadata or the Tooling API, so `place-cards.py` checks `AnalyticsSettings.ReplaceBlankMeasuresWithNulls` and tells you to click it; rlm-base-dev leaves it off. Liable summaries already invoiced never recompute (the job only takes New / InProgress / LiableSummaryComplete rows), so turn it on first. Once on, it can't be turned off. ## Order Product fields for delivery and progress The demo edits five packaged OrderItem fields: `CC_Delivered__c`, `CC_Delivered_Date__c`, `CC_Fulfilled__c`, `CC_Fulfilled_Date__c`, `CC_Percent_Complete__c`. The package doesn't ship an Order Product layout. In a scratch org built with the rlm-scratch-org skill, `place-cards.py` adds them to every Order Product layout; in another org, add them yourself (Setup > Object Manager > Order Product > Page Layouts). In production these come from Dynamic Revenue Orchestrator, an OMS or a record-triggered Flow calling **Sync Order Lines to NetSuite** (see [Placing the components](placing-components.md#sync-order-lines)). Editing an activated order's lines is what raises the price banner above. ## Revenue rule per product Set `Product2.CC_NS_Revenue_Rule__c` before the first booking. Booking and Product Sync stamp it on the NetSuite item (`custitem_cc_rev_rule`); Update POBs copies the item's rule onto a POB only while the POB's own rule is blank. The rlm-base-dev sample data sets none of them. | ProductCode | Rule | |---|---| | `QB-COMPLETE`, `QB-API`, `QB-API-MGMT`, `QB-API-REQT`, `QB-DB-TOKEN`, `QB-DB`, `QB-SUPP-2000` | Straight-Line | | `QB-TRN-ESSNTLS`, `QB-TRN-FUND` | Delivery | | `QB-SRV-SOW` | % Complete | | `QB-TOKENS-PACK`, `QB-TOKEN-DEF`, `QB-CPU-BLNG`, `QB-DATA-STORAGE-BLNG`, `QB-DATA-THPT-BLNG` | Event-Based | The shared demo NetSuite account holds items other demo orgs also use; a rule set here is stamped on the shared item, so keep these values. ## The three customers Kingsbridge Digital (London), Coralbay Technologies (Sydney) and Northlight Systems (Vancouver) ship with the QuantumBit sample accounts, with addresses and contacts. Set `Account.CurrencyIsoCode = 'USD'` on each before the opportunity exists (the opportunity takes the account's currency, and Continuous Control books one NetSuite currency; multi-currency is on the roadmap). The Continuous team builds the customers with an internal builder (`tools/demo/qb_demo.py`, scenarios `all-methods`, `ramp` and `grant-overage`) that drives the same Revenue Cloud APIs and Continuous Control entry points as the click path. Each deal goes quote → contract → order: the contract is created from the quote with Revenue Cloud's **Create Contract** action (`createContract`, `contractPriceOption = NET_UNIT_PRICE_ONLY`), given its term, renewal term (12 Months), pricing source (Last Transaction) and Source Quote, and activated; the order is created from the quote, linked to the contract while still Draft (contracts link only to Draft orders), then activated. Activation writes the `AssetContractRelationship` rows. ## Give the demo Follow [Demo the recognition methods](demo-recognition-methods.md). ## If something goes wrong | Symptom | Likely cause · fix | |---|---| | The card says Continuous Control isn't set up yet | Step 6 hasn't run. Click **Deploy prebuilt calls & mappings**, then book again. | | No card on a page | The browser cached the old page, or the org wasn't built with `install-cc.sh`. Hard-reload; if it's still missing, run `place-cards.py ` again, or set the page as default in App Builder. | | "Prices aren't up to date" on a booked order | Booking wrote NetSuite ids to the order lines; no price changed. Turn on **Hide Price Refresh Notification** in Revenue Settings. Don't reprice the order. | | Test connection fails with a signature error | The token and the consumer values belong to different integration records. Ask the Continuous team for a new token. | | "Order Line Sync needs one setup step after an upgrade." | The org was upgraded from before 3.3.0; an upgrade adds no value to the restricted `CC_NetSuite_Sync_Run__c.Type__c` picklist. Add **Order Line Sync** to it in Setup. | | A customer lands in NetSuite as "Kingsbridge Digital ()" | Another demo org already booked a customer with that name. The shared account needs a unique Customer ID (`entityid`); the sync run notes it. Nothing to fix. | | Booking ends "Completed with warnings" and lines didn't book | Some lines have no NetSuite item. Go back to step 7, apply the matches or create the items, then book again. Booking again is safe. | | "the user running this sync can't read " | The syncing user can't read a mapped field. Give read access to that field, or remove the mapping row. | | Anything else | Read the sync run's error; see [Monitoring & troubleshooting](monitoring-troubleshooting.md). | ## Clean up after a demo Nothing is required: the records stay in the shared account under your org's Source Org stamp. Tell the Continuous team when you retire the org, so they can revoke its token. # Demo the recognition methods Human page: https://taggalongtech.github.io/continuous-control/demo-recognition-methods.html Three QuantumBit customers show the recognition methods on the QuantumBit Complete Solution (`QB-COMPLETE`), from quote to recognized revenue. Kingsbridge Digital: Straight-Line, an amendment, Delivery, % Complete and Event-Based token overage on one contract. Coralbay Technologies: a three-year ramp with Straight-Line + Even Spread, billed a year in advance. Northlight Systems: the classic usage story, an included allowance and overage billed at the rate card, recognized as billed. Set the org up first with the [Demo install runbook](demo-install-runbook.md) (package 3.4.1+, ns-v3.3.1+, revenue rules, null measure handling, USD accounts). Method reference: [Recognition methods](recognition-methods.md). **Dates move with the calendar.** The figures below are the reference run's (October 2026: Kingsbridge and Coralbay start 2026-06-01, Northlight 2026-08-01, the amendment on the demo day, recognition through 2026-10, the demo month). The internal builder (`tools/demo/qb_demo.py`) shifts every date by the months between that reference month and today, so run in December and Kingsbridge starts August 1, its usage lands in August, the invoices run August to November and Northlight starts October 1. Whole months recognize the same amounts in any month; only day-weighted partial months (the amendment's proration, a mid-month delivery) move by a few dollars. The shift is saved per account at the first step, so a run resumed next month keeps its dates; `--shift-months 0` pins the reference dates. **Every deal goes quote → contract → order.** The contract is created from the quote (Revenue Cloud's **Create Contract** action, `createContract` with `contractPriceOption = NET_UNIT_PRICE_ONLY`, so the quote's net prices become contract item prices). The action fills Status and Start Date; the builder stamps what the launchpad's own New Contract flow stamps: `SourceQuoteId`, `ContractTerm`, `RenewalTerm2 = 12`, `RenewalTermUnit = Months`, `PricingSource = LastTransaction`, gives the contract item prices made from One-Time lines the contract's end date (a contract won't activate with a child price "missing an end date"), and activates the contract. The order is created from the quote (`createOrdersFromQuote`), linked to the contract while still Draft (contracts link only to Draft orders), then activated; activation writes the `AssetContractRelationship` rows. The Contract page shows Source Quote, Renewal Term and Pricing Source, and the Account's **Billing** tab shows its invoices and payments side by side (the Payments list is placed by `place-cards.py`). ## Demo 1: Kingsbridge Digital, four methods on one contract | # | Step | Revenue Cloud / Continuous Control | What to show | |---|---|---|---| | 1 | Quote, contract, order | Place Sales Transaction: `QB-COMPLETE` (Term Annual, 2026-06-01 → 2027-05-31) with `QB-API-MGMT` ×1, `QB-API-REQT` ×250 (Term Monthly), `QB-API` ×3 (Type = Flex), `QB-TRN-ESSNTLS` ×1, `QB-SRV-SOW` ×1 (`AssociatedQuoteLinePricing = NotIncludedInBundlePrice`, $25,000), `QB-DB-TOKEN` ×1 (the bundle's one usage choice). A configuration rule adds `QB-SRV-OG-PSH` ×250 even with default configuration off: delete it. Contract from the quote, then the order (above). Billing schedules come from submitOrder: confirm they exist before booking. | The Contract: Source Quote, term 12, renewal 12 Months. Order $101,000. | | 2 | Record usage, book | Right after activation, record June's token usage on the `QB-DB-TOKEN` asset (2,300 CPU minutes × 5 + 50 TB × 10 = 12,000 tokens, 10,000 granted) and orchestrate until the journals settle. **Book Order to NetSuite** (`order_booking`) → engine leg. | One POB per product with its rule; 3 Event-Based usage POBs at $0 from the usage billing schedules; billing schedule groups mirrored. | | 3 | Amend | `initiateAmendment` on the `QB-COMPLETE` asset as of the demo day: `QB-API` 3 → 5, plus `QB-TOKENS-PACK` ×1 with `BindingInstanceTargetId` = the `QB-DB-TOKEN` asset (a pack can't be sold standalone). Activate, **Book Order to NetSuite**. | Only changed lines travel. QB-API Booked 28,500 + 12,649.32 (2 × 9,500 × 243/365 on 2026-10-01) = 41,149.32, qty 5, one POB across both orders, re-spread prospectively. Tokens Pack: new Event-Based POB, Booked 100. | | 4 | Deliver training | `CC_Delivered__c` = true, `CC_Delivered_Date__c` = 2026-07-15 on the `QB-TRN-ESSNTLS` line → **Sync Order Lines** (`order_line_sync`). | Training POB start = end = 2026-07-15; 8,500 in Jul 2026. The run is an Order Line Sync, not a booking. | | 5 | Report progress | `CC_Percent_Complete__c` = 40 on `QB-SRV-SOW` → **Sync Order Lines** → **Recognize Revenue** (10,000). Then 75 → sync → Recognize (+8,750). | POB % Complete 40 then 75 (rolled up from the line); one catch-up row per engine run in the current period; posted rows never change. | | 6 | Invoice and collect | Invoice Scheduler one run per month (6/1 … 9/1) → **Sync Invoice to NetSuite** each; External payment + `applyPayment` on the first → **Sync Payment to NetSuite**. | The first invoice ($87,250 + tax) carries the year's subscriptions, the SOW, the training and the June token overage: 12,000 tokens − 10,000 granted = 2,000 × $0.50 = **$1,000** on `QB-TOKEN-DEF`. Billed rolls up per POB (92,000); the `QB-TOKEN-DEF` POB reads Booked 1,000 / Billed 1,000. Account → Billing tab: four invoices, one payment, the first Paid In Full. Payments run no engine leg. | | 7 | Close the month | **Recognize Revenue** on the RC Contract through the demo month (2026-10); **Refresh** the Revenue Summary card. The % Complete catch-up row is dated the month the engine runs, so recognize through that month. | NetSuite = card per POB and in total. | ![Kingsbridge Digital's Revenue Summary card after recognition through Oct 2026: one bar per obligation with its rule](https://taggalongtech.github.io/continuous-control/assets/shots/sf-revenue-summary-card.jpg) Expected through 2026-10 (reference run of 2026-10-01, amendment on 10/01, confirmed to the cent on 2026-10-02): Booked 114,749.32, Billed 92,000.00, **Recognized 57,956.17, Deferred 56,793.15**. API-MGMT 5,000.00 (1,000/mo) · DB-TOKEN 5,000.00 · API-REQT 6,250.00 (1,250/mo) · QB-API 13,456.17 (2,375.00 × 5 + the amendment line's first month 1,581.17 = 12,649.32 / 8) · Training 8,500.00 (Jul) · SOW 18,750.00 · token overage 1,000.00 (June, Event-Based, as billed). An amendment on another day moves only the QB-API figures. **Fulfillment is out of the standard demo.** `QB-COMPLETE` has no hardware component (its 20 components are software, usage, services and training; hardware exists only in the `QB-BDL-R750` and `QB-QRack-750` bundles), so the demo stays inside the one bundle. To show Fulfillment, add a hardware bundle as a second root on the same order and flag every one of its lines `CC_Fulfilled__c` + date before **Sync Order Lines**. **Token usage, one month.** Usage goes into the first month only: Revenue Cloud creates the monthly grant for a backdated asset's first period only, so a second or third month would bill every token as overage and misstate the story. Record it right after activation and orchestrate in the same sitting (an orchestration pass is org-wide, and the first one closes every past period of a new asset, empty; the org's `RLM_OrchestrateUsageManagement` schedule runs nightly). Usage billing ignores the scheduler's target date: a rated month lands on the first invoice run after orchestration. The overage bills with an amount only when **null measure handling** is on (runbook); without it `UsageBillingPeriodItem.OverageAmount` is 0 and Continuous Control skips the $0 line. ## Demo 2: Coralbay Technologies, a three-year ramp | # | Step | Revenue Cloud / Continuous Control | What to show | |---|---|---|---| | 1 | Quote | `QB-COMPLETE` 2026-06-01 → 2029-05-31 (36 months) with its default components sent explicitly at 36 months (the configurator's defaults come back 12 months long), and `QB-API` ×10 (Type = Prod) as its OWN line: a ramp deal can't be saved on a bundle component (`FIELD_INTEGRITY_EXCEPTION` on the cloned bundle relationship; help: "You can't create a ramp deal for a bundle product"). Outside the bundle QB-API prices at 15,000/unit, not 14,250. | — | | 2 | Ramp | `POST /connect/revenue-management/sales-transaction-contexts/{quoteId}/actions/ramp-deal-create` `{transactionId, transactionLineId: , subscriptionTerm: 36, subscriptionTermUnit: "MONTHS", segmentType: "YEARLY", executionSettings: {executePricing: true}}` → `ramp-deal-update` on the returned context: segment quantities 10 / 15 / 20 → Place Sales Transaction with the context id to save; `ramp-deal-view` shows 3 segments. In the UI: **Ramp** on the line. Some orgs answer every multi-segment ramp with an internal error (`UNKNOWN_EXCEPTION`, through the API and the line action alike); the builder then writes the three yearly `QB-API` lines itself with the same quantities and dates. NetSuite sees the same segments either way; only Salesforce's ramp-deal grouping is missing. | — | | 3 | Contract, order, book | Contract from the quote, order, activate, **Book Order to NetSuite**. | QB-API POB Booked 675,000.00 (150,000 + 225,000 + 300,000), qty 45 (a ramp sums segment quantities). Update POBs sees base-segment monthly rates vary (12,500 / 18,750 / 25,000) and checks **Even Spread (Full Term)** (`ratesVary`). | | 4 | Invoice and collect | Invoice Scheduler for 6/1 → **Sync Invoice**; External payment + `applyPayment` → **Sync Payment**. | Year 1 in advance: 178,500 (QB-API 150,000 · API-MGMT 12,000 · DB 12,000 · API-REQT 4,500), Paid In Full. Billed 178,500 of 760,500. | | 5 | Recognize | **Recognize Revenue** through 2026-10. | QB-API 93,750.00 = 5 × 18,750 flat (62,500 without Even Spread). Contract total 105,625.00: API-MGMT 1,000/mo, DB 1,000/mo, API-REQT 375/mo. | ## Demo 3: Northlight Systems, an allowance and overage | # | Step | Revenue Cloud / Continuous Control | What to show | |---|---|---|---| | 1 | Quote, contract, order | `QB-COMPLETE` (Term Annual, 2026-08-01 → 2027-07-31) with `QB-API-MGMT` ×1, `QB-API-REQT` ×100 (Term Monthly), `QB-API` ×2 (Type = Prod) and **`QB-DB`** ×1, the non-token database: 10 TB of storage a month included (`ProductUsageGrant`, overage chargeable), no CPU allowance, overage at the base rate card (storage $15/TB and CPU $0.005/min in the USD Term Annual entries). Contract from the quote, order, activate. | Order 58,500. | | 2 | Record usage, book | August: 50 TB of `UR-DATASTORAGE` and 5,000 minutes of `UR-CPUTIME` on the `QB-DB` asset; orchestrate. **Book Order to NetSuite**. | Rating: storage 50 − 10 granted = 40 TB × $15 = **$600**; CPU 5,000 × $0.005 = **$25**. Both read on the account's Usage tab (`UsageRatableSummary`). POBs: Straight-Line for the subscriptions, Event-Based `QB-DATA-STORAGE-BLNG` / `QB-CPU-BLNG` / `QB-DATA-THPT-BLNG` at $0. | | 3 | Invoice and collect | Scheduler runs for 8/1 (advance), 9/1 (August's usage in arrears) and 10/1 → **Sync Invoice** each; payment on the first → **Sync Payment**. | The 9/1 invoice carries the overage lines: `QB-DATA-STORAGE-BLNG` 40 TB $600 and `QB-CPU-BLNG` 5,000 min $25. Sync Invoice gives each its Event-Based POB Booked = Billed = the line amount. | | 4 | Recognize | **Recognize Revenue** through the demo month. | The usage POBs recognize their amount in the invoice's period; the subscriptions 1,000 (API-MGMT), 1,000 (DB), 2,375 (QB-API) and 500 (API-REQT) a month: 9,750 for two months, plus 625 of usage once its invoice is synced. | **Why two months back.** The monthly allowance exists only for a backdated asset's first period, and a usage period settles (`LiableSummaryComplete`, then invoiceable) only once the following month is under way: on 2026-10-01 September's rated rows were still `InProgress` and the 10/1 invoice did not pick them up. The builder therefore starts Northlight two months before the current month and bills the usage with the next month's run. ## Still out of the demo - Quantity and spend commitments (`QB-QTY-CMT`, `QB-MTY-CMT`): their entitlements stay PENDING in Revenue Cloud. - Token commits (`QB-CMT-TKN-*`): they need a separate commitment sale plus a `UsageCmtAssetRelatedObj` link. - `QB-SUPP-2000`: derived pricing leaves it unpriced in the sample data. - Hardware (`QB-BDL-R750`, `QB-QRack-750`): not part of `QB-COMPLETE`, so Fulfillment is left out (above). - There is no token drawdown (recognize-as-consumed) method; prepaid tokens recognize as billed. # Demo without a NetSuite account Human page: https://taggalongtech.github.io/continuous-control/simulated-netsuite.html Simulated NetSuite is a demo and evaluation mode, Salesforce package 3.4.0 and later. With it on, every NetSuite request is answered inside Salesforce with a successful reply and made-up NetSuite ids. No request leaves the org, nothing is created in NetSuite, and no NetSuite account, credentials, Remote Site Settings or NetSuite package are needed. The NetSuite half (ns-v3.3.1) is not involved at all. > **Demos and evaluation only.** Turn the switch off and clear the made-up ids before connecting a real account (see [Going live afterwards](#going-live-afterwards)). Who it is for: Salesforce SEs and partners demoing the Invoice, Payment, Refund and Credit Memo cards and Flow actions without a NetSuite account; evaluators who want to see the cards, Sync Runs and Calls & Mappings before NetSuite is set up. ## The switch | Item | Value | |---|---| | Field | `CC_NetSuite_Settings__c.Simulate_NetSuite__c` (org-level hierarchy custom setting **NetSuite Settings**) | | Label | Simulate NetSuite | | Type / default | Checkbox, `false` | | Help text | "Turn on to demo without a NetSuite account. Every sync completes with made-up ids and nothing reaches NetSuite. Turn off before connecting a real account." | | FLS | Read + edit from the `Continuous_Control` permission set | | Read by | `CcNsSettings.load().simulateNetSuite`, through `CcNsSimulatedNetSuite.enabled()` | | Wizard | The **Simulate NetSuite** switch at the top of wizard step 1, which calls `CcNsConnectionSetupController.setSimulateNetSuite(Boolean)`. Not in step 8. The setup script calls the same method, and the field can also be edited in Setup > Custom Settings > NetSuite Settings > Manage on the default organization level value | ## Turn it on in the wizard The documented path. Three wizard steps on the **NetSuite Connection** tab (`ccNsConnectionSetup`, `CcNsConnectionSetupController`), after installing 3.4.0 or later and assigning `Continuous_Control_Admin` and `Continuous_Control`: 1. **Step 1: Simulate NetSuite switch.** A switch at the top of step 1, styled like the step 8 switches, with help text "Turn on to demo without a NetSuite account. Every sync completes with made-up ids and nothing reaches NetSuite. Turn off before connecting a real account." - `setSimulateNetSuite(Boolean enabled)` (`@AuraEnabled`) upserts `CC_NetSuite_Settings__c.getOrgDefaults()` with `Simulate_NetSuite__c = (enabled == true)` and returns `getSetupStatus()`. `SetupStatus.simulateNetSuite` carries the stored value. A failure throws `CcNsErrors.toUser('Unable to change the Simulate NetSuite setting', e)`. - The LWC handler `handleSimulateToggle` clears the step 3 and step 4 results and toasts "Simulated NetSuite is on" (body "Every sync completes inside Salesforce. Nothing reaches NetSuite.") or "Simulated NetSuite is off" (body "Syncs go to the NetSuite account in step 1 again."). - While on, step 1 hides the credential form and the "How to get these" guide and reads "No credentials or Remote Site Settings are needed. Skip to step 6 to deploy the prebuilt calls, then step 7 to give your accounts and products made-up NetSuite ids." The step 1 chip is `credsConfigured || simulated`, and the step 2 chip reads Complete. - Turning it off restores the credential form. `CC_NetSuite_Cred__c` is never read or written by the switch, so stored credentials work again at once. 2. **Step 6: Deploy prebuilt calls & mappings.** Unchanged: `deployShippedCalls()` then `activateShippedCall(callKey)` per row with `needsActivate`. It makes no callouts. 3. **Step 7: Stamp every account and product.** While simulated, step 7 shows "Simulated NetSuite has no subsidiaries or items to read. Give every account and active product a made-up NetSuite id instead, so invoices, credit memos, payments and refunds can sync right away." and a **Stamp every account and product** button. - `stampSimulatedIds()` (`@AuraEnabled`) returns `StampResult {accounts, products}`. It reads up to 2,000 `Account` rows and up to 2,000 active `Product2` rows whose `NetSuiteRecordId__c` is null (`WITH USER_MODE`), sets `NetSuiteRecordId__c = CcNsSimulatedNetSuite.fakeId(Id)`, `NetSuiteSyncStatus__c = 'Completed'` and `NetSuiteSyncTimestamp__c = DateTime.now()`, and saves with `Database.update(rows, false)`. Partial success: the counts are the rows that saved. Records the running user cannot see or edit are skipped. - Refused while the switch is off (`CcNsSimulatedNetSuite.enabled()` false) with the `AuraHandledException` "Turn on Simulate NetSuite in step 1 first. Made-up ids are only for a simulated connection." A real connection never gets made-up ids from the wizard. - The step shows "N accounts and N products got a made-up NetSuite id." and a **Stamped** toast. Re-click it for orgs with more than 2,000 rows or after adding records; rows that already carry an id are never touched. - While simulated the step 7 chip is `productMatchCount > 0` (active `Product2` with a `NetSuiteRecordId__c`) instead of `legalEntityMatchCount > 0`. **Fetch from NetSuite** still runs and gets empty lists. 4. Place the sync cards: see [Transactions-only demo recipe](#transactions-only-demo-recipe). Steps 2 to 5, 8 and 9 are not needed. Steps 3 and 4 pass without a call if run (see below). ## How it works ### The seam `CcNsRestClient` is the single HTTP seam for every NetSuite request. When `CcNsSimulatedNetSuite.enabled()` is true: - `send(method, url, jsonBody, timeoutMs)` returns `CcNsSimulatedNetSuite.respond(method, url, jsonBody)` before building the `HttpRequest`, so `Http.send` is never called. This covers every RESTlet POST (`postRestlet`) and any custom endpoint a call points at. - `signedGet(fullUrl, timeoutMs)` returns `respond('GET', fullUrl, null)` before `Http.send`. Every GET (the engine Suitelet triggers for headless Update POBs and Update Revenue Plans) answers HTTP 200 with the body `OK simulated`. - The engine leg (`CcNsEngineQueueable`, chained after a booking, invoice or order line sync) builds those Suitelet URLs from `appBaseUrl()`. While simulated it returns the placeholder host `https://simulated.app.netsuite.com` instead of reading `CC_NetSuite_Env__mdt.Base_Url__c`, so a simulated org needs no step 5 links. The leg records `engine: done` with the contracts in `contractsRolled` and puts no warning on the card. Because the answer comes from the same seam, everything above it runs unchanged: the sync cards (`ccNetSuiteSyncCall`), the Flow actions, `CC_NetSuite_Sync_Run__c` records, the queueable chains, and the writebacks of `NetSuiteRecordId__c`, `NetSuiteSyncStatus__c = Completed` and `NetSuiteSyncTimestamp__c`. ### Credentials `CcNsCredentials.load()` fills each blank credential with a placeholder instead of throwing `CredentialException`: | Field | Placeholder when blank | |---|---| | `account` | `SIMULATED` (`CcNsSimulatedNetSuite.SIMULATED_ACCOUNT`) | | `consumerKey`, `consumerSecret`, `tokenId`, `tokenSecret` | `simulated` | | `restletScriptId` | `customscript_cc_control_rl` | Stored values in `CC_NetSuite_Cred__c` are left as they are and never overwritten, so turning the switch off restores the real connection. The request is never signed or sent, so placeholders are never seen by NetSuite. ### The answers `respond` parses the JSON body; a relay envelope (`target` plus a `payload` map) is unwrapped and answered by the inner request's `action`. Every POST answers HTTP 200 with a JSON body that always carries `ok: true`, `version: "3.3.0"` (`CcNsSimulatedNetSuite.VERSION`, at or above every shipped call's `requiresVersion` floor), `errors: []`, `notes: []` and `simulated: true`, plus: | `action` | Extra keys in the answer | |---|---| | `ping` | `pong: true`, `script: "simulated"`, `remainingUsage: "n/a"` | | `installcheck` | `account: "SIMULATED"`, `settingsId: "1"`, `recordTypes: []`, `scripts: []`, `searches: []`, `params: {}`, `engineEnabled: false`, `nativeSoAllowed: false` (wizard step 4 never sends it: `checkNsInstall()` answers "Simulated NetSuite is on. There is no NetSuite package to check" before any call) | | `postinstall` | `settingsId: "1"`, `settingsCreated: false`, `searches: []` | | `setupinfo` | `params: {}` | | `refdata` | `subsidiaries: []`, `currencies: []` | | `itemlist` | `items: []`, `page: 0`, `pageSize: 1000`, `pageCount: 1`, `total: 0`, `hasMore: false` | | `describe` | `fields: []`, `sublistFields: []`, `types: {}` | | `listrecordtypes` | `recordTypes: []` | | `settingsoptions`, `savesettings` | `settings: []` | | `pobs` | `pobs: []` (the POB mirror stays empty) | | `orderlines` | `partial: false`, `continueFrom: null`, `ids: {: , ...}` for every OrderItem line sent, `skipped: []`, `affectedContracts: []`, `counts: {lines: n, updated: n, unchanged: 0, skipped: 0}` where n is the number of lines in `ids`. The run reads as a normal line sync and chains the engine leg for the order's contract when that contract has a NetSuite id. | | anything else: `invoices`, `payments`, `refunds`, `customer`, `items`, `book`, `files`, custom actions | `partial: false`, `continueFrom: null`, `ids: {: , ...}`, `customIds: {}`, `counts: {records: }` | | `book` (in addition) | `contractNsId` = `fakeId(contract.sfId)`, `orderNsId` = `fakeId(orderId)` | `ids` holds the `sfId` values in the payload (maps and lists walked recursively) whose Salesforce object type the action creates a document for, the way the RESTlet answers only the records it wrote: | `action` | Object types answered | |---|---| | `invoices` | Invoice, CreditMemo, BillingScheduleGroup | | `payments` | Payment | | `refunds` | Refund | | `customer` | Account, Contact | | `items` | Product2 | | `orderlines` | OrderItem | | anything else (`book`, custom calls) | Invoice, CreditMemo, BillingScheduleGroup, Payment, Refund, Account, Contact, Product2, Contract, Order, OrderItem, plus the top-level `orderId` | Lines and application rows are never answered: the payment and refund legs write every returned id back as a Payment or Refund, so an application row's id in the answer would fail the run. Values that are not Salesforce ids are skipped. ### The made-up ids `CcNsSimulatedNetSuite.fakeId(sfId)` returns `String.valueOf(10000 + Math.mod(Math.abs(key.hashCode()), 89999))`, where `key` is `String.valueOf(Id.valueOf(sfId))`. The result is a five-digit number, stable per record, and the 15- and 18-character forms of an id give the same value. A re-sync answers the same id, so it updates the same "NetSuite record". ### What changes in the UI - **Deep links hidden.** `CcNsSyncCallController.deepLink` and `invoiceSearchLink` return `null` while simulated, so the card shows no **Open invoice in NetSuite**, **Open payment in NetSuite**, **Open refund in NetSuite**, **Open credit memo in NetSuite** or similar link, and no invoice search link. There is no NetSuite record behind a made-up id. - **Wizard banner.** Under the progress bar, while `SetupStatus.simulateNetSuite` is true: "Simulated NetSuite is on. Every sync completes inside Salesforce and nothing reaches NetSuite. Turn it off in step 1 before connecting a real account." - **Wizard steps 1 and 2** read Complete and need nothing. Step 1 hides the credential form and guide (see [Turn it on in the wizard](#turn-it-on-in-the-wizard)). - **Wizard step 3.** `testConnection()` succeeds and its message reads "Simulated NetSuite is on. Every sync completes inside Salesforce and nothing reaches NetSuite." The Source Org stamp (`CcNsSourceOrg.label()`) still shows. - **Wizard step 4.** **Verify installation** reports "Simulated NetSuite is on. There is no NetSuite package to check; every sync is answered inside Salesforce." without a call. `checkNsInstall()` returns early with `ok`, `installed`, `minVersionOk` and `settingsRowPresent` true, `version` 3.3.0 and `nsAccount` `SIMULATED`, so `Ns_Version__c` is not stamped; a blank stamp refuses nothing. - **Wizard step 6** (Deploy prebuilt calls & mappings) works as always; it makes no callouts. - **Wizard step 7** shows the **Stamp every account and product** button (`stampSimulatedIds()`). Its chip reads Complete once any active product carries a NetSuite id. **Fetch from NetSuite** gets the minimal `refdata` and `itemlist` answers: no subsidiaries, currencies or items. - The other read steps (5, 9) get the minimal answers above. - The card shows the normal green state: pill **Synced**, run status `Completed`. ## Setup script (command-line alternative) For an admin with the Salesforce CLI, a scripted install, or an agent: the same three wizard steps in one run. The wizard is the main path. `tools/demo/simulated-setup.apex` in the repo, published as https://taggalongtech.github.io/continuous-control/downloads/simulated-setup.apex. ``` sf apex run -f simulated-setup.apex -o ``` Building a scratch org with the [rlm-scratch-org skill](build-test-org.md)? `./install-cc.sh --simulated` installs the package and runs the same Apex (the script is embedded in the skill; keep the two copies in step). With the product checkout on the machine (`CC_REPO`, or `~/continuous-control`) it also builds the QuantumBit demo deals with `tools/demo/qb_demo.py`, by default: on a simulated org it runs each scenario through the invoices and payments and skips the engine-only steps; `--no-demo-data` skips them. Or paste it into Developer Console > Debug > Open Execute Anonymous Window. One transaction, calling the wizard's own Apex: 1. `CcNsConnectionSetupController.setSimulateNetSuite(true)` (wizard step 1). 2. If `CC_NS_Call__c` is empty, `CcNsConnectionSetupController.deployShippedCalls()` (wizard step 6) and `activateShippedCall(callKey)` for each row with `needsActivate`. Otherwise logs the existing call count and leaves calls alone. 3. `CcNsConnectionSetupController.stampSimulatedIds()` (wizard step 7), logging the `accounts` and `products` counts. Invoices, credit memos, payments and refunds can then sync without syncing the account or product first. 4. Logs a one-line clean-up script (below). Idempotent: re-running keeps the switch on, skips calls when any exist, and stamps only rows still without an id. ## Transactions-only demo recipe No Order Booking. Shows Invoice, Payment, Refund and Credit Memo sync. 1. Install the package (3.4.0 or later). Assign `Continuous_Control` and `Continuous_Control_Admin`. 2. In the wizard, turn on **Simulate NetSuite** (step 1), click **Deploy prebuilt calls & mappings** (step 6) and click **Stamp every account and product** (step 7). Or run the setup script. 3. Place the **Continuous: NetSuite Sync** card (`ccNetSuiteSyncCall`, Sync Mode / `callType` = `Auto`) on the Invoice, Payment and Refund record pages: **Header and Right Sidebar** template, top of the right sidebar, activated as the default (Payment and Refund: app default per app). See [Placing the components](placing-components.md). 4. Credit Memo: the package ships `Credit_Memo_Record_Page` with the card already at the top of the sidebar (`callType = CreditMemoSync`). Activate it as the org default. 5. Leave Order, Account and Product pages without the card. Accounts and products already carry made-up ids from the step 7 stamp (or the script). Then: | Step | Do | Card button | Flow action | |---|---|---|---| | 1 | Post an invoice | **Sync Invoice to NetSuite** | Sync Invoice to NetSuite | | 2 | Process a payment and apply it to that invoice | **Sync Payment to NetSuite** | Sync Payment to NetSuite | | 3 | Refund that payment (a refund must apply to a synced payment, or the button stays off) | **Sync Refund to NetSuite** | Sync Refund to NetSuite | | 4 | Post a credit memo | **Sync Credit Memo to NetSuite** | Sync Credit Memo to NetSuite | | 5 | Show the four runs on the Sync Runs tab and the calls on Calls & Mappings | | | Each sync writes back `NetSuiteRecordId__c` (made-up id) and `NetSuiteSyncStatus__c = Completed` on the document. See [Flow actions](ref-flow-actions.md) for the action inputs and outputs. ## Limits - Ids are made up. Nothing exists in NetSuite, and there are no deep links. - The Revenue Summary card and the `CC_POB__c` mirror stay empty: `pobs` answers none, and the engine GET answers `OK simulated` without building anything. The engine leg still reports `engine: done`, with no warning. - Reference data in wizard step 7 is empty (`refdata` and `itemlist` answer none), so there are no subsidiaries to sync and no items to match; the stamp stands in for product matching. Step 4's account settings list no choices. - Order Booking (`book`) and Order Line Sync (`orderlines`) also complete with made-up ids, but with no POBs or revenue to show. The transactions recipe leaves them out. - A simulated org must have the switch off and the stamps cleared before connecting a real account. ## Going live afterwards 1. Turn off **Simulate NetSuite** in wizard step 1 (`setSimulateNetSuite(false)`). The credential form comes back, and `stampSimulatedIds()` refuses from then on. 2. Clear the made-up ids on accounts and products in Execute Anonymous. The human page shows this script over several lines, and the setup script prints it as its last log line: ```apex List rows = new List(); for (Account a : [SELECT Id FROM Account WHERE NetSuiteRecordId__c != null]) rows.add(new Account(Id = a.Id, NetSuiteRecordId__c = null, NetSuiteSyncStatus__c = null)); for (Product2 p : [SELECT Id FROM Product2 WHERE NetSuiteRecordId__c != null]) rows.add(new Product2(Id = p.Id, NetSuiteRecordId__c = null, NetSuiteSyncStatus__c = null)); update rows; ``` It clears every non-null `NetSuiteRecordId__c` on Account and Product2, which in a simulated org are all made-up. `NetSuiteSyncTimestamp__c` is left as it is. 3. The clean-up line covers Account and Product2 only. Invoices, credit memos, payments and refunds synced while simulated also carry made-up `NetSuiteRecordId__c` values; clear those too, or connect the real account from a fresh org. 4. Install the NetSuite components ([Install the NetSuite components](install-netsuite.md)) and run every step of [the Connection wizard](connection-wizard.md). Credentials already stored in `CC_NetSuite_Cred__c` were never touched and work again once the switch is off. ## Tests `CcNsSimulatedNetSuiteTest`, including `wizardSwitchTurnsTheSimulationOnAndOff` and `stampGivesUnmatchedAccountsAndProductsIdsOnlyWhileSimulated` (refused while off, stamps only rows without an id). `CcNsSimulatedNetSuite.enabledOverride` (`@TestVisible`) forces the switch in tests without touching the custom setting. # The revenue lifecycle Human page: https://taggalongtech.github.io/continuous-control/revenue-lifecycle.html Continuous Control tells one story in three numbers: **Booked**, **Billed**, and **Recognized**. Every record in the system exists to move one of those numbers, and each one rolls up the same way — from the order line to the Performance Obligation to the Contract. ## Booked, Billed, Recognized | Number | Meaning | Moved by | |---|---|---| | **Booked** | Total contracted value — what the customer committed to | Booking an order (and its amendments) | | **Billed** | What has been invoiced, net of credit memos | Posted invoices and credit memos syncing to NetSuite | | **Recognized** | What has been earned and posted to the general ledger | Recognize Revenue journal entries | A fourth figure, **Deferred**, is derived: Booked minus Recognized — the revenue still on the balance sheet waiting to be earned. Cash (payments and refunds) settles accounts receivable but deliberately moves none of these numbers: revenue follows documents and delivery, not cash. ## How each number is computed **Booked** is the sum of every RC Order Line's amount (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), grouped into [Performance Obligations](performance-obligations.md). When an amendment books, its changed lines merge into the same POBs and Booked moves by exactly the delta. **Billed** is built from evidence, not estimates: every posted invoice line (and credit memo line, negatively) is tied to its POB as an *Obligation Detail* — a junction record naming the transaction, the line, and the amount. Billed on a POB is the sum of its Obligation Details, so you can always drill from the number to the exact invoice lines behind it. **Recognized** is the sum of posted revenue schedule rows — each one stamped with the journal entry that posted it. Like Billed, the number is a rollup of auditable records, never a computed guess. ## The waterfall on the Contract Both systems show the same waterfall. In NetSuite, the RC Contract renders it at the top of the record with the per-POB grouping below. In Salesforce, the **Revenue Summary** card on the Contract shows the identical numbers, mirrored from NetSuite: ![Revenue Summary card in the Contract sidebar: Booked $114,749, Billed $92,000 (80%), Recognized $57,956 (51%), Deferred $56,793, with per-POB bars](https://taggalongtech.github.io/continuous-control/assets/shots/sf-revenue-summary-card.jpg) *The Salesforce Contract's Revenue Summary card — live NetSuite numbers, per-POB breakdown, one-click refresh.* ## Deferred revenue and the GL The accounting model is deliberately simple and fully visible: - Products synced by Continuous Control point their income at your **Deferred Revenue** account — so when an invoice posts in NetSuite, it credits Deferred, not Revenue. - **Recognize Revenue** then draws revenue down from Deferred with one summary journal entry per period: debit Deferred Revenue, credit Revenue, one line pair per POB, each line carrying the customer and the POB for traceability. - Every posting transaction shows its debit/credit register on a **GL Impact** subtab, so the path from invoice to deferred balance to recognized revenue is inspectable record by record. > **Why invoices credit Deferred.** Billing and earning are different events. Billing moves cash expectations (A/R); earning is governed by the POB's [recognition method](recognition-methods.md). Keeping invoices in Deferred until the engine recognizes them is what makes the waterfall truthful. # The data model Human page: https://taggalongtech.github.io/continuous-control/data-model.html Continuous Control mirrors the order graph you already have in Revenue Cloud into a purpose-built set of NetSuite custom records, and lands financial documents as NetSuite's own native transactions. Nothing is remodeled — every NetSuite record is a recognizable twin of a Salesforce record. ## The two sides at a glance | Salesforce | | NetSuite | |---|---|---| | Account · Contacts | → | Customer · Contacts | | Product2 | → | Item | | Contract | → | RC Contract | | Order | → | RC Order | | OrderItem | → | RC Order Line | | BillingScheduleGroup | → | RC Billing Schedule Group | | BillingSchedule | → | RC Billing Schedule | | BillingPeriodItem | → | RC Billing Period | | Invoice · Credit Memo | → | Invoice · Credit Memo (native) | | Payment · Refund | → | Payment · Refund (native) | | CC_POB__c (mirror) | ← | RC POB (source of truth) | Every row but the last is matched Salesforce → NetSuite on the Salesforce id. The last row runs the other way: RC POB is the source of truth in NetSuite, and it mirrors back to the read-only `CC_POB__c` in Salesforce. Every NetSuite box above carries a **Source Org** field — free text, " (<18-character org id>)" — naming the Salesforce org the record came from. The exceptions this diagram doesn't show: NetSuite's run log and the Continuous Revenue Settings record carry none, download items cannot carry it either (the field cannot be added to them), and Customer Refund carries it only on an account whose refund form exposes the field. ## Salesforce objects On the Salesforce side, Continuous Control adds no parallel data model for the order graph — it reads the Revenue Cloud objects you already maintain. It adds: - **Four sync fields** on every synced object (Account, Contact, Product2, Contract, Order, OrderItem, BillingScheduleGroup, Invoice, InvoiceLine, CreditMemo, Payment, Refund, BillingAccount): the NetSuite record id, sync status, message, and timestamp. See [Sync status fields](ref-sync-status-fields.md). - **Performance Obligation** (`CC_POB__c`) — the read-only mirror of NetSuite's POBs, upserted after every sync (with or without the Continuous Revenue Engine on) so the Contract shows live Booked, Billed, and Recognized. NetSuite owns this data. - **NetSuite Sync Run** — one record per sync call, carrying the exact request and response JSON. See [Sync runs & idempotency](sync-runs-idempotency.md). - Delivery/fulfillment signal fields on OrderItem (`CC_Delivered__c`, `CC_Delivered_Date__c`, `CC_Fulfilled__c`, `CC_Fulfilled_Date__c`) and `CC_Percent_Complete__c` (**% Complete**, Percent, precision 5 scale 2, 40 = 40%) that any fulfillment source or person can set; Order Line Sync (`order_line_sync`) sends them to the RC Order Line — see [Recognition methods](recognition-methods.md). - `CC_POB__c.Percent_Complete__c` (**% Complete**, Percent, read-only mirror of the RC POB's `custrecord_cc_pob_pct_complete`, on the Performance Obligation layout's Revenue section). ## NetSuite custom records The NetSuite half adds thirteen custom records (all prefixed `customrecord_cc_`). The revenue-bearing core: | Record | Twin of | Role | |---|---|---| | RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) | Contract | Root of the graph; carries the Booked/Billed/Recognized rollups and the revenue waterfall | | RC Order | Order | One per booked order, including amendment orders | | RC Order Line | OrderItem | 1:1 line mirror: quantities, prices, dates, delivery signals, % Complete, amendment action | | RC POB | — | The unit of revenue; created by the engine, not the sync — see [Performance Obligations](performance-obligations.md) | | RC Obligation Detail | — | Junction from a NetSuite invoice/credit memo line to a POB; the evidence behind Billed | | RC Revenue Plan / Schedule | — | One plan per POB with monthly schedule rows; posted rows carry their journal entry | | RC Billing Schedule Group / Schedule / Period | BillingScheduleGroup / BillingSchedule / BillingPeriodItem | The [billing mirrors](billing-mirrors.md) | Supporting records: a run log for engine executions, and a settings record for engine behavior. Financial documents — invoices, credit memos, payments, refunds — are *not* custom records: they land as NetSuite's own native transactions so A/R, aging, and the GL work exactly as NetSuite users expect. The full field-level list is in the [NetSuite record reference](ref-netsuite-records.md). ## How records match across systems Every NetSuite record Continuous Control touches carries an external id derived from its Salesforce id — the raw id for core records, prefixed forms for billing records. Every sync resolves records by external id *before* writing, so a record that already exists is updated in place and one that does not is created. The response maps every Salesforce id to its NetSuite internal id, which Salesforce stamps back onto the source records. The result: the pairing is bidirectional, stable, and safe to re-run — see [Sync runs & idempotency](sync-runs-idempotency.md). # Performance Obligations Human page: https://taggalongtech.github.io/continuous-control/performance-obligations.html The Performance Obligation — POB — is Continuous Control's unit of revenue: one promise to a customer, with its own Booked, Billed, and Recognized amounts, its own recognition method, and its own revenue plan. Order lines group into POBs automatically; you never assign them by hand. ## What a POB is ASC 606 asks you to account for revenue per *performance obligation*, not per invoice or per order. In Continuous Control that concept is a first-class NetSuite record. Each POB carries: - The waterfall: **Booked** (sum of its lines), **Billed** (sum of its Obligation Details), **Recognized** (sum of its posted schedule rows). - A **recognition method** — inherited from the product, overridable per POB. See [Recognition methods](recognition-methods.md). - Recognition start and end dates, quantity, delivered/fulfilled rollups, and a **% Complete** (typed on the POB, or rolled up from its order lines). - Its member RC Order Lines (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), revenue plan, billing links, and recognition journal lines — each on its own subtab. ## Automatic grouping The **Update POBs** process groups every RC Order Line on a contract by its **product**. A line joins the contract's existing POB for that product, or a new POB is created if it is the product's first line. That single rule has useful consequences: - An [amendment](amendments.md) line for a product merges into the same POB as the base line — Booked moves by the delta, history intact. - A POB can span multiple orders on the contract (base order plus amendments). - There is no per-line assignment field, no unassigned queue, and nothing to misfile. POB names are enforced by the engine as `POB-{id} — {item name}`, so lists read consistently everywhere. Usage products get a special case: a billing-schedule item with no order line (a consumption SKU) gets its own Event-Based POB created automatically, so usage revenue "creates itself" as it bills. ## POB amount and quantity **Amount (Booked)** is simply the sum of member line amounts. **Quantity** is the total contracted units net of amendments, computed shape-aware: - A flat multi-year subscription (1 unit per year × 3 years) keeps the peak — quantity 1, not 3. - A genuine ramp (100 / 200 / 300 across segments) sums to the full contracted units — 600. - Amendment deltas then add or subtract (an upsell of +500 on a 100-unit base → 600). Quantity is a stable lifetime figure for reporting; it never affects amounts, billing, or recognition. ## Delivered and fulfilled signals For Delivery and Fulfillment methods, the POB waits for evidence. Salesforce OrderItem records carry source-agnostic signal fields — the `CC_Delivered__c` family: `CC_Delivered__c` / `CC_Delivered_Date__c` and `CC_Fulfilled__c` / `CC_Fulfilled_Date__c` — that your fulfillment process sets, from Dynamic Revenue Orchestrator, an OMS, a Flow or a person. A fifth field, `CC_Percent_Complete__c` (**% Complete**, Percent, 40 = 40%), carries progress for % Complete obligations. Once *every* member line carries the signal, Update POBs anchors the POB's recognition dates to the actual date, and the revenue plan schedules 100% in that period. **Order Line Sync sends them** (Salesforce 3.3.0 + ns-v3.3.0). The **Sync Order Lines** button on the Order's sync card, or the **Sync Order Lines to NetSuite** Flow action, sends those five fields for the order's booked lines to their RC Order Lines (`custrecord_cc_line_delivered`, `custrecord_cc_line_delivery_date`, `custrecord_cc_line_fulfilled`, `custrecord_cc_line_fulfilled_date`, `custrecord_cc_line_pct_complete`). No re-book. NetSuite writes only what changed, and a blank clears. The run then re-runs Update POBs, Update Revenue Plans (engine on) and the POB mirror for that contract whenever NetSuite found any of the lines. Update POBs rolls the lines' % Complete into the POB's `custrecord_cc_pob_pct_complete`: the |amount|-weighted average over all member lines, a line with no % counting as 0; once any member line carries one, Salesforce owns the POB's %. Details: [Recognition methods](recognition-methods.md), [Anatomy of the payload](payload-anatomy.md#orderlines). ![Kingsbridge Digital's Additional API POB in NetSuite: the waterfall, the Straight-Line rule, and its two order lines, the original and the amendment](https://taggalongtech.github.io/continuous-control/assets/shots/ns-pob-record.jpg) *A POB in NetSuite: waterfall on the main body, member lines, revenue plan, and billing links on subtabs.* ## The Salesforce mirror After every sync (with or without the Continuous Revenue Engine on), the contract's POBs mirror back to Salesforce as **Performance Obligation** (`CC_POB__c`) records: `Contract__c` (the parent Contract, relationship name **POBs** — `Contract.POBs__r` in a query or formula), `Product__c`, the waterfall `Booked__c` / `Billed__c` / `Recognized__c`, `Total_Quantity__c`, `Revenue_Rule__c`, `Rec_Start_Date__c` / `Rec_End_Date__c`, the delivered and fulfilled flags with their dates (`Delivered__c` / `Delivered_Date__c`, `Fulfilled__c` / `Fulfilled_Date__c`), and `Percent_Complete__c` (**% Complete**, read-only, on the layout's Revenue section; ns-v3.3.0's `pobs` action returns it as `pct`), plus the usual `NetSuiteRecordId__c` / `NetSuiteSyncTimestamp__c` pairing fields. The mirror powers the Contract's Revenue Summary card and standard Salesforce reporting. NetSuite owns the data; the mirror is read-only by convention and re-upserted on every refresh. See [Writebacks & the POB mirror](writebacks-pob-mirror.md). # Recognition methods Human page: https://taggalongtech.github.io/continuous-control/recognition-methods.html Six recognition methods cover the common revenue patterns. The rule is set once on the product, inherited by the POB, and drives how the revenue plan spreads the POB's amount across periods. To see the methods end to end with expected numbers, follow [Demo the recognition methods](demo-recognition-methods.md): Kingsbridge Digital (Straight-Line, amendment, Delivery, % Complete, Event-Based) and Coralbay Technologies (three-year ramp, Even Spread). ## The method list | Method | Recognizes | Typical use | |---|---|---| | **Straight-Line** | Ratably over each line's own service dates, day-weighted at the edges | Subscriptions, licenses, support | | **Straight-Line + Even Spread** | The POB total flat across the full term | Ramp deals a customer wants smoothed | | **Delivery** | 100% in the period the delivered signal arrives | One-time services, SOWs, training | | **Fulfillment** | 100% in the period the fulfilled signal arrives | Shippable goods, provisioned hardware | | **Event-Based** | As billed — every invoice line is a recognition event | Usage and consumption SKUs | | **% Complete** | Cumulatively, to a percentage set on the order products in Salesforce or on the POB in NetSuite | Projects and milestone work | ## Rule inheritance: item → POB → plan The chain is simple and visible at each step: 1. The **product** (NetSuite item) carries the Continuous Rev Rec Rule — set at sync time from the product's configured rule, or by an admin. 2. **Update POBs** copies the item's rule onto a POB *only when the POB's rule is empty* — a value set directly on the POB always wins. The fallback is Straight-Line. 3. **Update Revenue Plans** copies the POB's rule onto the plan and builds the schedule accordingly. To change how something recognizes, change the rule on the POB (for one contract) or on the item (for everything new), then re-run the engine — unposted schedule rows rebuild, posted rows are never touched. ## Straight-Line and day weighting Straight-Line amortizes *each member line over its own date range*, then sums by month. Partial months at the edges are day-weighted: a line starting mid-month recognizes only the days covered. This matters for mid-term changes — a cancellation effective the 13th credits exactly the remaining days of that month, and the schedule ends there rather than dribbling on. ## Even Spread for ramp deals A ramp deal (say Coralbay Technologies' Additional API at 10 / 15 / 20 units: $12,500/mo year one, $18,750 year two, $25,000 year three) normally recognizes following the ramp. Checking **Even Spread (Full Term)** on the POB flips it to recognize the total evenly — flat monthly across the whole term. Update POBs turns this on automatically when it detects a ramp (member segments whose monthly rates vary); it only ever flips the box on, never off, and posted periods stay frozen when the spread changes. ## Delivery and Fulfillment These methods wait for evidence from Salesforce. Any fulfillment source — Dynamic Revenue Orchestrator, an OMS, a Flow, a person — sets the delivered/fulfilled checkbox and date on the Order Product (`OrderItem.CC_Delivered__c` / `CC_Delivered_Date__c`, `CC_Fulfilled__c` / `CC_Fulfilled_Date__c`). When *every* line in the POB carries the signal, the engine anchors the POB's recognition dates to the actual date and schedules 100% in that period. Until then, the plan stays empty and nothing recognizes. **Getting the signal to NetSuite: Order Line Sync (Salesforce 3.3.0 + ns-v3.3.0).** The shipped call `order_line_sync` sends each booked line's Delivered, Delivered Date, Fulfilled, Fulfilled Date and % Complete to its RC Order Line (`customrecord_cc_order_line`, matched by external id = the OrderItem id), without re-booking the order. Two ways to start it: - The **Sync Order Lines** button on the Order's sync card (`ccNetSuiteSyncCall`, `CcNsSyncCallController.startOrderLineSync`) sends every line of the order. - The **Sync Order Lines to NetSuite** Flow action (`CcNsSyncOrderLinesInvocable`), built for a record-triggered Flow on Order Product; lines of one order share one run. See [Flow actions](ref-flow-actions.md) and [Placing the components](placing-components.md#sync-order-lines). NetSuite writes only the fields that changed on lines a booking already made (it never creates a record). The values mirror Salesforce, blanks included: un-checking Delivered and syncing clears the delivery date in NetSuite. The run then re-runs the engine leg for the order's contract whenever NetSuite found any of the lines, changed or not: **Update POBs** (the delivered/fulfilled rollups and the date anchoring), **Update Revenue Plans** (only while the Continuous Revenue Engine is on), and the POB mirror. So syncing again is also the repair when POBs look stale. A booking still carries the same four fields in its `lines[]` too; Order Line Sync is the path that doesn't resend the whole graph. Payload and response: [Anatomy of the payload](payload-anatomy.md#orderlines). > **Revenue Cloud's price banner.** Editing an activated order's Order Product fields (a person, a Flow, DRO) can make Revenue Cloud flag the order and show "Prices aren't up to date". No price changed. Order Line Sync itself writes nothing back to OrderItem (its result lives on the sync run), so the banner comes from the edit, not the sync. Turn on **Hide Price Refresh Notification** in Setup > Revenue Settings and don't reprice the order. See [Monitoring & troubleshooting](monitoring-troubleshooting.md). ## Event-Based (recognize as billed) An Event-Based POB recognizes exactly what has been billed, when it billed: every invoice line tied to the POB becomes a schedule row in the period of the invoice date (credit memos negative). This is how usage SKUs work — the consumption product's POB is created automatically when its billing schedules arrive, Booked equals Billed by construction, and each usage invoice becomes recognizable revenue in its own period. ## % Complete A % Complete POB carries a percentage (`custrecord_cc_pob_pct_complete`, a NetSuite PERCENT field) that you update as the work progresses. Each engine run tops the schedule up to *percentage × POB amount* with a catch-up row in the current period. A lower percentage never reverses posted revenue: the catch-up is only ever positive. **The value is always read as percentage points (ns-v3.3.0).** 40 is 40%, and 0.4 is 0.4%, whoever set it. That matches what NetSuite shows on the POB. Update Revenue Plans (`cc_rev_plan.js pctCompletePob`) reads `getValue` as points with no fraction shortcut, so enter 40 for 40%. The percentage can come from either side. Pick one place per POB. **From Salesforce (Order Line Sync, Salesforce 3.3.0 + ns-v3.3.0).** Set **% Complete** (`OrderItem.CC_Percent_Complete__c`, Percent, 40 = 40%) on each Order Product, then click **Sync Order Lines** or let a Flow call **Sync Order Lines to NetSuite**. NetSuite writes it to the RC Order Line (`custrecord_cc_line_pct_complete`), and the engine leg's **Update POBs** rolls the member lines up into the POB's % Complete: - POB % = Σ(line % × |line amount|) ÷ Σ|line amount| over **all** member lines, where a line with no % counts as 0 (work not started). When every member line is $0, it is the plain average over every line. Rounded to 2 decimals. - **Ownership.** Once any member line carries a % from Salesforce, Salesforce owns the POB's %: a value typed on the POB in NetSuite is replaced on the next Update POBs run. When no member line carries one, Update POBs leaves the POB's own value alone, so a % entered in NetSuite keeps working. - Clearing % Complete in Salesforce and syncing clears the line's value in NetSuite. Clearing it on every line of a POB hands the POB back to NetSuite at its last value. - A booking also carries `pctComplete` on its lines, so a first booking can land a %. A booking never clears one; only Order Line Sync does. - NetSuite refuses a value outside 0–100 for that line only (the rest of the order still lands). Update Revenue Plans runs as part of the engine leg while the Continuous Revenue Engine is on; **Recognize Revenue** stays a deliberate step (or the scheduled close). **From NetSuite.** Update the percentage on the POB → **Update Revenue Plans** → **Recognize Revenue**; bump it again next month and re-run to recognize the increment. The POB field's help reads: "Percent of work complete, which Update Revenue Plans recognizes. Enter it here, unless Salesforce sends it on the order lines." The Salesforce mirror carries the POB's value back as `CC_POB__c.Percent_Complete__c`, and the Contract's Revenue Summary card shows it on a % Complete obligation ("% Complete · 40%"). > **All methods share the same guarantees.** Posted schedule rows are frozen at their recognized amounts — re-running a plan re-spreads only the remainder over unposted periods (a prospective change, per ASC 606-10-25-13(a)). If a revision means recognized already exceeds the new total, the engine posts a single catch-down adjustment in the current open period rather than restating history. # Billing mirrors Human page: https://taggalongtech.github.io/continuous-control/billing-mirrors.html Salesforce is the billing system of record. So NetSuite users still get full billing visibility, Continuous Control mirrors the three-level Salesforce billing structure — groups, schedules, and periods — one-for-one into NetSuite, always from real data, never forecast. ## What gets mirrored | Salesforce | NetSuite mirror | Carries | |---|---|---| | BillingScheduleGroup | RC Billing Schedule Group (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) | Billing method (amount/evergreen/usage), type (advance/arrears), term, total billed and pending, next bill date | | BillingSchedule | RC Billing Schedule | Per-line schedule: frequency, period amount, totals, status, original/amendment category and chaining | | BillingPeriodItem | RC Billing Period | One row per real billing period: dates, amount, status, and — once billed — a link to the NetSuite invoice that consumed it | The mirrors are visibility-only: NetSuite renders them on the RC Contract and the customer record, but nothing in NetSuite bills from them. Billing runs stay in Salesforce. ## Groups, schedules, and periods Mirroring follows the Salesforce shape exactly. Groups are scoped to the *customer* (as in Salesforce), so they appear on the NetSuite customer record; schedules link to their group, their order line, and — once the engine runs — their POB; periods link to their schedule and group. Some group fields Salesforce leaves empty (start, end, item) are derived from the member schedules so the NetSuite header reads completely. Periods mirror only what Salesforce has actually generated: future periods appear when a Salesforce billing run creates them, and a re-sync removes any period rows Salesforce no longer has. Nothing is projected. ## How invoicing updates the mirrors When invoices sync, the payload carries a billing refresh alongside the documents: exactly the schedules those invoices consumed (found through each invoice line's billing period), their groups, and *all* of those schedules' periods. So immediately after an invoice sync, NetSuite shows the post-billing state — updated billed/pending amounts, period statuses, and each consumed period linked to the NetSuite invoice that billed it. See [Invoices & credit memos](invoices-credit-memos.md). ## Amendment schedules Amendment orders create new Salesforce billing schedules chained to the originals. The mirrors preserve that: an amendment schedule carries its category and a link to the original schedule it modifies, and it rides in the same group — so the group header's billed/pending totals reflect base plus amendment together. > **Relationship to revenue.** Billing mirrors inform people; they never drive recognition. Recognition is computed from the POB's [method](recognition-methods.md) and, for Event-Based POBs, from actual invoice lines (Obligation Details) — not from schedules. That separation is what keeps "what we plan to bill" and "what we have earned" honestly distinct. # Sync runs & idempotency Human page: https://taggalongtech.github.io/continuous-control/sync-runs-idempotency.html Two design decisions make Continuous Control safe to operate: every sync is a first-class record you can inspect, and every cross-system match is by stable id. Together they mean a failed or repeated sync never corrupts anything — you just run it again. ## The sync run record Every sync — booking, order line sync, invoice, credit memo, payment, refund, product, customer, POB refresh — creates a **NetSuite Sync Run** record before anything is sent. It carries: | Field | What it holds | |---|---| | Type | Order Booking · Invoice Sync · Credit Memo Sync · Payment Sync · Refund Sync · Product Sync · Customer Sync · Order Line Sync · POB Refresh · Custom Call. An *Order Line Sync* run is scoped by `Scope_Id__c` = the Order and never sets `Order__c`, so it never reads as the order's booking and never blocks a re-book; the Sync Runs tab filters it as its own type. *Cash App Pull* is also a stored value, but the call is hidden and the Sync Runs tab leaves its runs out. A *Product Sync* run started from the Connection wizard's **Create them in NetSuite** carries a batch of up to forty products: its Scope Id is the batch's first product and the payload lists them all | | Status | `Queued` → `Processing` → `Completed`, `Completed with warnings`, or `Failed`. A booking lands on **Completed with warnings** when the order landed in NetSuite but one or more lines have no matching NetSuite item — the card shows **Booked with warnings**; it counts as finished everywhere `Completed` does (idempotency, retention, success-rate). An Order Line Sync lands there when a line was skipped (no RC Order Line), refused by NetSuite, or left out at start for having no NetSuite id | | Payload JSON | The exact document POSTed to NetSuite — what you see in the card's "View JSON payload" viewer | | Result JSON | The NetSuite response: ok flag, per-section counts, the id map, and (later) the engine results merged in | | Error | Errors and warnings, one per line | | Scope | The record that triggered it (the order, invoice, batch run, payment…), started/completed timestamps | The [Sync Runs dashboard](monitoring-troubleshooting.md) is a live view over these records. Because the payload is stored *before* the call, a run that dies mid-flight still shows exactly what it attempted. ## External ids: how records match Every NetSuite record Continuous Control writes carries an **external id derived from the Salesforce id** — the raw 18-character id for customers, contacts, products, contracts, orders, and lines; prefixed forms (`bsg-`, `bs-`, `bp-`) for billing records. Before writing anything, the sync resolves all incoming external ids against NetSuite in bulk; records that resolve are updated in place, the rest are created. Two details protect manual work in NetSuite: - **Names are set only on create.** Rename a record in NetSuite and the rename survives every re-sync. - **Empty values never clear fields.** A blank in the payload leaves the NetSuite field untouched. Two deliberate exceptions mirror Salesforce instead: the packaged address rows ([Anatomy of the payload](payload-anatomy.md#how-field-mappings-ride-along)), and **Order Line Sync** (`orderlines`), whose five line fields clear in NetSuite when cleared in Salesforce — un-checking Delivered clears the delivery date, clearing % Complete clears the line's value. **A customer or contact is never matched by name.** One that resolves by neither its NetSuite id (the Account's **NetSuite Record Id**, checked by `trustedNsId`) nor its external id is created. NetSuite requires a unique entity id (Customer ID, `entityid`), and an account without auto-generated customer numbers fills it from the name, so the create fails ("A customer record with this ID already exists") when any other customer has that name: another Salesforce account's, another org's in a shared account, or one entered by hand. `saveEntity` (`cc_action_book.js`, used by booking and Customer Sync) retries only that error, with the entity id ` ()` cut to 83 characters, and adds a run note. Company name, contacts and transactions keep the plain name; any other save error still fails the run. To book onto a customer that already exists in NetSuite, set the Account's **NetSuite Record Id** to its internal id before the first booking (ns-v3.3.0). Two record families key on more than the Salesforce id alone: - **A performance obligation's** external id is `pob--` (audit N17, ns-v3.2.1), resolved with SuiteQL rather than the search API so a same-execution create is never missed. NetSuite refuses a second record with the same key, so two Update POBs runs that overlap land on one POB, not two — the run that loses the race re-reads and updates the POB the other run made. A POB made before ns-v3.2.1 gets the key stamped in place, with no change to its id or name, the next time Update POBs runs on its contract; a duplicate an earlier release left is not removed, and the keyed POB (or the oldest one) is the one lines and billing use. See [The engine Suitelets](engine-suitelets.md#update-pobs-cc_pob_update). - **An invoice or credit memo line** carries its Salesforce line id in `custcol_cc_sf_line_id` (audit N18, ns-v3.2.1). A re-sync matches the document's lines by that key instead of replacing the sublist, so a line's internal id — and the Obligation Detail and Event-Based plan signature keyed on it — stays put across an unrelated change. See [Invoices & credit memos](invoices-credit-memos.md#lines-matched-by-key). ## Why re-runs are safe Idempotency falls out of the id discipline: re-booking an order updates the same records it created the first time; re-syncing an invoice updates the same invoice. The response id map is stamped back onto Salesforce records the same way — an already-stamped record just gets the same id again. There is no duplicate risk, no ordering to respect, and "when in doubt, sync again" is the correct operational instinct. A re-sync that changes nothing writes nothing (audit N26, ns-v3.2.1). Booking, Update POBs, and the invoice and credit memo line match all compare against a live read of the record's current values first, and write only the fields that differ — so a repeat run doesn't reset a NetSuite record's last-modified date or fire an item's user events for no reason. A same-value Source Org stamp alone is never treated as a change. Anything the compare can't read — an admin's own mapping field, or a percent the read can't parse (from ns 3.3.0 a percent is read as `(field * 100)`, in points, because SuiteQL answers a percent as a fraction; a blank one reads as null) — is always treated as changed, so a doubt only ever costs an extra write, never a skipped one. Guards on the Salesforce side keep concurrent operators from tripping over each other: a second booking of the same order while one is in flight is refused, and a run stuck for more than ten minutes is treated as stale and superseded by the next attempt. An Order Line Sync has its own guard (`failStaleOrThrow` narrowed to Type `Order Line Sync` on the order's `Scope_Id__c`: "An order line sync for this order is already in progress."), and refuses while a live booking of the same order runs ("This order is being booked to NetSuite right now. Sync its lines when the booking finishes."), since both write the same RC Order Lines. A line sync never blocks a booking. Order Line Sync is safe to repeat: an unchanged line writes nothing, and a repeat re-runs the engine leg, which is the repair when POBs look stale. ## Several Salesforce orgs, one NetSuite account Every Continuous record NetSuite stores carries a free-text **Source Org** field, stamped ` ()` — for example `quantumbit.my.salesforce.com (00D5f000001AbCdEAO)` — the org's My Domain host and its stable 18-character org id, sent as `orgBaseUrl` and `orgId` on every push. It answers the question "which Salesforce org wrote this?" when one NetSuite account is the sync target for more than one org. Each Salesforce org creates its own NetSuite customer, contract, and order records — **by design**, not as a limitation. External ids are derived from Salesforce record ids, and two different orgs never share a Salesforce id, so there is nothing to deduplicate or match across orgs; the Source Org stamp is how you tell the resulting records apart afterward, not how the sync avoids creating them. That makes the safe shape **one production org per NetSuite account per business unit** — each with its own Connection wizard run. **Never point a sandbox and its production org at the same NetSuite account.** The two would compute identical external ids for what Continuous treats as different customers, contracts, and orders, and their syncs would overwrite each other's work. Because it is a plain text field, filter a saved search on Source Org with **"contains"** rather than "is" — matching the org id substring is what separates the orgs sharing an account cleanly, and survives a My Domain rename since the id is the stable part of the value. ## Partial runs and resume NetSuite meters server-side work per request, checked before every row rather than only between sections, so even one oversized section stops cleanly instead of failing outright. The endpoint returns everything completed so far plus a *continue from* marker — a section name, or a section and the row inside it — and the Salesforce side automatically sends a follow-up call that resumes from that point, re-entering only the rows not yet done. Same run record, results merged. Invoice, payment, and refund batches behave similarly, syncing in [configurable chunks](sync-settings.md) where each document is its own safe unit — it lands whole, or is left for the next leg. You see one run; the legs are an implementation detail. The full mechanics are in [Inside the RESTlet](inside-the-restlet.md). # One engine, two switches Human page: https://taggalongtech.github.io/continuous-control/one-engine-two-options.html Continuous Control ships one revenue engine, the **Continuous Revenue Engine**, and one switch for it on each side of the wire. Everything that keeps the contract's numbers true runs whether the engine is on or off; only the part that builds plans and posts journal entries is switchable. That is what lets an account with its own recognizer keep exactly one system recognizing revenue. ## What the Continuous Revenue Engine is The engine is the pair of processes that turn a contract's Performance Obligations into recognized revenue, plus their scheduled counterpart: - **Update Revenue Plans** builds one revenue plan per POB and its monthly schedule rows from the POB's [recognition method](recognition-methods.md). Every plan recognizes off the POB's **Booked** amount; Deferred is Booked minus Recognized. - **Recognize Revenue** posts one summary journal entry per period, debit Deferred Revenue and credit Revenue, after a preview. - The **scheduled portfolio close** runs the same plan and recognize steps across every contract on your cadence. Allocation across obligations is not part of the engine. Each POB recognizes the amount its lines booked; if your accounting requires allocating a contract's price across obligations, that belongs to the system that owns it, NetSuite Advanced Revenue Management or Revenue Cloud, not to Continuous Control. The operator view of the engine is [Running the revenue engine](revenue-engine.md); the implementation reference is [The engine Suitelets](engine-suitelets.md). ## What always runs The engine switch never touches the data. Whatever its position, every booking, invoice sync, scheduled run, and button click still does the following: - **Booking the record graph.** A booking from Salesforce still creates or updates the RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), RC Orders, RC Order Lines, and the billing mirrors in NetSuite. - **Update POBs.** Lines group into POBs, Booked rolls up, posted invoice and credit-memo lines map to Obligation Details, Billed rolls up, and billing schedules link to their POBs. This is data maintenance, not recognition, and it runs from the button, the Suitelet, the scheduled run, and every Salesforce sync. - **Billing rollups.** Booked and Billed on every POB and on the contract header stay current, so the waterfall on both sides is always true even when nothing is being recognized. - **The Salesforce POB mirror.** The contract's POBs still mirror back to Salesforce as Performance Obligation records after every sync, powering the Revenue Summary card and standard reporting. See [Writebacks & the POB mirror](writebacks-pob-mirror.md). ## The two switches The engine can be turned off on either side, and **either side off wins**: the engine runs only when both switches leave it on. | Side | Where | Switch | |---|---|---| | NetSuite | **Continuous Revenue Settings** record (the singleton created by the wizard's post-install step) | **Disable Continuous Revenue Engine** checkbox, unchecked by default | | Salesforce | [Connection wizard](connection-wizard.md) step 8, *Sync settings* | **Continuous Revenue Engine** switch, on by default | ### NetSuite: Disable Continuous Revenue Engine With the box checked: - The **Update Revenue Plans** and **Recognize Revenue** buttons disappear from the RC Contract, and **Update Revenue Schedule** disappears from the POB. **Update POBs** and **Update POB** stay. - The two engine Suitelets, called by any path, answer `DISABLED Continuous Revenue Engine is off in Continuous Revenue Settings — Update POBs still runs; revenue plans, schedules and journal entries are not produced.` and do nothing. - The scheduled portfolio run (and **Run Portfolio Close Now**) runs Update POBs only and logs the stage **Update POBs (engine off)** in the run log. - The RC Contract's **Revenue plans by POB** card shows the same sentence in place of the plan table. - Booking from Salesforce still creates the Contract, Orders, Lines, and POBs. The wizard's *Verify installation* reads this switch through the `installcheck` action (`engineEnabled`) and shows a hint when NetSuite reports the engine off. ### Salesforce: Continuous Revenue Engine The wizard switch writes `Disable_Revenue_Engine__c` on the sync settings. Its help text says it exactly: when on, a booking or invoice sync asks NetSuite to rebuild the contract's revenue plans after Update POBs; when off, Update POBs and the POB mirror still run and no revenue plans, schedules, or journal entries are produced. With the switch off, the Contract's Revenue Summary component reads *"Continuous Revenue Engine is off: Booked and Billed stay current, no revenue plans are built."* ### What a run reports The engine leg is always queued after a booking or invoice sync that has a contract to roll, and after an order line sync whenever NetSuite found any of its lines; the switches decide what it does once it runs. The sync run's result records the outcome under `engine`: | `engine` | Meaning | |---|---| | `"done"` / `"done with warnings"` | Update POBs, Update Revenue Plans, and the mirror all ran; `plansBuilt` is present | | `"disabled"` | The Salesforce switch is off; Update POBs and the mirror ran, no plans | | `"disabled in NetSuite"` | NetSuite's switch is off; Update POBs and the mirror ran, the plans Suitelet answered DISABLED | | `"skipped"` + `engineSkipReason` | Nothing to roll: no contract, the Order landed as a native transaction, or zero lines | `contractsRolled` and `pobsMirrored` are always present; `plansBuilt` is omitted whenever the engine did not run. A run with the engine off is a success, not a warning. ## Native Sales Orders Independently of the engine, NetSuite can keep **one cumulative native Sales Order per RC Contract**, built from the contract's RC Order Lines. Amendments append lines to it, each line keyed by its RC Order Line, and the document is stamped **Do Not Bill (Continuous)** on create so it never becomes a second source of invoices while Salesforce is billing. It writes item, quantity, rate, amount, description, discount percent, and billing frequency, and nothing else: it needs no NetSuite revenue feature at all. It has its own switch pair: - NetSuite: **Disable Native Sales Orders** on Continuous Revenue Settings (unchecked by default). Checked, the Create/Update Sales Order button is hidden on the contract, the Suitelet answers `DISABLED`, and a booking that asks for a native Sales Order records the note `native sales order skipped - Disable Native Sales Orders is checked in Continuous Revenue Settings`. The contract's Native Sales Order flag is never armed. `installcheck` reports this as `nativeSoAllowed`. - Salesforce: **Create Native Sales Order** in wizard step 8 is the org default that asks NetSuite for the cumulative Sales Order on every booking. A booking whose Order node targets a Sales Order directly (see [native transaction targets](calls-sales-orders.md#native-transaction-targets)) stands the generator down for that run and says so in the run's notes, so the two never collide. ## If you run NetSuite Advanced Revenue Management Continuous Control 3.0 neither depends on nor integrates with NetSuite Advanced Revenue Management. It does not read or write revenue arrangements or revenue elements, does not set revenue recognition rules on items, and does not stamp revenue dates on the Sales Orders it generates. Its POBs, plans, and journal entries are its own records. If your account already recognizes revenue through NetSuite Advanced Revenue Management, **turn the Continuous Revenue Engine off** (either switch will do; checking *Disable Continuous Revenue Engine* in NetSuite is the one that also hides the buttons). Both systems would draw down the same Deferred Revenue balance, so running both would double-post recognition. With the engine off you keep everything in [What always runs](#what-always-runs): the contract graph, Update POBs, Booked and Billed on every POB, and the Salesforce mirror. > **One recognizer per account.** If NetSuite Advanced Revenue Management posts recognition for your contracts, do not also run Update Revenue Plans or Recognize Revenue on them. The systems share the Deferred Revenue account, not awareness of each other. An add-on that generates NetSuite Revenue Elements from Continuous performance obligations is planned as a separate initiative; see the [roadmap](roadmap.md). # Install the Salesforce package Human page: https://taggalongtech.github.io/continuous-control/install-salesforce.html Continuous Control is a single, self-contained unlocked package. It installs into production, sandbox, developer, trial, and scratch orgs from an install link — no installation key, no prerequisite package. Demoing? Follow the [demo install runbook](demo-install-runbook.md). Continuous Control 3.4.1 pairs with the NetSuite package **ns-v3.3.1**. Run the Connection wizard against a NetSuite account on that release. ## Before you install - No org to install into yet? [Build a test org](build-test-org.md) creates a scratch org with Revenue Cloud and the QuantumBit sample catalog already configured, then installs the package. - Your org must have **Revenue Cloud with Billing enabled**. The package is org-dependent: it attaches its sync fields to Invoice, Billing Schedule, Credit Memo, Payment, and related objects, so installation fails if those objects are not present. - Recommended: carry the **Order Type** picklist values *Net New / Amendment / Renewal*. Optional — when `Order.Type` is blank, the sync derives *New* or *Amendment* from the order's action. ## Install the package The current release is **Continuous Control 3.4.1** (package version id `04t…`, published at release; see [Versions & compatibility](ref-versions.md)). Open the link for your org type while logged in as a System Administrator: - Production, developer, or trial org: `https://login.salesforce.com/packaging/installPackage.apexp?p0=` - Sandbox: `https://test.salesforce.com/packaging/installPackage.apexp?p0=` Choose **Install for Admins Only**. CLI alternative: ``` sf package install --package -o --wait 20 ``` Every release follows the same pattern: `…/packaging/installPackage.apexp?p0=` on `login.salesforce.com`, or `test.salesforce.com` for sandboxes. The current 3.4.1 id from the [versions page](ref-versions.md) drops straight in. > **Org-dependent unlocked package.** Because the package attaches to Revenue Cloud objects already in your org, there is no second package to install first, and its components stay visible and debuggable in your org (no managed namespace). ## Assign the permission sets The package ships two permission sets: - **Continuous Control** — assign to every admin, integration, and finance user who will trigger syncs or view runs. It grants the app, its runtime tabs, the sync-facing Apex entry points, and the sync objects and fields. - **Continuous Control Admin** — assign *in addition* to whoever will run the [NetSuite Connection](connection-wizard.md) wizard or edit Calls & Mappings / Configurable Calls. It grants the configuration objects and the mapping-editor and connection-setup Apex entry points. It is not a substitute for the base set. ``` sf org assign permset --name Continuous_Control -o sf org assign permset --name Continuous_Control_Admin -o ``` See the [security model](security-model.md) for exactly what each contains. **Field access for sync users.** The queries that read the fields a sync sends run in `USER_MODE`, as the user who started the sync (the card user, or the running user of the Flow that called the invocable action; the queueable legs run as that same user). That user needs read access (FLS) to every field mapped in Calls & Mappings, to every field an added object reads (relationship field, row filter, lookups, Name Source), and, for Order Booking's shipped rows, to the Revenue Cloud fields on Order, OrderItem and Contract (`Order.TotalRoundedLineAmount`, `OrderItem.NetTotalPrice`, `Contract.IsRenewalProcessEnabled` and similar). Those come with the org's Revenue Cloud permission set licenses, not with Continuous Control. A mapped field the user can't read is skipped with a run warning naming it (` / : the user running this sync can't read . Row skipped. …`); the rest of the record still syncs. See [Security model](security-model.md#salesforce-permissions). ## Restrict access to custom settings The NetSuite TBA secrets live in the org defaults of the hierarchy custom setting `CC_NetSuite_Cred__c` (see [Security model](security-model.md#credential-storage)). Its visibility is **Public**: the package is unlocked with no namespace, and Protected only hides a setting from code outside the owning namespace, so it would not help. Keep the org preference **Restrict access to custom settings** on (Setup → **Schema Settings**; metadata `SchemaSettings.enableAdvancedCSSecurity`). With it on, users need **Customize Application**, **View All Custom Settings**, or explicit custom setting access in a profile or permission set to read setting values through the API or SOQL. Neither package permission set grants custom setting access, so `Continuous Control` users can't read the secrets. The sync is unaffected: it reads the setting in Apex (`CC_NetSuite_Cred__c.getOrgDefaults()` in `CcNsCredentials.load()`), which the preference does not restrict. `CcNsPermissionSetAccessTest.runtimeUserSyncReadsTheCredentialsWithoutCustomSettingAccess` proves it with a Standard User holding only `Continuous_Control`, in a CI org that has the preference on. ## What the package adds to your org | Kind | Components | |---|---| | App + tabs | The **Continuous Control** app: NetSuite Connection, Calls & Mappings, NetSuite Calls, NetSuite Endpoints, Sync Runs, NetSuite Sync Runs, Performance Obligations | | Record-page cards | **Continuous: NetSuite Sync** (Order, Invoice, Batch Runs, Credit Memo, Payment, Refund, Product, Account) and **Continuous: Revenue Summary** (Contract) — placed by you in App Builder, see [Placing the components](placing-components.md) | | Flow actions | Twelve invocable actions for automation — see the [reference](ref-flow-actions.md) | | Objects | NetSuite Sync Run, Performance Obligation (`CC_POB__c`, Salesforce's mirror of the NetSuite RC POB — RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite; see [Performance obligations](performance-obligations.md)), field-mapping records, and org settings | | Fields | The four [sync status fields](ref-sync-status-fields.md) on each synced object, plus the delivery/fulfillment signals and % Complete on OrderItem (`CC_Delivered__c`, `CC_Delivered_Date__c`, `CC_Fulfilled__c`, `CC_Fulfilled_Date__c`, `CC_Percent_Complete__c`) and the read-only `CC_POB__c.Percent_Complete__c` mirror | > **Nothing happens yet.** The package sends no data anywhere until the [NetSuite Connection](connection-wizard.md) is configured and a user (or Flow) triggers a sync. ## Version compatibility The Salesforce package requires a minimum NetSuite package version; the Connection wizard's *Verify installation* step reads the installed NetSuite version and flags a too-old package. See [Versions & compatibility](ref-versions.md). # Build a test org Human page: https://taggalongtech.github.io/continuous-control/build-test-org.html Need an org to demo, evaluate, or test Continuous Control without touching a customer environment? Build a scratch org that already has Revenue Cloud and the QuantumBit sample company configured, then install the package on top. Three commands, about an hour, for SI partners preparing a demo or implementation and for internal teams who need a throwaway org. ## What you get A 30-day Developer Edition scratch org built by Salesforce's public [Agentforce Revenue Management Foundations Launchpad](https://github.com/bgaldino/rlm-base-dev) (`bgaldino/rlm-base-dev`, Apache-2.0): Revenue Cloud with Billing, Rating and Usage enabled, the **QuantumBit** sample product catalog, pricing, tax, billing and rating data, decision tables, the RLM record pages and apps, and a sales-rep persona user. Continuous Control is then installed from its package link and its two permission sets are assigned. > **No NetSuite connection.** The test org is the Salesforce half only. To sync anything you still [install the NetSuite components](install-netsuite.md) and run the [Connection wizard](connection-wizard.md), exactly as in a customer org. ## Prerequisites | Need | Why | |---|---| | A **Dev Hub**, authenticated as the default (`sf org login web --set-default-dev-hub --alias devhub`) | Scratch orgs are created from it. Any Dev Hub works, including a free Developer Edition (developer.salesforce.com/signup) with **Setup > Dev Hub > Enable Dev Hub** on. Tested 2026-10-06: it accepts every feature of the `dev` shape except `Chatbot`, the scratch org gets the same 105 permission set licenses as one from a partner Dev Hub, and `prepare_rlm_org` completes. Turn Dev Hub on before the CLI login: the CLI caches `isDevHub` at login and never re-checks. The free Developer Edition that comes with Revenue Cloud (the Trailhead sign-up) has no Dev Hub setting. Developer Edition allocation: 3 active, 6 per day. | | Salesforce CLI 2.x (`npm install -g @salesforce/cli`) and Node.js LTS | The launchpad drives the CLI through CumulusCI. | | Python 3.12 or 3.13 | CumulusCI runtime (3.14 is not supported yet). On macOS: `brew install python@3.12`. | | Google Chrome or Chromium | A few Setup pages have no Metadata API and are configured headlessly. | | git | To fetch the launchpad and the scripts. | The product's own requirements are on [Prerequisites](prerequisites.md). ## Get the scripts Download [`rlm-scratch-org.zip`](downloads/rlm-scratch-org.zip) (about 25 KB) and unpack it anywhere. It contains three plain shell scripts, a README, and reference notes; the folder also works as a Claude Code skill, which is optional: ``` curl -LO https://taggalongtech.github.io/continuous-control/downloads/rlm-scratch-org.zip unzip rlm-scratch-org.zip && cd rlm-scratch-org/assets ``` If you have access to the product repository, the same folder lives at `.claude/skills/rlm-scratch-org/`. ## 1. Bootstrap the toolchain ``` ./bootstrap.sh // once per machine, about 3 minutes ``` Clones the launchpad to `~/rlm-base-dev` and checks out the launchpad commit the scripts are tested with (`RLM_LAUNCHPAD_REF` in `assets/lib/env.sh`). The launchpad's `main` changes almost daily and can need a Salesforce release that scratch orgs don't run yet, so the pin only moves after a full build passes. Set `RLM_LAUNCHPAD_REF=main` to build from the latest anyway. It also creates a Python virtual environment with the CumulusCI version the launchpad's own CI uses, installs the SFDMU data-loader plugin, and runs the launchpad's own setup validation. It ends with `RESULT: bootstrap ok`; run it before every org. ## 2. Create the scratch org ``` ./create-org.sh qb-test --probe-only // about 5 minutes ``` Creates the empty org from the launchpad's `dev` shape. Salesforce rejects features a Dev Hub is not licensed for one at a time; the script drops each one, retries, and prints `DROPPED FEATURES (…)` so you know what is missing before spending an hour on the build. It ends with `RESULT: probe ok (rlm-base__qb-test)`. The CLI alias of every org built this way is `rlm-base__`. ## 3. Build it ``` ./create-org.sh qb-test --flow-only --background // 45 to 75 minutes tail -n 20 ~/.rlm-scratch-org/logs/qb-test-flow-*.log // watch progress cat ~/.rlm-scratch-org/logs/qb-test-flow-*.log.exit // prints 0 when the build succeeded ``` Runs the launchpad's 34-step `prepare_rlm_org` flow detached from your terminal and keeps the machine awake. If a step fails, re-run without `--background` once the cause is fixed; most steps are idempotent. Feature flags such as `--flag ux=false` give a lighter build; the reference file in the skill folder lists them. ## 4. Install Continuous Control ``` ./install-cc.sh qb-test // about 5 minutes ``` Checks that Revenue Cloud and Billing objects are present, installs the current release (**Continuous Control 3.4.1**, package version id `04tPc000000zaubIAA`), assigns the **Continuous Control** and **Continuous Control Admin** permission sets to you, verifies the package metadata and the NetSuite Connection tab, puts the cards on the record pages (`assets/place-cards.py`: the sync card at the top of the right sidebar on Header and Right Sidebar pages for Order, Invoice, Invoice Batch Run, Credit Memo, Payment, Refund, Product and Account, the Revenue Summary card on Contract, each page made the default org-wide and in the apps; `--no-cards` skips it; details in the [demo install runbook](demo-install-runbook.md#the-cards-on-your-pages)), then runs the ready-to-quote check below. It ends with `RESULT: continuous control installed (package) on rlm-base__qb-test`; exit code 8 means the package installed but the org can't quote yet. No NetSuite account to connect? Add `--simulated`. It turns on **Simulate NetSuite** (wizard step 1), deploys the prebuilt calls (step 6) and gives every account and product a made-up NetSuite id (step 7). The Invoice, Payment, Refund and Credit Memo cards then sync at once, and nothing reaches NetSuite. See [Demo without a NetSuite account](simulated-netsuite.md). On a machine with the Continuous product checkout, the install also builds the QuantumBit demo deals (Kingsbridge Digital and Coralbay Technologies); `--no-demo-data` skips them. Prefer the browser? While logged into the scratch org as its admin user, open the install link and choose **Install for Admins Only**, then assign the two permission sets in Setup: - [`https://login.salesforce.com/packaging/installPackage.apexp?p0=04tPc000000zaubIAA`](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tPc000000zaubIAA) Other versions and their links are on [Versions & compatibility](ref-versions.md); pass one with `--package `. The full install procedure, including what the package adds, is on [Install the Salesforce package](install-salesforce.md). ## 5. Check the org is ready to quote ``` ./org-ready.sh qb-test // a few seconds; minutes when it has to rebuild ``` The launchpad's last two build steps (`refresh_all_decision_tables`, `rebuild_search_index`) only start their work: the decision table refreshes are queued and the product catalog index build runs in the background, and the flow never checks either. A flow that stops before them (the UX step can time out) skips both. `org-ready.sh` checks and repairs them: - **Decision tables:** every Active table needs a completed refresh and the verdict **Fresh** from the launchpad's Decision Table Manager (`RLM_DecisionTableManagerController`: stale means an object the table reads changed after its last full sync). Stale, failed or never-synced tables get a full refresh (`refreshDecisionTable`), and the script waits for them. Without the controller it falls back to `DecisionTable.RefreshStatus` and says data changes can't be seen. - **Catalog index:** a live snapshot whose newest build completed after the last product, category or catalog change (`connect/pcm/index/snapshots`). Otherwise a FULL rebuild (`connect/pcm/index/deploy`), and the script waits for a newer completed build. A build already running is joined, not duplicated. It ends with `RESULT: org ready to quote (rlm-base__qb-test)`, or `NOT READY:` lines and exit code 8 (decision tables) or 9 (index). `--check-only` reports without changing anything; `--wait ` sets the wait (default 20). `install-cc.sh` runs it; run it again before quoting in an org built earlier. ## 6. Use the org ``` sf org open -o rlm-base__qb-test sf org open -o rlm-base__qb-test --path /lightning/n/CC_NetSuite_Connection // the Connection wizard ``` From here the org behaves like any customer org: [place the components](placing-components.md) on the Order, Invoice, Credit Memo, Payment, Product and Contract pages, [install the NetSuite half](install-netsuite.md), and walk the [Connection wizard](connection-wizard.md). The QuantumBit catalog gives you products, price books and a working quote-to-order path to book orders against. > **Scratch orgs expire.** The org lives 30 days. Rebuilding is the same commands. Calls and mappings are records (`CC_NS_Call__c`, `CC_NS_Call_Node__c`, `CC_NS_Mapping__c`, `CC_NS_Mapping_Detail__c`, `CC_NS_Endpoint__c`), not metadata, and there is no export yet ("Call packs" on the roadmap), so note your changes to redo them in the new org. ## Clean up ``` ./create-org.sh qb-test --delete ``` ## Troubleshooting | Symptom | What to do | |---|---| | `DROPPED FEATURES` lists a feature | `Chatbot` is expected: it needs **Enable Einstein Features** (a terms-of-service acceptance) in the Dev Hub, and no build step uses it. Any other feature is one your Dev Hub cannot grant; the build continues without it, and steps that need it may fail and can be switched off with a `--flag`. | | "The specified org devhub is not a Dev Hub" (`NotADevHubError`) | The CLI was logged in before Dev Hub was enabled and cached `isDevHub: false`. Log in again with `sf org login web --set-default-dev-hub --alias devhub`. | | The build stops with a permission set group *Outdated* | Salesforce is still recalculating. Re-run `./create-org.sh qb-test --flow-only`. | | A quote won't price, or Browse Catalogs finds no products | The decision table refresh or the catalog index hasn't finished, or never started because the build stopped early. Run `./org-ready.sh qb-test`. | | `INVALID_AUTH_HEADER` when running CumulusCI by hand | Use the scripts; they set the CLI variable CumulusCI needs to read org credentials. | | Every quote fails with a 404 from the constraints solver (`ConstraintsSolverException`) | The org was built with the launchpad's Constraint Builder on (`--flag constraints=true`). The scripts leave it off (`RLM_DEFAULT_FLAGS`), so build a new org without that flag. | | Scratch org limit reached | `sf org list limits -o `, then delete old orgs with `--delete`. | | Package install fails with missing objects | Billing was not enabled during the build. Check the build log for the `prepare_billing` step and re-run the flow. | The skill folder's `references/rlm-base-dev.md` carries the longer list, the flow's step map, and resume options. # Install the NetSuite components Human page: https://taggalongtech.github.io/continuous-control/install-netsuite.html The NetSuite half — the RC custom records (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), the revenue engine, and the sync endpoint — ships as a versioned release zip with a one-command installer. One download, one script, and the rest of setup happens from the Salesforce Connection wizard. Budget 30–45 minutes, most of it one-time auth setup. > **The connection's writes and reads go through one endpoint,** the **Continuous Control (RESTlet)** script, `customscript_cc_control_rl`, deployment `customdeploy_cc_control_rl`. Salesforce's revenue engine also calls the Update POBs and Update Revenue Plans Suitelets directly after a sync, alongside the RESTlet — see [The engine Suitelets](https://taggalongtech.github.io/continuous-control/engine-suitelets.html). That's why those two Suitelet deployments stay open to all roles, the same as the RESTlet. ## Before you install - **Administrator access** to the target NetSuite account. - The [required features](https://taggalongtech.github.io/continuous-control/prerequisites.html) enabled (SuiteCloud scripting, SDF, Token-Based Authentication, REST Web Services, OAuth 2.0; Accounting, A/R and Sales Orders on the Accounting tab). **Subsidiaries (OneWorld) and Multiple Currencies are required** — every synced record needs a subsidiary and a currency. NetSuite Advanced Billing is not required; Continuous Control ships its own billing schedule records and never touches the native ones. No NetSuite revenue feature is required. The deploy manifest also lists **Inventory** as an optional feature — it is not required, and it exists only so the item-level Source Org field also covers inventory items on accounts that sell them; non-inventory, service, and other-charge items carry the same field either way. - A workstation with **Node.js 18+** and the release zip. The current release is **ns-v3.3.1**: download [`continuous-control-netsuite-v3.3.1.zip`](https://taggalongtech.github.io/continuous-control/downloads/continuous-control-netsuite-v3.3.1.zip) (it pairs with Salesforce packages 3.3.0, 3.4.0 and 3.4.1; see [Versions & compatibility](https://taggalongtech.github.io/continuous-control/ref-versions.html)). ## Create the install-time credentials The installer authenticates with OAuth 2.0 client credentials — a certificate you generate and register once. (This is separate from the runtime token Salesforce will use; that comes below.) 1. In the unzipped release folder, generate the pair: ``` openssl req -new -x509 -newkey rsa:4096 -sha256 -days 730 -nodes \ -keyout continuous-key.pem -out continuous-cert.pem \ -subj "/CN=Continuous Control Installer" ``` Keep `continuous-key.pem` private — it never leaves your machine. 2. In NetSuite: **Setup → Integration → OAuth 2.0 Client Credentials (M2M) Setup → Create New**. Entity: you (or an integration user); Role: Administrator or any role with the SuiteCloud Development Framework permission; Application: **SuiteCloud Development Integration**; Certificate: upload `continuous-cert.pem`. 3. Copy the **Certificate ID** NetSuite displays. ## Run the installer ``` NS_ACCOUNT= \ NS_CERT_ID= \ NS_KEY_PATH=./continuous-key.pem \ ./install.sh ``` The script authenticates, **validates the whole project against your account** (nothing deploys if validation fails — a missing feature is the usual cause), asks for confirmation, and deploys. `./install.sh --validate-only` is a dry run. > **Always safe to re-run.** The deploy upserts components by their script ids and never deletes — a re-run or upgrade cannot damage existing data or configuration. ## Runtime credentials for Salesforce Salesforce calls NetSuite with Token-Based Authentication. Three one-time steps: 1. **Setup → Integration → Manage Integrations → New** — name it `Continuous Control`, check **Token-Based Authentication** (leave the TBA authorization flow and user credentials unchecked), save, and copy the **Consumer Key / Consumer Secret** (shown once). **Under OAuth 2.0, uncheck Authorization Code Grant** — NetSuite pre-checks it, and leaving it on fails with *Invalid Redirect URI*. Only Token-Based Authentication should be checked. 2. Assign the **Continuous Control Integration** role (`customrole_cc_integration`) to the integration user — **Setup → Users/Roles → Manage Users**, open the user, add the role on the Access tab, save. The installer ships this role as an SDF object; do not hand-edit it in the NetSuite UI — `install.sh` re-applies it on every deploy and overwrites changes made there. Its 27 permissions, exactly as the role XML carries them: | Permission key | Level | What it's for | |---|---|---| | `ADMI_LOGIN_OAUTH` | Full | Log in with an access token | | `ADMI_RESTWEBSERVICES` | Full | Call REST web services (the RESTlet) | | `LIST_CUSTRECORDENTRY` | Full | Every `customrecord_cc_*` record — the RC custom records | | `ADMI_CUSTRECORD` | View | Read custom record type metadata (`describe`, `listrecordtypes`) | | `ADMI_CUSTLIST` | View | Read custom list values | | `ADMI_CUSTOMSCRIPT` | View | Read script records — never edit or upload one | | `LIST_FIND` | Full | Run searches and SuiteQL queries | | `LIST_PUBLISHSEARCH` | Create | Build the `customsearch_cc_*` saved searches `postinstall` creates | | `TRAN_FIND` | View | Read transactions generally — the id and application-target checks (audit N15) read transaction types the role has no Edit permission on, such as a refund's target customer deposit | | `LIST_FILECABINET` | Full | Create and attach files in the `files` action | | `TRAN_CUSTINVC` | Edit | Invoices | | `TRAN_CUSTCRED` | Edit | Credit memos | | `TRAN_CUSTPYMT` | Edit | Customer payments | | `TRAN_CUSTRFND` | Edit | Customer refunds | | `TRAN_SALESORD` | Edit | Sales orders | | `TRAN_ESTIMATE` | Edit | Estimates | | `TRAN_CASHSALE` | Edit | Cash sales | | `TRAN_RTNAUTH` | Edit | Return authorizations | | `LIST_CUSTJOB` | Edit | Customers | | `LIST_CONTACT` | Edit | Contacts | | `LIST_ITEM` | Edit | Items the sync creates or updates | | `LIST_ACCOUNT` | View | Read GL accounts | | `LIST_SUBSIDIARY` | View | Read subsidiaries | | `LIST_CURRENCY` | View | Read currencies | | `LIST_TAXITEM` | View | Read tax items | | `LIST_TAXSCHEDULE` | View | Read tax schedules | | `LIST_PAYMETH` | View | Read payment methods | The eight transaction Edit permissions cover every native type an Order node or a custom node can target: sales order, estimate, invoice, credit memo, cash sale, return authorization (plus customer payment and customer refund for the `payments`/`refunds` actions). The role carries no vendor, employee, journal entry, or company-setup permission — nothing that can change scripts, roles, users, or tokens (`tools/ns-access-test.js` asserts this directly). Two cases need an **Administrator** token instead of the integration role: - A connection whose custom mappings write to vendors, employees, journal entries, or other record types the role doesn't cover. - The one-time `taxsetup` action, which creates Tax Agency vendors and nexuses — permissions this role deliberately does not carry. See [Moving a connection to the new role](#move-role) below if you're upgrading a connection that's on Administrator or another role today. 3. **Setup → Users/Roles → Access Tokens → New** — pick the integration, the integration user, and the role; copy the **Token ID / Token Secret** (shown once). Hand the four values plus the account id to whoever runs the [Salesforce Connection wizard](https://taggalongtech.github.io/continuous-control/connection-wizard.html) — they are entered once in step 1 and never displayed again. > If the connection fails with a signature error or a 401, confirm the token and the consumer key/secret came from the same integration record, user, and role, and that Token-Based Authentication and REST Web Services are both enabled (see Before you install). ## Moving a connection to the new role {#move-role} A connection running on Administrator, or on a role from before ns-v3.2.0, keeps working — every RESTlet deployment stays open to all roles in this release (the audience narrowing to Administrator plus the integration role is deferred to a later release). Move it when you're ready: 1. Assign the **Continuous Control Integration** role to the integration user, as in step 2 above. 2. **Setup → Users/Roles → Access Tokens → New** — create a token for that user and role. 3. In the Salesforce [Connection wizard](https://taggalongtech.github.io/continuous-control/connection-wizard.html), open step 1 and enter the new **Token ID** and **Token Secret**. The consumer key and consumer secret stay the same, since they belong to the integration record, not the token. `ping` and `installcheck` report the connection's role as `role: {id, scriptId, name}`. Wizard step 4 reads it and shows a note when the role is anything other than Administrator or `customrole_cc_integration`: *"This connection uses the \ role. Move it to the Continuous Control Integration role. A later release limits the endpoint to that role and Administrator."* The note is informational only in ns-v3.2.0 — it never fails Verify installation. ## The sync endpoint The deploy creates the endpoint Salesforce calls: the **Continuous Control (RESTlet)** script record, `customscript_cc_control_rl` with deployment `customdeploy_cc_control_rl`. Those are the ids a new connection uses: leave the two RESTlet fields blank in the [Connection wizard](https://taggalongtech.github.io/continuous-control/connection-wizard.html) and it takes them. Fill them in only to name a renamed deployment; a connection that already stores ids is never changed by saving. Because the record is created by the install, NetSuite registers all three entry points (POST, PUT, GET) on it automatically — there is no script record to re-save by hand. ## Finish from Salesforce In the Salesforce **Continuous Control** app → **NetSuite Connection** tab: steps 1–3 enter the credentials from above, add the two Remote Site Settings the page lists, and test the connection. Step 4 — **Verify installation** checks every record type, script, and saved search plus the installed package version; **Run post-install setup** creates the **Continuous Revenue Settings** record (`customrecord_cc_settings`, two Disable switches, both unchecked to start) and re-creates any `customsearch_cc_*` saved search missing from the connection. Every one, including the POB sublist search, ships as an SDF object, so this step is normally a no-op. Both actions are safe to re-run, and post-install must be re-run after an upgrade. Steps 5 and later handle environment discovery, field mappings, and reference data — see [The Connection wizard](https://taggalongtech.github.io/continuous-control/connection-wizard.html). ## Remaining NetSuite UI steps Three things NetSuite offers no deployment API for — full detail and tables in [NetSuite manual setup](https://taggalongtech.github.io/continuous-control/netsuite-manual-setup.html): 1. **Continuous Revenue Settings record** — post-install creates it (`customrecord_cc_settings`, one row). Choose its account values in the Connection wizard's step 4 (**NetSuite account settings**, ns-v3.1.0+): Subsidiary, Currency, Deferred Revenue Account, Revenue Account, Tax Schedule, Tax Item, Refund Bank Account, and Refund Payment Method. Step 4 marks any still on a shipped default and saves all eight in one write. Item Subsidiaries (a fallback list), the two Disable switches, the Default Rule, and Auto-Recognize are set on the record in NetSuite. The refund script parameters on the Continuous Control (RESTlet) deployment (`custscript_cc_restlet_refund_acct` · `custscript_cc_restlet_refund_pm`) are read only when the record's refund fields are blank. 2. **Custom Sublists** — attach five saved-search sublists (the POB list and others on the RC Contract and RC POB). The searches ship as SDF objects, pre-configured; attaching them is a few minutes of clicking. The **GL Impact** tab on transactions needs no attaching: a User Event script fills it on install. 3. **Preferred forms on the RC records** — RC POBs and the other nine RC record types each need a preferred custom form, so they open on their own subtabs instead of NetSuite's Translation/Workflow tabs. ## Upgrades and re-runs SDF upserts components by their script ids and never deletes, so a plain re-run of `./install.sh` against a newer release zip — validate, confirm, deploy — is always safe on an account already running 3.x. # NetSuite manual setup Human page: https://taggalongtech.github.io/continuous-control/netsuite-manual-setup.html A handful of finishing steps have no deployment API in NetSuite, so they are done once in its UI after the installer runs. Everything else — the settings record and all saved searches — is provisioned automatically by the Connection wizard's post-install step. > **The settings record carries two Disable switches.** Post-install creates the singleton **Continuous Revenue Settings** record (`customrecord_cc_settings`) with *Default Revenue Rule* and *Auto-Recognize on Scheduled Run*, plus two checkboxes that start unchecked: **Disable Continuous Revenue Engine** (checked, the engine is off for this account: Update POBs still runs, no revenue plans, schedules or journal entries are produced, and Salesforce shows a hint in wizard step 8) and **Disable Native Sales Orders** (checked, no booking generates a native Sales Order). Salesforce has a Continuous Revenue Engine switch of its own and either side off wins. They are Disable checkboxes on purpose: an unset checkbox reads as false, so unset means enabled. See [One engine, two switches](one-engine-two-options.md). ## Review the settings record and refund parameters Account-specific setup — subsidiary, currency, GL accounts, item subsidiaries, tax, and the refund bank account and payment method — lives on one record. It is created for you. From ns-v3.1.0 you choose these values in the Salesforce Connection wizard, step 4 (**NetSuite account settings**), which writes this record. You can also edit it here in NetSuite: > **One row, created by post-install.** The Salesforce wizard's **Run post-install setup** creates the singleton **Continuous Revenue Settings** record (`customrecord_cc_settings`) the first time it runs; wizard step 9 reads it back and flags any of its fields still on a shipped default. | Field | Meaning | |---|---| | Default Subsidiary | Default subsidiary for records the sync creates | | Default Currency | Default currency for records the sync creates | | Deferred Revenue Account | The account synced items post income to, and recognition draws from | | Revenue Account | The account recognition posts revenue to | | Item Subsidiaries | Fallback subsidiaries for new items. New items normally get every active subsidiary; this list is used only when that lookup fails. Not shown in the Salesforce wizard | | Default Tax Schedule | Applied to items the sync creates | | Default Tax Item | Default tax item applied to customers the sync creates | | Refund Bank Account | The account refund sync pays customer refunds from (`custrecord_cc_settings_refund_acct`, ns-v3.1.0+) | | Refund Payment Method | The payment method refund sync puts on customer refunds (`custrecord_cc_settings_refund_pm`, ns-v3.1.0+) | | Engine Off switch | **Disable Continuous Revenue Engine** — checked, Update POBs still runs but no revenue plans, schedules or journal entries are produced | | Native SO Off switch | **Disable Native Sales Orders** — checked, no booking generates a native Sales Order | | Default Rule | **Default Revenue Rule** — the rule an item without its own carries | | Auto-Recognize | **Auto-Recognize on Scheduled Run** — checked, the nightly close also posts recognition (only meaningful while the engine is on) | The two refund settings live on the settings record from ns-v3.1.0. Refund sync reads the record first, then the script parameters on the **Continuous Control (RESTlet)** deployment (`custscript_cc_restlet_refund_acct` · `custscript_cc_restlet_refund_pm`, TEXT internal ids) if the record fields are blank, then the shipped defaults. An account that set the parameters keeps working, and the wizard still marks those two settings as **Shipped default** until they're saved on the record. The Continuous Control — Update POBs, Continuous Control — Recognize Revenue, and Native Sales Order Suitelets carry no parameters of their own; all three read the settings record. > **The wizard sets and checks this for you.** Wizard step 4 lists the eight account settings with NetSuite's own choices for each, marks any still on a shipped default, and saves them to this record in one write. Step 9 shows the same values read-only. ## Attach the custom sublists The saved searches behind the record subtabs, including `customsearch_cc_pob_sublist`, ship with the deploy as SDF objects (and the wizard's post-install step re-creates any that go missing); the one manual piece is attaching the RC Contract and RC POB ones as Custom Sublists. The transaction **GL Impact** tab needs no attaching; see below. Under **Customization → Forms → Sublists**, add a row per search: > **A search needs an Available Filter before the picker offers it.** The custom-sublist picker only lists a saved search once that search carries an **Available Filter** of **Internal ID** — add it on the search's Available Filters subtab first, or the search simply will not show up to attach. | Search | Attach to | Available filter | |---|---|---| | `customsearch_cc_pob_sublist` | RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) | the contract field | | `customsearch_cc_order_lines_sublist` | RC Contract | the contract field | | `customsearch_cc_rev_je_sublist` | RC Contract | the contract field | | `customsearch_cc_rev_sched_sublist` | RC Contract | the contract field | | `customsearch_cc_pob_je_sublist` | RC POB — Revenue Recognition tab | the POB column | ### The GL Impact tab fills itself Every posting invoice, cash sale, credit memo, cash refund, journal entry, customer payment, and customer refund shows a **GL Impact** tab with its general-ledger lines: account, debit, credit, memo, and posting period, plus a line count and debit/credit totals. A transaction that hasn't posted shows "This transaction has not posted to the general ledger yet." The User Event script **Continuous Control — GL Impact Subtab** (`customscript_cc_gl_impact_ue`, file `cc_gl_impact_ue.js`, one deployment per record type: `customdeploy_cc_gl_invoice`, `_cashsale`, `_creditmemo`, `_cashrefund`, `_journal`, `_custpayment`, `_custrefund`) builds it in `beforeLoad` on view and edit. It loads `customsearch_cc_gl_impact`, adds an `internalid` filter for the open record, and renders up to 500 rows. The saved search's columns become the tab's columns, so you can add columns in the search UI. If the search is missing, the script falls back to the definition in `cc_search_defs.js`. The rows appear on the custom subtab `custtab_cc_gl_impact` when that subtab is on the form. NetSuite hides an empty custom subtab, so on a standard form the script adds its own tab labelled **GL Impact**. Why not a Custom Sublist: a Custom Sublist only scopes its search to the open record through an **Internal ID** Available Filter. SuiteScript can't set Available Filters, and the shipped search XML carries none, so every deploy wiped a hand-added filter and the sublist rendered nothing. > **Upgrading an account that attached GL Impact by hand?** Delete that Custom Sublist row (search `customsearch_cc_gl_impact`, tab `custtab_cc_gl_impact`) under **Customization → Forms → Sublists** (page title "Custom Sublists"). Without the Internal ID Available Filter and the sublist's Field link it isn't scoped to the record, so it lists every GL line in the account (seen in the demo account: 20,944 rows on one invoice). With the filter restored it would duplicate the script's tab. SDF can't delete it for you. ## Set the preferred forms on the RC records NetSuite custom-record forms cannot be deployed by SDF (an `object:import` of a record type comes back without its forms), so arrange them once in the UI. Every custom record shows NetSuite's own **Translation** and **Workflow** subtabs while those features are on in the account; a preferred form hides them so an RC record opens on its own subtabs. The Notes and Files subtabs are already off in the shipped record types (Files stays available only where a customer turns Allow Attachments back on). 1. Open any record of the type (for example an RC POB), choose **Customize → Customize Form**. 2. Name the form after the record type (*RC POBs*, *RC Contract*, …), give it an ID such as `custform_cc_pob`, and leave **Form is preferred** checked. 3. On the **Subtabs** tab, clear **Show** on *Translation* and *Workflow*. On the RC POB form also order the remaining subtabs *RC Order Lines, Revenue Recognition, RC Obligation Details, Billing*; on the RC Contract form *RC Orders, RC Order Lines, RC POBs, Financials, Revenue Recognition, Billing Schedule Groups, Sales Order*. 4. Save. Repeat for the ten RC record types: Contract, Order, Order Line, POBs, Obligation Detail, Revenue Plan, Revenue Schedule, Billing Schedule Group, Billing Schedule, Billing Period. Subtabs added by other SuiteApps (for example a document-management bundle's *Box Files*) are not on the form's subtab list and cannot be hidden here. ## Prepare pre-existing items Items the sync *creates* are configured automatically: income pointed at Deferred Revenue, non-fulfillable, available on the configured subsidiaries. Items that **already exist** in NetSuite and will be sold through Continuous need the same treatment by hand: income account → Deferred Revenue, and availability on the selling subsidiary. Also confirm your tax setup resolves a code for customer addresses outside your home nexus — customers the sync creates get the configured default tax item, which covers the common case. When no tax code resolves, invoice sync fails with `A tax line without an account cannot contain the tax amount` — set the Tax Item here or on the Continuous Revenue Settings record. # The Connection wizard Human page: https://taggalongtech.github.io/continuous-control/connection-wizard.html The **NetSuite Connection** tab is the single place the integration is configured. Nine collapsible steps, each with a status chip, a verify action, and a how-to guide beside it. Every step is safe to re-run, and green means configured. ![The NetSuite Connection wizard: nine steps with status chips and a progress bar](https://taggalongtech.github.io/continuous-control/assets/shots/sf-connection-wizard.jpg) *The wizard tracks progress across all nine steps; each step verifies itself independently.* All nine steps live in one LWC, `ccNsConnectionSetup`, backed by one Apex controller, `CcNsConnectionSetupController` (`salesforce/force-app/main/default/classes/CcNsConnectionSetupController.cls`). Each step below names its exact `@AuraEnabled` method(s) — read from that controller and from the LWC's handler functions, not inferred. ## Step 1 — NetSuite credentials - **Does:** stores the four OAuth 1.0a TBA secrets plus the account id and the RESTlet script/deploy ids. - **Inputs:** account id, consumer key, consumer secret, token id, token secret, RESTlet script id (blank = shipped `customscript_cc_control_rl`), RESTlet deploy id (blank = shipped `customdeploy_cc_control_rl`). - **Apex:** `saveCredentials(credsJson)` deserializes to `CredsDto {account, consumerKey, consumerSecret, tokenId, tokenSecret, restletScriptId, restletDeployId}`, upserts `CC_NetSuite_Cred__c.getOrgDefaults()`, then calls `CcNsEndpointRegistry.ensureSystemRows()` to seed the endpoint registry's system row. - **Done when:** `getSetupStatus().credsConfigured` is true — account, all four secrets, and `Restlet_Script_Id__c` are all non-blank. - **Re-run:** yes. A blank secret field on re-save keeps the stored value; a blank RESTlet id only fills in the shipped default when nothing is stored yet, so it never silently re-points an existing connection. - **Simulate NetSuite switch (3.4.0):** a switch at the top of the step, styled like the step 8 switches, with help text "Turn on to demo without a NetSuite account. Every sync completes with made-up ids and nothing reaches NetSuite. Turn off before connecting a real account." It calls `setSimulateNetSuite(Boolean enabled)`, which upserts `CC_NetSuite_Settings__c.getOrgDefaults()` with `Simulate_NetSuite__c = (enabled == true)` and returns `getSetupStatus()` (`SetupStatus.simulateNetSuite`). While on, the credential form and the how-to guide are hidden and the step reads "No credentials or Remote Site Settings are needed. Skip to step 6 to deploy the prebuilt calls, then step 7 to give your accounts and products made-up NetSuite ids." The step 1 chip is `credsConfigured || simulated`, so steps 1 and 2 both read Complete. A banner under the progress bar reads "Simulated NetSuite is on. Every sync completes inside Salesforce and nothing reaches NetSuite. Turn it off in step 1 before connecting a real account." Turning the switch off restores the credential form; `CC_NetSuite_Cred__c` is never touched by it. See [Demo without a NetSuite account](simulated-netsuite.md). Enter the NetSuite account id and the four runtime credentials created during the [NetSuite install](install-netsuite.md) (consumer key/secret, token id/secret). Secrets are stored in a Salesforce org setting and [never displayed again](credentials-auth.md) — re-saving with a blank secret keeps the stored value. **Moving a connection to the Continuous Control Integration role (ns-v3.2.0):** the consumer key and secret belong to the NetSuite integration record and stay the same. Only the token id/secret change — get a new token for the integration user on the new role, and re-save this step with those two fields. Step 4 names the current role and flags one that isn't the integration role or Administrator; see [Moving a connection to the new role](install-netsuite.md#move-role). > **OAuth 2.0 trap on the Integration record.** Under **OAuth 2.0**, uncheck **Authorization Code Grant** — NetSuite pre-checks it on a new record, and leaving it on fails the connection with *Invalid Redirect URI*. Before saving the record, check it against: Name (any), **State Enabled**, **Token-Based Authentication checked**, **TBA Authorization Flow unchecked**, **OAuth 2.0 both boxes unchecked**. See [Credentials & authentication](credentials-auth.md) for the full walkthrough and a screenshot of a correctly checked record. ### Check the entry point Continuous Control ships one RESTlet script record, **Continuous Control (RESTlet)**. Its script id is `customscript_cc_control_rl` and its deployment is `customdeploy_cc_control_rl`. Leave the two RESTlet fields blank on a new connection to use these shipped ids. Fill them in only if your NetSuite team renamed the deployment. Under the fields, step 1 shows which entry point this connection uses. It reads **Continuous Control (RESTlet)** for the shipped id, or **a custom deployment** for one of your own. Clear the two RESTlet fields to go back to the shipped id. ## Step 2 — Remote Site Settings - **Does:** shows the two exact Remote Site Setting entries to add in Setup and reuses the connection test to confirm they are in place. - **Inputs:** none directly — the two URLs are derived from the step 1 account id: `https://.restlets.api.netsuite.com` (`CC_NetSuite_Restlets`) and `https://.app.netsuite.com` (`CC_NetSuite_App`). - **Apex:** `testConnection()` — the same method step 3 calls. The LWC reads only whether the result's `failedStep` equals `'remoteSites'` to decide this step's chip; a `System.CalloutException` whose message contains "Unauthorized endpoint" is what sets it. - **Done when:** `testConnection()` no longer reports `failedStep == 'remoteSites'`. - **Re-run:** yes — read-only, no state changes. - **Simulated NetSuite:** the chip reads Complete while `SetupStatus.simulateNetSuite` is true; no remote sites are needed because no callout is made. Salesforce blocks callouts to hosts that are not allow-listed, and remote sites cannot be created programmatically — so the wizard shows the two exact entries to add in Setup (a RESTlet host and an app host derived from your account id), deep-links you there in a new tab so the wizard keeps your place, and verifies them via the connection test. ## Step 3 — Test the connection - **Does:** sends a signed `{"action":"ping"}` to the RESTlet and reports the NetSuite package version and the Source Org stamp. - **Inputs:** none — uses the stored credentials. - **Apex:** `testConnection()` — builds `CcNsCredentials`, POSTs via `CcNsRestClient`, and reads `pong`, `script`, `version`, `remainingUsage` from the response; on a successful `pong` it also calls `CcNsSourceOrg.label()` for the stamp shown on screen. - **Done when:** the result's `ok` is true (the RESTlet answered `pong: true`). `failedStep` names `credentials`, `remoteSites`, or `connection` on failure. - **Re-run:** yes — a read-only ping. - **Simulated NetSuite:** with `CC_NetSuite_Settings__c.Simulate_NetSuite__c` on, the ping is answered inside Salesforce by `CcNsSimulatedNetSuite` (version 3.3.0, `script: "simulated"`), blank credentials are filled with placeholders, and the message reads "Simulated NetSuite is on. Every sync completes inside Salesforce and nothing reaches NetSuite." The switch is in step 1; steps 1 and 2 need nothing while it is on. See [Demo without a NetSuite account](simulated-netsuite.md). Sends a signed ping to the Continuous endpoint in your NetSuite account and reports what came back. A green result names the RESTlet that answered and the NetSuite package version it is running (for example `ns-v3.3.0`), so it doubles as a quick check that both halves are on a [compatible pair](ref-versions.md). On failure it says *which layer* failed (missing remote site, bad credentials, wrong account) so the fix is obvious. A green result also shows **"Records from this org are stamped as () in NetSuite"** — the exact value this connection writes into the Source Org field on every synced record. It is the quickest way to confirm you are looking at the right org's records in NetSuite, especially with [more than one Salesforce org](prerequisites.md) pointed at the same NetSuite account. ## Step 4 — NetSuite components - **Does:** Verify installation confirms every Continuous record type, script, and saved search exists and reads the installed package version; Run post-install setup creates the Continuous Revenue Settings singleton and any missing saved searches. - **Inputs:** none — both run over the existing connection. - **Apex:** `checkNsInstall()` — POSTs `{"action":"installcheck"}`, compares the returned version against `MIN_NS_VERSION` (`'3.0.0'`) with `compareVersions()`, and stamps `CC_NetSuite_Settings__c.Ns_Version__c` when the reported version changed. `runNsPostInstall()` — POSTs `{"action":"postinstall"}`; the LWC then calls `checkNsInstall()` again to refresh the checklist. - **The connection's role (ns-v3.2.0):** `installcheck` (and `ping`) answer `role: {id, scriptId, name}` — additive, never fails the call. When the role is anything other than Administrator or `customrole_cc_integration`, `installcheck`'s `notes[]` carries the exact sentence *"This connection uses the \ role. Move it to the Continuous Control Integration role. A later release limits the endpoint to that role and Administrator."* (`` falls back to the role's scriptid, then its internal id, when the token's own role can't read the role list.) The LWC surfaces this note the same way it shows any `installcheck` note. Moving a connection: see [Moving a connection to the new role](install-netsuite.md#move-role). The RESTlet's own audience is still open to every role in ns-v3.2.0 — the note is advance notice, not an enforced gate; see `specs/plans/2026-09-27-ns-security.md`. - **Account settings panel (ns-v3.1.0+):** shown once Verify passes. `getNsAccountSettings()` (logic in `CcNsAccountSettings.load()`) POSTs `{"action":"settingsoptions","requiresVersion":"3.1.0"}` and renders one picker per setting, in this order: subsidiary, currency, deferred revenue account, revenue account, tax schedule, tax item, refund bank account, refund payment method. The options are NetSuite's own filtered lists (`getSelectOptions` on the native field; SuiteQL fallback; `truncated` past the cap, with a note to set the value on the record). A current value missing from the list is added so the picker can show it. A setting with nothing stored on `customrecord_cc_settings` shows **Shipped default**; a picked value shows **Changed**. **Save settings** calls `saveNsAccountSettings(valuesJson)` (`CcNsAccountSettings.save()`), which POSTs `{"action":"savesettings","requiresVersion":"3.1.0","values":{...}}` with every changed key plus every key still on a default, as shown, so saving the shown defaults counts as choosing them. It's all or nothing: NetSuite checks every value before one `submitFields` on the settings record. A refusal comes back per key (`keyErrors`, shown under the picker) and nothing is written. The fresh settings come back with the save, and the LWC re-runs `checkNsInstall()` only. **Discard changes** drops unsaved picks. Against an older RESTlet (`requiresVersion` refusal or unknown action) the result sets `upgradeNeeded`, the panel names the installed version and asks for ns-v3.1.0, and the older default-values warning shows instead. Nothing throws to the client. Wire contract: `specs/plans/2026-09-26-wizard-ns-settings.md`. - **Done when:** `checkNsInstall().ok` is true (zero missing components, `settingsRowPresent` true, `minVersionOk` true, no `errors`) and, on ns-v3.1.0+, `getNsAccountSettings()` answers `ok` with no setting `onDefault`. The chip reads **Choose settings** while any is left. On an older NetSuite release (`upgradeNeeded`) the install check alone decides. - **Re-run:** yes — the class doc marks post-install idempotent (it only creates what is missing), and Verify is the step to re-run after every NetSuite upgrade. - **Simulated NetSuite:** `checkNsInstall()` returns before any call with `ok`, `installed`, `minVersionOk` and `settingsRowPresent` true, `version` 3.3.0 (`CcNsSimulatedNetSuite.VERSION`), `nsAccount` `SIMULATED`, and the message "Simulated NetSuite is on. There is no NetSuite package to check; every sync is answered inside Salesforce." `Ns_Version__c` is not stamped. The account settings panel gets the minimal `settingsoptions` answer and lists no choices. Download the release zip from [Install the NetSuite components](install-netsuite.md) and have your NetSuite admin run the installer first; the rest of this step is two actions against the live connection. **Verify installation** confirms every Continuous record type, script, and saved search exists in the NetSuite account — **12 record types, 11 scripts, 18 saved searches** in ns-v3.3.0 — reads the installed package version against the required minimum, and lists every account setting still on a shipped default. Choose those in the **NetSuite account settings** panel below the checklist. **Run post-install setup** then creates the Continuous Revenue Settings record and builds all saved searches — the two provisioning tasks the installer cannot do. Both are idempotent. Verify also probes three **Source Org** fields directly — `custbody_cc_source_org` on a Sales Order, `custentity_cc_source_org` on a Customer, and `custitem_cc_source_org` on a Service Item — so a stamp field missing on any of the record types it touches shows up here rather than as a silent gap in NetSuite later. **Verify also remembers the version it read**, stamping it into the app's settings. That is what lets the call editor answer "does this configuration need a newer NetSuite package?" without another round trip, and what lets a run refuse before it sends when the package has been *downgraded* since the call was published. Re-run Verify after any NetSuite upgrade so the stamp matches the account — a blank stamp blocks nothing, it simply means nothing has been read yet. The floor itself is in [Versions & compatibility](ref-versions.md). NetSuite registers a script's GET and PUT entry points when the script record is created. The install creates the Continuous Control record, so it has GET, PUT, and POST from the start. Every packaged sync uses POST. GET and PUT matter only if you point a call at a [custom endpoint](calls-endpoints.md#endpoint-per-call) that expects PUT, or call the read-only actions with GET. ## Step 5 — Environment & deep links - **Does:** discovers the account's Contract/Order/RC POB record-type ids and invoice search id, then deploys them into `CC_NetSuite_Env__mdt` and `CC_NetSuite_Link__mdt` custom metadata. - **Inputs:** none for discovery. Deploy consumes the discovery result's `baseUrl`, `contractTypeId`, `orderTypeId`, `pobRecordTypeId`, `invoiceSearchId`. - **Apex:** `discoverEnvironment()` — POSTs `{"action":"setupinfo"}`. `deployEnvironment(discoveredJson)` — rewrites `CC_NetSuite_Env.Default` whole-record (anti-clobber: every field, not just the changed ones, rides along) plus `CC_NetSuite_Link.Contract` / `.Order` / `.CC_POB`, and calls `Metadata.Operations.enqueueDeployment()` asynchronously; requires System Administrator (Customize Application). `checkEnvironment()` — a thin wrapper on `getSetupStatus()` the LWC polls every few seconds after a deploy until the metadata matches what was discovered. - **Done when:** `getSetupStatus().envConfigured` is true (`envBaseUrl`, `contractTypeId`, `orderTypeId` all populated) and match the discovered values. - **Re-run:** yes, any time — discovery and deploy fail independently, and re-running is also how you fix drift after redeploying the SDF project or switching NetSuite accounts. Discovers your NetSuite account's record-type ids — Contract, Order, and the **RC POB** record type (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) — plus the invoice search id, over the connection, then deploys them into the environment settings that power every "Open in NetSuite" deep link. This runs a metadata deployment, so it needs a System Administrator. The step's check verifies the deployed values match what discovery found. Discovery and deploy are two separate calls, and each fails on its own: a failed lookup toasts **Discovery failed** with NetSuite's message, before anything is written; a failed metadata deployment (the case that needs a System Administrator) toasts **Deploy failed** instead, with what Salesforce reported. Either one is safe to re-run once the underlying problem is fixed. ## Step 6 — Deploy prebuilt calls & mappings - **Does:** seeds the un-scoped `CC ` mapping headers/rows (from before calls existed), then creates and publishes the nine packaged calls and their objects. Also hosts per-call Reset to shipped defaults. - **Inputs:** none for Deploy. Reset takes a call key — one from the picker, or every shipped call for Reset all. - **Apex:** `deployShippedCalls()` runs the private `seedMappings(false)` leg first (a failure there is folded into a warning rather than thrown, so the packaged calls still deploy), then `CcNsCallRegistry.deployShipped()` for the per-call rows. The LWC then calls `activateShippedCall(callKey)` once per returned row still flagged `needsActivate`, stopping after two consecutive failures. Reset: `previewResetCall(callKey)` produces the dry-run diff shown in the confirm panel, and `resetShippedCall(callKey)` runs on confirm. - **The only way in:** this button is the one thing that creates the packaged calls and their call-scoped mappings. No read path seeds them: `CcNsMappingAdminController.getCalls()` returns an empty list on an org that has not deployed (Calls & Mappings then shows *No calls are set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page.* with an **Open NetSuite Connection** button), the sync card's `CcNsSyncCallController.getCallSetup(callKey)` reports `deployed = false` and the card shows its not-set-up message instead of its button, and every run start refuses with `CcNsCallRegistry.notDeployedMessage(callKey)`. `CcNsConnectionSetupController.seedMappings()` stays public as an explicit anonymous-Apex tool. - **Seeded calls start Draft:** `CcNsCallRegistry.seedCalls()` inserts a new call with `Status__c = 'Draft'`. Only a publish (`CcNsCallLifecycle` activate) makes it `Active`, so Active always means "has a published `Active_Config_Json__c` snapshot". An org whose calls read Active but carry no snapshot is recovered by this step: `deployShipped()` reads such a call as unpublished (`published = false`), activates it, and reports it *Activated*. - **Done when:** the results table shows every packaged call **Active** or **Retired**, never **Needs attention**; the step chip reads **Complete** once every deployable call is published. "Published" is `Status__c = 'Active'` **and** a non-blank snapshot (`SetupStatus.shippedCallsActivated`), never the status alone. - **Live wording:** when every packaged call is present and published (`shippedCallsPresent == shippedCallsActivated == shippedCallsTotal`), the title reads **Prebuilt calls & mappings**, the help line reads *All 9 prebuilt calls are live. After an upgrade, click Check prebuilt calls to add anything new without changing a live call.* (the count comes from `shippedCallsTotal`), and the button reads **Check prebuilt calls**. It runs the same `deployShippedCalls()`: it adds anything a newer package ships and never republishes a live call. The toast title follows the button (*Check complete* / *Deploy complete*). With a retired call, or before the first deploy, the title and button read **Deploy prebuilt calls & mappings**. The chip reads **Not started** while no packaged call exists. - **Re-run:** Deploy is safe at any time, including after a package upgrade — it never republishes a call that is already live or one an admin retired. Reset **cannot be undone** once confirmed; re-running Reset itself is idempotent (a call already on shipped defaults reports no drift and republishes anyway). The step's own copy calls a call's record types its **objects**; the rest of these docs call them nodes. They are the same thing, and this section uses the word on screen. **Deploy prebuilt calls & mappings** creates everything the nine packaged calls need and publishes them, in one click. For each call it creates the call and its objects if they are missing, adds the built-in mapping rows to any object that has none, and activates a call that has never been activated. It never re-activates a call that is already live, it never restores an object you deleted on a call you have already published, and a packaged call an admin set Inactive stays inactive — Deploy reports that one as *Retired*, a neutral chip rather than an amber one, and leaves it alone rather than republishing it, so retiring a call is not undone by the next deploy. Those are your decisions, and [Reset to shipped defaults](calls-edit-mappings.md#reset-to-shipped-defaults) is how you undo one — or, for a retired call, activating it again in the editor. Beside the Deploy button, **Open Calls & Mappings** jumps straight to the editor for the call you want to look at next. The step's help line carries a live count of where the org stands — published calls out of the ones that can be published, retired calls named separately, record types with mapping headers, and total field rows — so you can see what a deploy would have to do before you press it. A results table reports one row per call, in six columns: | Column | Reads | |---|---| | Call | The call's name. | | Status | One of six: *Created and activated*, *Activated*, *Already active*, *Updated - activate in the editor* (a live call that gained new built-in objects or rows — review them and publish it yourself), *Retired* (a call you set Inactive; the row is neutral, and the warning says it was not republished), or *Needs attention*, with the reason in Warnings. | | Nodes | Objects this pass created on that call. | | Rows | Built-in mapping rows this pass added. | | Version | The version the call is on now. | | Warnings | Anything that did not go cleanly. The deploy never stops at the first problem; it reports and carries on. | Deploy and Reset publish without contacting NetSuite, because they run after their own DML and Apex forbids a callout then (`CcNsCallLifecycle.activateSkippingNsCallouts`). On a call with something to check on the NetSuite side (the packaged address nodes' RESTlet version floor on Order Booking and Customer Sync, or an object the org chose), the lifecycle reports *NetSuite checks were skipped because NetSuite wasn't contacted. To run them, open Calls & Mappings and click Activate on this call.* (`CcNsCallLifecycle.NS_NOT_CONTACTED_NOTE`). That is by design here and gives nothing to act on, so the Warnings column leaves it out (`CcNsCallLifecycle.withoutNotContactedNote`, applied by `deployShipped` and `resetCall`); every other warning is kept. Activate in Calls & Mappings runs the full check. An issue about the whole call is never shown with its internal `(call)` key: `CcNsCallLifecycle.describeIssue` renders it as the bare message, and a node's issue as *[] message*. A toast summarizes the pass: *All 9 prebuilt calls were already live* when there was nothing to do, or *N published, N already live, N need attention* when there was. ![Step 6 after a deploy: a results table with one row per packaged call showing status, nodes, rows and version, every row reading Already active](https://taggalongtech.github.io/continuous-control/assets/shots/sf-setup-deploy-calls.jpg) *One row per call, and the chip goes green when all of them are present and published.* The step chip reads **Complete** when every packaged call is present and every call that *can* be published is published, **Not started** when no packaged call exists yet, and **Needs attention** otherwise. A call you retired counts as done rather than holding the chip amber — nothing here is going to publish it, by your own decision. The step is safe to re-run at any time, and it is the step to re-run after upgrading the Salesforce package. On an upgrade to 3.3.0 it creates and activates **Order Line Sync** (`order_line_sync`, zero nodes, held to its own smaller query reserve, `DEPLOY_ZERO_NODE_QUERY_RESERVE`), and `seedShippedRows` adds the new built-in row `Order Line|CC_Percent_Complete__c|custrecord_cc_line_pct_complete` to Order Booking's Order Line object when that object is still on its shipped target, so a live Order Booking reports *Updated - activate in the editor*. Activate it in Calls & Mappings to publish the row. Booking sends `pctComplete` on its lines from built-in logic either way. ### Reset to shipped defaults, from the wizard The same step holds **Reset to shipped defaults**: pick a packaged call from the dropdown — each option reads as its name plus its status, *Not deployed*, *Needs activation*, *Active* or *Retired* — then *Reset selected…*, or *Reset all…* for every packaged call at once. Neither writes anything until you confirm. A panel opens titled **Reset to its shipped defaults?** carrying the server's own summary of that call's real drift, verbatim — added objects, re-pointed built-in objects, removed built-in objects (restored), renamed objects, switched-off objects (switched back on), reverse-direction objects, custom mapping rows, edited built-in rows, the endpoint override, the call name, the description, and the launch switches (both switched back on) — then **This cannot be undone.** and the buttons **Yes, reset** and **Cancel**. A call that has not drifted says so instead: * already matches its shipped defaults. Resetting it rebuilds the same objects and rows and publishes them again.* A retired call adds one more line, saying the reset also makes it Active again and that you can set it back to Inactive afterward. After the confirm, a second table reports what happened, in the same six columns: **Call · Result · Nodes · Rows · Version · Notes**, with a green **Reset** chip on each call that went back, the objects and rows rebuilt, and the call's new version — reset republishes, so the version goes up by one. A toast follows: *N of N calls back on their shipped defaults.* Three things are never touched: your [run history](monitoring-troubleshooting.md), your base field mappings (the un-scoped headers from before calls existed), and the org-wide Product item-type and item-default configuration, which lives on that un-scoped base header. The call's own scoped copy of those item defaults is rebuilt bare along with everything else scoped to the call, which changes nothing at run time — the payload reads the base header. Details in [Configurable calls](configurable-calls.md) and [Field mappings](field-mappings.md). ## Step 7 — Reference data - **Does:** reads NetSuite subsidiaries, currencies, and the item catalogue; syncs subsidiaries onto Salesforce Legal Entities; matches or creates NetSuite items for products. - **Inputs:** none for Fetch or Sync to Salesforce (both re-read live). Product apply takes `matchesJson: [{productId, nsId}]`. Create takes the list of unmatched `productIds`. Legal Entity match takes a `legalEntityId` and `nsRecordId`. - **Apex:** `fetchReferenceData()` POSTs `{"action":"refdata"}` for subsidiaries and currencies, then pages `{"action":"itemlist","page":N,"pageSize":1000}` up to 10 pages (`ITEMLIST_MAX_PAGES`), and the private `loadProducts()` proposes matches by exact, case-sensitive `ProductCode` = item Name/Number. `syncSubsidiaries()` re-reads subsidiaries only and upserts `LegalEntity` (match by `NetSuiteRecordId__c`, then exact name, else create). `matchSubsidiary(legalEntityId, nsRecordId)` stamps or clears one Legal Entity. `applyProductMatches(matchesJson)` stamps or clears `Product2.NetSuiteRecordId__c` on changed rows only. `createUnmatchedProducts(productIds)` refuses the whole batch if any named product shares a code with another active product, then calls `CcNsSyncRunService.startProductSyncBatch(ids)`; the LWC polls `getProductSyncProgress(runIds)` until every run finishes. - **Done when:** `getSetupStatus().legalEntityMatchCount` and `.productMatchCount` reflect the org's intent — there is no single pass/fail flag, the grid's own summary line is the read. - **Re-run:** yes. Fetch and Sync to Salesforce are safe any time (subsidiary sync matches before it creates, never duplicating a Legal Entity); Apply only writes rows that changed; Create only ever targets products still missing a NetSuite id, so re-running carries just the remainder. - **Simulated NetSuite (3.4.0):** the step reads "Simulated NetSuite has no subsidiaries or items to read. Give every account and active product a made-up NetSuite id instead, so invoices, credit memos, payments and refunds can sync right away." and shows a **Stamp every account and product** button. It calls `stampSimulatedIds()`, which returns `StampResult {accounts, products}`. It stamps `NetSuiteRecordId__c = CcNsSimulatedNetSuite.fakeId(Id)`, `NetSuiteSyncStatus__c = 'Completed'` and `NetSuiteSyncTimestamp__c` on up to 2,000 Accounts and up to 2,000 active Product2 rows that have no `NetSuiteRecordId__c` (`WITH USER_MODE`, `Database.update(rows, false)`, partial success; the counts are the rows that saved). The result line reads "N accounts and N products got a made-up NetSuite id." With the switch off it is refused: "Turn on Simulate NetSuite in step 1 first. Made-up ids are only for a simulated connection." While simulated, the chip reads Complete once `productMatchCount > 0` instead of `legalEntityMatchCount > 0`. **Fetch from NetSuite** still runs and gets empty lists. Re-clicking stamps only rows still without an id. Pulls your NetSuite subsidiaries, currencies and item catalogue over the connection. **Sync to Salesforce** mirrors each active subsidiary onto a Salesforce Legal Entity (creating one when none exists — NetSuite is the source of truth for subsidiaries), which is how a booked order lands in the right subsidiary. Currencies reconcile against the org's native multi-currency setup. ### Products → NetSuite items The same **Fetch from NetSuite** lists each active Salesforce product that has a NetSuite item, already stamped or proposed by code, beside that item, so a catalogue NetSuite already holds — an account set up before Continuous, or a second Salesforce org connected to the same account — is matched *before* the first booking rather than colliding with it (NetSuite keeps item names unique, so a booking that tries to create an item that already exists fails that line with *Uniqueness error*). Nothing is written until you click **Apply product matches**. ![Step 7 products grid: each active product beside the NetSuite item its code names, with In sync, Will stamp and No match chips](https://taggalongtech.github.io/continuous-control/assets/shots/sf-wizard-product-match.jpg) *Step 7 — products beside the NetSuite items their codes name. Adjust any row, then Apply.* ![Under the grid: '152 active products have no NetSuite item' with a Show unmatched products link and a Create 152 products in NetSuite button](https://taggalongtech.github.io/continuous-control/assets/shots/sf-wizard-product-create.jpg) *The unmatched products stay out of the grid; one button creates them all through Product Sync.* - **The match rule is code only, and exact.** A product is proposed the NetSuite item whose **Name/Number** equals the product's **Product Code** character for character, case included — NetSuite keeps item names unique case-sensitively, so `QB-API` and `qb-api` are two items and only the one spelled like the code is a match. There is no name or fuzzy matching, and a product with no code is never proposed anything. Inactive NetSuite items are read but never proposed. - **A code two active products share is never matched or created.** Both rows show no proposal, and **Create them in NetSuite** stays disabled naming the shared codes until the products are fixed and fetched again, because two items cannot carry one name. - **A product that already carries a NetSuite id stays as it is** (*In sync*). The wizard never re-proposes it; choose *– none –* and Apply to clear a wrong stamp, or pick another item to re-stamp it. - **The chips**: *In sync* (stamped, unchanged), *Will stamp* (an item is picked for an unstamped product), *Will re-stamp* and *Will clear*. The summary line counts each, plus the number of NetSuite items read. - **Products with no item are not in the grid.** They are counted on one line under it — *N active products have no NetSuite item* — with a **Show unmatched products** link that brings them into the grid for anyone who wants to hand-pick an item for one. - **Create them in NetSuite** takes every product on that line and runs [Product Sync](ref-shipped-calls.md) for all of them in the background, forty products to a run, through the same NetSuite items action the product page uses. The step shows *Creating… 3 of 8 batches done* while the runs go, then refreshes the grid so the new items show as *In sync*. A batch that fails lands in [Sync Runs](monitoring-troubleshooting.md) like any Product Sync failure, with the products that got no item named; the ones that landed are stamped either way, and running it again carries only the rest. The button waits while picks are pending, because a product whose code names an item is a match to apply, never a duplicate to create, and it refuses outright while two active products share a code. A product left alone is still created by its first booking, as before. - **Apply** writes **NetSuite Record Id** on each changed product, sets its sync status to Completed with a message saying it was matched in the wizard (or clears both), and reports *N stamped · N cleared · N unchanged*. **Discard changes** drops unapplied picks. Every later booking, invoice and product sync then reuses the matched item; the NetSuite item's own external id is left untouched. - **Lines on a call you build need the item's external id.** A line on the `item` sublist of a document you added (a custom call's lines, or a document added to a shipped call) sends `itemRef` with the Product2 Id as `parentEid` and no NetSuite id, so NetSuite finds the item by external id only, never by `NetSuiteRecordId__c`. A matched item keeps whatever external id it had, usually blank, and Product Sync never sets `externalid` on an item that already exists. So for those lines, set each matched item's External ID to its product's 18-character Salesforce id in NetSuite, for example with a CSV import that updates items keyed on Internal ID. An item carries one external id: with two Salesforce orgs on one account, only the org whose product id it holds can use it on those lines, while both orgs' bookings, invoices and Product Sync keep using their matches. Details: [A line's item is found by its external id](calls-add-objects.md#line-item-external-id). - The grid lists up to 2,000 active products and reads up to 10,000 items; beyond either the summary says so. ## Step 8 — Sync settings - **Does:** saves the org-wide `CC_NetSuite_Settings__c` runtime switches, deploys an optional retry-strategy class, and schedules or cancels the nightly purge cron job. The cash app pull schedule controls are hidden (Cash App Pull is hidden while `CcNsCallRegistry.CASH_APP_PULL_ENABLED` is off; `cashAppPullAvailable` returns false). - **Inputs:** `engineEnabled` (stored inverted, as `Disable_Revenue_Engine__c`), `invoiceChunkSize`, `notifyOnFailure`, `failureNotifyEmail`, `runRetentionDays`, `cashAppPullIntervalMinutes` (field hidden), `createNativeSalesOrder`, `allowNetSuiteBilling`. Separately, a Retry Strategy Apex class name that must implement `ICcSyncRetryStrategy`. - **Apex:** `saveSettings(settingsJson)` deserializes to `SettingsDto` and upserts `CC_NetSuite_Settings__c.getOrgDefaults()`. `saveRetryStrategy(className)` validates the class via `Type.forName()` plus an `instanceof ICcSyncRetryStrategy` check, then redeploys `CC_NetSuite_Env.Default` whole-record with the new `Retry_Strategy_Class__c`. `purgeRunsNow()` calls `CcNsRunPurgeSchedulable.purge()` and refuses if Run Retention Days is blank. `scheduleNightlyPurge()` / `cancelNightlyPurge()` wrap `System.schedule('0 0 3 * * ?', …)` / `System.abortJob()`. `scheduleCashAppPull()` / `cancelCashAppPull()` call `CcNsCashAppPullSchedulable.schedulePerSettings()` / `.unschedule()`. - **Done when:** `getSetupStatus()` reflects the saved values; `purgeScheduled` / `cashAppPullScheduled` and their `next*` timestamps confirm the cron jobs exist. - **Re-run:** yes throughout — saves are plain upserts, and both schedule actions cancel any existing job before creating the new one. Runtime behavior for every sync: the **Continuous Revenue Engine** switch, chunk sizes, failure notifications, run retention, **Create Native Sales Order** and **Allow NetSuite billing**, and the nightly run purge. Defaults match packaged behavior — change only what you need. Details in [Sync settings](sync-settings.md). The engine switch is the one to understand first. On, a booking or invoice sync asks NetSuite to rebuild the contract's revenue plans after Update POBs; off, Update POBs and the POB mirror still run and no revenue plans, schedules or journal entries are produced. NetSuite carries the same switch on its Continuous Revenue Settings record and either side off wins: when NetSuite reports its engine (or its native Sales Orders) off, the step shows a hint under the matching switch, so you can see from Salesforce why plans or Sales Orders are not appearing. Create Native Sales Order needs no NetSuite revenue feature; Allow NetSuite billing left off keeps every generated Sales Order stamped Do Not Bill (Continuous), so Salesforce stays the only source of invoices. ## Step 9 — Health - **Does:** run-audit stats over the last 7 days, a live sweep of every mapping row's Salesforce field path, and a NetSuite settings/drift check. - **Inputs:** none. - **Apex:** `getHealth()` aggregates `CC_NetSuite_Sync_Run__c` by `Type__c`/`Status__c` for `LAST_N_DAYS:7`, plus the most recent Failed run and total/oldest run counts. `validateAllMappings()` walks every `CC_NS_Mapping_Detail__c` row of `SourceType__c = 'SourceField'` through `CcNsMappingAdminController.describePath()` and reports the ones that no longer resolve. **Check NetSuite settings & drift** re-calls `discoverEnvironment()` — the same `setupinfo` action step 5 uses — and compares its `baseUrl`/`contractTypeId`/`orderTypeId` against the currently deployed `getSetupStatus()` values; there is no separate dedicated health-check action. - **Done when:** `validateAllMappings()` returns an empty list, and the drift check reports "Environment in sync with the live NetSuite account." - **Re-run:** yes — every call this step makes is read-only. Run-audit statistics, mapping validation, and NetSuite parameter and drift checks for the live connection — the place to look when something feels off, and a good periodic check after upgrades. The NetSuite-side check reads the **Continuous Revenue Settings** record (subsidiary, currency, deferred and revenue accounts, item subsidiaries, tax schedule, tax item, refund bank account and payment method, the two Disable switches, and the default rule) and flags any value still on its shipped default. The values are read-only here; change them in step 4. Details in the [settings reference](ref-settings.md). # Credentials & authentication Human page: https://taggalongtech.github.io/continuous-control/credentials-auth.html Salesforce authenticates to NetSuite with Token-Based Authentication — a consumer key/secret identifying the Continuous integration, and a token id/secret identifying your integration user and role. Entered once, stored server-side, never displayed again. ## The token flow Two credential pairs work together. The **integration record** in NetSuite (consumer key/secret) identifies the Continuous Control application; the **access token** (token id/secret) binds it to a specific user and role — which is what scopes everything the sync is allowed to touch. Every request Salesforce sends is signed with all four values using OAuth 1.0a with HMAC-SHA256; NetSuite verifies the signature and executes under the token's role. There are no passwords and no session cookies anywhere in the flow — see the [security model](security-model.md) for the signing detail. ## The Integration record's OAuth 2.0 trap The consumer key/secret pair comes from a NetSuite **Integration** record ([created during NetSuite install](install-netsuite.md)). **Under OAuth 2.0, uncheck Authorization Code Grant** — NetSuite pre-checks it on a new record, and leaving it on fails with *Invalid Redirect URI* the first time Salesforce authenticates. Only Token-Based Authentication should be checked. ![The NetSuite Integration record's Authentication tab: Token-Based Authentication checked, TBA Authorization Flow unchecked, and both OAuth 2.0 boxes unchecked](https://taggalongtech.github.io/continuous-control/assets/shots/ns-integration-auth-tab.jpg) *The Integration record's Authentication tab, checked correctly before saving.* Check the whole record against this list before you save it: | Field | Value | |---|---| | Name | Any name your team recognizes (Salesforce stores the consumer key/secret, not this name) | | State | Enabled | | Token-Based Authentication | Checked | | TBA Authorization Flow | Unchecked | | OAuth 2.0 | Both boxes unchecked | ## Entering credentials Credentials are entered in [Connection wizard step 1](connection-wizard.md): account id, the four secrets, and the RESTlet script/deploy ids (defaults provided). Sandbox account ids are entered as they appear in your NetSuite URL. ![Step 1 of the wizard: credential fields with the stored values shown masked as bullets plus last four characters](https://taggalongtech.github.io/continuous-control/assets/shots/sf-wizard-credentials.jpg) *Stored secrets appear only as a masked summary — last four characters, nothing more.* ## How secrets are stored and masked - Values live in a **Salesforce org-level custom setting**, readable server-side by the sync and never included in page markup after saving. - The wizard displays only a masked fingerprint (`••••` + the last four characters) so an admin can confirm which token is configured without ever seeing it. - Leaving a secret field **blank on re-save keeps the stored value** — you can update the account id or script ids without re-typing secrets. ## Rotating credentials To rotate: create a new access token in NetSuite (Setup → Users/Roles → Access Tokens), paste the new token id/secret into step 1, save, and run the step-3 connection test. The old token can then be revoked in NetSuite. The same procedure covers a new integration record (rotate the consumer pair) — nothing else in the configuration changes. > **Scope reminder.** The sync can only do what the token's NetSuite role permits. Use the shipped [Continuous Control Integration role](install-netsuite.md) rather than full Administrator — see its full permission list there. # Field mappings Human page: https://taggalongtech.github.io/continuous-control/field-mappings.html The **Calls & Mappings** tab makes the sync's field writing visible and editable. Built-in rows document what ships; edit one to override it, add a custom row to extend it — per node, with live validation on both sides of the wire. ![The mapping editor: node rail on the left (Customer, Product, Contract, Order, …), rows for the selected node on the right](https://taggalongtech.github.io/continuous-control/assets/shots/sf-mapping-editor.jpg) *One node per synced record type; each node lists its rows with origin, source, and NetSuite field id.* ## How mappings work Every sync payload is assembled from built-in logic (ids, amounts, dates, relationships — the structural fields that make the integration work), and then the active mapping rows for each node are applied on top. A row copies a Salesforce field (any dot-path, like `Account.Owner.Email`) or a fixed value into a named NetSuite field id. The extras ride the payload with each record and are merged *last* on the NetSuite side — so an edited row genuinely overrides the built-in value, and a custom row adds a field the sync would not otherwise touch. Blank Salesforce values are skipped: mappings never clear a NetSuite field. **Customer addresses are ordinary rows**, on the two packaged [address nodes](calls-edit-mappings.md#addresses) under Customer: the label, the default flag and the six address fields are eight mapping rows you can read, edit, re-source or switch off, exactly like any other. ## The mapping editor The left rail lists one node per synced record type — Customer, Product, Contract, Order, Order Line, the billing records, Invoice and its lines, Credit Memo, Payment, Refund, and their application sublists. Each node shows its target NetSuite record type and its rows in order: ![The Order node with eleven built-in rows mapping OrderNumber, EffectiveDate, Status, amounts and discounts to their NetSuite field ids](https://taggalongtech.github.io/continuous-control/assets/shots/sf-mapping-order-rows.jpg) *The Order node: built-in rows are the documentation of what the sync writes.* Editing is validated as you type: Salesforce source paths autocomplete and are checked against the schema; NetSuite field ids can be picked from a *live* field list fetched from your account over the connection; and a type-compatibility check warns or blocks when the two ends don't line up (a date into a checkbox blocks; a number into a text field warns). ## Choosing a node's NetSuite target Each node on the rail shows the NetSuite record type it writes to, with a glyph for what kind of thing that is: a custom record, a native transaction, an entity, an item, a standard NetSuite record, or a sublist line. Open a node to change it. The picker asks for the **kind** first and then the **type** within it, with a live list read from your account wherever NetSuite can supply one, and a **Check** that probes the type over your connection and answers the question you actually asked: *NetSuite knows this type — 214 fields*, or *NetSuite has no record type called *. Anything less conclusive — no credentials, an unreachable account, a permission error on a type that *is* there — is reported as the failure it was, because none of those is a verdict about the record type. **Standard record** is the wide kind: everything NetSuite masters, read live from your account and grouped by family — Entities, Items, Transactions, Billing, Other records. The list is already filtered to the features your account has switched on, so a chip you can see is a record type you have. If NetSuite cannot be reached the picker falls back to the shorter set this package curates and badges each row with the NetSuite feature it depends on, since nobody was able to check. Neither list is a limit: under the chips sits an **Other record type** field that takes any record type id, with its own **Check** — the account is what proves a record type exists, never the catalogue. Not everything is retargetable, and the editor says which is which. **Order** on Order Booking can become a native transaction or another custom record type — the custom-record kind is how a re-pointed Order goes back to `customrecord_cc_order` — **Product** can become any item type on any call, and nodes you added yourself can be anything allowed. A line or application node follows its document, so re-pointing Order moves Order Line to the matching line sublist by itself. The document and application nodes on Invoice, Credit Memo, Payment and Refund Sync are fixed. See [Native transaction targets](calls-sales-orders.md#native-transaction-targets) for what a re-point changes downstream. Every node's shipped target and its alternates are listed in [Shipped calls](ref-shipped-calls.md). The three application nodes — **Payment Applications**, **Refund Allocations**, and **Credit Memo Applications** — ship built-in rows like any other node, and a custom row can extend them the same way. A fixed target does not mean a fixed row set — it means the NetSuite record type cannot be changed. **Built-in rows are target-specific.** The rows that ship for the Order node describe the Continuous Control order record; on a Sales Order they would be writing fields that do not exist. So when you change a target, the editor offers the shipped row set for the new target and lets you choose. **Replace** swaps the built-in rows for the new target's set — for a Sales Order that is order date to `trandate`, order number to `otherrefnum`, and description to `memo`, with quantity, rate, amount, description and the two revenue dates on the lines. **Keep my rows** leaves every row exactly as it is. The offer covers the child node too when a line node follows its parent. Rows that no longer resolve on the new target lose their Built-in badge, and Validate names each one. **The suggested target fields follow the target too — on nodes you added as well as packaged ones.** The chips under the target column are keyed by the NetSuite record type, not by which node is asking, so a node you added yourself and pointed at a Sales Order is offered the same sales-order fields a re-pointed **Order** node is: `trandate`, `otherrefnum`, `memo`, `shipdate`, `orderstatus`. A line node under it gets the line set — `quantity`, `rate`, `amount`, `description`, `revrecstartdate`, `revrecenddate`, `taxcode`. A record type the package has no curated list for, such as a custom record of your own, has no chips to offer: there the live field list read from your account is the honest suggestion, and that is what the editor shows. ### Write options Under **Advanced**, a node you added carries a **Write options** panel — how its records get written, as a form rather than as JSON. It is empty by default and empty means *exactly what the sync always did*: a node whose panel you never open sends no write instructions at all. | Option | What it does | |---|---| | **Allow this node to create the record** | NetSuite entities — customer, contact, vendor, employee, partner — are updated but never created unless a node asks. Offered only on those, because everything else is created already. The fields NetSuite refuses to save the record without are named next to the checkbox, read live from the record type, so a missing subsidiary is something you learn here rather than from the first run. | | **Custom form** | The internal id of the NetSuite custom form the records are saved on. Blank uses the record type's preferred form. | | **Set by text, not by id** | List fields whose value is sent as text, so NetSuite matches it to a list entry by name instead of by internal id. Picked from the record type's own select and multiselect fields. | | **Clear when Salesforce is blank** | By default a blank Salesforce value leaves the NetSuite field alone. Fields listed here are emptied instead. | | **Lookups** | A NetSuite field filled by finding another record — a project, a class, a department — by its **external id**, which is the only lookup the writer performs. Each one names the NetSuite field, the table the record lives in, and a Salesforce field on this node's own object whose value is read per record; the field path gets the same dot-path typeahead the mapping rows have. A record whose Salesforce field is empty simply skips that lookup, with a warning on the run, rather than reporting the value as missing in NetSuite. A row that has not named all three parts is flagged and is not saved. | | **Sublist key fields** | For a node whose children are rows on its document: the column each sublist matches its rows on. Shown only where the node actually drives sublists, with the default as the placeholder — change one only when a sublist keys its rows elsewhere. | Write options this panel has no control for — a table override, a name mode, a lifted field block, a sublist's mode or its delete confirmation — are still readable and writable on the node record, and a config carrying them survives being opened and saved here untouched, at every level: the block, each lookup, and each sublist declaration. A Write Config that will not parse at all is shown as it stands and left exactly alone by Save. **Validate** is what checks the whole thing: every field id named has to exist on the target and every sublist declared has to be one the record actually has, and each finding names the node. > **Write options need NetSuite package 3.0.0.** An entry only carries write instructions when a node asks for some, so a call with none runs against any supported NetSuite build — and every 3.0 install already has ns-v3.0.0, the one floor 3.0.0 checks at install, so a Write Config on any node you added needs nothing more than that (a packaged node's entries are built by the section that owns them, so a value saved there is never sent and never raises the floor). **Validate** checks the account's package against it and blocks Activate with an error naming the version if it is somehow older. The verdict is published with the call, so a NetSuite *downgrade* after the fact is refused at run time rather than half-honoured. Target changes are **draft**, like everything else in the editor: the running sync keeps the last activated target until you Activate. A node that has moved off the type it ships with carries a **Retargeted** badge, and its editor offers a one-click reset back to the shipped default. ## Built-in vs custom rows | Row origin | Behavior | |---|---| | **Built-in** | Ships with the package; documents a field the sync writes. Unedited, it is exactly what happens; edited, your version wins. | | **Custom** | Added by you; writes an additional NetSuite field. Sequence order decides who wins if two rows target the same field (later wins). | **Order Line rows and Order Line Sync.** Order Booking's Order Line node ships built-in rows for the line status fields: `CC_Delivered__c` → `custrecord_cc_line_delivered`, `CC_Delivered_Date__c` → `custrecord_cc_line_delivery_date`, `CC_Fulfilled__c` → `custrecord_cc_line_fulfilled`, `CC_Fulfilled_Date__c` → `custrecord_cc_line_fulfilled_date`, and (Salesforce 3.3.0) `CC_Percent_Complete__c` → `custrecord_cc_line_pct_complete` (`CcNsShippedCatalog.coreRows()`; the target also appears in the Order Line suggestion chips, `CcNsMappingAdminController.TARGET_SUGGESTIONS`). On an org upgraded from 3.2.x, wizard step 6 adds the % Complete row to a live Order Booking whose Order Line node is still on its shipped target and reports the call *Updated - activate in the editor*; activate it to publish the row. The booking sends `pctComplete` from built-in logic either way. The shipped **Order Line Sync** call (`order_line_sync`) sends those five fields on their own, as a fixed field set with no nodes and no rows; it is the one path where a blank Salesforce value clears the NetSuite field. See [Shipped calls](ref-shipped-calls.md#order-line-sync). A short list of structural NetSuite fields is protected from mapping (record ids, external ids, entity links, record type) — the integration's correctness cannot be mapped away. The block also covers the **thirteen Source Org fields** the sync stamps on every record it touches (`custbody_cc_source_org`, `custentity_cc_source_org`, `custitem_cc_source_org`, and one `custrecord_cc_*_source_org` field per Continuous record type) — a mapping row can never set or clear them, on any node, because the stamp is the RESTlet's own and has to be trustworthy. It is worth saying what is *not* on the block list: **the address book's key column is `label`**, an ordinary row like any other. What NetSuite matches an address on is therefore configuration, not code, which is what lets a third address object write a third row simply by giving it a label of its own. ## Value maps A **Value Map** transform bridges a Salesforce picklist to a NetSuite list field: the editor loads both option lists — Salesforce picklist values and the NetSuite field's options, live — and you pair them up. At sync time the row translates the label to the NetSuite internal id. ## Transforms Three more transforms need no settings at all; you pick one from the same menu and the row does the rest. **First line** sends only the first line of a multi-line value, and **Remaining lines** sends everything after it, joined with a comma. That pair is how a Salesforce street becomes a NetSuite address: `400 Main St` on the first line and `Suite 210` on the second arrive as `addr1` and `addr2`. A street with only one line simply leaves `addr2` alone. **Country code** sends a country the way NetSuite stores it, as its two-letter ISO code. A value that is already a code passes straight through; a country name is looked up, so `United States` becomes `US` and `United Kingdom` becomes `GB`. A name the lookup does not recognize is skipped with a warning on the run, and the country in NetSuite is left as it was rather than guessed at. ## Product item types and defaults The Product node carries two extra panels, under **Also built into the sync** → **Edit** on the row. **Item types** decides which NetSuite item type a product becomes (non-inventory by default; service, other-charge, download, or inventory by rule) — the same choice you can make structurally by pointing the Product node's target at an item type, which sets the default for every product that call creates. An explicit rule in the Item types panel still wins over the node's target. **Item defaults** sets what new items are created with — an income account, a tax schedule, and subsidiaries (all active subsidiaries unless narrowed); the dropdowns list live NetSuite accounts, schedules, and subsidiaries by name. Defaults apply on item *create* only; after that, NetSuite masters its own item configuration and the sync will not overwrite it, including a mapping override. ### Recognition rule per item Each NetSuite item carries a revenue recognition rule, `custitem_cc_rev_rule`: Straight-Line for subscriptions, % Complete for services, Event-Based for usage, or Delivery/Fulfillment for point-in-time obligations. A POB inherits its item's rule; a rule set directly on a POB wins. Two ways to manage it: - **From Salesforce** — set **NetSuite Revenue Rule** (`CC_NS_Revenue_Rule__c`) on the product. Once set, Salesforce owns the rule: every sync of that product, including a re-sync of an already-linked item, stamps it onto the NetSuite item. - **In NetSuite** — leave the Salesforce field blank and curate the rule on the item directly. The sync never overwrites a NetSuite-curated rule; a usage-definition SKU defaults to Event-Based when the item is first created. Either way, changing the rule affects **new** obligations only — existing POBs keep the rule they inherited unless you edit the POB's rule directly. ### Per-product GL accounts To drive `incomeaccount` per product instead of the org-wide Item defaults value: 1. Create a picklist on Product (for example `Revenue Category`: SaaS, Services, Hardware). 2. On the Product node's **Also built into the sync** panel, click **Override** next to `incomeaccount` and pick that picklist as the source field. 3. Add a **Value Map** on the row — the NetSuite column lists the account's real GL accounts by name; map each picklist value to its account. New items then land with the mapped account at create; the create-only rule above still applies, so an existing item's account is never rewritten by a later sync. ## Document rows A **Document** row (available on Invoice, Credit Memo, and Contract nodes) names a Salesforce-generated document by title pattern; matching files are pushed to the NetSuite File Cabinet and attached to the corresponding record after the sync completes — contracts get their order form, invoices their PDF. > **This editor lives inside a call.** Every node above belongs to a versioned call (Order Booking, Invoice Sync, and the rest) with its own Draft → Validate → Activate lifecycle. A dropdown above the rail switches between calls; each call carries its own endpoint; nodes can be added, edited, re-pointed, switched off or removed; and you can build an entirely new call from scratch. See [Configurable calls](configurable-calls.md). # Configurable calls Human page: https://taggalongtech.github.io/continuous-control/configurable-calls.html Every sync — Order Booking, Invoice Sync, Payment Sync, and the rest — is a **call**: a versioned tree of **nodes**, each one a Salesforce record type mapped to a NetSuite record type. Calls carry the same Draft → Validate → Activate lifecycle as [field mappings](field-mappings.md). Admins can edit any shipped call, extend any of the seven push calls that have nodes with a Salesforce object of their own, or build an entirely new call from scratch, all from the admin UI with no code. Configuring calls requires the **Continuous Control Admin** permission set — see the [security model](security-model.md). This page covers what a call is and how a change is published. The detail of each kind of change lives on its own page: | Page | Covers | |---|---| | [Change what a call sends](calls-edit-mappings.md) | Renaming, switching off and deleting nodes; node dependencies; packaged nodes; the Billing/Shipping Address objects and re-sourcing them; Reset to shipped defaults | | [Book orders as Sales Orders](calls-sales-orders.md) | Native transaction targets on the Order node: replace vs add alongside, the six transactions, what changes downstream, Do Not Bill | | [Add your own objects to a call](calls-add-objects.md) | Extension nodes: the NetSuite record type prerequisite, the Add object wizard field by field, transaction nodes and their lines, the create-first customer, Write options, Validate's NetSuite leg, run-time behavior, limits | | [Build a call of your own](calls-create-call.md) | The New call wizard, the Quote to Estimate worked example, running a custom call from the card or Flow, call keys, deleting a call | | [Send a call somewhere else](calls-endpoints.md) | The endpoint bar, the three endpoint kinds, method/action precedence, Manage endpoints, Remote Sites, relay results | ## Calls and nodes The **Calls & Mappings** tab opens on a call selector — a dropdown listing every call, packaged ones first (Order Booking, Invoice Sync, Credit Memo Sync, Payment Sync, Refund Sync, Product Sync, Customer Sync, Order Line Sync, POB Refresh) and any calls you've created after them (the nine are catalogued in [Shipped calls](ref-shipped-calls.md)). Each option reads as the call's name plus its version, with a **Draft changes** suffix when it has unpublished edits (for example, "Order Booking · v4 · Draft changes"); the selected call's status, version, and draft-state chips render beside the dropdown. Eight of the nine push Salesforce data to NetSuite. Order Line Sync pushes a fixed field set with no nodes (its rail says *This call sends a fixed set of fields for each booked order line: … There is nothing to map.*), so custom objects can't be added to it; POB Refresh pulls data back and has no push graph to extend. A **+ New call** button sits next to the selector — see [Create your own call](calls-create-call.md#create-your-own-call) below. **Before step 6 the tab is empty.** The packaged calls are created only by **Deploy prebuilt calls & mappings** in [step 6 of the Connection wizard](connection-wizard.md) (`CcNsConnectionSetupController.deployShippedCalls`). Opening the tab never creates them: `CcNsMappingAdminController.getCalls()` is a pure read and returns an empty list on an org with no `CC_NS_Call__c` rows. The editor then skips `getNodesForCall` and shows one line, *No calls are set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page.*, with an **Open NetSuite Connection** button that navigates to the `CC_NetSuite_Connection` tab. A failed `getCalls()` shows its error toast instead, never the empty state. When the calls exist but `order_booking` is not among them, the editor opens on the first call it has. Selecting a call loads its own node rail — the record types that call touches, in the order they sync, nested where one is a child of another (Order Line under Order; Billing Schedule and Billing Period under Billing Schedule Group). A node is scoped to its call: the same Salesforce object can be mapped differently, or not mapped at all, on two different calls. Each node's mapping rows are exactly what [Field mappings](field-mappings.md) describes — built-in rows document packaged behavior, custom rows extend it, both editable per node. What this page adds is the call wrapper around that editor: versioning, the ability to add whole new nodes rather than just new fields on existing ones, and the ability to define a call yourself. ## The call lifecycle: Draft → Validate → Activate Editing a call's rows, toggles, or nodes saves a **draft** — the dropdown's selected-call chips show an amber **Draft changes** chip, and every sync in the meantime keeps running the last activated version. **Validate** checks the whole draft — Salesforce-side field and type checks, plus a live NetSuite-side check for anything the wizard added (below) — and reports errors and warnings without publishing anything. The panel groups them by node; an issue about the whole call (no active root node, the NetSuite RESTlet version floor, a NetSuite-side check that could not run) carries the internal node key `CcNsCallLifecycle.CALL_LEVEL_KEY` and is listed under *This call*, never under that key. **Activate** publishes the draft as the call's new version in one step: the chip flips to a green **Active** chip with the new version number, and every subsequent run of that call uses it. > **Everything waits for Activate, targets included.** A node's NetSuite target record type, its on/off state, its mapping rows, the nodes you add or remove, and the call's endpoint are all draft state: the running sync keeps using the last activated version until you publish. A target change never publishes on its own, so a half-finished re-point can never reach a live sync. # Change what a call sends Human page: https://taggalongtech.github.io/continuous-control/calls-edit-mappings.html Every node on a packaged call can be renamed, switched off, deleted or re-sourced, and any packaged call can be reset to how it shipped. All of it is draft state until the call is activated; see [the call lifecycle](configurable-calls.md#the-call-lifecycle-draft-validate-activate). ## Editing and removing shipped nodes Packaged nodes are editable. On any call, a node can be renamed, pointed at a different NetSuite record type, switched off, or deleted outright — the same controls that serve nodes you added yourself. | Action | What happens | |---|---| | **Rename** | Changes the label on the node rail. The node's key — the identity the payload, the run counts, and the NetSuite side all use — never changes, so nothing downstream moves with it. | | **Switch off** | Leaves the node out of the next sync completely: no records of that type are sent, NetSuite skips its whole section and names it in the run's `skippedNodes`, and — on Order Booking, the one call whose card mirrors its nodes — the sync card drops its checklist row. | | **Delete** | Removes the node and its mapping rows. The editor previews what goes with it (rows, child nodes, and the nodes that depend on it) and refuses a delete that would break the call: a node that still has children, or the call's root object. | Dependencies are enforced rather than assumed. Order lines need their Order, a billing schedule needs its group, a billing period needs both, and a line or application node needs its document — dropping one of those blocks Activate with a message naming both ends. Customer, Contract, and Product are the exception: they warn instead of blocking, because the payload can still carry a reference to them. > **Omitted nodes still resolve their links.** When Customer, Contract, or Product is switched off or deleted, the payload still carries a bare reference for each one — the Salesforce id plus the NetSuite id already stamped on the record — so an order still lands under the right customer, still links to its contract, and its lines still point at the right items. The catch is a record that has never been matched to NetSuite: the run is refused before it starts, naming what is missing. That refusal is unconditional for Customer and Product; for Contract it applies when the booking still lands an RC Order (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), which is the record that needs the contract link. Validate warns you about all of it at Activate time rather than at 2am. Deleted a packaged node you actually needed? [Reset to shipped defaults](#reset-to-shipped-defaults) is the way back. Nothing else puts it there: the background seed that fills in missing nodes runs only on a call that has never been activated, precisely so it can never quietly undo a deliberate deletion. ### Packaged nodes Most built-in objects write a record of their own and are marked **Built-in**. A few are **packaged** instead: the product ships them, names them and gives them their mapping rows, but they are ordinary configurable objects underneath, so their Salesforce object, their relationship field and every row are yours to change. **Billing Address** and **Shipping Address** are the two packaged objects this release ships, on Order Booking and on Customer Sync. - **Fully editable.** Change which Salesforce object they read, which field relates them to the customer, and which rows they send. The NetSuite side they write is fixed: they are rows in the customer's address book. - **Reset restores them.** [Reset to shipped defaults](#reset-to-shipped-defaults) puts the object, the relationship and the shipped rows back exactly as they ship, and the confirm panel counts what that would discard, including an address object you re-sourced. - **Deploy adds them.** **Deploy prebuilt calls & mappings** adds the two objects with their rows. Delete one afterwards and no later deploy puts it back, exactly as with any other built-in object. ### Moving and deleting nodes - **A node cannot be moved to a different parent.** Where a node sits decides its NetSuite parent link, its shipped row set and every follow rule downstream, so re-parenting is a bigger operation than an edit and is not supported this release — the editor refuses it by name rather than accepting the change and doing nothing. Delete the node and add it again under the parent you want; its mapping rows go with it, so budget for re-adding those. - **A node with children cannot be deleted until they are.** Deleting a node removes it and its mapping rows, and the editor refuses one that still has children rather than orphaning them: delete the children first and the parent frees up. Customer has children now (the two [address objects](#addresses)), so this is the common case rather than an edge one, and the delete preview names what stands in the way. Previously-synced NetSuite records from a deleted node are left as they were — nothing is retroactively cleaned up in NetSuite. Draft deletes don't take effect immediately — the running sync keeps the object until the next Activate. ## Addresses are nodes Addresses are objects you can edit. **Billing Address** and **Shipping Address** sit under **Customer** on Order Booking and on Customer Sync, each writing one row in the NetSuite customer's address book, each with its rows in the editor beside every other mapping. | Row | Reads | Writes | Transform | |---|---|---|---| | Label | the literal `Billing` (or `Shipping`) | `label` | none | | Default flag | the literal `true` | `defaultbilling` (or `defaultshipping`) | none | | Street, first line | `BillingStreet` | `addr1` | First line | | Street, the rest | `BillingStreet` | `addr2` | Remaining lines | | City | `BillingCity` | `city` | none | | State | `BillingState` | `state` | none | | Postal code | `BillingPostalCode` | `zip` | none | | Country | `BillingCountry` | `country` | Country code | Three [row transforms](field-mappings.md#transforms) do this without custom code: **First line** and **Remaining lines** split a multi-line Salesforce street across NetSuite's two address lines, and **Country code** sends `US` where Salesforce holds "United States". A row with no transform is a plain copy. - **The label is the key.** NetSuite matches an address book row by its label, so the label row is what makes a re-sync update the address it wrote last time instead of adding another one. Every address object needs a row that sets `label` (a literal, or a field), and Validate says so by name if one is missing. - **A blank address sends nothing.** An account with no street and no city produces no row at all, rather than an empty address. A country on its own is not an address either, so it does not earn a row. No address book row is ever deleted: whatever NetSuite already holds stays there. - **A cleared field clears in NetSuite.** Salesforce owns the whole address, so an address row sends every field it maps, blank ones included: clear a street in Salesforce and the next run clears it in NetSuite rather than leaving half the old address standing. `country` is the one field a blank never clears, because an address with no country validates nothing. - **Switched off or deleted means no address is sent.** Switch an address object off and that address stops being written; delete it and the same. NetSuite keeps whatever it already holds. - **Removing one of the two** leaves the other in charge of the default flags: NetSuite marks whichever address the run sends as the default for its kind, and a customer that ends up with exactly one address on it carries both flags. So dropping Shipping Address does not leave a customer with no default shipping address. - **This needs NetSuite ns-v3.0.0.** The address book is written as rows inside a subrecord. Every ns-v3.0.0 install supports it. See [Versions & compatibility](ref-versions.md). - **An address row is matched by its `label`, and `name` is still blocked.** `name` never comes from a mapping row (see the structural blocklist in [Add your own objects to a call](calls-add-objects.md#notes-and-limits)). An address book row does not use it at all, because NetSuite keys those rows on `label`, which IS an ordinary mapping row you can change (see [Addresses are nodes](#addresses)). ### Reading addresses from somewhere else The shipped rows read the Account's own address fields, which is where most orgs keep them. When yours live somewhere else, re-source the object. The worked example is **ContactPointAddress**, the standard object Salesforce uses when an account has many addresses: 1. Open **Billing Address** in Calls & Mappings and set its **Salesforce object** to `ContactPointAddress`. 2. Set the **relationship field** to `ParentId`, the lookup that points at the Account. (On the shipped object it reads *This record*, because the Account IS the customer's own record.) 3. Set the **row filter** to `AddressType = 'Billing'`, so this object sends the billing address and not every address on the account. 4. Re-point the rows at the new object's fields: Street to `addr1` (First line), Street to `addr2` (Remaining lines), City to `city`, State to `state`, PostalCode to `zip`, Country to `country` (Country code). Leave the two Value rows alone: `Billing` to `label` and `true` to `defaultbilling`. 5. Repeat on **Shipping Address** with `AddressType = 'Shipping'`, and Activate. Rows whose Salesforce field no longer exists on the new object are reported the moment you save, so the re-source and the re-mapping are one sitting rather than a surprise at the next run. **More than two addresses?** Add an object of your own the same way: **Add object**, Salesforce object `ContactPointAddress`, NetSuite target `customer.addressbook`, parent **Customer**, relationship `ParentId`, and a filter that picks the addresses you want. Picking **Customer** as the parent is enough on its own: the target is derived for you, and the wizard says what it is about to build, *Becomes rows on Customer's address book*. The shortcut is on the Customer object itself, where the *Also built into the sync* panel carries an `addressbook` line naming whatever writes the address book today, with an **Add address object** button beside it that opens the wizard already filled in and named **Address**. That preset name is deliberate: Billing Address and Shipping Address are reserved by the packaged pair, so the wizard offers a free name rather than one that Create would refuse. Rename it to whatever the third address is called, and give it a label of its own (the label is the key, so a third object with its own label writes a third row). NetSuite never deletes an address book row, so an address that stops being sent simply stays where it is. ## Reset to shipped defaults Any packaged call can be put back exactly the way it shipped, from the call's **settings** in the editor or from step 6 of the [Connection wizard](connection-wizard.md). In the editor it is the pencil beside the call name in the toolbar: the Call settings modal opens, and **Reset to shipped defaults…** sits at the far left of its footer. The action only appears on a call the product ships — a call you built has no shipped defaults to go back to. Reset shows what it is about to discard before it does anything, and asks you to confirm, because it cannot be undone. The confirm is the server's own sentence about that call's *real* drift, rendered verbatim: nodes you added, built-in nodes you re-pointed, removed (they are restored), renamed, switched off (they are switched back on) or flipped to the reverse direction, custom mapping rows, edited built-in rows, the endpoint override, the call name, the description, and the launch switches — *Show on the NetSuite sync card* and *Offer as a Flow action*, both switched back on. A call that has not drifted says so rather than listing nothing: * already matches its shipped defaults. Resetting it rebuilds the same objects and rows and publishes them again.* That is the sentence most admins will actually see, and it is why the confirm is worth reading rather than clicking through. Reset then rebuilds the call's shipped node graph and built-in rows, clears the endpoint override, and republishes the call — so it finishes Active on shipped defaults, one version higher than it started. Expect the version badge to jump: a reset is a publish. **A retired call comes back Active.** Setting a packaged call Inactive retires it, and the wizard's Deploy honors that decision. Reset is the opposite kind of action — explicit, per-call, confirmed intent to return to the shipped configuration, and Active is part of that configuration — so a reset republishes it live. The confirm panel says so before you commit, and adds that you can set it back to Inactive afterward if that is what you want. Reset deliberately leaves three things alone: your [run history](monitoring-troubleshooting.md), your base field mappings (the un-scoped headers from before calls existed), and the org-wide Product item-type and item-default configuration, which lives on that un-scoped base header. What is scoped to the call *is* rebuilt, including the call's own copy of those item defaults, which comes back bare — nothing reads that copy, so the payload's item defaults are unchanged either way. Resetting one call never touches another. # Book orders as Sales Orders Human page: https://taggalongtech.github.io/continuous-control/calls-sales-orders.html The Order node on Order Booking can target a native NetSuite transaction instead of the Continuous `customrecord_cc_order`. Product can target any item type. This page covers what that re-point does. ## Native transaction targets By default a booked order lands in the Continuous graph: an RC Order record with RC Order Lines, which the [revenue engine](revenue-engine.md) groups into performance obligations. On **Order Booking** you can point the **Order** node at a native NetSuite transaction instead, a Sales Order being the usual choice, and the same booking lands a real NetSuite document. ### Replace, or add alongside - **Replace.** Re-point the packaged **Order** node at Sales Order. **Order Line** follows automatically to the Sales Order's item sublist, because a line node always follows its document. The booking then lands a Sales Order with its lines and *no* RC Order or RC Order Lines at all. **There is nothing to add for the lines**, and the editor says so three times over — before, during and after the save — because going looking for a way to add the line child is the natural next move otherwise. The node editor states it the moment it opens (*Order Line rides this node's document — it follows automatically. There is nothing to add.*), names the exact move as you pick the new type (*Order Line will follow to salesorder.item.*), and the save confirmation names what went with it (*Its lines followed: Order Line → salesorder.item.*). - **Add alongside.** Leave Order on the Continuous graph and use **Add object** to add a transaction node of your own, built from one of your Salesforce objects with a line child under it. The booking lands both, and the revenue engine still runs on the Continuous graph. ### Which transactions you can choose The picker offers six. **Sales Order**, **Estimate** and **Return Authorization** are non-posting documents and are the intended choices. **Invoice**, **Cash Sale** and **Credit Memo** post to the general ledger, so the editor makes you acknowledge a warning before it will save one: an order booked straight to an invoice gets none of the per-line tax handling the invoice sync applies and no do-not-bill guard, and it books revenue and receivables the moment it lands. The **Order** node's picker also offers *custom record*, which is how a re-pointed Order goes back to `customrecord_cc_order`. The packaged RESTlet writes that record or a native transaction and nothing else there, so pointing Order at some other custom record type is refused with one error and the Order section is skipped. Not every node is retargetable. **Order** (on Order Booking) can become a transaction or another custom record type, **Product** can become any item type on any call, and nodes you added can be anything the editor allows. The document and application nodes on Invoice Sync, Credit Memo Sync, Payment Sync and Refund Sync keep fixed targets — their NetSuite writers are purpose-built around tax, applications and payment locks — and the editor says so instead of offering a picker. ![The Order node's editor with the target kind set to Standard record and the Transactions family showing Sales Order, Estimate, Return Authorization, Invoice, Cash Sale and Credit Memo](https://taggalongtech.github.io/continuous-control/assets/shots/sf-node-target-picker.jpg) *Kind first, then type: the picker reads the available types live from your NetSuite account.* ### What changes when the Order lands as a transaction - **The Continuous engine is skipped for that booking.** No RC order lines land, so there is nothing to group into obligations. The run records `engine: "skipped"` with the reason, and the card says the same in plain words. Recognition for that contract belongs to whichever system owns it; see [One engine, two switches](one-engine-two-options.md). - **The NetSuite-side Sales Order generator stands down.** If your org has **Create Native Sales Order** switched on, that generator does not run for a booking that already landed a mapped Sales Order, and the run's notes say so. You never get two Sales Orders for the same work. - **One document per Salesforce order.** Re-booking the same order updates the same document. An amendment is its own Salesforce order, so it books its own Sales Order rather than appending to the original — the opposite of the NetSuite-generated cumulative one. - **Lines are append-only.** Each line carries the Salesforce line id in a dedicated column (`custcol_cc_sf_line_id`). A matching line is updated in place, a new line is appended, and a line NetSuite has that Salesforce no longer sends is left exactly as it is and reported in the run's notes. Nothing is deleted, because a billed or fulfilled line cannot be. - **Lines carry commercial fields only.** The writer sets item, quantity, rate, amount and description, plus what the line's mapping rows add. It stamps no revenue recognition dates and no revenue rule on a line. An account that runs a NetSuite revenue feature can add those as extra mapping rows on the Order Line node. - **Billing schedules still mirror.** Schedules, groups and periods sync as usual; they simply carry no order-line link, since there is no RC order line to point at. The run counts them so the number is never a surprise. - **Writebacks move.** The Salesforce Order's NetSuite id becomes the document's id. Order lines get no id at all, because a sublist line has none of its own. If the order was booked to the Continuous graph earlier, its lines keep the RC Order Line ids they were stamped with then. - **The card follows.** Its checklist reads "Order → NetSuite Sales Order", and the link after a successful run reads "Open Sales Order in NetSuite". > **Screenshot to follow.** Screenshots of the mapped Sales Order in NetSuite and the card's Sales Order link will follow. > **Do not let both systems bill.** A Sales Order the **Order node** lands — and the one NetSuite generates for a contract — carries the **Do Not Bill (Continuous)** body field, stamped on create from the **Allow NetSuite billing** setting in [Connection wizard](connection-wizard.md) step 8. While it is checked, NetSuite refuses to bill that Sales Order, which is what keeps Salesforce the only source of invoices while Invoice Sync is running. To bill natively instead, turn the setting on (or clear the field on one document) and stop syncing Salesforce invoices for those contracts. A transaction node you added yourself is not stamped: it lands only the fields that node maps, with no billing guard and no contract link of its own, so check Do Not Bill on those documents yourself if NetSuite could otherwise bill them. ### Notes - **A quantity change takes one extra sync to settle on a transaction line.** NetSuite recomputes amount as rate × quantity in any save where the quantity changed, which overrides the amount Salesforce sent; the next sync re-asserts the Salesforce amount and it sticks. Re-run the booking, or wait for the next one, and the line matches Salesforce again. - **Re-pointing a node leaves its old NetSuite records alone.** RC records from earlier bookings are neither deleted nor migrated, and they keep the billing links they had. Their built-in mapping rows stop making sense on the new target, which is why the editor offers to replace them and Validate flags any that no longer resolve. # Add your own objects to a call Human page: https://taggalongtech.github.io/continuous-control/calls-add-objects.html An extension node adds a Salesforce object with no packaged mapping to a push call's graph, targeting a NetSuite custom record, a standard record, a transaction, or a line/sublist on a document you added. ## Before you start: create the NetSuite record type Both wizards, Add object here and New call in [Build a call of your own](calls-create-call.md), connect an existing NetSuite custom record type to Salesforce — neither creates one for you. Set the record type up first, in NetSuite: - A new **custom record type** (Customization → Records, Fields & IDs → Record Types) with a scriptid you'll recognize later. - The **fields** you want populated from Salesforce — one per field you plan to map. - If the object nests under an existing node's record (for example, a child of Billing Schedule Group, or of an Invoice), a **SELECT-type field** on the new record type that points at the parent's record type, with **Parent Subtab** set so NetSuite renders the child rows as a sublist on the parent record. Everything after this is done from the wizard against that live record type — the wizard reads it over your connection, it never provisions NetSuite schema. ## Extension objects: adding a custom object to a call Beyond editing what ships, an admin can add an entirely new object to any of the seven **push** calls' sync graphs — Order Booking, Invoice Sync, Credit Memo Sync, Payment Sync, Refund Sync, Product Sync, or Customer Sync — or to a call you've built yourself. A Salesforce object with no packaged mapping at all syncs to a NetSuite custom record you define. This is how adjacent data that lives in your own custom objects (a provisioning task, a fulfillment step, an internal approval) rides the same call as everything else, without writing Apex or shipping a package update. On Invoice Sync, Credit Memo Sync, Payment Sync, and Refund Sync you can nest a new node under the document itself — a child of the Invoice, Credit Memo, Payment, or Refund a run is processing, not just under Order Booking's graph. An extension node does not have to be a custom record. **Add object** offers the same two kinds the node editor does — **custom record** or **standard record** — so you can land, say, a fulfillment Sales Order built from your own object beside the Continuous graph, or a project, a class, a subscription. *Standard record* is the wide one: everything NetSuite masters, read live from your account and grouped by family (Entities, Items, Transactions, Billing, Other records), already filtered to the features your account has switched on — with an **Other record type** field and its own **Check** under the chips, because the catalogue is a convenience and never a limit. A transaction node needs two extra pieces of wiring, which the wizard asks for: the lookup on your object that names the **customer** (the transaction's NetSuite entity), and, on its line child, the lookup that names the **product** (the item each line sells). NetSuite refuses to save a transaction with no lines, so a transaction node always needs a line child: add the document node first, then add its line child with that node as its parent. Choosing a parent that writes a document makes the new node a **line** on it, and the wizard fills that in: the kind becomes *Line/sublist* and the target the parent's `item` sublist, which is what a line is on nearly every document. What it also does now is show the parent's *other* sublists beside it, read from that record's own describe — so a document whose rows do not live on `item` can say where they do, instead of being forced onto a sublist it may not have. The default never moves unless you move it. The two nodes then travel in one payload, the parent document first with its lines nested inside it. ### What a transaction node will and will not accept An extension document rides the payload as its own entry, and its lines ride inside that entry. Everything below follows from that one fact, and each rule is enforced at **Activate** — where you can still fix it — rather than surfacing as dropped records on a run that reported success. | Rule | Why | |---|---| | **A line node can only sit under a document node you added yourself.** Put one under a built-in document node — the packaged Order re-pointed at a Sales Order, say — and Activate refuses it, naming the built-in node and telling you to add your own document node and nest the line under that. | A built-in node is written by the call's own section, which has no channel for extra lines. Nested there, every line was silently dropped at run time while the run still said Completed. | | **Only the six writable transactions can carry your lines** — Sales Order, Estimate, Invoice, Cash Sale, Credit Memo, Return Authorization. Under any other transaction parent the wizard offers a record-type picker instead of deriving a line target, and says why. | Those six are the ones the packaged NetSuite writer handles generically. | | **Payments and refunds are not extension targets.** An extension node aimed at NetSuite's `customerpayment` or `customerrefund` is refused at Activate; the message lists the transaction types that *are* allowed. | Those two settlement documents are written only by the shipped Payment Sync and Refund Sync calls, applications and all. | | **An application sublist is a supported target — under a document of your own.** A node aimed at `.apply` nests into that document as application rows, the same way a line node nests as lines. What is still refused is putting one under a *packaged* document node, which is the general rule in the first row of this table, not a rule about applications. | Application rows are written in **match** mode: NetSuite never inserts one, it unchecks everything and re-applies the rows the payload names, each capped at its live open balance. That only works against rows NetSuite already holds, which is why the document has to be one this call landed or already had. | | **Rows on any other sublist need a key column.** A node on a sublist that is not `item` or `apply` is refused at Activate until its *document* node names the column its rows are keyed by, under [Write options](#write-options). The message carries the exact JSON to paste in. | The key column is how a re-sync updates the row it wrote last time instead of appending a second one. `item` and `apply` have conventions the writer knows; every other sublist is the record's own, so only you can say which column identifies a row. | | **Entity Field and Item Field must be lookups on the node's own Salesforce object**, and neither may be a relationship path (`Account.Id` is refused; `Id` itself is fine when the object *is* the record being pointed at). What they point *at* is derived from the lookup. For the Entity Field, and for an Item Field on any sublist other than `item`, Activate checks that the object on the other end carries the field the NetSuite id is read from (`NetSuiteRecordId__c`, or whatever **Entity NetSuite Id Field** names). An Item Field on the `item` sublist needs no such field: its item is found by external id (see [A line's item is found by its external id](#line-item-external-id)). A missing or mistyped one blocks Activate; one left behind on a node whose target no longer needs it is a warning, not a block. | NetSuite refuses a transaction with no customer and a line with no item, and the sync reads the named field straight off the record, which a dotted path can never satisfy. Creating a customer on the way in is the one place the lookup still has to be an Account — with anything else the document is dropped, and the run says so. | **A transaction node with no line child is a warning at Activate**, not a refusal: *"" writes salesorder but has no line child - NetSuite refuses a document with no lines. Add the line object (e.g. Order Product) under it with Add object.* It has to be a warning, because a document node is legitimately line-less for as long as it takes you to add the line under it, and a call you are halfway through building must still activate. Left unfixed it is not survivable, though — NetSuite rejects every run — so treat it as a to-do rather than as noise. The warning is raised only for nodes you added: a packaged document node ships with its line node, and a re-pointed one keeps the line that followed it. You do not have to wait for Validate to find it. Select a document node with no line child and the detail pane leads with the same fact and an **Add line object** button: it opens the Add object wizard already parented on that document, with the line object filled in when we can name one. The line object is found by shape rather than from a list — a child object with a lookup back to the document and a product lookup of its own — so your own document object is served as well as an Order is. If nothing qualifies, the wizard still opens on the right parent with the object picker empty. Two things are warnings rather than refusals, because they depend on the data rather than the configuration. A document whose lines *all* dropped — every one missing its product, or the record simply having no children — is reported per document, since NetSuite cannot save a transaction without at least one line. An account with no NetSuite id yet does not stop its document: the payload creates that customer on the way in — see [A customer created on the way in](#create-first) below. **A line's item is found by its external id.** A row on the `item` sublist carries `itemRef {parentTable:'item', parentEid:}` and no NetSuite id: Salesforce never reads the product's `NetSuiteRecordId__c` (or the node's Entity NetSuite Id Field) for it (`CcNsCustomNodeService.lineRow`; Activate asks for no NetSuite id field on its object, `CcNsCallLifecycle.validateEntityAndItemFields`). NetSuite looks first in the ids this run already landed, then runs `SELECT id, externalid FROM item WHERE externalid IN (…)` (`cc_action_custom.js`, `resolveNodeRef`). So the item must carry the product's 18-character Salesforce id as its external id. - Items that **Product Sync** or a booking created from this org carry it. Run Product Sync on products that have no NetSuite item yet. - Items matched by code in [wizard step 7](connection-wizard.md#product-matching) keep whatever external id they had, usually blank. **Apply product matches** writes only Salesforce fields (`Product2.NetSuiteRecordId__c` plus the sync status, message and timestamp), with no callout, and Product Sync on a matched item writes its mapped fields, the Source Org stamp and the revenue rule, never `externalid` (which is also blocked as a mapping target). Set the External ID on those items in NetSuite, for example with a CSV import that updates items keyed on Internal ID. - On **Order Booking** and **Product Sync**, a product that the same run also syncs as an item can be found without it: that run's item section puts the matched id among its landed ids. Anywhere else, including every call you build, only the external id finds it. - One NetSuite account shared by two Salesforce orgs: an item carries one external id, so only the org whose product id it holds can use it on these lines. The other org's bookings, invoices and Product Sync keep using their match. A line whose item is not found is dropped with `custom : line : no NetSuite item for `. ## A document and its line, in two steps A NetSuite transaction and its lines are two nodes, always — the document node writes the record, and a second node under it carries the lines. That shape is why the wizard asks for a parent before anything else, and it has two entrances: - **From a new call.** Create a call rooted on the document object (an Order as a Sales Order) and the wizard hands straight over to adding the line object under it. Two wizards, one continuous path. - **From an existing call.** Add the document node with **Add object**, then add the line object under it — either from the **Add line object** button that appears on the document node, or by choosing the document as the new node's parent, which derives the `.item` target for you. The one shape that is refused: a line under a *packaged* document node. A packaged document ships with its own line node and sends only those lines, so an extra one would be dropped at run time. The wizard says so where it happens and offers the way through: **Add a Sales Order node first** re-opens it pre-filled for step 1 — the same Salesforce object as the packaged node, writing the same NetSuite record type, as a node of your own — and remembers the line you were adding. Create it, and the wizard comes straight back for step 2 with the new node as the parent. A *Step 1 of 2* line rides along so the first Create never reads as the last one. Nothing is created behind your back: one Create is one node, and if you stop after step 1 the new document node simply carries the **Add line object** button until you finish. If you are re-pointing a packaged node rather than adding one — Order to Sales Order on Order Booking, say — there is nothing to add at all: the packaged Order Line follows its parent onto `salesorder.item` automatically, and the node editor says so before, during, and after the save. ## A customer created on the way in A NetSuite document cannot be saved without a customer, so the payload carries the customer. When a document needs an account NetSuite has not got, one **customer entry is added ahead of it** — deduplicated, so ten documents for one new account produce one customer — and the document resolves against it in the same run. Three things make it safe rather than surprising: - **It uses your own Customer Sync mapping.** The entry's fields are the rows the packaged **Customer Sync** call's Customer object would have applied, so a customer created this way is the same customer a deliberate sync would have made — not a bare name-and-id stub. An org that has never configured that call still gets the name and the external id, which is all NetSuite needs. - **It is the same record either way.** The external id is the Salesforce Account id — the one every other path stamps — so creating it here and syncing it deliberately later are the same upsert, never two customers. The new NetSuite id writes straight back onto the account, which is what makes the second run skip all of this. - **The entry is ours, and named as ours.** It travels under a node key beginning `__`, a prefix both wizards refuse for a display name, so it can never collide with an object of yours in the returned ids or the writeback. It also carries the opt-in NetSuite requires before it will create an entity at all, and a create takes its subsidiary from your deployment's default when the mapping supplies none. An **Entity Field** that points at something other than an Account still drops the document — now the message says why. Because whether a run needs this depends on the data rather than on the configuration, the requirement is not raised at Activate: the payload that actually carries a created customer says so on the wire, and the run is refused before its callout if the installed NetSuite package is too old (see [Versions & compatibility](ref-versions.md)). ## Write options: how an object's records are written Everything above says *what* to sync. **Write options** — collapsed under **Advanced** on any object you added, in both the node editor and the Add object wizard — says *how*, for decisions that are properties of the object: | Option | What it does | |---|---| | **Allow create** (UI: *Allow this node to create the record*) | NetSuite treats customers and contacts as update-only unless an entry opts in, because creating an entity by accident is the expensive mistake. Ticking this opts in, and the panel names the fields NetSuite will not save the record without, read live from the record type. | | **Custom form** | The form the record is saved on, chosen before any field is written — which matters when a form governs defaults or which fields exist. | | **Set by text** (UI: *Set by text, not by id*) | List fields written by their *label* rather than by NetSuite internal id, so a mapping can carry "Net 30" without Salesforce knowing what NetSuite calls it internally. A label the account does not have costs *that field and nothing else*: it is named in the run's errors, the value already on the record is left as it was, and every other field on the record — and every line under it — still writes. | | **Clear when blank** (UI: *Clear when Salesforce is blank*) | A blank Salesforce value has always meant "no opinion", never "erase". These are the fields where it should mean erase — and it takes both halves: the option on, and the field named. | | **Lookups** | A NetSuite field filled by looking another record up **by external id** — the Salesforce id of whatever it points at. Use it for a link Salesforce holds but has never stamped a NetSuite id for. A lookup that resolves nothing is reported per field, naming the value, and the record still writes. | | **Sublist key fields** | The column each of the object's sublists matches its rows on. Naming one replaces the writer's default for all of them, so the panel always emits the whole set — which is also why a sublist other than `item` or `apply` cannot activate until its column is named here. | The panel is **empty by default, and empty means exactly what the sync always did** — an object whose panel you never open sends no write instructions at all, and its payload carries no `write` block. Lookups and sublist keys are node-editor-only: neither has anything to act on until the object exists. The full field-by-field walkthrough is in [Field mappings](field-mappings.md#node-targets), and the wire shape these produce is annotated in [Anatomy of the payload](payload-anatomy.md#write-block). > **Write options need NetSuite ns-v3.0.0.** An entry only carries write instructions when an object asks for some, and every 3.0 install already has ns-v3.0.0, the one floor 3.0.0 checks at install — there is no separate per-call floor to work out. A call can still stamp `requiresVersion` above 3.0.0 for a future release, and the published call carries its verdict, so a NetSuite downgrade afterwards is refused before the run sends anything. See [Versions & compatibility](ref-versions.md). > **One nesting limit.** A new node can nest under most existing nodes on a call, but not under a line-item or application-sublist node (Order Line, Invoice Line, Credit Memo Line, Payment Application, and similar) — attach it to the parent document node instead (Order, Invoice, Credit Memo, Payment). The pull-only POB Refresh call has no push graph to extend, and Order Line Sync pushes a fixed field set with no nodes, so each node rail shows a message instead of an Add object control (`createCallNode` refuses Order Line Sync with *"Order Line Sync" sends a fixed set of fields. Custom objects can't be added to it.*). ## Add the object: the wizard walkthrough On any push call's node rail — Order Booking, Invoice Sync, Credit Memo Sync, Payment Sync, Refund Sync, Product Sync, Customer Sync, or a call you've built yourself — **Add object** opens a modal that builds one new node from a Salesforce object and a NetSuite record type. The fields are in the order the decisions actually happen — what to sync, where it hangs, what shape that makes it, and only then what to call it: | Field | What it does | |---|---| | **Salesforce object** | Type-ahead search over every accessible object; pick the one that carries the data you want synced. | | **Parent node** | Nest the new node under an existing one on this call, or leave it top-level. On Invoice Sync, Credit Memo Sync, Payment Sync, and Refund Sync this includes the transaction document node itself (Invoice, Credit Memo, Payment, Refund) — a line-item or application-sublist node can't be a parent; see the *One nesting limit* note under Write options. An **entity** parent (the Customer node, packaged or not) offers three shapes. *Line/sublist* is pre-selected: the new object becomes a row in that customer's address book, the shape the two [packaged address objects](calls-edit-mappings.md#addresses) already have, and the picker lists the customer's other sublists beside it. *Custom record* or *Standard record* makes it a NetSuite record of its own that points back at the customer: run *Check* on the type, then pick the field that holds the customer as the *NS parent field*. That is how a customer site, a billing group, or any other customer-related custom record is added. | | **What it becomes** | Not a field — a line the wizard writes for itself once an object and a parent are chosen, saying which of five shapes this node will take: a *line* on a document you added (it rides that document's `item` sublist by default, and the picker offers the parent's other sublists beside it), a *row* in an entity's sublist (under Customer it reads "Becomes rows on Customer's address book", one row per record, matched by label, and no Item Field is asked for because an address book row has no item), a record of its own *under* a parent, a record of its own at the *top level*, or — the one dead end — a line under a *packaged* document node, which is refused and explained on the spot. Read it before filling anything in; it is the shortest description of what **Create** is about to make. | | **Relationship field** | Shown once a parent is chosen — the lookup field on your object that points at the parent's object, so each record's parent can be resolved. **Required** whenever there is a parent: a child is reached by querying it off its parent, so **Create** waits for it. When the new object *is* the parent's object, the list leads with **This record (Id)**: an address read off the Account under the Customer node has no lookup to follow, because it is already on the record it belongs to. When the object has exactly one such lookup it is filled in for you (Order Product under an Order gives `OrderId`); when it has none, the wizard says which of the two reasons it is — this object has no lookup to that parent, or the field list could not be read and re-picking the parent retries it. | | **NetSuite record type + Check** | The record type you created above. Under the *custom record* kind the field is a searchable list of the custom record types read live from your NetSuite account — start typing to filter by name or scriptid, and picking a row fills the field and fires **Check** for you. Customers and contacts aren't in that list; they're chosen under the *standard record* kind, in its *Entities* group, as items and transactions are in theirs. You can still type any record type id by hand: **Check** confirms it live over your connection and answers in those terms — *NetSuite knows this type — 214 fields*, or *NetSuite has no record type called * — a real-time probe of the record type rather than a static list. For a *standard* record type — one this package keeps no curated list for — **Create** runs the same probe server-side before it inserts anything, so a type NetSuite has never heard of is refused rather than saved; anything less conclusive (no credentials, an unreachable account) creates the node and says so in the toast. For a *custom record* type, **Check** is the gate: the wizard's own New call flow requires a successful one, and on Add object it is the verdict to read before you press Create. If the live list can't be loaded — no NetSuite credentials configured yet, or the account briefly unreachable — the field notes that it's showing basic options only and the free-text path keeps working. | | **Display name** | The label shown on the rail and on the sync card's checklist. Defaults to the object's label; edit it to rename. Two names are refused, and the wizard says so as you type rather than on **Create**: a name a *packaged* node already answers to (“Order”, “Invoice”, and the rest — a node named after one could never be reached), and a name already used on this call. Each refusal offers a free name as one click — what NetSuite calls the record type this node writes (“Sales Order”), then “ Line” for a line node, then a numbered variant. | | **NS parent field** | Shown once Check succeeds and a parent is chosen — the SELECT-type field on the NetSuite record that carries the parent link (only fields NetSuite reports as SELECT type are offered). | | **Row filter** *(optional)* | Labeled **"Only sync records where… (optional SOQL WHERE fragment)"** — a WHERE clause narrowing which records of this object sync, e.g. `Status__c = 'Active'`. Records not matching are skipped at sync time; see the note below on how and when it runs. | | **Writeback field** *(optional)* | A text field on your Salesforce object to stamp with the NetSuite internal id once a record syncs — the same pattern every packaged node uses for its own NetSuite id field. | A collapsed **Advanced** section holds two more settings, off by default because most nodes never need to touch them: | Field | What it does | |---|---| | **Eid prefix** | Distinguishes this node's NetSuite records by external id. Auto-generated from the display name — it controls duplicate-safe external ids; override it if you'd rather choose your own. | | **Name source** | Which Salesforce field names the synced NetSuite record. Defaults to the record's own Name; leave it blank unless you want a different field driving the NetSuite record's name. | | **Entity NetSuite Id Field** | Which field on the record the **Entity Field** (or **Item Field**) points at carries its NetSuite internal id. Blank means `NetSuiteRecordId__c`, the field every packaged sync stamps — set it only when that lookup lands on an object keeping its NetSuite id somewhere else. Two objects reading different fields off the same record stay independent. A line on the `item` sublist never reads it: its item is found by external id (see [A line's item is found by its external id](#line-item-external-id)). | Advanced also carries a collapsed **Write options** panel — *how* this node's records get written: whether an entity may be created (with the fields NetSuite will not save it without named beside the checkbox), which custom form to save it on, and which list fields are set by text rather than by internal id. It is empty by default, and empty means exactly what the sync always did. The two remaining options — field-level lookups and per-sublist key fields — live in the node editor rather than here, because neither has anything to act on until the node exists; see [Write options](field-mappings.md#node-targets) for the full panel. **Create** adds the node to the call as a draft — it appears on the rail immediately with a **Custom** badge, nested under its parent if you chose one, and the dropdown's chips show the Draft changes chip. ## Field mapping rows on the new node Select the new node and it opens exactly like any other — an empty row grid. Add a row per NetSuite field you want populated: a Salesforce field (any accessible field on the object) or a fixed value, targeting a field id on your NetSuite record type. The same live NetSuite field suggestions and type-compatibility checks from [Field mappings](field-mappings.md) apply here. ## Validate and Activate Validate on a call with extension nodes adds a NetSuite-side leg: for each custom record type in the draft, it re-checks the type live and confirms every mapped field id — and, where one's configured, the NS parent field's SELECT type — still resolves. A field that doesn't exist, or a record type that's gone missing, blocks Activate with a specific error naming the node and field; NetSuite being briefly unreachable surfaces as one warning rather than blocking, since draft edits are never lost. Activate publishes the node the same way it publishes everything else on the call — one version, live from that point on. ## What rides the next sync Once activated, the extension node behaves like a packaged one: the next run of that call queries the node's Salesforce records (filtered to the run's scope via its parent relationship, and by the row filter if one is set), and for each one upserts a NetSuite record of the configured type — matched idempotently by external id, so re-running never duplicates. Records with a parent node get their NS parent field stamped with the parent's NetSuite id, so they render as a sublist under it exactly like the packaged Billing Schedule and Billing Period nodes do. If a writeback field is configured, the NetSuite internal id is stamped back onto the Salesforce record after sync. The sync card's checklist grows a row for the new node automatically, with a live count — no card configuration needed. > **Every document is one unit.** Order Booking's custom nodes ride its own row-level cursor (`continueFrom`/`customNodesFrom`); see [governance and resume](inside-the-restlet.md#governance-and-resume). Invoice Sync, Credit Memo Sync, Payment Sync, Refund Sync, Product Sync, and Customer Sync tie each document's extension-node records to that document: NetSuite writes the document (and, for invoices, the billing mirrors it consumed) together with its extension records, and reports the document synced only once everything under it landed. A document a leg can't finish is withheld — its id stays out of the run's results, so the next leg (or the next sync, for a single-document call) writes it again as a whole unit, the same idempotent recovery every call uses. A **parentless** extension node (no parent node — driven entirely by its own row filter, not scoped to any one document) isn't part of any document's unit: it re-runs its full query and re-emits every matching record on *every* chunk leg of a batch run. That's safe — the same idempotent upsert-by-external-id every call uses — but it repeats that node's full payload and NetSuite-side processing cost once per chunk, so a parentless node adds up fast on a call that also runs as a large, many-chunk batch. ## Notes and limits - **Rows targeting the NetSuite `name` field (and a short list of other structural fields) are dropped.** A record's name always comes from the node's **Name source** setting, never from a mapping row — this keeps the one thing that names every synced record in one place. The block has two halves: a record's identity (`id`, `externalid`, `recordtype`) is never writable at all, and the rest — `name`, `entity`, `customform`, `isinactive`, `item` — is blocked by default and can be lifted for one object under Write options. Moving a document that already exists to a different customer stays a refusal either way. - **The structural blocklist also covers the thirteen Source Org fields** the sync stamps on every Continuous record — `custbody_cc_source_org`, `custentity_cc_source_org`, `custitem_cc_source_org`, and one `custrecord_cc_*_source_org` field per Continuous record type. They sit with the never-writable identity fields, not the liftable ones: no Write option opts a node back into setting or clearing them, on a packaged node or one you added. The RESTlet stamps the running connection's host and org id itself, once, from the request header. - **A record type is proved against your account, not looked up on a list.** There is no allow-list on either side. A target has to be shaped like a NetSuite record type id, and then it is proved with a live metadata read — by the wizard before it creates the object, and again by NetSuite on the run (once per type per request). A type that does not exist is refused by name and the rest of the payload still lands. See [Pointing a node at any record type](ref-netsuite-records.md#any-record-type). - **Extension objects don't participate in orphan cleanup this release.** Packaged billing-period cleanup removes NetSuite rows a resync no longer sees; a custom node's NetSuite records are created and updated, but never deleted by the sync — a Salesforce record that's deleted or filtered out afterward leaves its NetSuite counterpart in place. - **Row filter is a SOQL WHERE fragment, evaluated fresh on every run** — not checked at save time. A typo in it surfaces as a query error on the next run, on that node only; the rest of the run is unaffected. Because it's raw SOQL, treat it as an admin-only, trusted-input setting rather than something end users compose. - **The node's query runs as the sync's user** (`USER_MODE` in `CcNsCustomNodeService.queryNode`): the row filter, the relationship field, the lookups and the records themselves are held to that user's object, field and sharing access. A filter on a field the user can't read skips the node with `Custom node : query failed (the user running this sync can't read ). Node skipped.` Every field name is re-checked against describe just before the query, and the filter's only bind is `:parentIds`. A filter whose parentheses or quotes don't balance (`a = 1) OR (b = 2` would widen the parent-scoped query) is refused: `Custom node : the filter's parentheses or quotes don't match. Fix the Filter on this node. Node skipped.` See [Security model](security-model.md#a-sync-sends-only-what-its-user-can-read). - **No deep link yet for a custom target on an unknown object.** "Open in NetSuite" is built from the environment metadata wizard step 5 deploys, and that only names the record types this release ships. A node you pointed at a record type outside that list writes and syncs normally; it just has no deep link on the Salesforce record page. Deep links for arbitrary custom targets are a 3.1 item. # Build a call of your own Human page: https://taggalongtech.github.io/continuous-control/calls-create-call.html **+ New call** creates a call registry row plus a root node in one step. The call then takes nodes, rows, Validate and Activate like any packaged call, and runs from the sync card or the Run NetSuite Call Flow action. ## Create your own call > **Re-pointing a shipped call is usually the answer.** If the object you have in mind is one a packaged call already syncs *and* that call's node for it can be re-pointed, sending it to a different NetSuite record type is *one re-point on that call*, not a new call: open the node, pick the record type, save, and any line nodes under it follow automatically. In this release that means **Order** on Order Booking and **Product** on Product Sync; the Invoice, Credit Memo, Payment and Refund document nodes are fixed (see [Shipped calls](ref-shipped-calls.md) for the per-node list). The New call wizard says so the moment you pick such an object, with a **Re-point instead** button that takes you straight to the node. Pick an object whose packaged node is *fixed* and the panel still tells you which call already syncs it, and that a new call here runs from the Flow action — but it offers no button, because there is no re-point to make. On an object with no built-in sync card (Order Product, say, which Order Booking and Order Line Sync both read), the panel says only *A new call here is a separate sync.*, since the card can run a custom call there. The two routes are not equivalent, which is why the wizard steers: a custom call rooted on an object that already has a built-in sync card can never be reached *from* that card — the built-in always wins the card's auto-resolution — so it runs from the Run NetSuite Call Flow action only. Build the new call anyway when you genuinely want a second, separate sync on the same object; nothing blocks it. **+ New call**, beside the dropdown, builds a brand-new call from scratch — its own row in the call registry plus a **root node**, in one step. Use this when the data you want to sync isn't a child of anything packaged — a standalone custom object with its own lifecycle (a provisioning workflow, an internal case, a renewal task) that deserves its own sync rather than riding along inside Order Booking or Invoice Sync — or a Salesforce object that should become a NetSuite transaction of its own, an Order booked as a Sales Order, an Opportunity as an Estimate. | Field | What it does | |---|---| | **Call name** | What shows in the dropdown. A live **call key preview** renders underneath as you type (lowercase, spaces and punctuation collapsed to underscores) — it's a preview only; the server confirms the final key on **Create** (auto-de-duplicated if your preview would collide with an existing call), and that confirmed key is what you'll use in Flow. A call's name also becomes the name of its root node, so the same reserved-name check applies: name a call “Order” and the wizard refuses it as you type and offers a free name instead. Naming it after another call you already have is allowed — the key is de-duplicated — but the wizard notes that the two will be hard to tell apart in the dropdown. | | **Root Salesforce object** | Type-ahead search over every accessible object. This is the object the call runs *on* — the record a user or a flow points the sync at. | | **NetSuite target** | Kind first — *custom record* or *standard record* — then the type. Exactly the same picker as the [Add object wizard](calls-add-objects.md#add-object): a custom record gets the searchable live record-type list plus **Check**, reporting the field count it found; a standard record gets your account's own catalogue grouped by family (Entities, Items, Transactions, Billing, Other records), no Check needed, with an **Other record type** box and its own Check underneath for anything the catalogue does not name. A transaction that posts to the general ledger says so on its own chip and takes an **acknowledgement** before Create will run, because booking one puts revenue on the books at sync time. | | **Entity field** *(transaction roots only)* | The lookup to Account on the root object, whose NetSuite customer becomes the document's entity — NetSuite cannot save a transaction without one. When the root object *is* Account, pick **This record (Id)**: the record is the customer. An object with no Account lookup at all can't root a transaction, and the wizard says so rather than leaving the list empty. At run time an account that hasn't been synced yet is skipped with "sync the account first". | | **Advanced** *(optional, collapsed)* | Eid prefix and Name source for the root node — the same two settings the Add object wizard collapses under Advanced, described in [Add your own objects to a call](calls-add-objects.md#add-object). | **Create** is held back until the name, root object and target are all in place — plus, for a custom record, a successful **Check** (a bad root record type here would create a dead call, so that gate is stricter than adding a single object to an existing call), for a transaction its **Entity field**, and for a GL-posting transaction its acknowledgement. On success the dropdown switches to the new call, showing its root node with a **Custom** badge and a **Draft changes** chip; from there, Add object, mapping rows, Validate, and Activate all work exactly as they do on any packaged call. > **Root nodes sync field values, not references.** A custom call's root record maps its own fields onto the NetSuite record — it does not carry a link to some other NetSuite record the way a nested child node does. If the new record needs to point at, say, an existing Billing Schedule Group in NetSuite, add that relationship as a *child* node nested under the root (or under whichever existing call's node already reaches that record) rather than expecting the root itself to resolve it. And a **transaction root needs a line child**: NetSuite refuses to save a document with no lines. You are taken straight there — **Create** on a transaction root switches to the new call and opens **Add object** on the line object, parented on the root, with the object filled in when we can name one (Order Product under an Order, say); it picks up the `salesorder.item` target automatically. Cancel out of it and the node keeps an **Add line object** button on its detail pane for whenever you come back. Until a line exists the call activates cleanly but every run comes back from NetSuite with "no lines to book". Running a custom call has two surfaces, same as any packaged one: - **The sync card, placed on the root object's record page.** Add the standard NetSuite sync card component to the object's Lightning record page in App Builder; once the call is Active it auto-resolves — the card finds the matching call by its root object and configures itself, no card setup needed. **Refresh the page once after activating a brand-new call** before the card will pick it up — the lookup that finds a call for a page is cached for performance, so a tab that was already open when you activated won't see it until it reloads. On an object with a built-in sync — Order, Invoice, Credit Memo, Payment, Product, Account, Refund, and the two batch runs — the built-in card always wins the auto-resolution, so a custom call rooted on one of those objects is reached either through the Run NetSuite Call Flow action, or by naming it: the card has an optional **Call Key** property that runs one specific call regardless of what the page's object would resolve to. Leave it blank on every ordinary placement — the nine built-in object pages included, where **Sync Mode** on Auto is the whole configuration (an explicit Sync Mode also overrides this property outright). A key naming no active, card-enabled call rooted on that page's object is ignored, and the card behaves exactly as it would with the property empty. - **The "Run NetSuite Call" Flow action.** Takes a **call key** and a record id; wire it into any record-triggered, scheduled, or screen flow the same way you'd wire the dedicated Book Order or Sync Invoice actions. The confirmed call key is shown in the mapping editor's Validate/Activate toolbar once you've selected the call, for copying into the flow. A non-packaged call can also be removed: **Delete call**, next to the dropdown's chips, offers a two-step confirm ("Delete *Call name*? All its objects and mapping rows are removed. This cannot be undone.") and is only offered for calls you created — the nine packaged calls can't be deleted. Deletion is immediate and permanent: the call, its nodes, and every mapping row disappear right away, not on next Activate. Any past sync runs made under that call keep their history; they simply lose the call reference (deleting a call doesn't delete its run log). ## Worked example: a Quote booked as a NetSuite Estimate Start to finish, the way a first call of your own actually goes. Quote has no packaged sync, so **New call** is the right route (the wizard offers no re-point, because there is nothing to re-point), and the sync card on the Quote page will find the call by itself once it is active. The same steps build any document-plus-lines call — an Opportunity as a Sales Order, a custom Provisioning Request as a Return Authorization — with the object names swapped. **Two things have to be true in NetSuite first.** The products on the quote must exist as NetSuite items whose **external id is the product's 18-character Salesforce id**, because that is how a custom call's line finds its item. Each line row carries `itemRef {parentTable:'item', parentEid:}` and no NetSuite id (`CcNsCustomNodeService.lineRow`); NetSuite resolves it with `SELECT id, externalid FROM item WHERE externalid IN (…)` and never reads `Product2.NetSuiteRecordId__c`. - **Products with no NetSuite item yet:** run **Product Sync** on them once. The items it creates get the product's id as their external id. - **Items that already exist in NetSuite:** set each item's External ID to its product's 18-character Salesforce id in NetSuite, for example with a CSV import that updates items keyed on Internal ID. Matching them by code in the Connection wizard's [Reference data step](connection-wizard.md#product-matching) is not enough for a custom call: **Apply product matches** writes only Salesforce fields (`Product2.NetSuiteRecordId__c` plus the sync status, message and timestamp) and makes no callout, so the item keeps whatever external id it had (usually blank). Product Sync does not change that afterwards: on a matched item it writes the mapped fields, the Source Org stamp and the revenue rule, never `externalid`, which is also blocked as a mapping target. Bookings, invoices, credit memos and Product Sync send the matched id itself, so matching is enough for them. - **Two Salesforce orgs on one NetSuite account:** an item carries one external id, so only the org whose product id it holds can use it on a custom call's lines. Setting it to this org's id takes it from the other org's custom calls; the other org's bookings, invoices and Product Sync keep using their match. A line whose item NetSuite cannot find is dropped at run time with `custom : line : no NetSuite item for `, and a document whose lines all drop is reported and not saved. The account does *not* have to be synced: if it has no NetSuite id yet, the run creates the customer on the way in (see [A customer created on the way in](calls-add-objects.md#create-first)). 1. **Create the call.** Calls & Mappings → **+ New call**. Call name *Quote to Estimate* — the key preview underneath reads `quote_to_estimate`. Root Salesforce object *Quote*. NetSuite target: kind **Standard record**, then **Estimate** under Transactions — a non-posting document, so no general-ledger acknowledgement. Entity field fills itself with *Account ID*, the only Account lookup on Quote. **Create**. 2. **Add the line object.** Because the root is a transaction, Create hands straight over to **Add object** with the parent set to *Quote to Estimate* and *Quote Line Item* already suggested. The line under it says *Becomes a line on Quote to Estimate (estimate.item)*; Relationship field fills with *Quote ID* and Item field with *Product ID*, each the only candidate. Accept the suggested display name or call it *Estimate Line*. **Create**. The rail now shows the two nodes, both with a Custom badge, and the call carries a Draft changes chip. 3. **Map the fields.** Select the root node: the grid offers Estimate's curated chips — `trandate`, `otherrefnum`, `memo`, `expectedclosedate`, `duedate` — beside the live field list. Add rows such as `otherrefnum` ← Quote Number, `memo` ← Description, `expectedclosedate` ← Expiration Date. Select the line node and add `quantity` ← Quantity, `rate` ← Sales Price, `description` ← Line Description. Three things are never rows: the customer comes from the Entity field, the item from the Item field, and the external id from the Salesforce record id (with the node's eid prefix) — a row aimed at `entity`, `item` or `name` is dropped. Optionally set a **Writeback field** on each node — a text field on Quote (say `NetSuite_Estimate_Id__c`) and one on Quote Line Item — to hold the NetSuite internal ids after each run. 4. **Validate, then Activate.** Validate reads Estimate's fields over your connection and checks every row's target, the Entity and Item fields, and the version of the NetSuite package the call needs — a call on its shipped write defaults asks for nothing beyond the install floor. Activate publishes version 1; the Validate/Activate toolbar shows the confirmed call key for copying into Flow. 5. **Run it.** Two ways, as with any call. Drop the NetSuite sync card onto the Quote record page in App Builder (refresh the page once after activating) and press it; or wire the **Run NetSuite Call** Flow action with Call Key `quote_to_estimate` and the Quote's record id — a record-triggered flow on *Status = Accepted* is the usual shape. Either way one payload leaves Salesforce carrying the action `custom` — the generic writer, which every call you create uses unless you point the call at [another endpoint](calls-endpoints.md#endpoint-per-call) — and lands on the Continuous Control RESTlet. NetSuite upserts the Estimate by external id, appends its lines keyed by the Quote Line Item ids, creates the customer first if it had to, and answers with the ids. The run appears on **Sync Runs** with its payload and result, and the writeback fields are stamped. 6. **Run it again.** The same Estimate is updated in place, existing lines are matched by their key, a line added to the quote is appended, and nothing is duplicated. Change the mapping later — a new row, another child object — and the call goes back to Draft; Validate and Activate publish version 2 while version 1 keeps serving until then. ### Call keys A call key is the name Flow and the sync card's **Call Key** property use. Leave the key blank in the wizard and it is derived from the call name — lower-cased, every run of characters that is not a letter or digit collapsed to one underscore, trimmed, capped at 60 — and de-duplicated with `_2`, `_3` when it would collide. Typing a key yourself is allowed, but a typed key that collides with any existing call is refused rather than renamed. The nine packaged keys are taken: `order_booking`, `invoice_sync`, `credit_memo_sync`, `payment_sync`, `refund_sync`, `product_sync`, `customer_sync`, `order_line_sync`, `pob_refresh`. So is `cash_app_pull`, the key of the hidden Cash App Pull call. | Call name | Key | Why | |---|---|---| | Quote to Estimate | `quote_to_estimate` | Spaces become underscores. | | Provisioning Requests | `provisioning_requests` | A standalone custom object with its own sync. | | Renewal Tasks → NetSuite | `renewal_tasks_netsuite` | The arrow and its spaces collapse to one underscore. | | Partner Accounts (Vendors) | `partner_accounts_vendors` | Punctuation is dropped; the trailing underscore is trimmed. | | Quote to Estimate, created a second time | `quote_to_estimate_2` | Collisions get a numeric suffix; the wizard warns that the two will be hard to tell apart. | Name a call for the Salesforce object and what it becomes — *Quote to Estimate*, *Opportunity to Sales Order* — rather than for the team or the moment; the key is permanent once a flow names it, and a call named after a packaged node (*Order*, *Invoice*) is refused as you type because its root node would take a reserved name. ## Notes - **A custom call's root node syncs field values only** — see the note in [Create your own call](#create-your-own-call) on reaching other NetSuite records from a nested child instead. # Send a call somewhere else Human page: https://taggalongtech.github.io/continuous-control/calls-endpoints.html Each call posts to an endpoint: by default the packaged Continuous Control RESTlet from the connection. A call can instead name a shared endpoint row: another RESTlet in the same account (relayed through the entry script) or an external URL. ## The endpoint a call uses Every call shows the endpoint it will use, above its node rail: the HTTP method, the URL, and the `action` keyword its payload carries — for example `POST · https://…/restlet.nl?script=…&deploy=… · book`. Left alone, all nine packaged calls use the RESTlet URL from your connection with POST and their own default action: Order Booking sends `book`, Invoice Sync and Credit Memo Sync `invoices`, Payment Sync `payments`, Refund Sync `refunds`, Product Sync `items`, Customer Sync `customer`, Order Line Sync `orderlines`, and POB Refresh `pobs`. Keep Order Line Sync off a relay endpoint: the relay doesn't lift its `skipped` or `continueFrom` keys (see [Shipped calls](ref-shipped-calls.md#order-line-sync)). ![The endpoint bar above the node rail showing a POST pill, the RESTlet URL, and the action keyword book](https://taggalongtech.github.io/continuous-control/assets/shots/sf-call-endpoint-bar.jpg) *Every call says where it posts, with what method, and under which action. The account host is masked here.* The pencil on that bar opens the **endpoint picker**. An endpoint is a named row you pick from a list, not a URL you retype per call — so the same middleware or partner script can be named once and used by several calls, and correcting its script id in one place moves every call that uses it. | Kind | Where the request goes | |---|---| | **Continuous entry script** | The packaged RESTlet. The URL is *built* from your connection settings plus the endpoint's script and deployment ids, so it is never stored twice and follows the connection when it moves. This is what every call uses when you pick nothing. | | **Another RESTlet in this account** | Any other RESTlet in the *same* NetSuite account — a script your team or a vendor already owns. Salesforce still posts to the Continuous entry script, which forwards the payload from the inside and wraps the answer. That is why this kind needs **no extra credentials and no second Remote Site entry**: token-based auth is bound to the account, the forward runs under the same role, and nothing leaves it. The mechanics are in [Relaying to another RESTlet](ref-restlet-actions.md#relay). | | **External URL** | Posted at directly. The host needs a Remote Site Setting, and the request is still signed with this account's credentials — so an outside host only works if it forwards to that account. | The picker also carries the two per-call overrides that are about *how* rather than *where*: **HTTP method** (POST, or PUT where your endpoint requires it — the packaged RESTlet treats PUT as an alias of POST) and **Action** (the keyword in the payload that says what this is; changing it on a packaged call raises a warning, because the packaged RESTlet dispatches on it). An endpoint row can name a method and a default action of its own, which apply to every call pointed at it that does not name its own. The order is the same in the editor and at run time: **the call's own override wins, then the endpoint's, then the packaged default** — an endpoint is shared, so a value typed against one particular call outranks it. For a relay endpoint the value this settles on is the action the *target* script is asked to run; the request itself always goes out as `relay`. ### Manage endpoints **Manage endpoints**, beside the picker, is where the rows are authored: a key (the short name calls refer to it by, set once), a label, the kind, the script and deployment ids or the URL, an optional method and default action, a description, and an **Active** box. Endpoints are deliberately *not* draft config. Saving one takes effect on the next run of every call using it, with nothing to activate — only the *pick* waits for Activate, because that is a change to the call. The modal says which is which. - **Continuous Control's own endpoint is seeded and part-locked.** `continuous_control` is always the Continuous Control entry script (`customscript_cc_control_rl`), and it is the only seeded row. A seeded row's label, kind and script ids are maintained for you, so editing them here would silently undo itself; the description and Active box are yours. A **Connection default** badge marks the row your [Connection wizard](connection-wizard.md) settings currently post to, which is where a call that names no endpoint of its own goes. It cannot be deleted. - **Retire rather than delete.** Unchecking Active takes an endpoint out of the picker and makes Activate refuse any call pointed at it — but calls already live on it keep running, because stopping traffic is what deactivating the *call* is for. An endpoint a call points at cannot be deleted at all: the message names how many calls, because moving them is the actual next step. - **Use script ids, not internal ids.** A relay target named by internal id is refused, because Salesforce cannot tell whether the number is the Continuous entry script itself — and a relay pointed back at the endpoint it posts through would recurse. Naming the entry script outright is refused for the same reason, at Activate rather than at run time. **Reset to default** clears the pick and the two overrides and puts the call back on the connection's endpoint. The bar flags the state: **Override** when the live call is not on the packaged endpoint, **Not published** when a change is saved but not activated. > **A new host needs a Remote Site Setting first.** Salesforce refuses callouts to hosts that are not allow-listed, so an External URL endpoint on a new host fails with "Unauthorized endpoint" until an admin adds it under Setup > Security > Remote Site Settings. Validate probes the host and turns a missing Remote Site into a blocking error, so this surfaces before you activate rather than on a live run. An in-account endpoint of either kind needs nothing new: the request goes to the host you already allow-listed. Two callouts always ignore a call's endpoint and use the connection's own: the mapping editor's live NetSuite field lookups, and Validate's NetSuite-side checks (the record-type describes and the RESTlet version floor). Only the packaged RESTlet can answer those, so sending them anywhere else could only fail. Validate's endpoint probe is the one call that does go to an External URL host — proving Salesforce can reach it is its whole job, and it is what turns a missing Remote Site into the blocking error above. Every run records the endpoint it used, the URL, the method and the action, so where a payload actually went is [visible after the fact](monitoring-troubleshooting.md). A relayed run records both halves at once, as `relay:`: the action alone would hide that it travelled through the entry script, and `relay` alone would hide what the target was asked for. > **What happens when a relay target answers something else.** If the target speaks the Continuous wire format, the entry script lifts its ids and errors and the run behaves exactly like a direct call — writebacks and continuation included. If it answers anything else, that is still a *delivery*: the run completes with a note naming the HTTP status the target returned, and the raw answer is kept on the run's result JSON, which is the only place it can be read afterwards. A relayed HTTP error, or a relay that never reached the target, fails the run and carries NetSuite's own words. # Sync settings Human page: https://taggalongtech.github.io/continuous-control/sync-settings.html A small set of org-wide switches governs runtime behavior. Defaults match the packaged behavior — most orgs change nothing here. All of them live in [Connection wizard step 8](connection-wizard.md), with help text on every setting. ## Where settings live Settings are org-level Salesforce custom settings, edited through the wizard (they can also be seen in Setup). They take effect on the next sync — no deployment, no restart. The full field list is in the [settings reference](ref-settings.md). NetSuite's own account-specific setup — subsidiary, currency, GL accounts, item subsidiaries, tax — lives on a matching NetSuite record, the singleton **Continuous Revenue Settings** record, reviewed in [NetSuite manual setup](netsuite-manual-setup.md) and checked by [Connection wizard step 9](connection-wizard.md). Two settings stay as script parameters on the RESTlet deployment: Refund Bank Account and Refund Payment Method. Everything else is a field on that record. ## Invoice chunk size Invoice batch syncs travel in chunks — one call per chunk, each chunk its own safe unit of work (**Invoice chunk size**, `Invoice_Chunk_Size__c`: default 100 documents, configurable 1–200). Lower it if your invoices carry very many lines; the sync self-paginates either way, so the setting is purely a tuning knob. ## Continuous Revenue Engine After a booking, an order line sync, or an invoice/credit-memo sync completes, NetSuite runs **Update POBs** for the contract and the POB mirror carries Booked and Billed back to the Salesforce Contract. That part always happens. The **Continuous Revenue Engine** switch decides whether the sync also asks NetSuite to rebuild the contract's revenue plans afterwards. Its help text says it in one breath: *When on, a booking or invoice sync asks NetSuite to rebuild the contract's revenue plans after Update POBs. When off, Update POBs and the POB mirror still run; no revenue plans, schedules or journal entries are produced.* Leave it on unless another system owns revenue recognition for these contracts. NetSuite has a switch of its own, **Disable Continuous Revenue Engine** on the Continuous Revenue Settings record, and **either side off wins**. When NetSuite reports its switch off, the wizard shows a hint under this one saying so: syncs still run Update POBs and the POB mirror, but NetSuite will not build revenue plans until it is switched on there. With the engine off on either side, the Update Revenue Plans and Recognize Revenue buttons are hidden in NetSuite, the scheduled portfolio run performs Update POBs only, the sync run records `engine: "disabled"` (or `"disabled in NetSuite"`), and the Contract Revenue Summary card reads "Continuous Revenue Engine is off: Booked and Billed stay current, no revenue plans are built." Recognition is never automatic from a sync either way. The full picture is in [One engine, two switches](one-engine-two-options.md). ## Create Native Sales Order and Allow NetSuite billing **Create Native Sales Order** asks NetSuite to also keep one cumulative native Sales Order per booked contract, built from its RC Order Lines (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite), on top of the Continuous records. Use it when you need item fulfillment or native billing; no NetSuite revenue feature is required, and it can also be switched on for a single contract from the RC Contract's **Create Sales Order** button in NetSuite (labelled **Update Sales Order** once one exists). NetSuite has a matching **Disable Native Sales Orders** switch on Continuous Revenue Settings; when it is checked, bookings do not generate a Sales Order and the wizard shows a hint under this switch saying so. **Allow NetSuite billing** decides who bills those Sales Orders. Leave it off (the default) and each one is stamped *Do Not Bill (Continuous)* on create, so Salesforce keeps billing and NetSuite blocks invoicing the order, which avoids double-billing. Turn it on to let NetSuite bill the native Sales Orders it generates. ## Failure notifications Failure alerts are off by default. Turn on **Notify on sync failure** (`Notify_On_Failure__c`) and a failed run raises a Salesforce notification (the bell, custom notification type `CC_Sync_Failure`) to the user who started it. If **Failure notify email** is also set, the same message is emailed to that address — useful for an integration inbox watched by the team rather than the individual clicker. Both are gated by the toggle: with it off, nothing is sent (`CcNsFailureNotifier`). ## Run retention & purge Sync run records carry full request/response JSON, which adds up. **Run retention days** (`Run_Retention_Days__c`; blank or 0 keeps runs forever) plus the nightly purge job (**Schedule nightly purge** / **Cancel schedule** in the wizard) deletes Completed and Failed runs older than the window — a run that finished **Completed with warnings** purges on the same schedule as Completed, since it is a finished run like any other, not one waiting on anything. Schedule or cancel the purge job from the wizard; run it on demand with *Purge now*. ## Cash application pull schedule (hidden) Cash App Pull is hidden, so wizard step 8 hides its interval field and its **Schedule cash app pull** / **Cancel pull schedule** buttons. When it returns: **Cash app pull interval (minutes)** (`Cash_App_Pull_Interval_Minutes__c`, default 60, minimum 15) sets the cadence; 15 and 30 use staggered hourly cron jobs, and the interval applies when the schedule is created or recreated. Full detail in [Shipped calls](ref-shipped-calls.md#cash-app-pull-hidden). # Placing the components Human page: https://taggalongtech.github.io/continuous-control/placing-components.html Continuous Control surfaces in three places: a sync card on record pages, a revenue card on the Contract, and Flow actions for automation. The cards are placed once per record page in Lightning App Builder; only the Credit Memo page ships pre-built. ## The sync card **Continuous: NetSuite Sync** is one card that adapts to the record page it sits on. Dropped on an Order it books orders; on an Invoice Batch Run it syncs the run's posted invoices; on an Invoice, Credit Memo, Payment, Payment Batch Run, Refund, Product, or Account it syncs that record. The mode resolves automatically from the page's object — a design attribute can override it, and there is a NetSuite deep-link attribute for unusual environments. App Builder design attributes: **Sync Mode**, **Call Key**, **NetSuite Link Override**. On completion the Order-mode link reads **Open RC Contract in NetSuite**. **Sync Order Lines (Order mode).** Once the order is booked (it has a NetSuite id and wasn't booked as a native NetSuite transaction, which has no RC Order Lines), and while the card is ready or done and no line sync is running, the Order card shows a secondary link-style **Sync Order Lines** button at the bottom left, tooltip *Sends each line's Delivered, Fulfilled and % Complete to NetSuite. Safe to repeat.* It calls `CcNsSyncCallController.startOrderLineSync(orderId)` for every line of the order and polls the run on its own timer. It reports on its own status line under the checklist and never changes the booking's rows, pill or buttons: *Syncing order lines to NetSuite…*, then *Order lines synced. N updated, M unchanged.*, *Order lines synced with warnings. …* or *Order line sync failed. …*. After a reload the card reads the last line sync back (`PreBookSummary.lastLineSyncRunId` / `lastLineSyncStatus` / `lastLineSyncAt` / `lastLineSyncResult`, found by `Scope_Id__c` = the order and Type `Order Line Sync`) and resumes polling one still in flight. Before the prebuilt calls exist (only [Connection wizard step 6](connection-wizard.md) creates them), a built-in mode's card asks `CcNsSyncCallController.getCallSetup(callKey)` (cacheable) and, on `deployed = false`, shows its eyebrow and one line in place of the checklist and button: *'"" isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then reload this page.'* A custom call's card never shows it, since the card only resolves a custom call whose row exists. Any wire error leaves the card as it was; the start then refuses with the same readable message. Whatever the mode, the card behaves the same way: a pre-sync summary of what will travel, a progress checklist while the run executes, a green Booked/Synced state with counts and a NetSuite deep link when it completes, and a *View JSON payload* toggle showing the exact document sent. ## The Revenue Summary card **Continuous: Revenue Summary** goes on the Contract record page. It renders the Booked / Billed / Recognized waterfall with a per-POB breakdown from the mirrored POB records (a % Complete POB's rule chip adds its `Percent_Complete__c`, e.g. "% Complete · 40%", via `CcContractRevenueController` `pctComplete`), a *Refresh from NetSuite* action that re-pulls the POBs on demand, and an *Open in NetSuite* deep link to the RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite). Design attributes: card title, hide-zero-booked POBs, currency code override (App Builder labels **Card Title**, **Hide $0-Booked POBs**, **Currency Code**, **NetSuite Base URL Override**). ![The Revenue Summary card on a Contract record page](https://taggalongtech.github.io/continuous-control/assets/shots/sf-revenue-summary-card.jpg) *The Revenue Summary card on the Contract — place it in the sidebar region.* ## Adding cards in App Builder 1. Open the record page (Order, Invoice Batch Run, Contract, …) → gear menu → **Edit Page**. 2. Drag the Continuous card from the custom components list into the sidebar region. 3. Save and, if the page is new, activate it for the app/profile combinations that need it. Recommended placements: Order, Invoice Batch Run, Invoice, Payment, and Account pages for the sync card; the Contract page for Revenue Summary. The shipped Credit Memo page already carries the card in credit-memo mode. Also add the **Performance Obligations** related list to the Contract page layout — it is a standard related list, not a Continuous component, and is not auto-added by the package. ## Flow actions for automation Everything the cards do is also available as invocable Flow actions in the **Continuous Control** category — twelve actions: eleven covering booking, order line sync, each document sync and the POB refresh, plus **Run NetSuite Call**, which runs any configurable call by its Call Key. All are fire-and-track: they start a run, return its id immediately, and the run record carries the outcome. Typical wirings: - A record-triggered flow on Invoice (Status becomes Posted) → **Sync Invoice to NetSuite** — hands-free invoice sync for manually posted invoices. - A scheduled flow → **Refresh POBs from NetSuite** per active contract — keeps Revenue Summary current without clicks. - An approval-flow finish → **Book Order to NetSuite** — booking as the last step of your own process. The cards work standalone (a human clicks the button); Flow wiring is optional. A common starting set, one record-triggered flow per object: | Flow trigger | Action to call | |---|---| | Order → Status becomes Activated | **Book Order to NetSuite** | | Invoice → Status becomes Posted | **Sync Invoice to NetSuite** | | Credit Memo → Status becomes Posted | **Sync Credit Memo to NetSuite** | | Payment → Status becomes Processed | **Sync Payment to NetSuite** | | Your fulfillment source signals delivery | Set `CC_Delivered__c` / `CC_Delivered_Date__c` (and/or Fulfilled, or `CC_Percent_Complete__c`) on the Order Product | | Order Product → Delivered, Fulfilled, their dates or % Complete change | **Sync Order Lines to NetSuite** | Many teams start with the cards only and add Flows once trust is built. ### Recipe: send delivery and progress with a record-triggered Flow 1. **Object:** Order Product (`OrderItem`). **Trigger:** a record is updated. **Optimize for:** Actions and Related Records (after save). 2. **Entry conditions (All):** `NetSuiteRecordId__c` Is Null = False (only booked lines), and a formula or OR group so it runs when any of `CC_Delivered__c`, `CC_Delivered_Date__c`, `CC_Fulfilled__c`, `CC_Fulfilled_Date__c`, `CC_Percent_Complete__c` Is Changed = True. 3. **Action:** category **Continuous Control** → **Sync Order Lines to NetSuite**, `Order Product Id` = `{!$Record.Id}`. 4. Optional: store the returned **Sync Run Id** or route `Status = Error` to a fault path. `CcNsSyncOrderLinesInvocable` groups the transaction's requests by order (`CcNsSyncRunService.groupOrderLines`), so a DRO step or data load updating 200 lines of one order starts one run. Lines of different orders in one save start in the background, one run per order. A payload over about 900 lines is refused at start ("This order has too many lines to sync in one run. Sync the lines in smaller groups from a Flow."), so an update touching more lines than that should run in smaller batches. A line sync refuses while a booking of the same order is running; a Flow firing on the booking's own `NetSuiteRecordId__c` writeback is skipped by the writeback guard. Editing an activated order's lines can raise Revenue Cloud's "Prices aren't up to date" banner — see [Monitoring & troubleshooting](monitoring-troubleshooting.md). Exact labels and inputs are in the [Flow actions reference](ref-flow-actions.md). # Booking an order Human page: https://taggalongtech.github.io/continuous-control/booking-an-order.html Booking is the moment a Salesforce order becomes a NetSuite revenue contract. One click sends the entire graph — customer, contacts, addresses, products, contract, order, lines, and billing structure — in a single call, and the card reports back with counts and deep links. ## Before you book - The order must be **Activated** — the card gates on status and says so if it isn't. - The order must reference a **Contract**. The contract is the root of the NetSuite revenue graph, so booking without one is refused with a clear message. - The order's Legal Entity decides the NetSuite subsidiary (set up in [wizard step 7](connection-wizard.md)). ## The pre-book checklist Before anything is sent, the card queries the graph and shows exactly what will travel — the account and contacts, the contract, the order, each line, and the billing schedule groups, schedules, and periods, with live counts. It also shows whether the order was already booked, and when. The account's **addresses** travel with it, as rows on the customer's NetSuite address book. They ride the two packaged [address objects](calls-edit-mappings.md#addresses) under Customer, so which Salesforce record each one reads, and which fields it sends, is yours to change in Calls & Mappings. ![Kingsbridge Digital's activated order with the sync card on the right: Booked, seven green rows, 64 records synced, and the Sync Order Lines button](https://taggalongtech.github.io/continuous-control/assets/shots/sf-order-booked.jpg) *The card on an activated order after booking: every object row green, with counts.* ## What happens when you click Book The click starts a tracked [sync run](sync-runs-idempotency.md): Salesforce assembles the whole payload, stores it on the run, and sends it as one signed call. The card polls the run every second, advancing each checklist row as the response confirms it. The run itself lands on one of three terminal statuses — `Completed`, `Completed with warnings`, or `Failed`. On success every row goes green and the card shows **Booked**; when the order landed but one or more lines have no matching NetSuite item, the run is `Completed with warnings` and the card shows **Booked with warnings** with the count of affected lines. Those lines, and any rows that hang off them such as billing schedules and periods, show as short (below). Fixing the item (wizard step 7, or the item directly in NetSuite) and clicking Book again resolves it. On failure the card goes red with the error, and nothing needs cleaning up — a re-book after fixing the cause updates the same records. When the run finishes, each row's count becomes what the run **landed** (`result.counts`: `customer` + `contacts`, `contract`, `order`, `lines`, `bsgs`, `schedules`, `periods`, and `custom:` for extension rows), not what the order holds. A row that landed fewer records than the order holds is **short**: an amber warning icon in place of the green tick, the label keeping the order's own count, and the status reading * of synced* (for example *Order Lines (7) · 0 of 7 synced*). A short row makes the pill **Booked with warnings** even when the run itself said `Completed`, and it doesn't count toward the *n/7 objects* progress line. A row that reported no count keeps the order's count and reads green. **After a page reload** the card shows the same thing, because it reads the last run back rather than guessing from the order. `getPreBookSummary` returns `lastRunStatus` plus `lastRunResult` (`CcNsSyncCallController.LastRunResult`: `counts`, `error` = the run's `Error__c`, `failedCount`, `contractsRolled`, `engine`, `engineSkipReason`), built by `lastRunResultOf` for a finished run only (null while it's `Queued`/`Processing`, when the card resumes polling instead). The LWC maps it through the same `runOutcome` and `finishedRow` the live poll uses, so the counts, the short rows, the strip text (*Completed with warnings: …*) and the engine-skipped note match what the live run showed. A last run that ended `Failed` reloads as the live failure looks: rows waiting, the red strip with the run's error (or *The last sync failed. Open the Sync Runs tab for details, then try again.* when it has none), the button offered again, and the pill **Booking failed**. A booked order whose last run reported no `counts` block at all (an order booked by a seeder, or middleware answering a bare `ok:true`) keeps the order's counts, all green. The single-document cards (invoice, credit memo, payment, refund, product, account) read their own summaries' `lastRunResult` the same way; the batch-run cards have no reload path. > **Booking before setup fails.** If the calls were never deployed, the card shows *'"Order Booking" isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then reload this page.'* in place of its button (`CcNsSyncCallController.getCallSetup`), and a start from anywhere else (a Flow, the API) is refused with *'"Order Booking" isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then try again.'* (`CcNsCallRegistry.notDeployedMessage`). A call that exists but was never published gets *"Continuous Control isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then try again."* (`CcNsPayloadBuilder.NOT_SET_UP_MESSAGE`). Either way, run **Deploy prebuilt calls & mappings** in the [Connection wizard](connection-wizard.md), then book again. In the background, the engine legs then run automatically: NetSuite groups the lines into POBs, builds revenue plans (while the Continuous Revenue Engine is on), and the POBs mirror back to the Salesforce Contract — typically 20–30 seconds behind the booking itself. The technical trace is in [The Book Order call flow](book-order-call-flow.md). ## The booking summary The completed card carries the working links: **Open RC Contract in NetSuite** (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) deep-links to the contract the booking created or updated, and **Sync again** re-books idempotently — the safe answer to "did that take?". Both work the same whether the run finished Booked or Booked with warnings. ## Viewing the JSON payload *View JSON payload* expands the exact document sent to NetSuite — the same JSON stored on the run — with a copy button and a shortcut to the [field mapping editor](field-mappings.md) that shaped it. It reads exactly like [Anatomy of the payload](payload-anatomy.md) describes. ![The card's JSON viewer showing the billing period section of a real payload, including mapped extra fields](https://taggalongtech.github.io/continuous-control/assets/shots/sf-json-viewer.jpg) *The JSON viewer: the actual wire payload, including any mapping extras riding along.* ## Card reference (labels as the app shows them) Component: `ccNetSuiteSyncCall`, mode `OrderBooking`. | Element | Text | |---|---| | Card title and button | Book Order to NetSuite | | Subtitle | One call mirrors the full order graph into NetSuite. | | Working button | Booking to NetSuite… | | Status pills | Ready to book · Booking · Booked · Booked with warnings · Booking failed · Activate first | | Short row (landed fewer than the order holds) | of synced, with an amber warning icon (tooltip "Not all records synced") | | Done line | Booked to NetSuite · | | Gate hint (not activated) | Activate the order to enable booking to NetSuite. | | Gate hint (no Contract) | This order has no Contract. Add one before you book to NetSuite. | | Main deep link | Open RC Contract in NetSuite (the run may supply its own caption) | | Second deep link | Open Sales Order in NetSuite (shown only when the run returned a sales order URL different from the main link) | | Re-run button | Sync again (tooltip: "Idempotent, re-runs the same sync") | | Payload toggle | View JSON payload / Hide JSON payload; panel caption "The single API call sent to NetSuite", buttons **Edit field mappings** and copy | | Secondary button (booked orders) | **Sync Order Lines**, link-style, bottom left (tooltip: "Sends each line's Delivered, Fulfilled and % Complete to NetSuite. Safe to repeat.") | | Line-sync status line (under the checklist) | Syncing order lines to NetSuite… · Order lines synced. N updated, M unchanged. · Order lines synced with warnings. … · Order line sync failed. …, with the finish time | Checklist rows (counts from the pre-book summary, live SOQL): Customer (Account · Contacts · Addresses → NetSuite Customer), Contract (Contract → RC Contract), Order (Order → RC Order), Order Lines (OrderItem → RC Order Line), Billing Schedule Groups (BillingScheduleGroup → RC Billing Schedule Group), Billing Schedules (BillingSchedule → RC Billing Schedule), Billing Periods (BillingPeriodItem → RC Billing Period). Customer-configured extension nodes on the activated Order Booking call are appended as extra rows. The card polls the run (`CC_NetSuite_Sync_Run__c`) every second. A finished run with warnings uses the amber warning strip; a failure uses the red error strip. A reload shows the same strip and pill, read back from the last run (`lastRunResult`). ## Sending delivery and progress: Sync Order Lines A booked order's card also offers **Sync Order Lines** (Salesforce 3.3.0 + ns-v3.3.0). It starts an Order Line Sync (`CcNsSyncCallController.startOrderLineSync` → `CcNsSyncRunService.startOrderLineSync(orderId, null)`) for every line of the order: each line's Delivered, Delivered Date, Fulfilled, Fulfilled Date and % Complete go to its RC Order Line, and NetSuite then re-runs Update POBs, Update Revenue Plans (engine on) and the POB mirror for the contract. This replaces re-booking for delivery and progress updates. - **Shown when** the order has a NetSuite id and wasn't booked as a native NetSuite transaction (`PreBookSummary.bookedAsTransaction`; a native transaction has no RC Order Lines), the card is ready or done, and no line sync is running. - **Separate from the booking.** The run is Type `Order Line Sync` with `Scope_Id__c` = the order and no `Order__c`, so it never becomes the card's "last booking", never changes the booking's rows, pill or buttons, and never blocks a re-book. It reports on its own status line; after a reload the card reads the last line sync back (`lastLineSyncRunId`, `lastLineSyncStatus`, `lastLineSyncAt`, `lastLineSyncResult`) and resumes polling one still in flight. - **Refusals** (a toast): "This order isn't in NetSuite yet. Book it first." · "This order's contract isn't in NetSuite yet. Book the order first." · "This order is being booked to NetSuite right now. Sync its lines when the booking finishes." · "An order line sync for this order is already in progress." · "None of these order lines are in NetSuite yet. Book the order again to add them." · "This order has no order lines to sync." · "This order has too many lines to sync in one run. Sync the lines in smaller groups from a Flow." · the NetSuite version floor (needs ns-v3.3.0). - **Warnings** on a finished run: "N order line(s) have no NetSuite record yet, so … weren't sent. Book the order again to add …" (lines added after the booking), "NetSuite has no order line for N of these lines. Book the order first.", and any per-line NetSuite error. Editing an activated order's lines can raise Revenue Cloud's "Prices aren't up to date" banner; the sync itself writes nothing to OrderItem. See [Monitoring & troubleshooting](monitoring-troubleshooting.md). For automation, the **Sync Order Lines to NetSuite** Flow action does the same per Order Product ([Flow actions](ref-flow-actions.md)). ## Re-booking Re-booking is a first-class operation, not a workaround. Add a line, fix a price, extend the contract — click book again and the same NetSuite records update in place (see [idempotency](sync-runs-idempotency.md)). Amendment orders book through the very same button with their own semantics — see [Amendments](amendments.md). > **Prefer automation?** The **Book Order to NetSuite** Flow action does exactly what the button does — wire it into an approval or activation flow and track the run it returns. See [Flow actions](ref-flow-actions.md). # Amendments Human page: https://taggalongtech.github.io/continuous-control/amendments.html Upsells, downsells, and mid-term changes arrive in Revenue Cloud as amendment orders. They book through the same button as the original — Continuous Control detects the amendment, sends only what changed, merges it into the existing revenue structure, and adjusts recognition prospectively. ## How amendments are detected Revenue Cloud marks an amendment order with its originating action (*Amend*). The sync reads that: the booking payload is flagged as an amendment, the resulting RC Order (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) carries type *Amendment*, and each line carries its Salesforce order action ("Add", "Amend") so NetSuite can tell base from change forever after. ## What syncs on an amendment Only **changed lines** travel — lines with a non-zero amount delta. Each carries its prorated amount and its own effective date range (a February upsell on a contract ending in December carries eleven months of value, not twelve). The amendment's billing schedules ride along too, chained to the original schedules they modify — see [Billing mirrors](billing-mirrors.md). ## POB merging Because [POB grouping](performance-obligations.md) is by contract + product, an amendment line for an existing product merges into that product's existing POB: Booked moves by exactly the delta, quantity nets (an upsell adds, a downsell subtracts), and the POB now spans both orders. A line for a brand-new product creates a new POB. Nothing is re-keyed and no history is lost. ## Prospective recognition Recognition adjusts the way ASC 606 prescribes for this kind of modification (prospective, per 606-10-25-13(a)): periods already posted are **frozen at their recognized amounts**, and the engine re-spreads the remainder — new total minus already recognized — across the remaining unposted periods, shaped by the amended lines' own dates. Past months never restate. ## Downsizing and catch-down Reductions get the same discipline with two refinements: - A mid-month cancellation is **day-weighted**: the effective month nets to exactly the days that were live, and the schedule ends there — no residual trickle. - If the contract shrinks below what has already been recognized, the engine posts a single **catch-down** adjustment in the current open period — one visible reversing entry (debit Revenue, credit Deferred), rather than a restatement or a string of negative months. > **Operationally identical.** From the operator's chair an amendment is just another booking: activate the amendment order, click **Book Order to NetSuite** (the card itself shows no amendment badge; the Sync Runs tab shows the run type as "Order Booking · Amendment", from the run's amendment flag), and let the engine legs roll the change through POBs, plans, and the Salesforce mirror. # Invoices & credit memos Human page: https://taggalongtech.github.io/continuous-control/invoices-credit-memos.html Salesforce bills; NetSuite carries the A/R. Posted invoices and credit memos sync as NetSuite's own native transactions — with Salesforce-owned tax so totals match to the cent — and the billing mirrors and POB Billed roll forward in the same click. ## What qualifies to sync The batch sync targets invoices that are **Posted**, not yet synced, created since the batch run started, whose billing account is already matched to a NetSuite customer. Credit memos qualify by the same rule. Two deliberate exclusions keep the books clean: - Plain negative invoices don't sync — Revenue Cloud converts them to credit memos, and the credit memo is the document that travels. The run summary reports how many were skipped for this reason. - Mixed-sign invoices (see below) sync their positive residual only. ## Syncing a batch run On the Invoice Batch Run page, the sync card shows how many posted invoices and credit memos are in scope, then syncs them in [configurable chunks](sync-settings.md) — one call per chunk, invoices first, credit memos following. The run self-paginates: each chunk's writebacks flip documents out of scope, so interrupted runs simply continue where they left off. A batch with some failures completes with a failure count and per-document detail in the run's error log, and only the failed documents are picked up next time. ## Syncing a single document The same card on an Invoice or Credit Memo page (or the matching [Flow actions](ref-flow-actions.md)) syncs just that document — including an explicit re-sync of one that already synced. A record-triggered flow on invoice posting makes this fully hands-free. ## Tax: Salesforce owns it Salesforce computed the tax when it billed; NetSuite must not recompute it differently. So each line travels with its effective tax rate derived from the Salesforce tax amount, which overrides NetSuite's own nexus calculation — the NetSuite invoice totals equal the Salesforce totals exactly, cent for cent. If a payment already locks a synced invoice's lines, the sync falls back to patching tax in place (see [Lines matched by key](#lines-matched-by-key) below). ## Lines matched by key {#lines-matched-by-key} Audit N18, ns-v3.2.1. Each item line carries `custcol_cc_sf_line_id`, the Salesforce line id — the same key the generic transaction writer uses (see [Inside the RESTlet](inside-the-restlet.md#the-transaction-writer)). A re-sync (`syncLines` in `cc_action_invoices.js`) matches the document's lines to the payload's by that key: a matched line is updated only in the fields that differ, an unmatched payload line is added, and a document line Salesforce no longer sends is removed (bottom first, so the remaining indexes hold). When nothing on the document differs at all — header or lines — nothing is saved. A document written before ns-v3.2.1 carries no line keys yet. Its first re-sync adopts each unkeyed line for the payload line of the same item — an exact match on amount and quantity first, then any remaining line of that item — and keys it in place, so the line's internal id never moves. Because Update POBs keys each Obligation Detail on that id (`CT-OD--`), and an Event-Based plan's inputs signature is built from its Obligation Details, both stay put across the upgrade too. New lines are always appended after the existing ones, which is how NetSuite adds a sublist line; when Salesforce has added lines since the last sync, the document's line order can end up differing from Salesforce's. **Locked lines.** A line tied to a linked transaction — a customer payment, an applied credit, a recognized schedule — can't be updated or removed. NetSuite reports one of a fixed set of error names for this: `CANT_DELETE_LINE`, `CANT_DELETE_TRAN_LINE`, `CANT_DELETE_TRAN_LINES`, `CANT_EDIT_TRAN`, `CANT_UPDATE_AMT`, `CANT_UPDATE_LINKED_TRANS_LINES` (`LOCKED_LINE_ERRORS`, matched on `e.name`, never on message text). On one of those, the line-matching save is abandoned and `patchLockedTax` runs instead: it loads the document non-dynamically and writes `taxrate1` on each line that needs it, matched by key or, for an unkeyed line, by item — nothing else on the document changes, and `ctx.errors` notes that lines were locked. Any other error is the document's own and is reported as it is; it is never mistaken for a lock. ## Credit memos Posted credit memos land as native NetSuite credit memos, with a Salesforce deep link (`custbody_cc_sf_url`) stamped on them the same way payments carry one back to the Salesforce payment. Their applications ride along: a credit memo applied to an invoice in Salesforce applies to the same invoice in NetSuite — including when the invoice and its credit memo travel in the same payload (the application resolves against the invoice that landed moments earlier). Each application row on the wire carries its own Salesforce id (`sfId`) alongside the invoice it targets and the amount, the same as a payment or refund allocation — traceable back to the exact Salesforce record that created it. Applying a NetSuite credit memo natively needs nothing from Continuous at all; both directions coexist. ## Mixed-sign invoices When Revenue Cloud's line-level negative conversion splits an invoice — converted negative lines offset by a new credit memo, leaving a positive residual — the sync handles the whole shape: the invoice lands with only its positive residual lines, the credit memo lands beside it, and the application between them carries over, leaving the NetSuite pair settled exactly like the Salesforce pair. ## Billing mirrors refresh Each chunk also refreshes the [billing mirrors](billing-mirrors.md) its invoices consumed — the schedules (found through each invoice line's billing period), their groups, and all their periods — and links each consumed period to the NetSuite invoice that billed it. Then the engine legs fire: Obligation Details build, **Billed** rolls up on each POB and the contract, and the refreshed POBs mirror back to Salesforce. One click, everything consistent. **Which contracts the engine legs touch.** Only the contracts the synced documents billed. Each leg works them out from its own synced documents (`CcNsInvoiceSelector.queryContractsTouchedBy`): - An invoice line goes through its consumed billing periods, then the schedule, the order and the contract. - A converted credit memo line follows the invoice line it came from. - A credit memo line tied to an order product follows that product's order. - A standalone credit memo follows the invoices it's applied to. The run accumulates these in `Result_Json__c.affectedContracts`, and the final leg hands them to `CcNsEngineQueueable`. That job processes up to 6 contracts per execution and checks the callout count and callout time before every call. The rest chain to the next execution, so no contract is dropped, however many a run touches. The run's status is set when the documents land. If the engine job dies, the run keeps its status, and the result records `engine: failed` (see [Monitoring](monitoring-troubleshooting.md)). **Counts and warnings cover the whole batch.** Per-leg counts are summed across legs, not maxed, so a 250-invoice batch reports 250. Each leg adds its new warnings under the earlier ones, so a warning from the first chunk survives to the end. Each leg writes back only its own documents. **Very large runs.** `Result_Json__c` and `Payload_Json__c` are 131,072-character fields. Over about 120,000 characters: - The stored result is compacted to its counts, errors, contract list and document ids, marked `compacted: true`. - The stored payload is cut short with a note. NetSuite always receives the full payload. ## Card reference (labels as the app shows them) Component: `ccNetSuiteSyncCall`. | Record | Card title / button | Done line | Deep link | Checklist rows | |---|---|---|---|---| | Invoice Batch Run | Sync Invoices to NetSuite | Invoices synced to NetSuite | View invoices in NetSuite | Invoices · Invoice Lines (SF tax carried) · Obligation Details (Update POBs → Obligation Details + Billed rollup) | | Invoice | Sync Invoice to NetSuite | Invoice synced to NetSuite | Open invoice in NetSuite | Invoice · Invoice Lines · Obligation Details | | Credit Memo | Sync Credit Memo to NetSuite | Credit memo synced to NetSuite | Open credit memo in NetSuite | Credit Memo · Credit Memo Lines · Obligation Details (negative ODs reduce Billed) | Status pill when done: Synced. Re-run button: Sync again. Gate hints the card shows (button disabled unless noted): - Invoice not posted: "Post the invoice to enable syncing to NetSuite." - Credit memo not posted: "Post the credit memo to enable syncing to NetSuite." - Single negative invoice: "Negative invoice. NetSuite can't accept it, so convert it to a credit memo in Revenue Cloud Billing and sync the credit memo instead." - Batch run with negatives (button stays enabled): " negative invoice(s) will be skipped. NetSuite cannot accept them. Issue credit memos in Revenue Cloud Billing instead." - Batch run already done: "All posted invoices for this run are already in NetSuite." - Billing account not matched: "The billing account is not booked to NetSuite yet. Book its order first." Flow actions: **Sync Invoice to NetSuite**, **Sync Invoice Batch Run to NetSuite**, **Sync Credit Memo to NetSuite** (see [Flow actions](ref-flow-actions.md)). # Payments & refunds Human page: https://taggalongtech.github.io/continuous-control/payments-refunds.html Cash settles in both systems. Processed Salesforce payments mirror into NetSuite as customer payments with their invoice applications; refunds mirror as customer refunds against open credits. Settlement moves A/R only — revenue, deliberately, never. ## Syncing a payment A processed payment on a NetSuite-matched account syncs from the Payment page card, the Payment Batch Run card (all processed payments since the run), or the Flow actions. The NetSuite customer payment lands with the Salesforce document number, date, memo, and a deep link back to the Salesforce payment. ## How applications mirror The payment's invoice allocations travel with it, and NetSuite's apply list is set to **match Salesforce exactly** — replace semantics. If Salesforce shows the payment applied to invoices A and B, NetSuite shows the same two applications; if Salesforce fully unapplies it, NetSuite unapplies too. A payment that has *never* been allocated in Salesforce leaves any NetSuite-native applications alone — whoever applied first owns the state. Each application is capped at the NetSuite invoice's open balance as a safety net. Every application row also carries its own Salesforce id (`sfId`) on the wire, so each allocation is traceable back to the exact PaymentLineInvoice or RefundLinePayment record that produced it. ## Refunds A refund syncs as a NetSuite customer refund applying against the customer's open credit — the credit memo (or unapplied payment) it refunds in Salesforce. A refund with no syncable application is skipped with a warning rather than landing unanchored: a NetSuite refund must refund something. Like every other Continuous transaction, a synced Customer Refund carries the [Source Org stamp](sync-runs-idempotency.md) naming the Salesforce org that created it — where the account's NetSuite version supports the field on that transaction type. ## Why there is no engine run Unlike bookings, order line syncs and invoice syncs, settlement syncs never trigger the revenue engine. That is by design: an application settles accounts receivable, but earning is governed by documents and delivery — recognizing revenue because cash arrived would be wrong under ASC 606. Payments and refunds therefore touch A/R and nothing else. > **Settlement runs Salesforce to NetSuite in this release.** Payments, refunds and credit memos applied in NetSuite don't come back to Salesforce yet. The pull that would do this, Cash App Pull, is hidden until it is redesigned; see [Shipped calls](ref-shipped-calls.md#cash-app-pull-hidden). ## Card reference (labels as the app shows them) Component: `ccNetSuiteSyncCall`. | Record | Card title / button | Done line | Deep link | Checklist rows | |---|---|---|---|---| | Payment | Sync Payment to NetSuite | Payment synced to NetSuite | Open payment in NetSuite | Payment · Invoice Applications (PaymentLineInvoice → NetSuite payment apply) · Invoice Balances (revenue unaffected) | | Payment Batch Run | Sync Payments to NetSuite | Payments synced to NetSuite | View payments in NetSuite | Payments · Invoice Applications · Invoice Balances | | Refund | Sync Refund to NetSuite | Refund synced to NetSuite | Open refund in NetSuite | Refund · Payment Applications (RefundLinePayment → NetSuite refund apply) · Credit Balances (Dr A/R, Cr bank) | Gate hints: - Payment not processed: "Process the payment to enable syncing to NetSuite." - Refund not processed: "Process the refund to enable syncing to NetSuite." - Payment with no active invoice applications (informational, button stays enabled): "This payment has no active invoice applications, so syncing mirrors it as unapplied in NetSuite. Sync again whenever its applications change." - Refund not applied to a synced payment (button disabled): "This refund isn't applied to a NetSuite-synced payment. NetSuite customer refunds must refund an open credit. Apply it to a synced payment first." - Account not matched: "The billing account is not booked to NetSuite yet. Book its order first." NetSuite-owned settlement (unreachable, because no direction can be set to `NS_to_SF`; kept for when Cash App Pull returns): when a settlement node's stored direction is `NS_to_SF` (`CcNsNodeDirection`), the Payment or Refund card renders as a pull card instead (title "Pull payments from NetSuite" / "Pull refunds from NetSuite", pill "NetSuite-owned", button **Pull from NetSuite**). Refunds go pull-only when either the Refund or the Refund Allocations node is flipped. Payment and Credit Memo cards whose applications are NetSuite-owned show a secondary **Pull latest from NetSuite** button. Credit memo documents always push (Salesforce to NetSuite). Flow actions: **Sync Payment to NetSuite**, **Sync Payment Batch Run to NetSuite**, **Sync Refund to NetSuite**. # Running the revenue engine Human page: https://taggalongtech.github.io/continuous-control/revenue-engine.html Three buttons on the RC Contract (RC = Revenue Cloud; the RC records are the Salesforce Revenue Cloud order graph mirrored into NetSuite) in NetSuite close the loop: **Update POBs**, **Update Revenue Plans**, and **Recognize Revenue**. Update POBs is data maintenance and always runs. The other two are the **Continuous Revenue Engine**: Update Revenue Plans runs automatically after every sync while the engine is on, and Recognize Revenue, the one that posts to the general ledger, always shows you a preview first. ![Coralbay Technologies' RC Contract in NetSuite: the engine buttons and the Revenue Summary banner with Booked, Billed and Recognized, and a bar for each POB](https://taggalongtech.github.io/continuous-control/assets/shots/ns-rc-contract.jpg) *The RC Contract: waterfall, engine buttons, and the grouping table showing every line's POB.* ## The three processes | Process | Does | Runs | |---|---|---| | **Update POBs** | Groups lines into POBs, rolls Booked, builds Obligation Details → Billed, links billing records | Always: automatically after syncs, on the scheduled run, or by button | | **Update Revenue Plans** | Builds/refreshes each POB's monthly schedule from its method, recognizing off Booked | Engine on: automatically after syncs, or by button | | **Recognize Revenue** | Posts period journal entries — Dr Deferred / Cr Revenue | Engine on: manually, or via the scheduled close if enabled | Each POB record also carries scoped versions (*Update POB*, *Update Revenue Schedule*) that process just that POB and return. ## Update POBs Re-derives the revenue structure from current facts: groups any new lines into their POBs, re-rolls Booked and quantity, ties every posted invoice/credit-memo line to its POB as an Obligation Detail and rolls Billed, links billing schedules and periods to their POBs, and re-rolls the contract header. Idempotent — running it twice changes nothing the second time. Update POBs is not part of the engine switch: it always runs, so Booked and Billed stay current on every POB and on the contract whether or not revenue plans are being built. ## Update Revenue Plans Builds one plan per POB and its monthly schedule rows per the POB's [method](recognition-methods.md), spreading the POB's Booked amount (Deferred is Booked minus Recognized). Rows already posted are untouched; unposted rows re-spread prospectively. A plan rebuilds only when something it's built from actually changed — its rule, dates, amount, or lines — so a re-save that changes none of that, such as Update POBs rolling Billed or a recognition rollup, leaves the plan alone. On a very large contract the process may still report "processed N of M — run again"; only the POBs that changed count toward that total, so a contract with no real changes converges instead of rebuilding from scratch, and clicking again (or the next automatic run) finishes it. ## Recognize Revenue The button opens a form, and a plain view of it **never posts anything**. You pick the action (Recognize or Reverse) and the through-period, and the form previews the outcome: before/after Booked / Billed / Recognized / Deferred balances, the journal lines per period and POB with their debit and credit accounts, catch-up rows named with their destination period, and a collapsed history of every posted period with its journal entry link. Submitting asks for confirmation, then posts. ![The Recognize Revenue form: action select, through-period, preview of balances and journal lines](https://taggalongtech.github.io/continuous-control/assets/shots/ns-recognize-form.jpg) *Recognize Revenue: everything previews before anything posts.* Each posted period gets **one summary journal entry**: debit Deferred Revenue, credit Revenue, one line pair per POB — every line carrying the customer, the POB, and a readable memo ("QB API Management Solution · Jun 2026 · Straight-Line, month 6 of 36"). If a schedule row belongs to a closed period, its amount posts as a catch-up in the open period you chose. ## Reversing The same form's Reverse action unwinds recognition from a period you choose (inclusive; the default clears everything). The preview is explicit per journal entry: deleted outright if its period is still open, or reversed into the current open period if the period has closed. Schedule rows return to Planned and the rollups re-derive — reversing then re-recognizing is a clean, auditable do-over. ## Turning the engine off The Continuous Revenue Engine, meaning Update Revenue Plans and Recognize Revenue, can be switched off on either side. In NetSuite, check **Disable Continuous Revenue Engine** on the Continuous Revenue Settings record; in Salesforce, turn the **Continuous Revenue Engine** switch off in wizard step 8. Either side off wins. With NetSuite's switch checked: - The **Update Revenue Plans** and **Recognize Revenue** buttons are hidden on the RC Contract, and **Update Revenue Schedule** is hidden on the POB. **Update POBs** and **Update POB** stay. - Any call to the two Suitelets, from a bookmarked URL or a headless leg, answers `DISABLED Continuous Revenue Engine is off in Continuous Revenue Settings — Update POBs still runs; revenue plans, schedules and journal entries are not produced.` and does nothing. - The contract's **Revenue plans by POB** card shows the same sentence instead of the plan table. Booking from Salesforce still lands the contract, orders, lines, and POBs, and the POB mirror still refreshes. The full picture, including what a Salesforce run reports, is on [One engine, two switches](one-engine-two-options.md). ## Portfolio close & the scheduled run {#portfolio-close-the-scheduled-run} Beyond the per-contract buttons, the engine runs portfolio-wide from the **Continuous Control** tab in NetSuite's navigation: - **Run Portfolio Close** submits the close job across every contract — Update POBs, Update Revenue Plans, and Recognize Revenue when Auto-Recognize is checked — and takes you to NetSuite's job-status page to watch it. With the engine off it runs Update POBs only and the run log records the stage **Update POBs (engine off)**. - **Recognize Revenue** opens the batch recognition form (no contract pre-selected — every contract with plans is in scope). - **Close Run Log** lists every engine execution with its trigger, counts, and status — the close's audit trail. Exception searches (recognition backlog, close status by contract) sit in the same menu. The same close job is **schedulable**: it's the Map/Reduce script `customscript_cc_engine_sched`, deployment `customdeploy_cc_engine_sched`. It ships `NOTSCHEDULED` — no recurrence — so nothing runs on its own until an admin sets one. In NetSuite, go to **Customization → Scripting → Script Deployments**, open **Continuous Control — Scheduled Refresh**, and set a recurrence on the **Schedule** subtab (nightly is typical). `installcheck` reads the deployment's status over SuiteQL and adds a note when it's still `NOTSCHEDULED`; the Salesforce Connection wizard's step 4 shows that note the same way it shows the role-move note. If the account's SuiteQL doesn't expose the deployment's status, the note is skipped rather than guessed. Posting recognition stays manual unless you explicitly enable **Auto-Recognize on Scheduled Run** in the NetSuite-side Continuous Revenue Settings — most teams keep the posting click human. That switch has nothing to do until a schedule exists: it only fires from inside a scheduled run, so with the deployment unscheduled, checking it recognizes nothing. A contract too large to finish in one run is reported **Partial** in the run log rather than failed: whatever finished on that run is kept, and the next scheduled run (or another Run Portfolio Close) carries on rather than starting over — current plans stay current, and recognition resumes at the first period that isn't posted yet. ## Labels reference (as NetSuite shows them) | Where | Label | |---|---| | RC Contract buttons | Update POBs · Update Revenue Plans · Recognize Revenue | | POB record buttons | Update POB · Update Revenue Schedule | | Recognize form, Action field | "Recognize: post revenue through a period" / "Reverse: unpost revenue from a period" | | Recognize form, period field | Recognize Through Period (recognize) / Reverse From Period (inclusive) (reverse); closed periods are suffixed "(closed)" | | Recognize form, submit button | Create Journal Entries (recognize) / Reverse Revenue (reverse) | | Contract card when engine off | Revenue plans by POB, showing `ENGINE_OFF_TEXT` from `cc_settings_lib.js` | | Continuous Control center tab links (`custcentercategory_cc_overview`) | Run Portfolio Close · Recognize Revenue · Recognition Backlog (`customsearch_cc_ready_to_recognize`) · Manage Accounting Periods · Close Run Log | | Continuous Revenue Settings fields | Disable Continuous Revenue Engine · Auto-Recognize on Scheduled Run | The Recognize Revenue form is served by `cc_rev_rec_run.js` with client script `cc_revrec_cs.js` (re-renders on action or period change and asks for confirmation before submit). # Monitoring & troubleshooting Human page: https://taggalongtech.github.io/continuous-control/monitoring-troubleshooting.html Observability is built in, not bolted on: every API call to NetSuite is a run you can open, filter, and read — down to the exact JSON that crossed the wire. When something fails, the answer is on this tab. ![The Sync Runs dashboard: KPI tiles for runs, success rate, failures and last sync; per-type status bars; a filterable run table](https://taggalongtech.github.io/continuous-control/assets/shots/sf-sync-runs.jpg) *The Sync Runs tab: KPIs, per-type health bars, and the live run table.* ## The Sync Runs dashboard The **Sync Runs** tab shows four KPIs (runs, success rate, failures, last sync), a health bar per sync type, and the run table — filterable by type and status, newest first, each row linking to its source record (the order, invoice, payment…). The status filter includes **Completed with warnings** as its own value, alongside Queued, Processing, Completed, and Failed, so an operator can isolate warned runs on their own. The type filter includes **Order Line Sync** (Salesforce 3.3.0). The type filter and the health bars leave out Cash App Pull, which is hidden (see [Shipped calls](ref-shipped-calls.md#cash-app-pull-hidden)). **Completed with warnings** counts as a finished, successful run for the **Success** KPI — the same way it is terminal everywhere `Completed` is — so a warned booking does not drag the rate down. It gets its own amber pill (`cc-pill--warn`) in the run table so it still reads differently from a clean Completed row. Each per-type health bar reports the warned subset inline: a bar reading **12 ✓ (3 with warnings)** means all twelve finished, three of them with something to say. ## Reading a run Expanding a run loads its payload and result JSON on demand. The reading order that solves most mysteries: 1. **Status + error snippet** — the one-line diagnosis. 2. **Result JSON** — NetSuite's answer: per-section counts, the id map, any errors; engine results (contracts rolled, plans built, POBs mirrored) merge in ~20–30 seconds after completion. 3. **Payload JSON** — exactly what was sent, mapping extras included. If a value is wrong in NetSuite, this shows whether it left Salesforce wrong. Each run also records **where it went** — the endpoint URL, the HTTP method, and the action it sent — shown as a `POST · https://… · book` chip on the run row, with the full URL in the cell's tooltip. When a call is pointed at a [custom endpoint](calls-endpoints.md#endpoint-per-call), this is the fastest way to confirm which endpoint a failure actually hit. On the sync card itself, a finished run's message — whether it is a warning or just something worth knowing — renders on an amber strip rather than the red failure strip: a **Completed with warnings** booking shows its "N lines have no NetSuite item" text there, prefixed *Completed with warnings:*; a plain **Completed** run carrying a softer note (say, a partial per-record failure count) shows the same text unprefixed. Neither means the run failed — the card only turns red while a run is genuinely not done. Each checklist row then shows what the run **landed**, read off `Result_Json__c.counts`. A row that landed fewer records than the record holds is short: an amber warning icon, * of synced*, and a **… with warnings** pill even on a plain `Completed` run. Short rows are judged on the booking and single-document cards only; a batch run, a pull, or a custom call keeps its green rows, and so does a row that carries its own done text (the engine story, an unapplied payment, no addresses to push). A **reload** shows the same thing: every card summary returns `lastRunStatus` plus `lastRunResult` (`CcNsSyncCallController.LastRunResult`: `counts`, `error`, `failedCount`, `contractsRolled`, `engine`, `engineSkipReason`), which the card maps through the same code as the live poll. A last run that ended `Failed` reloads with the red strip, the run's error and a **Booking failed** / **Sync failed** pill, the button offered again. See [Booking an order](booking-an-order.md#what-happens-when-you-click-book). ## Failure notifications A failed run notifies the initiating user through the Salesforce bell, and optionally emails a configured address — see [Sync settings](sync-settings.md). Batch invoice runs are partial-failure aware: the run completes with a failed-document count and per-document errors, and only the failures return to scope next run. ## Common errors | Symptom | Likely cause · fix | |---|---| | "Unauthorized endpoint" on test or sync | The host is not allow-listed — wizard step 2 lists the two standard entries; a call pointed at a [custom endpoint](calls-endpoints.md#endpoint-per-call) needs its own Remote Site as well | | Validate reports "Host … has no Remote Site Setting" | The same cause, caught before Activate — add the host under Setup > Security > Remote Site Settings and validate again | | Run Failed with an HTTP error naming an unfamiliar URL | The call points at an endpoint with a wrong script or deployment id. Fix the endpoint, or click **Reset to default** in the endpoint editor. Then re-activate and re-run | | '"" isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then try again.' | The prebuilt call has no row: step 6 of the [Connection wizard](connection-wizard.md) never ran. Only that step creates the calls. Run **Deploy prebuilt calls & mappings**, then sync again. Every start refuses before writing anything: a toast on the card, `Status = Error` on a Flow action, or a Failed run on a record the Flow started in the background | | The sync card shows '"" isn't set up yet. … then reload this page.' instead of its button | Same cause, caught before anyone clicks. Deploy in step 6, then reload the record page | | Calls & Mappings shows "No calls are set up yet." | Same cause. The tab never creates calls; click **Open NetSuite Connection** and deploy in step 6 | | "Continuous Control isn't set up yet. Deploy the prebuilt calls in step 6 of the NetSuite Connection page, then try again." | The call exists but was never published (no activated snapshot, whatever `Status__c` says). Run **Deploy prebuilt calls & mappings** in the [Connection wizard](connection-wizard.md); it publishes every call that isn't live. Then book or sync again. A booking shows this as a toast or Flow fault with no run row. A customer sync marks its run Failed | | A card row reads " of synced" with an amber warning icon | The last run landed fewer of that row's records than the record holds (`lastRunResult.counts` vs. the pre-run summary). The strip carries the run's `Error__c`. Fix what it names, then **Sync again** | | The card's pill reads "Booking failed" / "Sync failed" on load | The newest run on the record is `Failed` (`lastRunStatus`). The red strip shows its `Error__c`, or the fallback *The last sync failed. Open the Sync Runs tab for details, then try again.* Fix the cause and run again | | "NetSuite reported no order landed for order …" | The call reached NetSuite but produced no order. Read the run's errors first, then its payload — a booking that lands nothing is deliberately marked Failed rather than green | | HTTP 401 / invalid signature | A credential is wrong or the token was revoked — re-enter in step 1, re-test in step 3 | | "A booking run for this order is already in progress" | A live run exists — wait for it; a run stuck >10 minutes is auto-superseded by the next attempt | | **Sync Order Lines** refused: "This order isn't in NetSuite yet. Book it first." / "This order's contract isn't in NetSuite yet. Book the order first." | The Order (or its Contract) has no `NetSuiteRecordId__c`. Book the order first | | "This order is being booked to NetSuite right now. Sync its lines when the booking finishes." | A live (non-stale) booking run of the same order (`Order__c`). A line sync waits for it because both write the RC Order Lines. Sync again once the booking finishes. A line sync never blocks a booking | | "An order line sync for this order is already in progress." | A live Order Line Sync run on the same order (`Scope_Id__c`, Type `Order Line Sync`). Wait; a stale one (>10 minutes) is superseded | | "Order Line Sync needs one setup step after an upgrade. In Setup, add "Order Line Sync" to the Type picklist on NetSuite Sync Run, then try again." | The org was upgraded from before 3.3.0: an upgrade adds no value to the existing restricted `CC_NetSuite_Sync_Run__c.Type__c` picklist, so the run can't be filed (`CcNsSyncRunService.runTypeAvailable`). Add the value in Setup > Object Manager > NetSuite Sync Run > Type; step 6 shows the same reminder until it's there | | "None of these order lines are in NetSuite yet. Book the order again to add them." / "This order has no order lines to sync." | None of the requested lines carries a `NetSuiteRecordId__c` (lines added after the booking), or the order has no lines. Book again, then sync the lines | | "This order has too many lines to sync in one run. Sync the lines in smaller groups from a Flow." | The serialized payload would exceed `CcNsOrderLinePayloadBuilder.MAX_PAYLOAD_CHARS` (131,000, about 900 lines), since each leg sends the stored copy. Call **Sync Order Lines to NetSuite** per subset of Order Products | | "This call needs Continuous NetSuite package 3.3.0 or later, and the connected account is running . …" on an order line sync | Verify last stamped a package below 3.3.0 (`CcNsSyncRunService.checkNsVersionFloor`). Install ns-v3.3.0, then run **Verify installation** (step 4) so the stamp updates. If the stamp is blank or current but NetSuite is older, NetSuite refuses instead and the run is Failed with "this call needs Continuous Control NetSuite package 3.3.0; this account runs … Nothing was written." | | Order line sync `Completed with warnings`: "NetSuite has no order line for of these lines. Book the order first." | Salesforce holds a NetSuite id for those lines but NetSuite found no RC Order Line with that external id (`skipped`). Book the order again, then sync the lines | | Order line sync `Completed with warnings`: " order line(s) have no NetSuite record yet, so … weren't sent. Book the order again to add …" | Some of the order's lines were never booked; the rest were sent. Book again to add them | | Order line sync error: "Order line belongs to another Salesforce org: . Only that org can update it, so nothing was written." | The RC Order Line's Source Org names a different org id (`stampedByOtherOrg`, compared by org id, not host). Update it from the org that booked it | | Order line sync error: "Order line : % Complete is not a number from 0 to 100. …" / "…: the delivered date is not a date. …" | That one line was refused; the others landed. Fix the value in Salesforce and sync again | | Order line sync `engine: skipped` — "NetSuite found none of these order lines, so the POBs had nothing to update." | NetSuite matched none of the lines, so no engine leg ran. Book the order, then sync its lines | | POBs look stale after a delivery or % change | An engine leg didn't finish. Click **Sync Order Lines** again: it re-runs Update POBs → Update Revenue Plans → POB mirror whenever NetSuite finds any line, changed or not | | A % Complete POB recognized far less than expected | Update Revenue Plans reads `custrecord_cc_pob_pct_complete` as points (ns-v3.3.0): 0.4 is 0.4%, not 40%. Enter 40 for 40% and run Update Revenue Plans | | "has no Contract. Add a Contract to the order" | The order has no Contract. Set it and book again. | | Order card says "Activate first" | The order isn't Activated — the sync only books activated orders | | A booked order shows "Prices aren't up to date. Refresh Prices." | Revenue Cloud sets `Order.ValidationResult = TransactionIncomplete` ("not configured and priced") whenever an order line is updated after activation through the sObject API, and the pricing banner shows for that value. Booking writes `OrderItem.NetSuiteRecordId__c` (`CcNsWritebackService`), so every booked order gets it, although no price changed; the Order header writeback alone doesn't. Continuous doesn't change Revenue Cloud's behavior. Don't use **Refresh Prices** on a booked order: repricing can change its amounts from what NetSuite holds. To hide the banner, turn on **Hide Price Refresh Notification** in Setup > Revenue Settings (`RevenueManagementSettings.hidePriceRefreshNtfcn`); it hides the banner on quotes too | | Invoice skipped from a batch | Not Posted, already synced, account not NetSuite-matched, or a fully-converted negative (its credit memo syncs instead) — see [what qualifies](invoices-credit-memos.md) | | "NetSuite returned a web page instead of data." | NetSuite answered a RESTlet call with HTML (a login or error page: expired token, wrong RESTlet URL). Run **Test the connection** in wizard step 3, fix what it reports, sync again. Every leg (booking, invoice, credit memo, payment, refund, product, customer, custom call, Cash App Pull, POB mirror read) reports it the same way | | "NetSuite still had work left after 25 legs, so the run stopped." / "… didn't move its resume point …" | A booking or custom call chained more than `MAX_CONTINUATION_LEGS` (25) continuation legs, or NetSuite answered `partial` without advancing `continueFrom` / `customNodesFrom`. The run is marked Failed with what landed kept. Read the errors, then run again | | "NetSuite reached its usage limit after `` of `` ``. The next leg syncs the other ``." / "…Sync the other `` again." | An invoice, payment, refund, or product batch ran into NetSuite's usage limit partway through. Everything before the stop already landed | Nothing to do on a batch — the next leg picks up the rest. On a single-document call, run the sync again | | "`` `` stays unsynced because NetSuite reached its usage limit." / "Account `` stays unsynced because NetSuite reached its usage limit." | The one document (or account) a leg was writing didn't finish — its extension records or billing mirrors didn't all land, so its id, and its custom-node records, were left out | For a batch call ("A batch retries it on its next leg"), nothing to do. Otherwise ("Sync it again"), sync that document or account again | | "NetSuite reached its usage limit before `` mapped record(s) were written. Sync again to write ``." | Extension-node records that belong to no document (a parentless node) didn't all land before the leg ran out of usage | Sync again | | "The `` call stopped on an error in NetSuite: ``. Check the script log in NetSuite, then run it again." | An error outside the action's own handling reached the RESTlet's dispatcher — something failed before NetSuite could even attempt the write | Open the script log in NetSuite for the detail, fix what it names, then run the call again | | "`` schedule row(s) are still being posted by another run … Run again in a few minutes to finish." | Recognize Revenue found rows another run (the scheduled close, or a second manual run) marked as posting moments ago, and left them alone rather than risk two journal entries for the same period | Wait a few minutes for the other run to finish, then run Recognize Revenue again | | Card says "revenue engine didn't finish in NetSuite" | The sync succeeded; the chained `CcNsEngineQueueable` job died. The run keeps its status and warnings, and `Result_Json__c` gets `engine: failed` + `engineError`. Run **Refresh POBs** later | | Wizard step 4 shows "This connection uses the `` role. Move it to the Continuous Control Integration role. A later release limits the endpoint to that role and Administrator." (ns-v3.2.0) | `installcheck`'s `role` is neither Administrator nor `customrole_cc_integration` — advisory only, this release, since the RESTlet stays open to every role | Assign the [Continuous Control Integration role](install-netsuite.md) to the integration user, create a new access token for that user and role, and enter it in [wizard step 1](connection-wizard.md); the consumer key/secret stay the same | | A run note: "``: NetSuite id `` belongs to a different record. It was ignored, and the record was looked up by its Salesforce id." (ns-v3.2.0) | The `nsId` Salesforce sent for that record is real, but its externalid belongs to a different Salesforce record — `claimedByOther` refused to trust it | Nothing, usually — NetSuite fell back to the externalid lookup, exactly as if no id had been sent. If the wrong NetSuite record was updated, find which Salesforce record actually owns that externalid | | A run note: "``: "``" is not a NetSuite id. It was ignored, and the record was looked up by its Salesforce id." (ns-v3.2.0) | The `nsId` Salesforce sent isn't a NetSuite internal id shape at all | Nothing — the externalid lookup ran instead. Check the source of that id if it recurs | | A run error: "``: Files of type .`` are not accepted, so the file was not saved. Send a document or an image, such as a PDF." (ns-v3.2.0) | The generated document's extension is a script or markup type (`.js`, `.html`, and similar), or another type outside the accepted list | Regenerate the document as an accepted type, or send a PDF | | A run error: "``: The file name cannot be used, so the file was not saved. Send a plain file name with no slashes." or "folder: The folder name "``" cannot be used. Nothing was saved. Name one subfolder of Continuous Documents with no slashes, or leave the name out." (ns-v3.2.0) | The document's file name, or the payload's `folderName`, has a path separator or another disallowed character | Fix the name at the source — a Salesforce document title shouldn't carry a slash — and sync again | | A run error: "``: Continuous does not attach files to `` records, so the file was not saved. Attach it to a record Continuous syncs, such as an invoice." (ns-v3.2.0) | The document's target record type isn't one Continuous syncs (see [`files`](ref-restlet-actions.md#files-folders-and-extensions)) | Attach the document to a record Continuous syncs | | A run error: "`` is not on the relay allowlist, so nothing was sent. Add its script id to Relay Allowlist on Continuous Revenue Settings, or relay to a listed RESTlet." (ns-v3.2.0) | A call's relay target isn't on the settings record's **Relay Allowlist** | Add the target's script id to Relay Allowlist, or leave the list blank to allow any RESTlet except the Continuous endpoint | | A run error naming a loop: "This call was already relayed by `