Comprehensive NetSuite SDF best practices based on the SAFE Guide (12 principles + appendices). Generates Object XML for all 14 script types, enforces governance limits, security patterns, and defensive coding. Includes N/cache, N/query, concurrency limits, OAuth 2.0 guidance, legacy TBA guardrails, CustomTool runtime patterns, REST Web Services (2026.1 features), and 140+ documented pitfalls. Essential for SuiteApp and Account Customization development.
التثبيت
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا.عرض على GitHub
name
netsuite-sdf-safe-guide
description
Comprehensive NetSuite SDF best practices based on the SAFE Guide (12 principles + appendices). Generates Object XML for all 14 script types, enforces governance limits, security patterns, and defensive coding. Includes N/cache, N/query, concurrency limits, OAuth 2.0 guidance, legacy TBA guardrails, CustomTool runtime patterns, REST Web Services (2026.1 features), and 140+ documented pitfalls. Essential for SuiteApp and Account Customization development.
license
The Universal Permissive License (UPL), Version 1.0
metadata
{"author":"Oracle NetSuite","version":"1.0"}
NetSuite SDF Safe Guide
Description
Comprehensive guide for NetSuite SuiteCloud Development Framework (SDF) projects, incorporating the SAFE Guide (SuiteApp Architecture Framework for Excellence) principles. This skill provides:
Appendices: Concurrency limits, N/query joins, N/cache samples, and other static reference material
How to Use This Skill
Manual Invocation (Slash Command)
Invoke this skill at any time by typing:
/netsuite-sdf-safe-guide
Or use natural language triggers:
"Generate objects" – Scan SuiteScripts folder and create missing Object XML files.
"Create object for this script" – Generate Object XML for the current/specified file.
"Check my deployment XML" – Review and validate existing Object XML files.
"Review my SuiteScript" – Review code for best practices, pitfalls, and governance issues.
Optional Coding Assistant Activation Example
For NetSuite SuiteCloud Development Framework (SDF) projects, supported coding assistants may preload this skill from project settings.
Here is an example for Claude, which is one of the supported coding assistants:
Step 1: Create or edit .claude/settings.local.json in your SDF project root:
With skill preloading configured, an assistant will automatically apply SDF best practices when you:
Work on any NetSuite SDF project (SuiteApps or Account Customization projects).
Create or modify SuiteScript files (all 14 script types including Suitelets, RESTlets, User Event Scripts, etc.).
Work with Object XML files, custom records, custom fields, or other SDF objects.
Configure deployment settings, manifest.xml, or deploy.xml.
Ask questions about NetSuite SDF development.
Encounter deployment errors or script issues.
Intended Audience
This skill is designed for Technical Architects, Developers, and System Administrators working with NetSuite SuiteScript and the SuiteCloud Development Framework (SDF). It is a practical companion to expert guidance and hands-on experience, not a replacement for the official NetSuite documentation. The focus is SuiteScript 2.1 with callouts to 2.0/1.0 only where meaningful differences exist.
When to Use This Skill
Proactive Invocation (Recommended)
This skill should be invoked automatically when:
The user is working on a NetSuite SDF project.
The user creates or modifies SuiteScript files.
The user asks questions about SDF deployment, script types, or NetSuite development.
The user encounters deployment errors or script issues.
Manual Invocation
Commands: "generate objects", "create object for this script", "check my deployment XML".
Questions: "What's wrong with my RESTlet?", "Why won't my script deploy?", "How do I add a button?".
Best practices: "Review my SuiteScript", "Is this the right approach?", "What's the pattern for...".
Object Generation Mode
When the user says "generate objects" without specifying a file:
Scan the entire /SuiteScripts/ folder (including subfolders) for all .js files.
For each .js file found:
Check if it has a valid @NScriptType annotation.
Check if an object XML file already exists for it.
If no object exists and it has a valid script type, generate the object.
Skip files that:
Already have corresponding object XML files.
Don't have a valid @NScriptType annotation.
Are schema files (*_schema.json).
Provide a summary of all files processed.
Single File Processing
Only process a single file when:
The user explicitly specifies a file path.
The user says "for this script" or "for this file" (use the currently open file in the IDE).
The user references a specific script by name.
Best Practices Review Mode
When reviewing code or answering questions:
Reference the Common Pitfalls section for known issues.
1. Normalize filename (lowercase, underscores, no hyphens)
2. Calculate script ID with prefix (customscript_ or custtoolset_)
3. If script ID > 40 chars:
Truncate filename to fit within 40 char limit
Warn the user: "Script ID truncated to 40 characters"
4. Validate: script ID exactly matches expected pattern (no script type in ID)
Human-Readable Name Generation
Convert filename from camelCase/snake_case to Title Case:
getcampaignpreflight → "Get Campaign Preflight"
ue_update_customer → "UE Update Customer"
RESTlet_API_Handler → "RESTlet API Handler"
Algorithm:
Split on underscores and capital letters.
Capitalize first letter of each word.
Keep acronyms as-is.
Join with spaces.
Step 5: Gather Required Information
For ClientScript, UserEventScript, and MassUpdateScript:
Prompt the user for the record type:
What record type should this [ClientScript/UserEventScript/MassUpdateScript] be deployed to?
Examples:
- Standard records: customer, salesorder, invoice, contact, etc.
- Custom records: [customrecord_yourrecordname]
For CustomTool:
Automatically Create Schema File:
Looking for: /SuiteScripts/[subfolder]/[normalized_filename]_schema.json
If NOT found:
Automatically create a basic schema template at the expected location.
Notify the user: "Created schema template at [path]".
Schema file will be referenced in the object XML
NOTE (NetSuite 2026.1+): CustomTool was renamed to ToolSet in 2026.1. Use custtoolset_ prefix (not customtool_) and <toolset> root element (not <tool>). The visibility element is now <exposetoaiconnector> (was <exposeto3rdpartyagents>). See SuiteAnswers 1024036.
Step 6: Generate XML Object File
Create the appropriate XML structure based on script type:
<?xml version="1.0" encoding="UTF-8"?><manifestprojecttype="SUITEAPP"><projectname>[Your Project Name]</projectname><frameworkversion>1.0</frameworkversion><dependencies><features><featurerequired="true">SERVERSIDESCRIPTING</feature></features></dependencies><!-- rest of manifest --></manifest>
Logic:
Check if manifest.xml already contains <dependencies> tag.
If YES: Verify SERVERSIDESCRIPTING feature exists, add if missing.
If NO: Insert entire <dependencies> block after <frameworkversion>.
Note: This is required for all server-side script deployment to NetSuite. The only exceptions are ClientScript (client-side) and CustomTool (has different requirements). UserEventScript also requires this feature. Without this, deployment will fail.
Additional Feature Dependencies
When your project includes custom fields applied to transaction bodies (custbody_*), the following features must be declared in manifest.xml if the field applies to the corresponding transaction type:
Feature String
Required When Field Applies To
RECEIVABLES
Customer Payment transactions
PAYABLES
Vendor Payment transactions
EXPREPORTS
Expense Report transactions
ADVRECEIVING
Item Receipt transactions
OPPORTUNITIES
Opportunity transactions
WEBSTORE
Web Store transactions
MULTILOCINVT
Transfer Order transactions
When your project includes custom fields applied to items (custitem_*) with inventory or assembly applicability:
Feature String
Required When Field Applies To
INVENTORY
Inventory Items
ASSEMBLIES
Assembly Items
Example: A project with custbody_* fields on transaction bodies AND custitem_* fields on inventory/assembly items needs:
NetSuite SDF uses bracket notation to indicate file references within the File Cabinet.
The brackets signal to the deployment system that this is a relative path within the SuiteApp/Account Customization bundle.
Without brackets, NetSuite cannot resolve the file path during deployment.
FileCabinet File Permissions for .ss and .ssp Files
.ss (SuiteScript Classic) and .ssp (SuiteScript Server Pages) files require explicit <permission> configuration in their SDF FileCabinet <file> definition. Without this, file permissions revert to account defaults after deployment.
2025.2 compatibility: This format deploys to both 2025.2 AND 2026.1 accounts but does not support execution logs. Use this only when target_netsuite_version is "2025.2".
If no schema file exists and the user declined to create one, omit the <rpcschema> line.
CustomTool does NOT use scriptdeployments structure.
NetSuite 2026.1+: Uses custtoolset_ prefix and <toolset> root element. NetSuite 2025.2: Uses customtool_ prefix and <tool> root element. The correct format is determined by target_netsuite_version in ~/.claude/netsuite-connector-config.json.
Automatically create schema file if not present.
DEPLOYMENT FIX: CustomTool does NOT support <description>, <isinactive>, <notifyadmins>, <notifyowner>, <defaultfunction>, <title>, <runasrole>, or <allroles> elements; these cause SDF validation errors.
<exposetoaiconnector>T</exposetoaiconnector> (2026.1+) or <exposeto3rdpartyagents>T</exposeto3rdpartyagents> (2025.2): enables tool for external MCP clients.
Permkey Validation: When generating <permissions> blocks, load the netsuite-sdf-roles-and-permissions skill and validate all <permkey> values against references/permissions.json. Custom record permkeys (customrecord_*) should be validated against the project's own custom record Object XML files. See netsuite-sdf-roles-and-permissions.
ClientScript requires a record type; prompt the user for it.
Requires SERVERSIDESCRIPTING feature in manifest.xml.
DEPLOYMENT FIX: ClientScript does not support <runasrole> or <allroles> in deployment.
UserEventScript Template
<usereventscriptscriptid="customscript_[filename]"><name>[Human Readable Name]</name><scriptfile>[/SuiteScripts/[subfolder]/[filename].js]</scriptfile><description></description><isinactive>F</isinactive><notifyadmins>F</notifyadmins><notifyowner>T</notifyowner><scriptdeployments><scriptdeploymentscriptid="customdeploy_[filename]"><isdeployed>T</isdeployed><loglevel>DEBUG</loglevel><recordtype>[USER_PROVIDED]</recordtype><!-- SECURITY: Use least-privilege custom role. See 05-security-privacy.md. --><runasrole>[CUSTOM_ROLE_OR_REMOVE]</runasrole><status>RELEASED</status><!-- NOTE: allroles=T selects internal roles only. Add <audslctrole> for external/portal roles. --><allroles>T</allroles></scriptdeployment></scriptdeployments></usereventscript>
SECURITY WARNING: Never use <runasrole>ADMINISTRATOR</runasrole> in production. This grants the script full admin privileges, enabling privilege escalation. Always create a custom role with minimum required permissions. See 05-security-privacy.md and netsuite-owasp-secure-coding.
Notes:
UserEventScript requires a record type; prompt the user for it.
Entry points are defined in the JS file: beforeLoad, beforeSubmit, afterSubmit.
Requires SERVERSIDESCRIPTING feature in manifest.xml.
<allroles>T</allroles> selects internal roles only. If the script needs to be accessible to external/portal roles (for example, Customer Center or Partner Center), add <audslctrole> elements for each external role.
Suitelet Template
<suiteletscriptid="customscript_[filename]"><name>[Human Readable Name]</name><scriptfile>[/SuiteScripts/[subfolder]/[filename].js]</scriptfile><description></description><isinactive>F</isinactive><notifyadmins>F</notifyadmins><notifyowner>T</notifyowner><scriptdeployments><scriptdeploymentscriptid="customdeploy_[filename]"><isdeployed>T</isdeployed><!-- SECURITY: isonline=F requires login. NEVER set to T unless the Suitelet is explicitly designed for anonymous/public access. See Pitfall #135. --><isonline>F</isonline><loglevel>DEBUG</loglevel><!-- SECURITY: Use least-privilege custom role. See 05-security-privacy.md. --><runasrole>[CUSTOM_ROLE_OR_REMOVE]</runasrole><status>RELEASED</status><title>[Human Readable Name]</title><!-- NOTE: allroles=T selects internal roles only. Add <audslctrole> for external/portal roles. --><allroles>T</allroles></scriptdeployment></scriptdeployments></suitelet>
Notes:
Entry point is defined in the JS file: onRequest.
Requires SERVERSIDESCRIPTING feature in manifest.xml.
<isonline>F</isonline> is required — this ensures the Suitelet requires authentication. NEVER set <isonline>T</isonline> unless the Suitelet is explicitly designed for anonymous/public access (for example, a customer-facing portal page). Suitelets that create, modify, or query data must ALWAYS require login. While NetSuite defaults to F when omitted, always include it explicitly to prevent accidental exposure. See Pitfall #135.
<allroles>T</allroles> selects internal roles only — if the script needs to be accessible to external/portal roles (for example, Customer Center or Partner Center), add <audslctrole> elements for each external role.
Saved Search Creator Suitelet Pattern
IMPORTANT: SDF cannot create saved searches from scratch — the <savedsearch> XML requires a system-generated binary definition blob. The recommended approach is to include a Saved Search Creator Suitelet in your SDF project that programmatically creates the search via N/search on first run. After the search exists, import it into your project with suitecloud object:import for ongoing SDF lifecycle management. See Pitfall #125 for full details.
Security posture: choose the execution model before generating this Suitelet.
Default/self-service model: omit <runasrole> so the Suitelet runs as Current Role, keep <allroles>T</allroles> only when every internal role should be allowed to create searches under its own permissions, and do not make the created search public by default.
Controlled bootstrap model: if elevated permissions are required to create a canonical SDF-managed search, use a least-privilege custom execution role whenever possible. Use <runasrole>ADMINISTRATOR</runasrole> only for a one-time bootstrap with an explicit admin/developer audience, never with <allroles>T</allroles>.
Object XML — requires login, restricted audience, default Current Role execution:
<suiteletscriptid="customscript_create_saved_search"><name>Saved Search Creator</name><scriptfile>[/SuiteScripts/[subfolder]/create_saved_search.js]</scriptfile><description>One-time Suitelet to create saved searches that cannot be hand-authored in SDF XML. Run once, then deactivate.</description><isinactive>F</isinactive><notifyadmins>F</notifyadmins><notifyowner>T</notifyowner><scriptdeployments><scriptdeploymentscriptid="customdeploy_create_saved_search"><isdeployed>T</isdeployed><!-- SECURITY: NEVER set isonline=T for Suitelets that create/modify data. This Suitelet creates saved searches — it MUST require login. --><isonline>F</isonline><loglevel>DEBUG</loglevel><!-- SECURITY: Omit runasrole to run as Current Role. If elevated execution is required, use a least-privilege custom role; use ADMINISTRATOR only with explicit admin/developer audience for a one-time bootstrap. --><!-- <runasrole>[CUSTOMROLE_SAVED_SEARCH_CREATOR]</runasrole> --><status>RELEASED</status><title>Saved Search Creator</title><!-- SECURITY: Do not combine elevated execution with allroles=T. Use explicit roles for privileged creator Suitelets. --><allroles>F</allroles><audslctrole>[CUSTOMROLE_SDF_DEVELOPER]</audslctrole></scriptdeployment></scriptdeployments></suitelet>
SuiteScript 2.1 — create_saved_search.js:
/**
* @NApiVersion 2.1
* @NScriptTypeSuitelet
* @NModuleScopeSameAccount
*/define(['N/search', 'N/ui/serverWidget'], (search, serverWidget) => {
constonRequest = (context) => {
if (context.request.method === 'GET') {
// Show form with a button to create the search.const form = serverWidget.createForm({ title: 'Saved Search Creator' });
form.addSubmitButton({ label: 'Create Saved Search' });
context.response.writePage(form);
return;
}
// POST. Create the saved search.const savedSearch = search.create({
type: search.Type.TRANSACTION,
title: 'SG Open Sales Orders - SDF Deployed',
id: 'customsearch_sdftest_open_so',
filters: [
search.createFilter({ name: 'type', operator: search.Operator.ANYOF, values: ['SalesOrd'] }),
search.createFilter({ name: 'status', operator: search.Operator.ANYOF, values: ['SalesOrd:B'] })
],
columns: [
search.createColumn({ name: 'internalid', label: 'Internal ID' }),
search.createColumn({ name: 'trandate', label: 'Date' }),
search.createColumn({ name: 'type', label: 'Type' }),
search.createColumn({ name: 'tranid', label: 'Document Number' }),
search.createColumn({ name: 'entity', label: 'Customer' }),
search.createColumn({ name: 'memo', label: 'Memo' }),
search.createColumn({ name: 'amount', label: 'Amount' }),
search.createColumn({ name: 'statusref', label: 'Status' })
]
});
const searchId = savedSearch.save();
// SECURITY: Do not make generated searches public by default.// If broader visibility is required, import the search into SDF first, then set// the approved audience/public flag through the normal review process.
context.response.write(`Saved search created successfully. Internal ID: ${searchId}`);
};
return { onRequest };
});
After the search is created:
Run suitecloud object:import --type savedsearch --scriptid customsearch_sdftest_open_so --destinationfolder /Objects --excludefiles.
The imported <savedsearch> XML with the binary blob is now managed by SDF.
Set <isinactive>T</isinactive> on the Suitelet to deactivate it (or remove it from the project entirely).
Review the imported saved search audience. Do not mark it public unless the business owner explicitly approves public visibility.
Key field mappings for common transaction searches:
Filter Value
Meaning
SalesOrd
Sales Order transaction type
SalesOrd:B
Pending Fulfillment status
SalesOrd:A
Pending Approval status
SalesOrd:F
Billed status
statusref
Human-readable status text (vs status which shows codes)
Default to HTML. Always use HTML unless the user explicitly requests a different type.
Change portlettype when user specifies:
The user mentions "form portlet" or "data entry" → use FORM.
The user mentions "list portlet" or "table/grid" → use LIST.
The user mentions "links portlet" or "navigation" → use LINKS.
If unclear, ask the user which type they need.
Portlet Type
User Keywords
SuiteScript Methods
HTML (default)
"html", "content", "display", "custom"
portlet.html = "..."
FORM
"form", "input", "submit", "data entry"
portlet.addField(), portlet.setSubmitButton()
LIST
"list", "table", "grid", "columns", "rows"
portlet.addColumn(), portlet.addRow()
LINKS
"links", "navigation", "menu"
portlet.addLine(), portlet.addRow()
Notes:
Portlet requires SERVERSIDESCRIPTING feature in manifest.xml (like Suitelet and Restlet).
Portlet omits <runasrole> intentionally - per NetSuite documentation, omitting this tag causes the script to run as the current user's role (the user viewing the dashboard).
The <portlettype> element is at the portlet level, NOT inside scriptdeployment.
<allroles>T</allroles> selects internal roles only — if the portlet needs to be accessible to external/portal roles (for example, Customer Center or Partner Center), add <audslctrole> elements for each external role.
BundleInstallationScript does NOT use scriptdeployments structure.
DEPLOYMENT FIX: Entry point function XML elements (<beforeinstallfunction>, <afterinstallfunction>, etc.) are NOT supported and will be ignored - entry points are auto-detected from the JavaScript file.
Requires SERVERSIDESCRIPTING feature in manifest.xml.
<deploy><configuration><path>~/Objects/customscript_[filename].xml</path></configuration><files><!-- other files --></files><objects><!-- other objects --></objects><!-- Place at end for after-install behavior --><script><path>~/Objects/customscript_[filename].xml</path><run>customdeploy_[filename]</run></script></deploy>
application.xml — Pre-Uninstall Lifecycle Hook
application.xml is an optional SDF file that registers SDFInstallationScripts to run at specific points in the SuiteApp lifecycle. The most common use is <beforeundeploy>, running cleanup logic before uninstall begins.
Comparison: BundleInstallationScript vs application.xml
<?xml version="1.0" encoding="UTF-8"?><application><hooks><beforeundeploy><!-- Reference an SDFInstallationScript Object XML and its deployment. --><script><path>~/Objects/customscript_[uninstall_cleanup].xml</path><run>customdeploy_[uninstall_cleanup]</run></script></beforeundeploy></hooks></application>
When to use application.xml:
Your SuiteApp creates API tokens or OAuth credentials that must be revoked before removal.
Your SuiteApp registers webhooks or external subscriptions that must be de-registered.
Your SuiteApp creates data in external systems that must be cleaned up before the SuiteApp is removed.
You need to archive or migrate data BEFORE the SuiteApp's custom records are deleted.
When NOT to use:
General install/update logic → use BundleInstallationScript instead.
Post-uninstall cleanup → not possible (SuiteApp is already gone).
Can optionally have <returntype> and <returnrecordtype> for returning values to workflow.
If the script returns a value to a workflow field, add:
<returntype>SELECT</returntype><returnrecordtype>[customrecordtype_name or customlist_name]</returnrecordtype>
Requires SERVERSIDESCRIPTING feature in manifest.xml
DEPLOYMENT FIX: WorkflowActionScript REQUIRES <recordtype> in deployment; prompt user for record type.
DEPLOYMENT FIX:<defaultfunction> element is NOT supported and will be ignored; entry point is auto-detected.
DEPLOYMENT FIX: WorkflowActionScript does NOT support <title>, <runasrole>, or <allroles> in deployment.
Workflow Object XML Template (SuiteFlow)
IMPORTANT: This is NOT a script type, it's a workflow object (customworkflow) that defines SuiteFlow workflows as SDF-deployable XML. Workflows have states, transitions, actions, and conditions; no .js script file needed (unless using <customaction> which references a WorkflowActionScript).
<workflowscriptid="customworkflow_[name]"><name>[Human Readable Name]</name><description>[PURPOSE]</description><recordtypes>[RECORD_TYPE]</recordtypes><initoncreate>T</initoncreate><initonvieworupdate>F</initonvieworupdate><inittriggertype>BEFORELOAD</inittriggertype><initcontexts></initcontexts><initeventtypes></initeventtypes><initsavedsearchcondition></initsavedsearchcondition><isinactive>F</isinactive><islogenabled>T</islogenabled><keephistory>ONLYWHENTESTING</keephistory><releasestatus>TESTING</releasestatus><runasadmin>F</runasadmin><workflowstates><workflowstatescriptid="workflowstate_[state_name]"><name>[State Display Name]</name><description></description><donotexitworkflow>F</donotexitworkflow><positionx>243</positionx><positiony>133</positiony><workflowactionstriggertype="[TRIGGER_TYPE]"><!-- Action elements here (see Action Type Reference below) --></workflowactions><workflowtransitions><workflowtransitionscriptid="workflowtransition_[name]"><tostate>[scriptid=customworkflow_[name].workflowstate_[target]]</tostate><triggertype>AFTERSUBMIT</triggertype><initcondition><formula></formula><type>VISUAL_BUILDER</type></initcondition></workflowtransition></workflowtransitions></workflowstate></workflowstates></workflow>
Trigger Types for <workflowactions> and <workflowtransitions>:
BEFORELOAD — Before record loads (server, UI + API)
BEFORESUBMIT — Before record saves (server)
AFTERSUBMIT — After record saves (server)
ONENTRY — When entering the state (server, runs with first server trigger)
ONEXIT — When exiting the state (server)
BEFOREUISUBMIT — Before UI submit (client-side only)
AFTERUISUBMIT — After UI submit (client-side only)
Script ID: customworkflow_ prefix (15 chars) → 25 chars remaining for name. 40 char max total.
State script IDs: workflowstate_ prefix, scoped within the workflow.
Action script IDs: workflowaction_ prefix, scoped within the state.
Transition script IDs: workflowtransition_ prefix, scoped within the state.
Cross-references: Use bracket notation [scriptid=customworkflow_name.workflowstate_target] for <tostate> and <buttonaction>.
releasestatus: ALWAYS deploy as TESTING first, never RELEASED on first deploy. Promote to RELEASED only after validation.
keephistory: Use ONLYWHENTESTING for development, ALWAYS for audit-critical workflows.
runasadmin: Only effective when the deployer has admin role; ignored for non-admin deployers.
PITFALL — Trigger order: If workflow initiates on AFTERSUBMIT, BEFORELOAD actions in the entry state won't execute on the triggering transaction.
PITFALL — ONENTRY timing: ONENTRY does NOT execute independently, it runs with the first server trigger (BEFORELOAD, BEFORESUBMIT, or AFTERSUBMIT) after state entry.
PITFALL — UE script conflicts: Workflow and User Event script execution order is NOT guaranteed on the same record — avoid both modifying the same field.
PITFALL — Workflow custom fields: Fields created as "Workflow" type cannot be used in client-side actions (BEFOREUISUBMIT, AFTERUISUBMIT).
PITFALL — NOTINITIATING: Replaced NOTRUNNING as the non-running status, both values accepted for backward compatibility.
No manifest feature needed: Workflows deploy as objects, not scripts, no SERVERSIDESCRIPTING feature required.
SPA (Single Page Application) Template
IMPORTANT: SPA scripts use a DIFFERENT object type than traditional scripts. Both SpaServerScript and SpaClientScript are defined within a SINGLE <singlepageapp> object.
CRITICAL: Always reference the Oracle SPA samples for working examples. The local SuiteCloud CLI validator and the server-side deployment use DIFFERENT XML element names — always validate locally before deploying.
SPA uses custspa_ prefix (8 chars) instead of customscript_.
Maximum scriptid length is 28 characters total.
Both client and server scripts are referenced in the SAME object XML.
SPA scripts do NOT have separate deployment records.
Requires SERVERSIDESCRIPTING feature in manifest.xml.
SPA primarily supported in SuiteApp projects; Account Customization support added in 2025.1.
SpaServerScript MUST export initializeSpa — NOT onAction, onRequest, or other entry points.
SpaClientScript MUST export run — NOT start or other entry points. No @NScriptType annotation needed.
SPA server scripts should be minimal (add stylesheets, validate roles) — business logic belongs in separate library modules called from the client via server actions.
The url field must be globally unique across ALL SPAs in the account — collisions cause cryptic deployment errors.
SPA Deployment Pitfalls:
"Items you have requested in the record have been deleted" — This cryptic error during SPA creation can be caused by: (1) URL collision with existing SPA, (2) audienceallroles=T referencing deleted roles in 2025.1+ accounts, (3) publisher restrictions on STDDEMO/demo accounts, (4) wrong entry point names in server/client scripts. Always deploy non-SPA objects first, then add the SPA in a subsequent deploy.
Failed SPA deployments leave ghost installs — A failed deploy that includes the SPA will leave the SuiteApp in FAILED status on the Installed SuiteApps page. You MUST uninstall before retrying. Consider deploying without the SPA first to establish a clean base.
Oracle samples vs local validator discrepancy — Oracle GitHub samples use element names like <clientscript>, <serverscript>, <assetfolder> (without brackets), but the local SuiteCloud CLI validator requires <clientscriptfile>, <serverscriptfile>, <assetsfolder> with bracket notation. Always validate locally first.
Secret Object XML (imported reference, not secret value):
First create the secret in NetSuite UI.
Mark it available to the SuiteApp when the project needs to import it.
Import the secret object into the project as /Objects/custsecret_vendor_api_token.xml
(src/Objects/custsecret_vendor_api_token.xml in a standard SuiteCloud project layout).
Secret object XML contains only the reference:
<secretscriptid="custsecret_vendor_api_token"/>
CRITICAL: Object Filename 40-Character Limit
Object filename = script ID + .xml.
The script ID portion (before .xml) MUST be ≤ 40 characters.
This is the same 40-character limit used for the scriptid attribute in the XML.
All filenames must be lowercase.
Hyphens must be converted to underscores.
If the script ID would exceed 40 chars, truncate the filename portion to fit.
*SPA scripts don't have traditional deployments - they're part of the singlepageapp object which defines both client and server scripts together.
Basic CustomTool Schema Template
When creating a schema file for CustomTool, use this template:
{"tools":[{"name":"[toolFunctionName]","description":"[Description of what this tool does]","inputSchema":{"type":"object","properties":{},"required":[]},"annotations":{"title":"[Human Readable Title]","readOnlyHint":true,"idempotentHint":true,"destructiveHint":false,"openWorldHint":false}}]}
CustomTool JavaScript Entry Points
The schema name and the JS exported function name must match exactly (case-sensitive). If they don't, the tool silently fails to route — no error, the AI agent simply cannot call it.
Declare as "type": "string" with description "JSON string containing…" — JSON.parse() in JS. NetSuite runtime may not pass nested objects cleanly.
Descriptions
Write comprehensive, multi-sentence descriptions. AI agents rely entirely on the description to understand the tool. Include parameter format examples inline.
required array
Always specify which fields are required — agents use this to validate before calling.
readOnlyHint
true for query/validation tools (safe to retry). false for create/update tools.
idempotentHint
true if calling N times = same result as 1. false for create operations.
destructiveHint
false for read-only or non-destructive tools. Omit (or true) for tools that create/update/delete — default is true. AI agents use this to decide whether to seek confirmation.
openWorldHint
false for tools that only access NetSuite data (closed system).
Query parameters
Never expose raw SuiteQL, table names, field names, joins, WHERE fragments, or ORDER BY text through MCP schemas. Use allowlisted dataset IDs, fixed query templates, parameter binding, filter validation, field/table allowlists, and sensitive-record exclusions.
MCP Integration
CustomTools become MCP tools automatically through this lifecycle:
Deploy via SDF — suitecloud project:deploy.
Register — NetSuite reads the RPC schema and registers the tool.
Discover — MCP servers query available tools; schema maps directly to MCP definition.
Invoke — AI agent calls the tool; NetSuite routes to the JS function matching schema.tools[0].name.
Return — JS return value is serialized to JSON and sent back to the agent.
The <exposetoaiconnector> flag controls visibility (renamed from <exposeto3rdpartyagents> in NetSuite 2026.1 — value semantics unchanged):
T — available to external MCP clients AND NetSuite AI
F — only visible to NetSuite's built-in AI assistant
The RPC schema IS the MCP tool definition — name, description, inputSchema, and annotations map 1:1. What you write in the schema is exactly what the AI agent sees.
CustomTool Error Handling
Always return structured errors — never let exceptions propagate:
If the .js file doesn't contain a valid @NScriptType:
Error: Cannot determine script type.
Please ensure your SuiteScript file has a JSDoc comment with @NScriptType annotation.
Example:
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
File Already Exists
If an object XML file already exists:
An object file already exists for this script:
[path/to/existing/file.xml]
Would you like to:
1. Overwrite the existing file
2. Cancel
3. View the existing file
Invalid Record Type
If the user provides an invalid record type format:
Record type should be either:
- A standard record name (lowercase): customer, salesorder, invoice
- A custom record reference: [customrecord_yourname]
Please try again.
Example Usage Scenarios
Scenario 1: Creating Object for New CustomTool
User: "I just created a new CustomTool script at /SuiteScripts/tools/emailvalidator.js. Can you create the object file?"
Assistant:
1. Reads /SuiteScripts/tools/emailvalidator.js.
2. Parses @NScriptType CustomTool.
3. Checks for /SuiteScripts/tools/emailvalidator_schema.json.
4. Asks: "Would you like to create a schema file? (Y/N)".
5. Generates /Objects/custtoolset_emailvalidator.xml with proper structure (2026.1+ naming).
6. If the user said yes, also create `emailvalidator_schema.json`.
Scenario 2: Creating Object for ClientScript
User: "Create an object for my client script at /SuiteScripts/customer_validation.js".
Assistant:
1. Reads the file.
2. Parses @NScriptType ClientScript.
3. Asks: "What record type should this ClientScript be deployed to?".
4. User responds: "customer".
5. Generates /Objects/customscript_customer_validation.xml.
6. Sets <recordtype>customer</recordtype> in deployment.
Scenario 3: Batch Creation
User: "I have 5 new scripts in /SuiteScripts/batch/ that need objects created".
Assistant:
1. Lists all .js files in /SuiteScripts/batch/.
2. For each file:
- Parse script type.
- Gather required info.
- Generate object.
3. Provides summary of created files.
<status>RELEASED for: All other script types with deployments
Required fields are prompted for (recordtype for ClientScript, UserEventScript, MassUpdateScript, WorkflowActionScript).
Manifest and Schema
Schema files are auto-created for CustomTool.
manifest.xml updated with SERVERSIDESCRIPTING feature for all server-side scripts.
Files are created in correct /Objects/ location.
<permkey> values validated against netsuite-sdf-roles-and-permissions (exact ID match required).
Best Practices Enforcement
Check for N+1 query patterns and recommend batch operations.
Recommend Suitelet-as-API pattern instead of RESTlet for user-facing AJAX.
Recommend postMessage for popup-to-parent communication.
Recommend module-level code for critical Client Script initialization.
Apply appropriate logging configuration (DEBUG for dev, omit for production).
Defensive Coding
Check field values before overwriting (don't assume you're the only script).
Use runtime.executionContext to control when script runs.
Verify records exist before loading (use a targeted search or try/catch when existence is uncertain).
Check for existing records before creating (idempotent operations).
Wrap non-critical afterSubmit operations in try/catch.
Use flag fields for script coordination when needed.
Validate required fields before processing.
Consider using script parameters to enable/disable features.
Governance Awareness
Know your script type's usage unit limit (1,000 for UE/Suitelet/Client, 10,000 for Scheduled).
Check getRemainingUsage() before loops with expensive operations.
Use search.lookupFields() (1 unit) instead of record.load() (5-10 units) for field reads.
Offload heavy processing to Scheduled/Map-Reduce scripts.
Use console.log() in Client Scripts (not log.* which is ignored on forms).
Handle search result limits (1,000 standard, 4,000 saved search).
Implement yielding for long-running Scheduled Scripts.
Common Pitfalls to Avoid
Script ID Length: Maximum 40 characters total (prefix + filename).
Filename Normalization: All filenames must be lowercase with underscores.
Hyphen Conversion: Replace all - with _ in filenames and script IDs.
Script ID Matching: Script ID must exactly match pattern: customscript_[filename] or custtoolset_[filename] (NO script type in ID).
Truncation Awareness: Long filenames will be truncated to fit 40-char limit.
Bracket Notation for Record Types: Custom records must be wrapped: [customrecord_name].
CRITICAL - Bracket Notation for File Paths: All <scriptfile> and <rpcschema> values MUST be wrapped in square brackets: [/SuiteScripts/filename.js] - deployment WILL FAIL without brackets.
Path Consistency: Always use forward slashes, always start with /SuiteScripts/.
CustomTool Special Case: No scriptdeployments, minimal elements only — name, scriptfile, rpcschema, exposetoaiconnector, permissions. Uses <toolset> root element and custtoolset_ prefix in NetSuite 2026.1+ (legacy: <tool> root and customtool_ prefix).
SERVERSIDESCRIPTING Manifest: Must update manifest.xml with SERVERSIDESCRIPTING feature for ALL server-side scripts EXCEPT CustomTool (BundleInstallationScript, MapReduceScript, MassUpdateScript, Portlet, Restlet, ScheduledScript, SDFInstallationScript, Suitelet, UserEventScript, WorkflowActionScript). CustomTool deploys and executes without this feature dependency.
Deployment Status Values:
NOTSCHEDULED: Required for MapReduceScript and ScheduledScript (queued execution scripts).
RELEASED: Use for all other script types with deployments.
Using wrong status will cause deployment to FAIL.
Prefix Usage: custtoolset_ for CustomTool (2026.1+; legacy customtool_), customscript_ for all other script types (no script type suffix).
Run As Role - Not Supported: <runasrole> is NOT valid for: ClientScript, Restlet, ScheduledScript, MapReduceScript, MassUpdateScript, WorkflowActionScript. Only Suitelet, UserEventScript, and Portlet support it (Portlet intentionally omits to run as current user).
All Roles Access - Not Supported: <allroles> is NOT valid for: ClientScript, Restlet, ScheduledScript, MapReduceScript, MassUpdateScript, WorkflowActionScript.
Title - Not Supported: <title> is NOT valid for: ClientScript, MassUpdateScript, WorkflowActionScript, UserEventScript.
XML Character Encoding: NEVER use HTML entities (<, >) for XML tag brackets - always use literal < and > characters.
Scripts Without Deployments: BundleInstallationScript, SDFInstallationScript, and CustomTool do NOT have scriptdeployments structure.
Entry Point Functions - Not Supported in XML:
<defaultfunction> is NOT valid for: MassUpdateScript, ScheduledScript, WorkflowActionScript (entry points are auto-detected).
BundleInstallationScript entry point XML elements (<beforeinstallfunction>, etc.) are NOT supported (auto-detected from JS).
SDFInstallationScript has NO XML entry point elements - uses run function defined in JS, triggered via deploy.xml.
Record Type Required: ClientScript, UserEventScript, MassUpdateScript, AND WorkflowActionScript all require <recordtype> in their deployment.
CustomRecordType - Invalid Fields: <allowinlineinsert> is NOT a valid field for customrecordtype XML - SDF validation will warn about this. Do NOT include it in custom record definitions.
CustomList - Abbreviation Requires MATRIXITEMS: The <abbreviation> field in customlist values requires the MATRIXITEMS feature. Avoid using abbreviation unless MATRIXITEMS is already enabled, or simply omit it.
CUSTOMRECORDS Manifest Requirement: When a project contains ANY customrecordtype, the manifest.xml MUST include <feature required="true">CUSTOMRECORDS</feature> in the dependencies section. Deployment will FAIL without this.
SPA Uses Different Prefix: SPA scripts use custspa_ prefix (8 chars), NOT customscript_. Maximum scriptid is 28 characters total.
SPA Object Structure: Both SpaServerScript and SpaClientScript are defined in a SINGLE <singlepageapp> object - do NOT create separate objects for each.
SPA Invalid Fields: <notifyadmins>, <notifyowner>, <notifyuser>, and <executeas> with value "CURRENT_ROLE" are NOT valid for singlepageapp - validation will fail.
SPA Entry Points: SpaServerScript uses initializeSpa(context), SpaClientScript uses run(context) - these are different from other script types.
N/log Module Required: If using log.debug(), log.error(), or log.audit(), you MUST import the N/log module in the define() statement. The log object is NOT available globally in SuiteScript 2.x - script will fail silently without this import.
clientScriptModulePath in SuiteApps: When using form.clientScriptModulePath in a SuiteApp, you MUST use the full File Cabinet path (for example, /SuiteApps/com.publisher.appid/scripts/my_cs.js), NOT a relative path like ./my_cs.js. Relative paths work in Account Customization projects but NOT in SuiteApps.