Writing / Netsuite

Export NetSuite Custom Field Data Dictionary

Build a SuiteScript export of custom-field definitions with SuiteQL pagination, CSV escaping and columns adapted to your Records Catalog.

A custom-field data dictionary is a spreadsheet of definitions: script IDs, labels, types, owning records and list targets. SuiteScript can build that file from SuiteQL—provided every table and column ID comes from your account’s Records Catalog.

This guide covers export mechanics: paging, CSV safety and adaptation. It does not claim a universal metadata schema. Discovery remains How to Query NetSuite Custom Record Fields With SuiteQL.

Adapt the query to the catalog first

Before writing the script:

  1. Open Setup → Records Catalog as the execution role.
  2. Select the SuiteScript and REST Query API channel.
  3. Confirm the metadata record for the field family you need (for custom-record fields, search CustomRecordCustomField—it is not universally available).
  4. Copy the exact field IDs for every column you will export.
  5. Confirm any join to type metadata in the catalog.

The SQL strings in this article are illustrative. Replace record and column names with catalog IDs. If the record is missing, stop and follow Why CustomRecordCustomField Is Missing From SuiteQL.

Separate families and definition versus data

Export one custom-field family per query (custom-record fields, entity fields, transaction body fields, and so on). Do not mix definition rows with instance values from customrecord_... tables.

Background map: NetSuite Custom Field Metadata Tables Explained.

Illustrative SuiteQL

Use only columns your catalog lists. Names below show intent, not a guaranteed schema:

SELECT
    field_definition.id,
    field_definition.scriptid,
    field_definition.label,
    field_definition.fieldtype,
    field_definition.recordtype,
    field_definition.selectrecordtype
FROM
    CustomRecordCustomField field_definition
ORDER BY
    field_definition.scriptid,
    field_definition.id

A deterministic ORDER BY that includes a unique key is required for safe paging. Details: SuiteQL Result Limits and Pagination Explained.

Optional type labels belong in a catalog-confirmed join—see How to Join CustomRecordCustomField to CustomRecordType—or a second lookup after export.

Page with runSuiteQLPaged

Definition inventories can exceed the 5,000-row runSuiteQL limit. Prefer query.runSuiteQLPaged(), which also requires a unique and unambiguous sorting order:

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/query', 'N/file', 'N/log', 'N/runtime'], (query, file, log, runtime) => {
  // Replace this SQL with IDs from your Records Catalog.
  const METADATA_SQL = `
    SELECT
      field_definition.id,
      field_definition.scriptid,
      field_definition.label,
      field_definition.fieldtype,
      field_definition.recordtype,
      field_definition.selectrecordtype
    FROM
      CustomRecordCustomField field_definition
    ORDER BY
      field_definition.scriptid,
      field_definition.id
  `;

  const HEADERS = [
    'id',
    'scriptid',
    'label',
    'fieldtype',
    'recordtype',
    'selectrecordtype',
  ];

  function escapeCsv(value) {
    if (value === null || value === undefined) {
      return '';
    }
    const text = String(value);
    if (/[",\r\n]/.test(text)) {
      return `"${text.replace(/"/g, '""')}"`;
    }
    return text;
  }

  function rowToCsv(values) {
    return values.map(escapeCsv).join(',');
  }

  function createCsv() {
    const outputFolderId = Number(
      runtime.getCurrentScript().getParameter({
        name: 'custscript_field_dictionary_folder',
      }),
    );

    if (!Number.isInteger(outputFolderId) || outputFolderId <= 0) {
      throw new Error('Set a writable File Cabinet folder in the script parameter');
    }

    const csvFile = file.create({
      name: 'custom_field_data_dictionary.csv',
      fileType: file.Type.CSV,
      contents: '',
      encoding: file.Encoding.UTF8,
    });

    csvFile.folder = outputFolderId;
    csvFile.appendLine({ value: rowToCsv(HEADERS) });

    return csvFile;
  }

  function writePagedRows(csvFile) {
    const paged = query.runSuiteQLPaged({
      query: METADATA_SQL,
      pageSize: 1000,
      customScriptId: 'custscript_field_data_dictionary',
    });

    let rowCount = 0;

    for (const pageRange of paged.pageRanges) {
      const page = paged.fetch({ index: pageRange.index });
      for (const result of page.data.results) {
        const map = result.asMap();
        csvFile.appendLine({
          value: rowToCsv(HEADERS.map((key) => map[key])),
        });
        rowCount += 1;
      }
    }

    return rowCount;
  }

  function execute() {
    const csvFile = createCsv();
    const rowCount = writePagedRows(csvFile);
    const fileId = csvFile.save();

    log.audit({
      title: 'Custom field dictionary exported',
      details: { fileId, rowCount },
    });
  }

  return { execute };
});

Notes:

  • Create a numeric script parameter named custscript_field_dictionary_folder and select a File Cabinet folder the deployment role may write to.
  • customScriptId helps NetSuite identify the query in performance tools.
  • Oracle requires a unique, unambiguous sort order for paged SuiteQL. Include a unique key after the human-readable sort columns.
  • SuiteQL is limited to 100,000 results without SuiteAnalytics Connect. For larger inventories, filter by family, type or script ID ranges and run multiple exports.
  • Appending each CSV row avoids building the complete export string and row array in memory. NetSuite documents a 10 MB limit when creating a new file object; File.appendLine() is the safer pattern for a growing text or CSV file, provided each individual line remains below the per-line limit.

CSV escaping rules used above

Value Output behaviour
null / undefined Empty cell
Contains ,, " or newlines Wrap in double quotes; escape " as ""
Ordinary text Written as-is

Labels and help text often contain commas. Skipping escaping produces a broken spreadsheet.

Bind parameters for filtered dictionaries

When exporting one custom-record type, filter with bind parameters—never string-concatenate IDs into SQL:

const sql = `
  SELECT
    field_definition.id,
    field_definition.scriptid,
    field_definition.label
  FROM
    CustomRecordCustomField field_definition
  WHERE
    field_definition.recordtype = ?
  ORDER BY
    field_definition.scriptid,
    field_definition.id
`;

const paged = query.runSuiteQLPaged({
  query: sql,
  params: [customRecordTypeInternalId],
  pageSize: 1000,
  customScriptId: 'custscript_field_dict_one_type',
});

Dynamic optional filters: Build Dynamic SuiteQL WHERE Clauses Without Breaking Bind Parameters.

Multi-family exports

For a wider dictionary:

  1. Maintain one verified SQL string per field family.
  2. Run each through the same paging and CSV helpers.
  3. Add a constant family column in SuiteScript (customrecord, entity, transactionbody, …) so the spreadsheet stays readable.
  4. Resolve select-target IDs in a second pass if needed: How to Find a Custom Field’s List or Record Type With SuiteQL.

Governance and operations

  • Prefer a scheduled or map/reduce script for large accounts.
  • Log row counts and file IDs, not full field dumps containing sensitive help text if policy forbids it.
  • Re-run catalog verification after feature or permission changes.
  • Treat SDF XML as a complementary source for project-controlled objects, not as a drop-in replacement for the live analytics export: SuiteQL Metadata vs SuiteCloud XML for Custom Records.

What not to ship unchanged

  • Column lists copied from this article without catalog confirmation
  • Joins that are not documented in your Records Catalog
  • Unescaped CSV concatenation
  • runSuiteQL alone on an unbounded definition set
  • Building the entire CSV as one in-memory string
  • Credentials, production folder structures or client-specific script IDs in public repositories

The durable pattern is thin: verified SuiteQL, deterministic order, paged fetch, strict CSV escaping, and headers that match the analytical field IDs your account actually exposes.