The quick answer
A client handler cannot call server-only N/file, N/encode or a Suitelet response. A browser-capable workbook library can generate XLSX bytes. In this exercise, the local workbench wraps them in a Blob and exposes a download link. Verify the downloaded file’s workbook parts and cell types; a filename or download-request message is insufficient.
This walkthrough runs supplied JavaScript in a local browser workbench, using a pinned SheetJS 0.20.3 build and strict AMD/currentRecord stand-ins. The stand-ins read a known Transfer Order snapshot. They do not connect to a NetSuite account. The original forum thread has no confirmed working resolution; this exercise proves a bounded browser file-generation path.
Before you start
The fictional TO7312 contains two existing item rows: code 00123, quantity 4; code 00456, quantity 2. The fixed browser fixture contains the same rows. The exercise exports the displayed item code as text and quantity as a number. It does not export unsaved edits or select arbitrary account records.
The workbench invokes the exported onButtonClick directly. Native custom-button attachment, client-module loading, AMD alias resolution, View-mode currentRecord access, permissions and download policies remain unverified. Test those in the intended account and role before deployment; such account testing is outside this guide’s evidence. A server-prepared, authorized snapshot is another production design to assess separately.
SheetJS’s AMD instructions describe a NetSuite module alias and report an Oracle evaluation issue. The browser build documentation recommends vendoring the library. A local harness pass does not resolve an account-specific loading problem.
01. Identify the rows
In the illustration, inspect Items on fictional TO7312. No NetSuite account record is required for this local exercise. Confirm codes 00123 and 00456, with quantities 4 and 2. The local workbench reads a separate snapshot of those inputs. The record page provides context; its native Export button is not being deployed or tested here.
02. Expose the browser error
Open export_items.js in the Client Export Workbench. The module exports pageInit and onButtonClick. Its reduced handler calls file.create with an intentionally undeclared file binding. Click Run browser handler and observe the actual ReferenceError: file is not defined.
N/file is server-only. Adding its import does not make it a client API. The original source also mixed server-only encoding and a response object; this reduced exercise stops at the first file call.
03. Generate workbook bytes
Replace the failing handler body with the workbook implementation. Keep pageInit and onButtonClick exported. The supplied currentRecord stand-in supports only the shown sublist reads and throws for unsupported requests.
The loop reads each known item’s text and numeric quantity. It places String(code) and Number(qty) into the worksheet rows. This fixture already has valid numeric inputs. Production code needs explicit validation: converting an empty value to Number would silently produce zero.
The final calls create a workbook, add an Items sheet and return binary XLSX data to the local workbench:
const book = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(book, XLSX.utils.aoa_to_sheet(rows), 'Items');
return XLSX.write(book, {bookType: 'xlsx', type: 'array'});
Here XLSX is the pinned browser library supplied by the local AMD stand-in, and rows is the header plus the two snapshot rows. The returned value belongs to the local adapter interface. A native custom button does not automatically turn returned bytes into a download. This does not rename XML or use File Cabinet APIs.
04. Download the workbook
Click Save, then Run browser handler. The editor retains the source; the handler executes the visible code with the known snapshot and returns workbook bytes. The workbench normalizes ArrayBuffer or typed-array output and creates a Blob with the XLSX MIME type. It exposes a regular Download workbook link backed by those exact bytes. Click that visible link to download transfer-order.xlsx. The adapter does not synthesize an anchor click. Native file delivery needs a separately tested integration.
The browser file documentation describes browser download behavior. In this exercise, the user activates the adapter’s visible anchor. Browser settings and policy determine whether a prompt appears and where the file is saved. The output message alone does not validate the file.
05. Verify the actual file
Click Inspect downloaded workbook and choose the file just downloaded. The local inspector opens an unchanged retained copy named captured-transfer-order.xlsx. Workbook Inspector reads that file’s bytes with separate Python zipfile archive checks and openpyxl workbook readback, independent of SheetJS. It checks archive integrity and required workbook, worksheet and relationship members before reading the cells.
Confirm ZIP integrity: PASS, Items, and exactly 2 item rows. A2 is text 00123 and B2 is numeric 4. A3 is text 00456 and B3 is numeric 2. The leading zeros survive because the item-code cells are strings.
This is a local file inspector, not Microsoft Excel. These checks prove the expected workbook structure and typed data for this captured fixture; they do not prove identical rendering in every spreadsheet application or native NetSuite deployment behavior.
Common questions
Can I use N/file or N/encode in a client script?
N/file and N/encode are server-only. Generate the file with an appropriate browser library, or put server operations in a separately designed, authorized server endpoint.
Can a client handler use context.response.writeFile?
A client script does not own a Suitelet ServerResponse and cannot call context.response.writeFile. ServerResponse.writeFile belongs to server execution. A Suitelet download is a separate architecture requiring authorization, data retrieval and response handling.
Is renaming XML or CSV to .xlsx enough?
No. A genuine XLSX contains an OOXML package with workbook and worksheet parts and relationships. The file’s extension does not generate those bytes. This exercise creates a new workbook from typed rows; it does not convert a legacy export.
How are leading zeros preserved?
Use string values for item-code cells. The exercise writes 00123 and 00456 as strings and verifies their readback types. Quantities are numbers. Production code should validate each input type rather than infer it from appearance.
Does this prove currentRecord works on a viewed Transfer Order?
No. A strict snapshot-backed stand-in provides only the methods used here. Test actual View-mode reads in the target account and role, or assess an authorized server-prepared snapshot. The local adapter does not establish that integration.
Can I load the library from a CDN?
This exercise uses a locally supplied, pinned library. It does not verify CDN loading, account CSP, AMD configuration or File Cabinet delivery. Review and pin the library, then test its intended loading design in the target account.
Does this include unsaved item edits?
No. The fixture contains two existing static rows. Actual form-state reads, read-only mode and sublist text require separate account testing. The local adapter does not establish those behaviors.
Is this a tested design for large exports?
Only this two-row fixture was tested. Browser memory, generation time and data exposure need separate assessment for a larger export. A server or asynchronous design may be appropriate; neither is implemented here.
Does the custom Export button now work in NetSuite?
Native attachment and deployment remain unverified. The local workbench directly invokes the exported handler and consumes its returned bytes. A native button does not automatically consume that return value. A deployed client function needs its own tested browser download integration or a separately authorized server endpoint. The visible download anchor and file readback prove the local adapter path, while native loading, download integration, permissions and data access remain separate checks.