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:
- Open Setup → Records Catalog as the execution role.
- Select the SuiteScript and REST Query API channel.
- Confirm the metadata record for the field family you need (for custom-record fields, search
CustomRecordCustomField—it is not universally available). - Copy the exact field IDs for every column you will export.
- 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_folderand select a File Cabinet folder the deployment role may write to. customScriptIdhelps 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:
- Maintain one verified SQL string per field family.
- Run each through the same paging and CSV helpers.
- Add a constant
familycolumn in SuiteScript (customrecord,entity,transactionbody, …) so the spreadsheet stays readable. - 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
runSuiteQLalone 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.