Export a NetSuite Custom-Field Data Dictionary With SuiteScript
Build a SuiteScript export of custom-field definition metadata with SuiteQL: catalog-confirmed columns, pagination, CSV escaping and per-account query adaptation.
A custom-field data dictionary is a snapshot of definitions: script IDs, labels, types, owning records—not the values stored on transactions or custom-record rows. SuiteScript plus SuiteQL can produce a CSV (or file cabinet object) once the Records Catalog confirms which analytical records and columns exist in your account.
This guide supports How to Query NetSuite Custom Record Fields With SuiteQL. It is a pattern for export, not a universal schema dump.
The exact FROM record and selected columns must be adapted to the account’s Records Catalog under the SuiteScript and REST Query API channel for the role that runs the script. Do not treat CustomRecordCustomField or any column list below as guaranteed.
Scope the export
Decide explicitly:
| Include | Exclude (unless you add separate jobs) |
|---|---|
| Field-definition metadata for chosen families | Instance row values |
| Catalog-confirmed columns only | Guessed joins from UI labels |
| One family per query (recommended) | A single invented mega-UNION |
Custom-record fields, transaction body fields, entity fields and other families may require separate queries. See NetSuite Custom Field Metadata Tables Explained.
Confirm the query offline first
- Open Setup → Records Catalog as the script execution role.
- Select the SuiteScript and REST Query API channel.
- Locate each metadata record you will export.
- Copy analytical field IDs for at least: internal id, script id, label, field type (if present), owning type/parent (if present), inactive-style flag (if present).
- Run a three-column probe in a query tool before coding the export (column reference).
If the definition record is missing, fix visibility first (missing CustomRecordCustomField).
Illustrative SuiteQL (adapt every identifier)
SELECT
field_definition.id,
field_definition.scriptid,
field_definition.label,
field_definition.fieldtype,
field_definition.recordtype
FROM
CustomRecordCustomField field_definition
ORDER BY
field_definition.scriptid,
field_definition.id
Notes:
fieldtypeandrecordtypeare illustrative. Drop or replace them if the catalog does not expose them.- The
ORDER BYmust be unique enough for paging—includeidas a final tie-breaker. - Filter inactive or single-type subsets only with confirmed predicates and bind parameters (dynamic WHERE and binds).
Paginate: do not rely on runSuiteQL alone
Large accounts exceed the non-paged SuiteQL row cap. Use query.runSuiteQLPaged() with a deterministic order. Details and limits: SuiteQL Result Limits and Pagination.
Illustrative SuiteScript pattern (Scheduled or Map/Reduce friendly):
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/query', 'N/file', 'N/log'], (query, file, log) => {
// Replace this SQL with catalog-confirmed IDs for the execution account/role.
const SUITEQL = `
SELECT
field_definition.id,
field_definition.scriptid,
field_definition.label,
field_definition.fieldtype,
field_definition.recordtype
FROM
CustomRecordCustomField field_definition
ORDER BY
field_definition.scriptid,
field_definition.id
`;
const HEADERS = ['id', 'scriptid', 'label', 'fieldtype', 'recordtype'];
function escapeCsvCell(value) {
if (value === null || value === undefined) {
return '';
}
const text = String(value);
if (/[",\r\n]/.test(text)) {
return `"${text.replace(/"/g, '""')}"`;
}
return text;
}
function rowToCsv(map) {
return HEADERS.map((key) => escapeCsvCell(map[key])).join(',');
}
function execute(context) {
const lines = [HEADERS.join(',')];
const paged = query.runSuiteQLPaged({
query: SUITEQL,
pageSize: 1000,
});
for (const pageRange of paged.pageRanges) {
const page = paged.fetch({ index: pageRange.index });
for (const result of page.data.results) {
lines.push(rowToCsv(result.asMap()));
}
}
const csvFile = file.create({
name: 'custom_field_data_dictionary.csv',
fileType: file.Type.CSV,
contents: lines.join('\n'),
// Set folder to a File Cabinet folder internal ID available to the role.
folder: -15,
});
const fileId = csvFile.save();
log.audit({
title: 'Custom field dictionary exported',
details: `fileId=${fileId}, rows=${lines.length - 1}`,
});
}
return { execute };
});
What you must change before production
- SQL record and columns — catalog IDs for the account and role.
- HEADERS — must match the selected column aliases / field IDs returned in
asMap(). folder— a real File Cabinet folder the role can write to (-15is only a placeholder pattern; use your folder ID).- Script deployment — role, schedule, and governance-friendly volume.
- Families — add separate queries or script parameters for other custom-field families.
CSV escaping rules used above
- Null/undefined → empty cell.
- Values containing comma, quote or newline → wrap in double quotes.
- Embedded quotes → doubled (
"→"").
That is enough for a practical dictionary file. If labels can contain leading zeros or spreadsheet-sensitive text, keep the file as pure CSV and open it with explicit text import options in spreadsheet tools.
Governance and volume
- Prefer one scheduled run over interactive Suitelets for full-account extracts.
- Stay within paged-result ceilings; if you need more than SuiteQL paging allows, filter by family, owning type or script-ID prefix and run multiple exports.
- Log counts and file IDs, not full field dumps that may include sensitive labels in shared logs.
What this export is not
- Not a substitute for SDF object XML in source control.
- Not proof of instance-level data access for each field.
- Not a cross-account schema contract—re-verify columns after major customisation or role changes.
- Not automatically complete for every custom-field family.
For deployment definitions, use SuiteCloud import (import custom record objects) and compare purposes in SuiteQL Metadata vs SuiteCloud XML.