What comes back
Why does SuiteQL return CustInvc instead of Invoice for transaction type?
Short answer
The type column holds internal codes: CustInvc, VendBill, ExpRept, CuTrSale, RevArrng and so on. A filter written against the display name Invoice matches no row. Use the code, or filter on BUILTIN.DF(type), which selected the same rows.
Because type stores the internal record type code, not the display name. CustInvc is an invoice, VendBill a bill, ExpRept an expense report. A filter naming 'Invoice' matches nothing. Use the code, or filter on BUILTIN.DF(type).
Minimal reproduction
SELECT UPPER(type) AS s, LOWER(type) AS t, LENGTH(type) AS l FROM transaction FETCH FIRST 3 ROWS ONLY
Output:
{ s: "EXPREPT", t: "exprept", l: 7 }
{ s: "CUSTINVC", t: "custinvc", l: 8 }
{ s: "CUSTINVC", t: "custinvc", l: 8 }
Seven and eight characters. Expense Report, one of the display names the same column resolves to through BUILTIN.DF, is fourteen. BUILTIN_RESULT.TYPE_STRING(type) does not translate either: it returns the same raw codes.
The codes
Grouping by type across the whole table returned these 25 codes:
CardChrg CardRfnd CashRfnd CashSale Check
Commissn CustCred CustDep CustInvc CustPymt
CustRfnd CuTrSale DepAppl Estimate ExpRept
Journal Opprtnty PurchOrd RevArrng SalesOrd
Transfer VendBill VendCred VendPymt Deposit
The per-group counts in that probe add up to the table's own COUNT(*), so the list accounts for every row in the account tested; narrower probes on the same column returned strict subsets of it. This is what one account happened to contain, so treat it as a starting list rather than the complete set of codes NetSuite defines. Note that no code in it contains a space.
The fix
Filter on the code:
SELECT COUNT(*) AS n FROM transaction WHERE type = 'VendBill'
Or filter on the display name through BUILTIN.DF:
SELECT COUNT(*) AS n FROM transaction WHERE BUILTIN.DF(type) = 'Bill'
Both returned the same count, selecting the identical row set. That equivalence was verified for one pair, VendBill and Bill, not for every type.
A simple CASE on the raw column matches those literals correctly. Run across the whole table with a GROUP BY, every VendBill row and every Journal row landed in its own bucket, with no leakage into the ELSE branch, and SUM(CASE WHEN type = 'VendBill' THEN 1 ELSE 0 END) agreed with the grouped count exactly. Two codes, not all 25, but hard-coding a code in a CASE is sound.
To see display names instead:
SELECT BUILTIN.DF(type) AS t, COUNT(*) AS n FROM transaction GROUP BY BUILTIN.DF(type) FETCH FIRST 20 ROWS ONLY
As run, that query hit its own 20-row cap, so what came back is a slice and not the full set of display names. It specified no order; alphabetised, the 20 were Bill, Bill Credit, Bill Payment, Cash Refund, Cash Sale, Check, Client Credit, Commission, Credit Card Charge, Credit Card Refund, Credit Memo, Customer Deposit, Customer Refund, Estimate, Expense Report, Invoice, Opportunity, Payment, Revenue Arrangement, Sales Order.
The two lists do not line up one to one by inspection, and the run did not establish the full mapping, so derive the pairing in your own account.
Why this happens
type is stored as the internal identifier. BUILTIN.DF resolves that identifier to its display value at query time, which is why the same predicate expressed either way selected the same rows. Nothing in the engine rewrites a display-name literal into a code for you, so a filter naming 'Invoice' compares a code column against a string that is not in the column.
Where the evidence stops: no probe in this run executed WHERE type = 'Invoice'. That it matches no row is derived from the enumerated distinct values of type, which account for every row in the table and contain no display names at all. The derivation is solid, but it is a derivation, and the run did not observe what such a filter actually returns.
How to tell if you are affected
SELECT type, COUNT(*) AS n FROM transaction GROUP BY type
Look at the first column. If the values read CustInvc and ExpRept, the column holds codes, and any filter or CASE in your codebase that compares type against Invoice or Expense Report is selecting nothing from it. If instead that column comes back full of display names, you are not affected and your literals are fine. The fast audit is to grep your queries for type = followed by a string containing a space, which no code in the list above has.
Scope
One production account on NetSuite 2026.1, transaction table. Every probe behind this page ran through N/query.runSuiteQL; not one was run against the REST endpoint, so the finding is untested there.
How this was established
8 probe runs against one live NetSuite 2026.1 account on the Administrator role, through N/query.runSuiteQL. Where this page draws a boundary around a finding, that boundary is the edge of what was actually run.
Related
- Does BUILTIN.DF silently drop rows in SuiteQL?
No. Wrapping a column in BUILTIN.DF returned the same row count as the bare table in every form tested. The gap people see is COUNT(BUILTIN.DF(col)) skipping nulls, which is ordinary COUNT(expr) behaviour, not a row drop.
- Why is my quoted column alias lowercased in SuiteQL results?
Returned JSON keys come back lowercased on both engines even when the alias is double-quoted. SELECT id AS "Transaction ID" returns the key "transaction id": the space survives, the capitals do not, so row["Transaction ID"] is undefined.
- What does SuiteQL mean by Unknown identifier 'x'. Available identifiers are: {transaction=transaction}?
The column does not exist on that table. Despite the wording, "Available identifiers" lists the table aliases in scope, not the columns you could have used. Adding a FETCH FIRST clause can hide this error behind a syntax error.
From the blog
- Why Your NetSuite AP Aging Never Ties to the General Ledger
The AP Aging report and the accounts payable balance on the balance sheet disagree, and the difference is not a missing bill. Three specific traps in how NetSuite stores payables, and the SuiteQL that finds each one.
- NetSuite Saved Search Formulas: What Breaks and Why
Field IDs, NULL propagation, main line, and the difference between Criteria, Results and Summary. The parts of NetSuite saved search formulas that fail quietly, and how to read the failure.