Writing / Netsuite

Reusable SuiteQL Query Template

A practical SuiteQL starting template for named columns, bind parameters, optional filters, deterministic ordering and N/query execution.

Most SuiteQL queries I write need the same foundations: explicit columns, documented inputs, bind parameters, predictable aliases and a deterministic order.

Starting from a consistent template makes the important business logic easier to see and reduces mistakes in the parameter array.

The template

import query = require('N/query');

interface QueryInput {
  subsidiaryId: number;
  startDate: string;
  includeInactive?: boolean;
}

function getRows(input: QueryInput) {
  const conditions = [
    'record.subsidiary = ?',
    "record.trandate >= TO_DATE(?, 'YYYY-MM-DD')",
  ];

  const params = [
    input.subsidiaryId,
    input.startDate,
  ];

  if (!input.includeInactive) {
    conditions.push("record.isinactive = 'F'");
  }

  const suiteql = `
    SELECT
      record.id AS id,
      record.name AS name,
      record.lastmodifieddate AS last_modified_date
    FROM
      customrecord_example record
    WHERE
      ${conditions.join('\n      AND ')}
    ORDER BY
      record.lastmodifieddate,
      record.id
  `;

  return query.runSuiteQL({
    query: suiteql,
    params,
    customScriptId: 'custscript_example_query',
  }).asMappedResults();
}

Replace the illustrative record and field IDs with values confirmed in your account’s Records Catalog.

Document inputs beside the builder

Keep the parameter contract close to the query:

/**
 * @param subsidiaryId NetSuite internal ID.
 * @param startDate Inclusive date in YYYY-MM-DD format.
 * @param includeInactive When true, do not add the inactive filter.
 */

This makes the order and expected type of every ? placeholder visible before the SQL is edited.

Add conditions and parameters together

The most common parameter bug is changing the SQL without changing the parameter array in the same order.

Keep both mutations adjacent:

if (input.departmentId != null) {
  conditions.push('record.department = ?');
  params.push(input.departmentId);
}

Do not interpolate a value into the query string:

// Avoid this.
conditions.push(`record.department = ${input.departmentId}`);

Bind parameters preserve types, avoid quoting mistakes and separate the query structure from its values.

Handle optional inputs in JavaScript

It is possible to express an optional filter in SQL by binding the same value twice:

AND (? IS NULL OR record.department = ?)

But the parameter must then appear twice:

params.push(departmentId, departmentId);

For a growing query, conditionally adding the entire predicate is usually clearer and produces fewer placeholder-order mistakes.

See Build Dynamic SuiteQL WHERE Clauses Without Breaking Bind Parameters for a reusable builder pattern.

Use explicit columns

Prefer:

SELECT
    record.id,
    record.name,
    record.lastmodifieddate

over:

SELECT *

Explicit columns make result mappings stable and avoid retrieving large or calculated fields that the script never uses. Oracle includes this in its SuiteQL performance guidance.

Give mapped results predictable aliases

asMappedResults() uses column aliases as object keys:

SELECT
    record.id AS id,
    record.lastmodifieddate AS last_modified_date

The returned row is straightforward to consume:

for (const row of rows) {
  log.debug({
    title: row.id,
    details: row.last_modified_date,
  });
}

Always make paging order deterministic

If this template is switched to runSuiteQLPaged(), its order must be unique and unambiguous.

This may contain ties:

ORDER BY record.lastmodifieddate

Use a unique final key:

ORDER BY
    record.lastmodifieddate,
    record.id

Read SuiteQL Result Limits and Pagination Explained before using the query for a large result set.

Be cautious with WITH clauses

An earlier version of this template placed all inputs in a common table expression. That can be pleasant to read, and some query environments may accept it, but Oracle’s current SuiteQL performance documentation lists WITH clauses as unsupported.

For a reusable public template, keeping parameters in JavaScript and using ordinary bind placeholders is the more portable choice across SuiteQL execution channels.

Debug the completed query safely

Log the query structure and useful parameter metadata, but avoid logging sensitive parameter values:

log.debug({
  title: 'SuiteQL execution',
  details: {
    customScriptId: 'custscript_example_query',
    parameterCount: params.length,
    conditionCount: conditions.length,
  },
});

The template is intentionally boring. That is its value: query-specific complexity stays in the selected fields, joins and conditions instead of being repeated in execution boilerplate.