Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

The NetSuite Pro

The NetSuite Pro Logo The NetSuite Pro Logo

The NetSuite Pro Navigation

  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • Advanced PDF Templates in NetSuite
  • Blog
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask A Question
  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • Advanced PDF Templates in NetSuite
  • Blog
  • Contact Us
Home/ NetSuite Scripting/Advanced Suitelets (NetSuite)

Advanced Suitelets (NetSuite)

🔹 What is an “Advanced” Suitelet?

An Advanced Suitelet goes beyond a simple form. It can:

  • Act as a multi-step wizard (with Next/Back).
  • Render dynamic sublists with server-side filtering and client-side helpers.
  • Upload/parse files (CSV/JSON) and show results.
  • Communicate with Client Scripts for richer UX.
  • Use caching, saved searches, SuiteQL, and governance-safe patterns.

🔹 Key Concepts & Building Blocks

  • Server UI: N/ui/serverWidget to build forms, fields, sublists, buttons.
  • State: Pass page state via request parameters or hidden fields.
  • Data: Pull from N/search / SuiteQL; paginate for large results.
  • Client Interop: Attach a Client Script for validation, field changes, and AJAX (via Suitelet endpoints).
  • Files: Handle uploads with N/file.
  • Performance: Use N/cache and partial rendering.

✅ Example 1 — Multi-Step Wizard (3 Steps + Pagination)

Use this pattern for guided data entry (e.g., mass updates, bulk email, config setup).

/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 * Advanced Suitelet: Multi-Step Wizard with Pagination
 */
define(['N/ui/serverWidget','N/search','N/runtime','N/redirect','N/log'],
(form, search, runtime, redirect, log) => {

  const PARAM_STEP = 'step';
  const STEP = Object.freeze({ ONE: '1', TWO: '2', THREE: '3' });

  const onRequest = (ctx) => {
    try {
      const req = ctx.request, res = ctx.response;
      const step = (req.parameters[PARAM_STEP] || STEP.ONE);

      if (req.method === 'GET') {
        if (step === STEP.ONE) return renderStep1(ctx);
        if (step === STEP.TWO) return renderStep2(ctx);
        return renderStep3(ctx);
      }

      // POST handlers
      if (step === STEP.ONE) return handleStep1Post(ctx);
      if (step === STEP.TWO) return handleStep2Post(ctx);
      return handleStep3Post(ctx);

    } catch (e) {
      log.error('Wizard Error', e);
      const f = form.create({ title: 'Error' });
      f.addField({ id:'custpage_err', type: 'inlinehtml', label:' '})
       .defaultValue = `<div style="color:#b00;">${escapeHtml(e.message || e.toString())}</div>`;
      ctx.response.writePage(f);
    }
  };

  // ---- Step 1: Filters ----
  function renderStep1(ctx) {
    const f = form.create({ title: 'Wizard: Step 1 – Choose Filters' });
    f.addField({ id:'custpage_category', type:'select', label:'Category', source:'classification' });
    f.addField({ id:'custpage_pagesize', type:'integer', label:'Page Size' }).defaultValue = '50';
    f.addSubmitButton({ label:'Next ▶' });
    ctx.response.writePage(f);
  }
  function handleStep1Post(ctx) {
    const p = ctx.request.parameters;
    redirect.toSuitelet({
      scriptId: runtime.getCurrentScript().id,
      deploymentId: runtime.getCurrentScript().deploymentId,
      parameters: { step: STEP.TWO, category: p.custpage_category || '', pagesize: p.custpage_pagesize || '50', page: '1' }
    });
  }

  // ---- Step 2: Results + Pagination ----
  function renderStep2(ctx) {
    const p = ctx.request.parameters;
    const page = parseInt(p.page || '1', 10);
    const pageSize = Math.max(1, Math.min(1000, parseInt(p.pagesize || '50', 10)));
    const category = p.category || '';

    const f = form.create({ title: 'Wizard: Step 2 – Review & Select' });
    // carry state
    f.addField({ id:'custpage_category', type:'text', label:'category', displayType:'hidden' }).defaultValue = category;
    f.addField({ id:'custpage_pagesize', type:'text', label:'pagesize', displayType:'hidden' }).defaultValue = String(pageSize);

    // Sublist
    const sl = f.addSublist({ id:'custpage_results', type:'list', label:'Items' });
    sl.addMarkAllButtons();
    sl.addField({ id:'custpage_select', type:'checkbox', label:'Pick' });
    sl.addField({ id:'custpage_itemid', type:'text', label:'Item Internal ID' });
    sl.addField({ id:'custpage_itemname', type:'text', label:'Item Name' });

    // Fetch data (Saved Search example)
    const results = searchItems(category, page, pageSize);
    results.data.forEach((r, i) => {
      sl.setSublistValue({ id:'custpage_itemid', line:i, value: String(r.id) });
      sl.setSublistValue({ id:'custpage_itemname', line:i, value: r.name || '' });
    });

    // Pagination buttons
    if (page > 1) {
      f.addButton({
        id:'custpage_prev', label:'◀ Previous',
        functionName:`goPage(${page-1})`
      });
    }
    if (page < results.totalPages) {
      f.addButton({
        id:'custpage_next', label:'Next ▶',
        functionName:`goPage(${page+1})`
      });
    }

    // Next step submit
    f.addSubmitButton({ label:'Continue ▶' });

    // Client helper for pagination
    f.clientScriptModulePath = './cs_wizard_pagination.js';

    ctx.response.writePage(f);
  }
  function handleStep2Post(ctx) {
    // Collect selected lines
    const lineCount = Number(ctx.request.getLineCount({ group:'custpage_results' })) || 0;
    const selected = [];
    for (let i=0; i<lineCount; i++) {
      const pick = ctx.request.getSublistValue({ group:'custpage_results', name:'custpage_select', line:i });
      if (pick === 'T') {
        const id = ctx.request.getSublistValue({ group:'custpage_results', name:'custpage_itemid', line:i });
        selected.push(id);
      }
    }
    redirect.toSuitelet({
      scriptId: runtime.getCurrentScript().id,
      deploymentId: runtime.getCurrentScript().deploymentId,
      parameters: {
        step: STEP.THREE,
        selected: selected.join(',')
      }
    });
  }

  // ---- Step 3: Confirm & Execute ----
  function renderStep3(ctx) {
    const f = form.create({ title: 'Wizard: Step 3 – Confirm & Execute' });
    f.addField({ id:'custpage_selected', type:'longtext', label:'Selected IDs' })
     .defaultValue = (ctx.request.parameters.selected || '');
    f.addSubmitButton({ label:'Run Action' });
    ctx.response.writePage(f);
  }
  function handleStep3Post(ctx) {
    const ids = (ctx.request.parameters.custpage_selected || '').split(',').filter(Boolean);
    // TODO: perform your bulk action (e.g., set a field, create related records)
    const f = form.create({ title: 'Done' });
    f.addField({ id:'custpage_msg', type:'inlinehtml', label:' '})
     .defaultValue = `<div>Processed ${ids.length} record(s).</div>`;
    ctx.response.writePage(f);
  }

  // ---- Helpers ----
  function searchItems(category, page, pageSize) {
    const s = search.create({
      type: 'item',
      filters: category ? [['class','anyof',category]] : [],
      columns: ['internalid','itemid']
    });
    const start = (page-1)*pageSize, end = start + pageSize;
    const all = s.run().getRange({ start, end }) || [];
    // NOTE: to compute total pages, run a count or a lightweight duplicate query
    const count = s.runPaged({ pageSize:1000 }).count;
    return {
      data: all.map(r => ({ id: r.getValue('internalid'), name: r.getValue('itemid') })),
      total: count,
      totalPages: Math.max(1, Math.ceil(count / pageSize))
    };
  }

  function escapeHtml(str=''){ return String(str).replace(/[&<>"']/g, s=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[s])); }

  return { onRequest };
});

Client Script (pagination helper): cs_wizard_pagination.js

/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * Helper: Handle pagination button clicks
 */
define(['N/url','N/currentRecord'], (url, currentRecord) => {
  function goPage(page) {
    try {
      const rec = currentRecord.get();
      const params = {
        step: '2',
        page: String(page),
        category: rec.getValue('custpage_category') || '',
        pagesize: rec.getValue('custpage_pagesize') || '50'
      };
      const link = url.resolveScript({
        scriptId: runtimeScriptId(), // filled by NetSuite at runtime
        deploymentId: runtimeDeploymentId(), // ditto
        params
      });
      window.location.href = link;
    } catch (e) { console.log('goPage error', e); }
  }
  // NetSuite injects these at runtime; leave as global placeholders
  /* global runtimeScriptId:false, runtimeDeploymentId:false */
  return { goPage };
});

📌 What this shows

  • Multi-step flow using query params.
  • Dynamic sublist with select checkboxes.
  • Lightweight pagination without blowing governance.

✅ Example 2 — Dynamic Sublist + Client Validation + Inline Search

Great for “picker” UIs (choose items/customers), with fast, partial refresh behavior.

/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 * Dynamic Sublist with Filter Bar and Client Validation
 */
define(['N/ui/serverWidget','N/search','N/log'], (serverWidget, search, log) => {
  const onRequest = (ctx) => {
    try {
      if (ctx.request.method === 'GET') return render(ctx);
      return handlePost(ctx);
    } catch (e) {
      log.error('Dyn Sublist Error', e);
      const f = serverWidget.createForm({ title:'Error' });
      f.addField({id:'custpage_err',type:'inlinehtml',label:' '}).defaultValue = `<pre>${e.message}</pre>`;
      ctx.response.writePage(f);
    }
  };

  function render(ctx) {
    const q = (ctx.request.parameters.q || '').trim();
    const f = serverWidget.createForm({ title:'Dynamic Picker' });

    const filter = f.addField({ id:'custpage_q', type:'text', label:'Search Item Name' });
    filter.defaultValue = q;

    const sl = f.addSublist({ id:'custpage_res', type:'list', label:'Results' });
    sl.addField({ id:'custpage_pick', type:'checkbox', label:'Pick' });
    sl.addField({ id:'custpage_id', type:'text', label:'ID' });
    sl.addField({ id:'custpage_name', type:'text', label:'Name' });

    // Query
    const data = findItems(q, 200);
    data.forEach((row, i) => {
      sl.setSublistValue({ id:'custpage_id', line:i, value:String(row.id) });
      sl.setSublistValue({ id:'custpage_name', line:i, value:row.name });
    });

    f.addButton({ id:'custpage_btn_search', label:'Search', functionName:'onSearch' });
    f.addSubmitButton({ label:'Add Selected' });

    f.clientScriptModulePath = './cs_dynamic_picker.js';
    ctx.response.writePage(f);
  }

  function handlePost(ctx) {
    const count = ctx.request.getLineCount({ group:'custpage_res' });
    const picked = [];
    for (let i=0;i<count;i++){
      if (ctx.request.getSublistValue({ group:'custpage_res', name:'custpage_pick', line:i }) === 'T') {
        picked.push(ctx.request.getSublistValue({ group:'custpage_res', name:'custpage_id', line:i }));
      }
    }
    const f = serverWidget.createForm({ title:'Added!' });
    f.addField({ id:'custpage_msg', type:'inlinehtml', label:' '})
     .defaultValue = `<div>Selected: ${picked.join(', ') || '(none)'}</div>`;
    ctx.response.writePage(f);
  }

  function findItems(q, limit=200){
    const filters = q ? [['itemid','contains',q]] : [];
    const s = search.create({ type:'item', filters, columns:['internalid','itemid']});
    return (s.run().getRange({ start:0, end:limit })||[]).map(r=>({
      id:r.getValue('internalid'), name:r.getValue('itemid')
    }));
  }

  return { onRequest };
});

Client Script (cs_dynamic_picker.js):

/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * Adds search behavior & front-end validation
 */
define(['N/currentRecord','N/url','N/notification'], (currentRecord, url, notification) => {
  function onSearch() {
    try {
      const rec = currentRecord.get();
      const q = rec.getValue('custpage_q') || '';
      if (q.length && q.length < 2) {
        notification.show({ title:'Hint', message:'Type at least 2 characters for better results.', type: notification.Type.INFORMATION });
      }
      // Reload Suitelet with q param
      const href = addParam(window.location.href, 'q', encodeURIComponent(q));
      window.location.href = href;
    } catch (e) { console.log('onSearch error', e); }
  }
  function addParam(urlStr, key, val) {
    const u = new URL(urlStr);
    u.searchParams.set(key, val); return u.toString();
  }
  return { onSearch };
});

📌 What this shows

  • Client-side search without rebuilding the whole Suitelet logic.
  • Basic validation and UX nudges.

✅ Example 3 — File Upload + CSV Parse + Results Preview

Perfect for mass imports/updates with confirmation before commit.

/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 * Upload a CSV, parse, preview, then confirm
 */
define(['N/ui/serverWidget','N/file','N/log'], (ui, file, log) => {
  const onRequest = (ctx) => {
    try {
      if (ctx.request.method === 'GET') return renderUpload(ctx);
      return handleUpload(ctx);
    } catch (e) {
      log.error('Upload Error', e);
      const f = ui.createForm({ title:'Error' });
      f.addField({ id:'custpage_err', type:'inlinehtml', label:' '}).defaultValue = `<pre>${e.message}</pre>`;
      ctx.response.writePage(f);
    }
  };

  function renderUpload(ctx){
    const f = ui.createForm({ title:'CSV Import – Step 1: Upload' });
    f.addField({ id:'custpage_csv', type:'file', label:'CSV File' });
    f.addSubmitButton({ label:'Preview ▶' });
    ctx.response.writePage(f);
  }

  function handleUpload(ctx){
    const csvFile = ctx.request.files.custpage_csv;
    if (!csvFile) throw new Error('No file received.');
    const text = csvFile.getContents();
    const rows = parseCsv(text).slice(0, 200); // cap preview

    // Preview form
    const f = ui.createForm({ title:'Step 2: Preview' });
    const sl = f.addSublist({ id:'custpage_preview', type:'list', label:'First 200 Rows' });
    // assuming simple CSV with cols: sku, qty
    sl.addField({ id:'custpage_sku', type:'text', label:'SKU' });
    sl.addField({ id:'custpage_qty', type:'text', label:'Qty' });
    rows.forEach((r,i)=>{
      sl.setSublistValue({ id:'custpage_sku', line:i, value:r[0]||'' });
      sl.setSublistValue({ id:'custpage_qty', line:i, value:r[1]||'' });
    });

    // keep original content in hidden field for POST2 (confirm)
    f.addField({ id:'custpage_payload', type:'longtext', label:'payload', displayType:'hidden' }).defaultValue = text;
    f.addSubmitButton({ label:'Confirm & Process' });
    ctx.response.writePage(f);
  }

  function parseCsv(txt='') {
    // very naive CSV split for demo; replace with robust parser as needed
    return txt.split(/\r?\n/).filter(Boolean).map(line => line.split(','));
  }

  return { onRequest };
});

📌 What this shows

  • N/file upload handling.
  • Safe preview before processing.
  • Use a second POST to commit changes (Map/Reduce recommended for big jobs).

🔹 Advanced Tips & Patterns

1) Client-Server Communication

  • Add form.clientScriptModulePath to attach a Client Script.
  • For AJAX-like flows, set up a second Suitelet endpoint that returns JSON and call it via https.request from the browser (or use N/url.resolveScript + fetch).

2) Caching & Performance

  • Use N/cache for small lookups (e.g., list of accounts, static maps).
  • Paginate large searches; avoid loading >1,000 rows at once.
  • Use SuiteQL for complex joins (faster than joined searches sometimes).

3) Governance

  • Suitelets run synchronously; keep heavy work off the request path.
  • For bulk actions, queue a Map/Reduce and show a status page or task ID.

4) Security

  • Validate all parameters (never trust query string).
  • Hide internal IDs unless needed.
  • Use permissions/roles to restrict access; check runtime.getCurrentUser().

5) UX Enhancements

  • Mark-all buttons on sublists.
  • Inline HTML fields for callouts, warnings, and quick help.
  • Add icons/emojis in labels sparingly to guide users.

🔹 Common Gotchas

  • “You have attempted to access a page that is not available” → Role/permission or deployment audience issue.
  • Blank pages after submit → Forgot response.writePage() or redirect.
  • Lost state → Persist critical data in hidden fields or query params.

✅ Wrap-Up

Advanced Suitelets let you build rich, guided tools right inside NetSuite—perfect for admin dashboards, mass actions, import wizards, and partner toolings. Use multi-step flows, dynamic sublists, client helpers, and offload heavy lifting to Map/Reduce for scale.

Share
  • Facebook

Leave a ReplyCancel reply

Sidebar

Ask A Question

Stats

  • Questions 6
  • Answers 6
  • Best Answers 0
  • Users 2
  • Popular
  • Answers
  • Rocky

    Issue in running a client script in NetSuite SuiteScript 2.0 ...

    • 1 Answer
  • admin

    How can I send an email with an attachment in ...

    • 1 Answer
  • admin

    How do I avoid SSS_USAGE_LIMIT_EXCEEDED in a Map/Reduce script?

    • 1 Answer
  • admin
    admin added an answer The issue is usually caused by following Wrong script file… September 14, 2025 at 10:33 pm
  • admin
    admin added an answer Steps to send an Invoice PDF by email: define(['N/email', 'N/render',… August 28, 2025 at 3:05 am
  • admin
    admin added an answer This error means your script hit NetSuite’s governance usage limit… August 28, 2025 at 3:02 am

Top Members

Rocky

Rocky

  • 1 Question
  • 21 Points
Begginer
admin

admin

  • 5 Questions
  • 2 Points

Trending Tags

clientscript netsuite scripting suitescript

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

© 2025 The NetSuite Pro. All Rights Reserved