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
    • NetSuite Customization
    • NetSuite Integration
    • NetSuite Advanced PDF Templates
    • NetSuite Reporting & Analytics Guide
    • Real-World NetSuite Examples
  • Blog
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask A Question
  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • NetSuite Customization
    • NetSuite Integration
    • NetSuite Advanced PDF Templates
    • NetSuite Reporting & Analytics Guide
    • Real-World NetSuite Examples
  • Blog
  • Contact Us
Home/ Real-World NetSuite Examples/Auto-Assign Sales Rep by Territory (Lead/Customer)

Auto-Assign Sales Rep by Territory (Lead/Customer)

πŸ’Ό Business problem

SDRs spend time manually routing new leads/customers to the right rep by country/state/postal code. Mistakes slow follow-ups. We’ll auto-assign the salesrep at creation (and on edits) using maintainable, non-code rules.


🧠 Approach

  • User Event (beforeSubmit) on Lead/Prospect/Customer.
  • Read Ship To (fallback to Bill To) β†’ country, state, zip.
  • Match against a JSON ruleset (script parameter) or an optional Custom Record map.
  • Set salesrep to the rep’s employee internal ID.
  • Optional role bypass lets managers override manually.

πŸ”§ Parameters (Deployment)

  • custscript_terr_rules_json (Long Text) β€” JSON rules (see below)
  • custscript_terr_bypass_roles (CSV) β€” role IDs that skip auto-assign
  • custscript_terr_apply_on_edit (Checkbox) β€” reassign on edit if rep is empty

Example rules JSON

[
  { "country":"CA", "state":"ON", "zip_from":"",    "zip_to":"",    "salesrep":"1234", "note":"Canada - Ontario" },
  { "country":"CA", "state":"BC", "zip_from":"",    "zip_to":"",    "salesrep":"1235", "note":"Canada - British Columbia" },
  { "country":"US", "state":"CA", "zip_from":"90000","zip_to":"96162","salesrep":"2001", "note":"US - California zips" },
  { "country":"US", "state":"TX", "zip_from":"",    "zip_to":"",    "salesrep":"2002", "note":"US - Texas any zip" },
  { "country":"GB", "state":"",   "zip_from":"",    "zip_to":"",    "salesrep":"3001", "note":"UK all" }
]

Empty zip_from/zip_to means β€œany zip” in that state/country. First match wins.


🧩 User Event (SuiteScript 2.1)

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * Title: UE | Auto-Assign Sales Rep by Territory
 * Author: The NetSuite Pro
 */
define(['N/runtime','N/log'], (runtime, log) => {

  const P_RULES = 'custscript_terr_rules_json';
  const P_BYPASS = 'custscript_terr_bypass_roles';
  const P_APPLY_ON_EDIT = 'custscript_terr_apply_on_edit';

  const beforeSubmit = (ctx) => {
    const rec = ctx.newRecord;
    // Only on entity: lead/prospect/customer
    if (!['lead','prospect','customer'].includes(rec.type)) return;

    const isCreateLike = [ctx.UserEventType.CREATE, ctx.UserEventType.COPY, ctx.UserEventType.XEDIT].includes(ctx.type);
    const isEdit = ctx.type === ctx.UserEventType.EDIT;

    // Role bypass?
    const bypassRoles = csv(runtime.getCurrentScript().getParameter({ name: P_BYPASS }));
    if (bypassRoles.includes(String(runtime.getCurrentUser().role))) return;

    // Should we run?
    const applyOnEdit = runtime.getCurrentScript().getParameter({ name: P_APPLY_ON_EDIT }) === 'T';
    const currentRep = safeGet(rec, 'salesrep');
    if (isEdit && !applyOnEdit && currentRep) return;

    try {
      const geo = readGeo(rec); // {country, state, zip}
      if (!geo.country) return;

      const rules = loadRules();
      const match = findMatch(rules, geo);
      if (!match || !match.salesrep) return;

      // Only set if empty or we're allowed to overwrite on edit
      if (!currentRep || applyOnEdit) {
        rec.setValue({ fieldId: 'salesrep', value: Number(match.salesrep) });
        // Optional audit note if you keep a field like custentity_assignment_note
        // safeSet(rec, 'custentity_assignment_note', `Auto-assigned to ${match.salesrep} (${match.note || ''})`);
      }

    } catch (e) {
      log.error('Territory assign error', e);
    }
  };

  // ---------- helpers ----------
  function readGeo(rec){
    // Prefer Ship To; fall back to Bill To; final fallback to entity fields
    const shipCountry = safeGet(rec, 'shipcountry') || safeGet(rec, 'custentity_ship_country');
    const shipState   = safeGet(rec, 'shipstate') || safeGet(rec, 'custentity_ship_state');
    const shipZip     = (safeGet(rec, 'shipzip') || '').toString();

    let country = (shipCountry || safeGet(rec, 'country') || '').toString().toUpperCase();
    let state   = (shipState   || safeGet(rec, 'state')   || '').toString().toUpperCase();
    let zip     = shipZip.replace(/\s+/g,'');
    return { country, state, zip };
  }

  function loadRules(){
    try {
      const json = runtime.getCurrentScript().getParameter({ name: P_RULES }) || '[]';
      const arr = JSON.parse(json);
      return Array.isArray(arr) ? arr : [];
    } catch (_e) { return []; }
  }

  function findMatch(rules, geo){
    const c = geo.country, s = geo.state, z = onlyDigits(geo.zip);
    // 1) country+state+zip range
    let r = rules.find(x => eq(x.country,c) && eq(x.state,s) && inZipRange(x, z));
    if (r) return r;
    // 2) country+state (any zip)
    r = rules.find(x => eq(x.country,c) && eq(x.state,s) && empty(x.zip_from) && empty(x.zip_to));
    if (r) return r;
    // 3) country-only default
    r = rules.find(x => eq(x.country,c) && empty(x.state));
    return r || null;
  }

  function inZipRange(rule, z){
    const from = onlyDigits(rule.zip_from);
    const to   = onlyDigits(rule.zip_to);
    if (!from && !to) return true;      // no zip constraint
    if (!z) return false;
    if (from && to) return z >= from && z <= to;
    if (from && !to) return z >= from;
    if (!from && to) return z <= to;
    return false;
  }

  function onlyDigits(s){ s = (s||'').toString(); const m = s.match(/\d+/g); return m ? m.join('') : ''; }
  function eq(a,b){ return String(a||'').toUpperCase() === String(b||'').toUpperCase(); }
  function empty(v){ return v==null || String(v).trim()===''; }
  function safeGet(rec, fieldId){ try { return rec.getValue({ fieldId }); } catch(_e){ return null; } }
  function csv(s){ return (s||'').split(',').map(x=>x.trim()).filter(Boolean); }

  return { beforeSubmit };
});

βœ… Testing checklist

  1. Add rules JSON (use your employee internal IDs).
  2. Create a Lead in CA / ON β†’ rep auto-assigned to Ontario rep.
  3. Create a Customer in US / CA with ZIP 94016 β†’ assigned to the CA ZIP range rep.
  4. Edit an existing record with rep blank β†’ assignment runs (if apply_on_edit = T).
  5. Login as a bypass role β†’ rep remains unchanged.

🌱 Enhancements

  • Custom Record Map: Instead of JSON, point to customrecord_territory_map with fields (country/state/zip_from/zip_to/salesrep). Load once and cache.
  • Round-Robin per territory: When a territory has multiple reps, pick by modulo on customer internal ID or last digit of ZIP.
  • Channel ownership: If leadsource = Partner, set partner instead of (or in addition to) salesrep.
  • Reassignment lock: If custentity_rep_locked = T, skip changes even on edit.
  • Geo cleanup: Normalize Canadian postcodes by first 3 chars (FSA) and route by FSA instead of full code.

πŸ“Œ Summary

ObjectiveResult
Route leads/customers instantlyRep auto-assigned on save
Maintain rules without codeJSON/custom record mapping
Avoid overwritesRole bypass + edit behavior flag
Share
  • Facebook

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
  • 22 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