NewNetSuite 2025.2 — What's new
intermediateSuiteScript25 min read

NetSuite Scheduled Scripts: Automating Background Tasks

Build scheduled scripts in SuiteScript 2.1 for automated batch processing. Learn scheduling, governance management, rescheduling patterns, and monitoring.

Prerequisites

  • SuiteScript 2.1 basics
  • Understanding of NetSuite script deployment
  • Basic search knowledge
SuiteScriptScheduled ScriptAutomationNetSuite DevelopmentBatch Processing

Scheduled Scripts are the workhorses of NetSuite automation. They run in the background on a defined schedule, processing records, syncing data, and performing maintenance tasks without user interaction. If you need to process thousands of records overnight, send batched emails, or sync data with an external system on a recurring basis, Scheduled Scripts are the right tool.

What Are Scheduled Scripts?

A Scheduled Script is a server-side SuiteScript that runs asynchronously on a time-based schedule or via an on-demand trigger. Unlike User Event Scripts that fire on record actions, Scheduled Scripts operate independently with a governance limit of 10,000 units.

Common use cases include:

  • Nightly data synchronization with external systems
  • Periodic record cleanup and archival
  • Batch email notifications and digests
  • Automated report generation
  • Data integrity checks and corrections

Scheduled Scripts vs. Map/Reduce Scripts

FeatureScheduled ScriptMap/Reduce Script
Governance limit10,000 units10,000 units per stage
ParallelismSingle-threadedMulti-threaded (up to 5 queues)
Restart on failureManual reschedulingAutomatic per-key retry
Best forSequential processing, simple batchesLarge datasets, parallelizable work
ComplexityLowerHigher

Choose a Scheduled Script when your processing is straightforward and sequential. Move to Map/Reduce when you need parallel execution, automatic retry, or are processing millions of records.

Basic Structure

Every Scheduled Script has a single entry point: the execute function.

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/search', 'N/record', 'N/log'], (search, record, log) => {
  const execute = (context) => {
    log.audit('Script Start', `Trigger type: ${context.type}`);
    // Your batch processing logic goes here
    log.audit('Script Complete', 'Processing finished successfully');
  };
 
  return { execute };
});

The context.type tells you how the script was triggered: SCHEDULED (by deployment schedule), ON_DEMAND (via N/task), USER_INTERFACE (manually from deployment page), ABORTED, or SKIPPED.

Script Parameters for Configurable Behavior

Hardcoding values makes scripts inflexible. Script parameters let administrators change behavior without modifying code:

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/runtime', 'N/search', 'N/log'], (runtime, search, log) => {
  const execute = (context) => {
    const script = runtime.getCurrentScript();
 
    const savedSearchId = script.getParameter({ name: 'custscript_search_id' });
    const batchSize = script.getParameter({ name: 'custscript_batch_size' }) || 500;
    const notifyEmail = script.getParameter({ name: 'custscript_notify_email' });
 
    if (!savedSearchId) {
      log.error('Configuration Error', 'Saved Search ID parameter is required');
      return;
    }
 
    const results = search.load({ id: savedSearchId });
    // Process results...
  };
 
  return { execute };
});

Add parameters under the Script record's Parameters tab. This lets you reuse the same script across multiple deployments, each configured differently.

Governance Management

Every SuiteScript API call consumes governance units. Scheduled Scripts have a 10,000-unit budget per execution. Common costs: record.load() = 10 units, record.save() = 20 units, search.create() = 5 units, email.send() = 20 units, http.request() = 10 units.

Always monitor governance within loops:

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/runtime', 'N/search', 'N/record', 'N/log'], (runtime, search, record, log) => {
  const GOVERNANCE_THRESHOLD = 200;
 
  const execute = (context) => {
    const script = runtime.getCurrentScript();
    let processedCount = 0;
 
    const orderSearch = search.create({
      type: search.Type.SALES_ORDER,
      filters: [
        ['status', 'anyof', 'SalesOrd:B'],
        ['datecreated', 'onorbefore', 'daysago7']
      ],
      columns: ['entity', 'total', 'tranid']
    });
 
    orderSearch.run().each((result) => {
      if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) {
        log.audit('Governance Limit', `Stopping at ${processedCount} records`);
        return false;
      }
 
      try {
        const orderRecord = record.load({ type: record.Type.SALES_ORDER, id: result.id });
        orderRecord.setValue({ fieldId: 'custbody_reviewed', value: true });
        orderRecord.save();
        processedCount++;
      } catch (e) {
        log.error(`Error processing order ${result.id}`, e.message);
      }
 
      return true;
    });
 
    log.audit('Execution Summary', `Processed ${processedCount} orders`);
  };
 
  return { execute };
});

Rescheduling Patterns

When you have more records than one execution can handle, use N/task to reschedule the script and continue from where you left off:

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/runtime', 'N/search', 'N/record', 'N/task', 'N/log'],
  (runtime, search, record, task, log) => {
 
  const GOVERNANCE_THRESHOLD = 500;
 
  const execute = (context) => {
    const script = runtime.getCurrentScript();
    const lastProcessedId = script.getParameter({ name: 'custscript_last_id' }) || 0;
 
    const invoiceSearch = search.create({
      type: search.Type.INVOICE,
      filters: [
        ['mainline', 'is', 'T'],
        ['status', 'anyof', 'CustInvc:A'],
        ['internalidnumber', 'greaterthan', lastProcessedId]
      ],
      columns: [
        search.createColumn({ name: 'internalid', sort: search.Sort.ASC }),
        'entity', 'total'
      ]
    });
 
    let processedCount = 0;
    let currentId = lastProcessedId;
    let hasMoreRecords = false;
 
    invoiceSearch.run().each((result) => {
      if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) {
        hasMoreRecords = true;
        return false;
      }
 
      currentId = result.id;
      try {
        record.submitFields({
          type: record.Type.INVOICE, id: result.id,
          values: { custbody_batch_processed: true }
        });
        processedCount++;
      } catch (e) {
        log.error(`Error on Invoice ${result.id}`, e.message);
      }
      return true;
    });
 
    log.audit('Batch Complete', `Processed ${processedCount}. Last ID: ${currentId}`);
 
    if (hasMoreRecords) {
      try {
        const scriptTask = task.create({
          taskType: task.TaskType.SCHEDULED_SCRIPT,
          scriptId: runtime.getCurrentScript().id,
          deploymentId: runtime.getCurrentScript().deploymentId,
          params: { custscript_last_id: currentId }
        });
        scriptTask.submit();
        log.audit('Rescheduled', `Continuing from ID: ${currentId}`);
      } catch (e) {
        log.error('Reschedule Failed', e.message);
      }
    }
  };
 
  return { execute };
});

Key rescheduling tips: sort search results by internal ID for consistent ordering, use a separate "Not Scheduled" deployment for on-demand runs, and always wrap the reschedule call in try-catch since the queue might be full.

Scheduling Options

Configure recurring schedules through the deployment record:

  • Every 15 / 30 / 60 minutes - Near-real-time sync needs
  • Daily - Specify time of day (e.g., 2:00 AM)
  • Weekly - Choose day and time
  • Monthly / Yearly - Choose day of month and time

Create multiple deployments of the same script for different schedules or configurations, such as customdeploy_sync_daily_us (Daily at 2 AM, Region = US) and customdeploy_sync_daily_eu (Daily at 10 PM, Region = EU).

Practical Examples

Nightly Order Status Sync

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * @NModuleScope SameAccount
 */
define(['N/runtime', 'N/search', 'N/https', 'N/log'],
  (runtime, search, https, log) => {
 
  const execute = (context) => {
    const script = runtime.getCurrentScript();
    const apiKey = script.getParameter({ name: 'custscript_api_key' });
 
    const orderSearch = search.create({
      type: search.Type.SALES_ORDER,
      filters: [
        ['mainline', 'is', 'T'],
        ['lastmodifieddate', 'onorafter', 'yesterday'],
        ['status', 'anyof', ['SalesOrd:B', 'SalesOrd:D', 'SalesOrd:F']]
      ],
      columns: ['tranid', 'entity', 'status', 'total', 'shipaddress']
    });
 
    const ordersToSync = [];
    orderSearch.run().each((result) => {
      if (script.getRemainingUsage() < 300) return false;
      ordersToSync.push({
        netsuite_id: result.id,
        order_number: result.getValue('tranid'),
        status: result.getText('status'),
        total: result.getValue('total')
      });
      return true;
    });
 
    // Send in batches of 50
    let synced = 0;
    for (let i = 0; i < ordersToSync.length; i += 50) {
      if (script.getRemainingUsage() < 300) break;
      const batch = ordersToSync.slice(i, i + 50);
      try {
        const response = https.post({
          url: 'https://fulfillment.example.com/api/v2/orders',
          headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
          body: JSON.stringify({ orders: batch })
        });
        if (response.code === 200) synced += batch.length;
        else log.error('API Error', `Status: ${response.code}`);
      } catch (e) {
        log.error('Sync Error', e.message);
      }
    }
    log.audit('Sync Complete', { total: ordersToSync.length, synced });
  };
 
  return { execute };
});

Weekly Data Cleanup and Archival

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * @NModuleScope SameAccount
 */
define(['N/runtime', 'N/search', 'N/record', 'N/task', 'N/log'],
  (runtime, search, record, task, log) => {
 
  const GOVERNANCE_THRESHOLD = 400;
 
  const execute = (context) => {
    const script = runtime.getCurrentScript();
    const phase = script.getParameter({ name: 'custscript_cleanup_phase' }) || 'archive';
 
    if (phase === 'archive') {
      const cutoff = new Date();
      cutoff.setDate(cutoff.getDate() - 90);
      const cutoffStr = `${cutoff.getMonth() + 1}/${cutoff.getDate()}/${cutoff.getFullYear()}`;
      let count = 0;
 
      search.create({
        type: 'customrecord_activity_log',
        filters: [['created', 'before', cutoffStr], ['isinactive', 'is', 'F']],
        columns: [search.createColumn({ name: 'internalid', sort: search.Sort.ASC })]
      }).run().each((result) => {
        if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) return false;
        try {
          record.submitFields({
            type: 'customrecord_activity_log', id: result.id,
            values: { isinactive: true }
          });
          count++;
        } catch (e) { log.error(`Archive Error ${result.id}`, e.message); }
        return true;
      });
 
      log.audit('Archive Complete', `Archived ${count} records`);
      // Chain to purge phase
      const nextTask = task.create({
        taskType: task.TaskType.SCHEDULED_SCRIPT,
        scriptId: script.id, deploymentId: script.deploymentId,
        params: { custscript_cleanup_phase: 'purge' }
      });
      nextTask.submit();
 
    } else if (phase === 'purge') {
      let count = 0;
      search.create({
        type: 'customrecord_temp_staging',
        filters: [['created', 'before', 'daysago7'], ['custrecord_staging_status', 'is', 'Complete']],
        columns: [search.createColumn({ name: 'internalid', sort: search.Sort.ASC })]
      }).run().each((result) => {
        if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) return false;
        try { record.delete({ type: 'customrecord_temp_staging', id: result.id }); count++; }
        catch (e) { log.error(`Delete Error ${result.id}`, e.message); }
        return true;
      });
      log.audit('Purge Complete', `Deleted ${count} temp records`);
    }
  };
 
  return { execute };
});

Daily Email Notification Digest

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * @NModuleScope SameAccount
 */
define(['N/runtime', 'N/search', 'N/record', 'N/email', 'N/log'],
  (runtime, search, record, email, log) => {
 
  const execute = (context) => {
    const script = runtime.getCurrentScript();
    const senderId = script.getParameter({ name: 'custscript_sender_id' });
 
    // Group unsent notifications by recipient
    const recipientMap = {};
    search.create({
      type: 'customrecord_notification_queue',
      filters: [['custrecord_notif_sent', 'is', 'F'], ['created', 'within', 'yesterday', 'today']],
      columns: ['custrecord_notif_recipient', 'custrecord_notif_subject',
                'custrecord_notif_message', 'custrecord_notif_priority']
    }).run().each((result) => {
      if (script.getRemainingUsage() < 500) return false;
      const rid = result.getValue('custrecord_notif_recipient');
      if (!recipientMap[rid]) recipientMap[rid] = [];
      recipientMap[rid].push({
        id: result.id,
        subject: result.getValue('custrecord_notif_subject'),
        message: result.getValue('custrecord_notif_message'),
        priority: result.getText('custrecord_notif_priority')
      });
      return true;
    });
 
    // Send digest to each recipient
    let sent = 0;
    for (const [recipientId, notifications] of Object.entries(recipientMap)) {
      if (script.getRemainingUsage() < 300) break;
      try {
        let body = '<h2>Your Daily Digest</h2><ul>';
        notifications.forEach((n) => { body += `<li><strong>[${n.priority}]</strong> ${n.subject}: ${n.message}</li>`; });
        body += '</ul>';
 
        email.send({
          author: senderId, recipients: parseInt(recipientId),
          subject: `Daily Digest - ${notifications.length} items`, body
        });
        notifications.forEach((n) => {
          record.submitFields({ type: 'customrecord_notification_queue', id: n.id, values: { custrecord_notif_sent: true } });
        });
        sent++;
      } catch (e) { log.error(`Email Error ${recipientId}`, e.message); }
    }
    log.audit('Digest Complete', { recipients: sent, total: Object.keys(recipientMap).length });
  };
 
  return { execute };
});

Using N/task to Chain Scripts

Beyond rescheduling, N/task lets you trigger Map/Reduce scripts, CSV imports, and other scheduled scripts:

// Trigger a Map/Reduce script
const mrTask = task.create({
  taskType: task.TaskType.MAP_REDUCE,
  scriptId: 'customscript_process_results',
  deploymentId: 'customdeploy_process_results',
  params: { custscript_mr_search_id: savedSearchId }
});
const taskId = mrTask.submit();
 
// Check task status
const status = task.checkStatus({ taskId });
log.debug('Status', { status: status.status, pct: status.getPercentageCompleted() });

Error Handling and Notification on Failure

Wrap your main logic in try-catch-finally, track statistics, and send alerts for critical failures:

const execute = (context) => {
  const script = runtime.getCurrentScript();
  const adminId = script.getParameter({ name: 'custscript_admin_id' });
  const stats = { processed: 0, errors: 0, errorDetails: [], startTime: new Date() };
 
  try {
    processRecords(script, stats);
  } catch (e) {
    log.error('Fatal Error', { message: e.message, stack: e.stack });
    stats.errorDetails.push({ type: 'FATAL', message: e.message });
  } finally {
    stats.duration = (new Date() - stats.startTime) / 1000;
    log.audit('Summary', stats);
 
    if (stats.errors > 0 && adminId) {
      const errorList = stats.errorDetails.map((e) => `- ${e.recordId || 'N/A'}: ${e.message}`).join('\n');
      email.send({
        author: adminId, recipients: adminId,
        subject: `[ALERT] Scheduled Script - ${stats.errors} failures`,
        body: `Processed: ${stats.processed}\nErrors: ${stats.errors}\nDuration: ${stats.duration}s\n\n${errorList}`
      });
    }
  }
};

Monitoring and Logging Best Practices

  • log.debug() - Development detail. Disable in production by setting deployment log level to Audit.
  • log.audit() - Milestones: script start, completion, record counts. Always active.
  • log.error() - Errors that need attention but do not stop execution.
  • Structured logging - Pass objects instead of strings for searchable logs:
log.audit('Execution Summary', {
  recordsProcessed: 150, duration: 45, errors: 3,
  scriptId: runtime.getCurrentScript().id
});

Check execution history at Customization > Scripting > Script Deployments > [Your Deployment] > Execution Log. For scripts that run frequently, consider writing summary stats to a custom record so you can build saved searches and dashboards around execution history.

Deployment Configuration Tips

SettingRecommendation
StatusReleased for production, Testing during development
Log LevelAudit for production, Debug during development
Execute As RoleMinimum necessary permissions
QueueAssign to a specific processor queue to avoid contention

NetSuite processes scheduled scripts through queues (typically 5 slots per account). Schedule heavy scripts during off-peak hours and stagger start times by 15-30 minutes to prevent queue congestion.

Build your scripts to be idempotent: running the same script twice on the same data should produce the same result. This protects you from partial execution issues, since any records already processed before a failure remain processed with no automatic rollback.

Test in this order: manual trigger via the deployment page, small dataset with limited parameters, sandbox validation, then production release.

Next Steps

Now that you know how to build Scheduled Scripts, explore these related topics:


Need automated processes in your NetSuite environment? Contact our development team to discuss your automation needs.

Need hands-on training?

Our corporate training programs go beyond tutorials with personalized instruction using your actual NetSuite environment.

Get in Touch