Every NetSuite developer has shipped a SuiteScript that worked on dev but failed silently in production because some custom field wasn't deployed. The script ran fine for a week, did nothing useful, and nobody noticed until the customer asked "are the invoices submitting?"

The fix is an init check. On first run, validate every custom field exists with the expected type, and verify any API credentials work. If anything's wrong, alert someone with an actionable message and refuse to process records. Once it passes, persist a flag so subsequent runs skip the check.

I run this in every production SuiteScript now. It catches deployment mistakes before they cost you customer trust.

The pattern

Two pieces: an inventory of what the script expects, and validators that prove the live environment matches. The inventory is a static list of custom fields per record type with their expected field types. The validators have two layers (field metadata checks and API credential checks) but the discipline is the same: read the live environment, compare to the inventory, return a structured list of mismatches.

Run both on first script execution. If anything fails, send a critical alert and exit before processing any records. If both pass, set a flag on the deployment so the check skips next time.

Listing expected fields

Define what the script expects up front, in code. This serves both as runtime validation and as documentation of what the script depends on.

expected-fields.js
const INVOICE_FIELD_CHECKS = Object.freeze([
  { id: 'custbody_external_submitted', expectedType: 'checkbox',           label: 'External Submitted' },
  { id: 'custbody_external_error_msg', expectedType: ['text', 'textarea'], label: 'External Error Message' },
  { id: 'custbody_external_reference', expectedType: ['text', 'textarea'], label: 'External Reference ID' },
]);

const ITEM_FIELD_CHECKS = Object.freeze([
  { id: 'custitem_product_category', expectedType: 'select', label: 'Product Category' },
]);

expectedType can be a string or an array, because NetSuite sometimes treats a short-form text field as "text" and a longer one as "textarea" depending on how it was configured. Allowing either reads as "we accept any text field for this purpose."

Validating against live metadata

NetSuite gives you a way to enumerate fields on a record type at runtime: create an in-memory record with isDynamic: true and call getFields(). The record is never saved. You're just asking NS what fields it would have.

validate-fields.js
const validateFields = (recordType, checks, recordLabel) => {
  const errors = [];
  try {
    const rec = record.create({ type: recordType, isDynamic: true });
    const fields = rec.getFields();

    for (const { id, expectedType, label } of checks) {
      if (!fields.includes(id)) {
        errors.push(recordLabel + ' field "' + label + '" (' + id + ') does not exist.');
        continue;
      }
      const field = rec.getField({ fieldId: id });
      const validTypes = Array.isArray(expectedType) ? expectedType : [expectedType];
      if (field && !validTypes.includes(field.type)) {
        errors.push(
          recordLabel + ' field "' + label + '" (' + id + ') ' +
          'expected type "' + validTypes.join(' or ') + '" ' +
          'but found "' + field.type + '".'
        );
      }
    }
  } catch (e) {
    errors.push('Failed to validate ' + recordLabel.toLowerCase() + ' fields: ' + e.message);
  }
  return errors;
};

This catches the four most common deployment mistakes:

  • Custom field wasn't created in the production account at all.
  • Field ID typo (script expects custbody_submitted, field is custbody_submited).
  • Field type mismatch (script expects a checkbox, someone deployed it as a select).
  • Field exists but isn't applied to this record type.

Each one returns a specific, human-readable error message, not just "validation failed."

Validating API credentials

If your script hits an external API, validate the credentials work with a minimal request. The cheapest thing that reveals an auth problem.

For most APIs, that's a GET on an "info" endpoint, or a POST with just the auth header and an empty body. You want a response that proves you're authenticated without doing any actual work.

try {
  const response = https.post({
    url: https.createSecureString({ input: BASE_URL + '{custsecret_site_id}' }),
    body: https.createSecureString({ input: 'token={custsecret_api_token}' }),
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  });
  if (response.code === 403) {
    errors.push('API auth failed (403). Confirm both secrets exist with the exact IDs the script expects.');
  }
} catch (e) {
  errors.push('API connection failed: ' + e.message);
}

If auth fails, include a link to the API Secrets settings page in the error message so the admin can click straight to where the fix lives. Actionable beats descriptive.

Persisting the pass flag

The init check is expensive and you don't need to run it every time. Once it passes, set a deployment-level parameter and read that parameter at the start of every run.

mark-init-passed.js
const markInitCheckPassed = () => {
  try {
    const scriptObj = runtime.getCurrentScript();

    search.create({
      type: 'scriptdeployment',
      filters: [
        ['script.scriptid', 'is', scriptObj.id],
        'AND',
        ['scriptid', 'is', scriptObj.deploymentId],
      ],
      columns: ['internalid'],
    }).run().each((result) => {
      record.submitFields({
        type: record.Type.SCRIPT_DEPLOYMENT,
        id: result.id,
        values: { custscript_init_check_passed: true },
      });
      return false; // one result is enough
    });
  } catch (e) {
    log.error({ title: 'markInitCheckPassed', details: 'Deployment update failed: ' + e.message });
  }
};

The custscript_init_check_passed parameter starts as false. At the top of execute, read it. If false, run the init check. If it passes, mark it true and continue. If it fails, alert and exit.

The result: every fresh deployment runs the check once, on its first execution. Every subsequent run skips it (cheap), and any deployment migration to a new environment automatically re-validates because the new deployment's flag starts false.

When to invalidate

The flag should reset when:

  • The script is migrated to a new account (new deployment, new flag, automatic re-validation).
  • The custom fields the script depends on are changed (manually unset the flag).
  • The API credentials are rotated (you might want a separate auth-only re-check on every Nth run for that case).

For most cases, the natural "new deployment, fresh flag" path handles things. Manual invalidation is rare enough that it's fine to require admin action.

The alert is the contract

When the init check fails, the alert is the whole product. The admin gets an email that says, in plain English, exactly what's missing. The email has a link to the relevant settings page. They fix the issue. The next run passes. The script starts working. Nobody had to read SuiteScript logs.

Write the alert as if the recipient has never seen the script before. Include the script name, the deployment ID, the exact field IDs that failed, and a link to fix each one. Make it easy.

The init check is one of those patterns that costs you almost nothing on the first deploy and saves you a customer-trust incident later. I haven't shipped a production SuiteScript without one in years.

Related