The header location SuiteQL says is empty
The sales order shows a location on its header. The script reads it fine. SuiteQL says it’s null. All three are telling the truth about where they look.
The confusing part
A user event on sales orders was making decisions by header location. While checking its work, we queried the order directly:
SELECT t.id, t.tranid, t.location, BUILTIN.DF(t.location) AS loc
FROM transaction t
WHERE t.id = ?
Null. But the order plainly had a location in the UI, and the script logged reading it correctly with record.getValue('location'). For a few minutes it looked as though the script was reading something that didn’t exist.
Where it actually lives
In SuiteQL, header-level values that NetSuite stores per line — location is the common one — live on the transaction’s main line, the line row flagged as representing the header:
SELECT tl.transaction, tl.location, BUILTIN.DF(tl.location) AS loc
FROM transactionline tl
WHERE tl.transaction = ?
AND tl.mainline = 'T'
That returns the header location the UI shows. The record API reads it from the record, so scripts see it either way. SuiteQL only sees it where you ask.
Filtering transactions by header location
When you need header-level transaction fields filtered by header location, filter through the main line:
SELECT t.id, t.tranid, t.trandate
FROM transaction t
WHERE t.type = 'SalesOrd'
AND t.id IN (SELECT tl.transaction
FROM transactionline tl
WHERE tl.mainline = 'T' AND tl.location = ?)
transactionline.location without mainline = 'T' matches on any line, and returns orders whose header is somewhere else entirely.
The habit to keep
When SuiteQL and the record API disagree, it’s almost never that one of them is wrong. They’re looking in different places. Before concluding a script is misreading data, query the main line and, if you need to be sure, look at the record’s XML view, which shows exactly what is stored.
Related: SuiteQL doesn’t accept INTERVAL ’30’ MINUTE, and why that column prints blank on your PDF.