Skip to main content

Scheduled Script Development

Scheduled Scripts run automatically at specified intervals to perform batch processing, report generation, and system maintenance tasks.


When to Use Scheduled Scripts

Use CaseExample
Batch processingUpdate prices for all items
Report generationDaily sales summary emails
Data cleanupArchive old records
IntegrationsSync data with external systems
NotificationsSend reminder emails

Scheduled Script Execution Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ SCHEDULED SCRIPT EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ SCHEDULING OPTIONS │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SCHEDULE │ │ ON DEMAND │ │ QUEUE-BASED │
│ (Cron-like) │ │ (Manual Run) │ │ (From Code) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ Every Hour │ User clicks │ task.submit()
│ Daily at 2 AM │ "Run Now" │ from another
│ Weekly on Mon │ in deployment │ script
│ │ │
└─────────────────────┼─────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ SCRIPT QUEUED │
│ ────────────────────────────────────────────────────────────────│
│ • Added to processing queue │
│ • Waits for available processor │
│ • Queue position depends on priority │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ PROCESSOR PICKS UP │
│ ────────────────────────────────────────────────────────────────│
│ • Script starts execution │
│ • Parameters passed via deployment │
│ • Governance units start depleting │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ execute(context) │
│ ────────────────────────────────────────────────────────────────│
│ • Main entry point │
│ • Process data in batches │
│ • Monitor governance units │
│ • Reschedule if needed │
└──────────────────────────────┬───────────────────────────────────┘

┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ COMPLETE │ │ RESCHEDULE │ │ FAILED │
│ (Success) │ │ (Yielded) │ │ (Error) │
└─────────────────┘ └────────┬────────┘ └─────────────────┘

│ Automatic
▼ requeue
┌─────────────────┐
│ CONTINUE FROM │
│ WHERE LEFT OFF │
└─────────────────┘

Governance Management

┌─────────────────────────────────────────────────────────────────────────────┐
│ GOVERNANCE UNITS EXPLAINED │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ SCHEDULED SCRIPT LIMIT: 10,000 Units │
└──────────────────────────────────────────────────────────────────┘

Each API call consumes units:

┌────────────────────────────┬──────────────┐
│ Operation │ Units Used │
├────────────────────────────┼──────────────┤
│ record.load() │ 10 │
│ record.save() │ 20 │
│ record.create() │ 10 │
│ search.run().each() │ 10 │
│ search.lookupFields() │ 1 │
│ email.send() │ 20 │
│ http.get() │ 10 │
│ file.load() │ 10 │
└────────────────────────────┴──────────────┘


MONITORING PATTERN:

┌──────────────────────────────────────────────────────────────────┐
│ │
│ for each record: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Check remaining units │ │
│ │ runtime.getCurrentScript().getRemainingUsage() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Units < 500? │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ No │ Yes │
│ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────────────────┐ │
│ │ Process │ │ Save progress │ │
│ │ Record │ │ Reschedule script │ │
│ └───────────────┘ │ task.create() + submit() │ │
│ └───────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘

Basic Scheduled Script Structure

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @NModuleScope SameAccount
*/
define(['N/search', 'N/record', 'N/runtime', 'N/log'],
(search, record, runtime, log) => {

/**
* Main entry point
* @param {Object} context
* @param {string} context.type - Scheduled script execution type
*/
const execute = (context) => {
log.audit('Script Start', `Execution type: ${context.type}`);

try {
// Get script parameters
const script = runtime.getCurrentScript();
const batchSize = script.getParameter({
name: 'custscript_batch_size'
}) || 100;

// Main processing logic
processRecords(batchSize);

log.audit('Script Complete', 'Processing finished successfully');

} catch (e) {
log.error('Script Error', e.message);
}
};

/**
* Process records in batches
*/
const processRecords = (batchSize) => {
const script = runtime.getCurrentScript();
let processedCount = 0;

// Create search
const mySearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', 'SalesOrd:B'] // Pending Fulfillment
],
columns: ['tranid', 'entity', 'total']
});

// Process results
mySearch.run().each((result) => {
// Check governance
const remaining = script.getRemainingUsage();
if (remaining < 500) {
log.audit('Governance', `Low units (${remaining}). Stopping.`);
return false; // Stop processing
}

// Process this record
processRecord(result);
processedCount++;

// Stop at batch size
return processedCount < batchSize;
});

log.audit('Processing Complete', `Processed ${processedCount} records`);
};

/**
* Process individual record
*/
const processRecord = (result) => {
const recordId = result.id;
const tranId = result.getValue('tranid');

log.debug('Processing', `Order: ${tranId}`);

// Add processing logic here
};

return { execute };
});

Rescheduling Pattern

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/search', 'N/record', 'N/runtime', 'N/task', 'N/log'],
(search, record, runtime, task, log) => {

const GOVERNANCE_THRESHOLD = 500;

const execute = (context) => {
const script = runtime.getCurrentScript();

// Get last processed ID (for resume)
const lastProcessedId = script.getParameter({
name: 'custscript_last_id'
}) || 0;

log.audit('Script Start', `Resuming from ID: ${lastProcessedId}`);

const result = processRecords(lastProcessedId);

if (result.needsReschedule) {
rescheduleScript(result.lastId);
}
};

const processRecords = (startFromId) => {
const script = runtime.getCurrentScript();
let lastProcessedId = startFromId;
let needsReschedule = false;

// Search with ID filter for resuming
const mySearch = search.create({
type: search.Type.CUSTOMER,
filters: [
['internalidnumber', 'greaterthan', startFromId]
],
columns: [
search.createColumn({ name: 'internalid', sort: search.Sort.ASC }),
'companyname',
'email'
]
});

mySearch.run().each((result) => {
// Check governance
if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) {
log.audit('Governance', 'Threshold reached, rescheduling');
needsReschedule = true;
return false;
}

// Process record
processCustomer(result);
lastProcessedId = result.id;

return true; // Continue to next
});

return {
lastId: lastProcessedId,
needsReschedule: needsReschedule
};
};

const processCustomer = (result) => {
// Processing logic
log.debug('Processing', result.getValue('companyname'));
};

const rescheduleScript = (lastId) => {
try {
const scriptTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: runtime.getCurrentScript().id,
deploymentId: runtime.getCurrentScript().deploymentId,
params: {
'custscript_last_id': lastId
}
});

const taskId = scriptTask.submit();
log.audit('Rescheduled', `Task ID: ${taskId}, Resume from: ${lastId}`);

} catch (e) {
log.error('Reschedule Failed', e.message);
}
};

return { execute };
});

Reschedule Flow Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│ RESCHEDULE PATTERN FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ FIRST RUN │
│ custscript_last_id = 0 (empty) │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ Search: ID > 0 │
│ Process: Customer 1, 2, 3, 4, 5... │
└──────────────────────────────┬───────────────────────────────────┘


┌─────────────────┐
│ Governance < │
│ Threshold? │
└────────┬────────┘

Yes ───────┴─────── No
│ │
▼ ▼
┌────────────────────────────┐ ┌────────────────────────────┐
│ RESCHEDULE │ │ COMPLETE │
│ ───────────────────────── │ │ ───────────────────────── │
│ task.create({ │ │ All records processed │
│ params: { │ │ Script ends normally │
│ custscript_last_id: │ │ │
│ 500 ◄── Last ID │ │ │
│ } │ │ │
│ }) │ │ │
└────────────────┬───────────┘ └────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ SECOND RUN │
│ custscript_last_id = 500 │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ Search: ID > 500 │
│ Process: Customer 501, 502, 503... │
│ (Continues where left off) │
└──────────────────────────────────────────────────────────────────┘

Script Parameters

Define Parameters in XML

<?xml version="1.0" encoding="UTF-8"?>
<scheduledscript scriptid="customscript_daily_report_ss">
<name>Daily Report Scheduled Script</name>
<scriptfile>[/SuiteScripts/ScheduledScripts/daily_report_ss.js]</scriptfile>
<description>Generates and emails daily reports</description>
<isinactive>F</isinactive>

<!-- Script Parameters -->
<scriptcustomfields>
<scriptcustomfield scriptid="custscript_batch_size">
<label>Batch Size</label>
<fieldtype>INTEGER</fieldtype>
<description>Number of records to process per run</description>
<defaultvalue>100</defaultvalue>
</scriptcustomfield>

<scriptcustomfield scriptid="custscript_email_recipient">
<label>Email Recipient</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype> <!-- Employee -->
<description>Who receives the report</description>
</scriptcustomfield>

<scriptcustomfield scriptid="custscript_last_id">
<label>Last Processed ID</label>
<fieldtype>INTEGER</fieldtype>
<description>For resume functionality</description>
<displaytype>HIDDEN</displaytype>
</scriptcustomfield>
</scriptcustomfields>

<!-- Deployments -->
<scriptdeployments>
<scriptdeployment scriptid="customdeploy_daily_report_ss">
<status>RELEASED</status>
<title>Daily Report - Morning Run</title>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>F</allroles>
<audslctrole>
<role>[Administrator]</role>
</audslctrole>
</scriptdeployment>
</scriptdeployments>
</scheduledscript>

Read Parameters in Script

const execute = (context) => {
const script = runtime.getCurrentScript();

// Get script parameters
const batchSize = script.getParameter({
name: 'custscript_batch_size'
}) || 100;

const recipientId = script.getParameter({
name: 'custscript_email_recipient'
});

const lastId = script.getParameter({
name: 'custscript_last_id'
}) || 0;

log.debug('Parameters', {
batchSize,
recipientId,
lastId
});
};

Complete Example: Daily Sales Report

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @NModuleScope SameAccount
* @description Generates daily sales report and emails to managers
*/
define(['N/search', 'N/email', 'N/runtime', 'N/render', 'N/file', 'N/log', 'N/format'],
(search, email, runtime, render, file, log, format) => {

/**
* Main execution
*/
const execute = (context) => {
log.audit('Daily Report', 'Starting execution');

try {
const script = runtime.getCurrentScript();

// Get parameters
const recipientId = script.getParameter({
name: 'custscript_report_recipient'
});

const dayOffset = script.getParameter({
name: 'custscript_day_offset'
}) || 1;

if (!recipientId) {
log.error('Configuration Error', 'No recipient configured');
return;
}

// Calculate date range
const dateRange = getDateRange(dayOffset);

// Gather report data
const reportData = gatherReportData(dateRange);

// Generate report
const reportHtml = generateReport(reportData, dateRange);

// Send email
sendReport(recipientId, reportHtml, dateRange);

log.audit('Daily Report', 'Completed successfully');

} catch (e) {
log.error('Report Error', e.message);
throw e;
}
};

/**
* Calculate date range for report
*/
const getDateRange = (dayOffset) => {
const today = new Date();
const reportDate = new Date(today);
reportDate.setDate(reportDate.getDate() - dayOffset);

// Start of day
const startDate = new Date(reportDate);
startDate.setHours(0, 0, 0, 0);

// End of day
const endDate = new Date(reportDate);
endDate.setHours(23, 59, 59, 999);

return {
start: format.format({
value: startDate,
type: format.Type.DATE
}),
end: format.format({
value: endDate,
type: format.Type.DATE
}),
display: format.format({
value: reportDate,
type: format.Type.DATE
})
};
};

/**
* Gather sales data for report
*/
const gatherReportData = (dateRange) => {
const data = {
orders: [],
summary: {
totalOrders: 0,
totalAmount: 0,
averageOrder: 0
},
topProducts: [],
topCustomers: []
};

// Get sales orders
const orderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', dateRange.start, dateRange.end],
'AND',
['status', 'noneof', 'SalesOrd:C'] // Not cancelled
],
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'entity' }),
search.createColumn({ name: 'total' }),
search.createColumn({ name: 'salesrep' }),
search.createColumn({ name: 'status' })
]
});

orderSearch.run().each((result) => {
const amount = parseFloat(result.getValue('total')) || 0;

data.orders.push({
id: result.id,
tranId: result.getValue('tranid'),
customer: result.getText('entity'),
amount: amount,
salesRep: result.getText('salesrep') || 'Unassigned',
status: result.getText('status')
});

data.summary.totalOrders++;
data.summary.totalAmount += amount;

return true;
});

// Calculate average
if (data.summary.totalOrders > 0) {
data.summary.averageOrder = data.summary.totalAmount / data.summary.totalOrders;
}

// Get top products
data.topProducts = getTopProducts(dateRange);

// Get top customers
data.topCustomers = getTopCustomers(dateRange);

return data;
};

/**
* Get top selling products
*/
const getTopProducts = (dateRange) => {
const products = [];

const productSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'F'],
'AND',
['trandate', 'within', dateRange.start, dateRange.end],
'AND',
['item.type', 'noneof', 'Discount', 'Subtotal']
],
columns: [
search.createColumn({
name: 'item',
summary: search.Summary.GROUP
}),
search.createColumn({
name: 'quantity',
summary: search.Summary.SUM,
sort: search.Sort.DESC
}),
search.createColumn({
name: 'amount',
summary: search.Summary.SUM
})
]
});

let count = 0;
productSearch.run().each((result) => {
products.push({
item: result.getText({ name: 'item', summary: search.Summary.GROUP }),
quantity: result.getValue({ name: 'quantity', summary: search.Summary.SUM }),
amount: result.getValue({ name: 'amount', summary: search.Summary.SUM })
});

count++;
return count < 10; // Top 10
});

return products;
};

/**
* Get top customers
*/
const getTopCustomers = (dateRange) => {
const customers = [];

const customerSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', dateRange.start, dateRange.end]
],
columns: [
search.createColumn({
name: 'entity',
summary: search.Summary.GROUP
}),
search.createColumn({
name: 'internalid',
summary: search.Summary.COUNT
}),
search.createColumn({
name: 'total',
summary: search.Summary.SUM,
sort: search.Sort.DESC
})
]
});

let count = 0;
customerSearch.run().each((result) => {
customers.push({
customer: result.getText({ name: 'entity', summary: search.Summary.GROUP }),
orders: result.getValue({ name: 'internalid', summary: search.Summary.COUNT }),
total: result.getValue({ name: 'total', summary: search.Summary.SUM })
});

count++;
return count < 5; // Top 5
});

return customers;
};

/**
* Generate HTML report
*/
const generateReport = (data, dateRange) => {
let html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; border-bottom: 2px solid #4CAF50; }
h2 { color: #666; margin-top: 30px; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
th { background-color: #4CAF50; color: white; padding: 12px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #ddd; }
tr:hover { background-color: #f5f5f5; }
.summary-box {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.metric {
display: inline-block;
margin: 10px 30px 10px 0;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
}
.metric-label {
color: #666;
font-size: 14px;
}
.amount { text-align: right; }
</style>
</head>
<body>
<h1>Daily Sales Report</h1>
<p>Report Date: ${dateRange.display}</p>

<div class="summary-box">
<div class="metric">
<div class="metric-value">${data.summary.totalOrders}</div>
<div class="metric-label">Total Orders</div>
</div>
<div class="metric">
<div class="metric-value">$${formatNumber(data.summary.totalAmount)}</div>
<div class="metric-label">Total Revenue</div>
</div>
<div class="metric">
<div class="metric-value">$${formatNumber(data.summary.averageOrder)}</div>
<div class="metric-label">Average Order</div>
</div>
</div>
`;

// Top Customers Table
if (data.topCustomers.length > 0) {
html += `
<h2>Top Customers</h2>
<table>
<tr>
<th>Customer</th>
<th>Orders</th>
<th class="amount">Total</th>
</tr>
`;

data.topCustomers.forEach(customer => {
html += `
<tr>
<td>${customer.customer}</td>
<td>${customer.orders}</td>
<td class="amount">$${formatNumber(customer.total)}</td>
</tr>
`;
});

html += '</table>';
}

// Top Products Table
if (data.topProducts.length > 0) {
html += `
<h2>Top Products</h2>
<table>
<tr>
<th>Product</th>
<th>Quantity</th>
<th class="amount">Revenue</th>
</tr>
`;

data.topProducts.forEach(product => {
html += `
<tr>
<td>${product.item}</td>
<td>${product.quantity}</td>
<td class="amount">$${formatNumber(product.amount)}</td>
</tr>
`;
});

html += '</table>';
}

// Recent Orders
if (data.orders.length > 0) {
html += `
<h2>All Orders (${data.orders.length})</h2>
<table>
<tr>
<th>Order #</th>
<th>Customer</th>
<th>Sales Rep</th>
<th>Status</th>
<th class="amount">Amount</th>
</tr>
`;

data.orders.forEach(order => {
html += `
<tr>
<td>${order.tranId}</td>
<td>${order.customer}</td>
<td>${order.salesRep}</td>
<td>${order.status}</td>
<td class="amount">$${formatNumber(order.amount)}</td>
</tr>
`;
});

html += '</table>';
}

html += `
<p style="color: #999; font-size: 12px; margin-top: 40px;">
This report was automatically generated by NetSuite.
</p>
</body>
</html>
`;

return html;
};

/**
* Format number with commas and decimals
*/
const formatNumber = (value) => {
const num = parseFloat(value) || 0;
return num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
};

/**
* Send report email
*/
const sendReport = (recipientId, reportHtml, dateRange) => {
email.send({
author: runtime.getCurrentUser().id,
recipients: [recipientId],
subject: `Daily Sales Report - ${dateRange.display}`,
body: reportHtml
});

log.audit('Email Sent', `Report sent to employee ID: ${recipientId}`);
};

return { execute };
});

Schedule Configuration

Via NetSuite UI

  1. Customization → Scripting → Script Deployments
  2. Find your deployment
  3. Click Schedule tab
  4. Configure schedule:
┌─────────────────────────────────────────────────────────────────────────────┐
│ SCHEDULE CONFIGURATION │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ FREQUENCY OPTIONS │
│ ────────────────────────────────────────────────────────────────│
│ │
│ ○ Single Event ─────► Runs once at specified date/time │
│ │
│ ○ Daily ─────► Every day at specified time │
│ Start Time: [06:00] End Time: [06:30] │
│ Repeat: Every [1] day(s) │
│ │
│ ○ Weekly ─────► Specific days of week │
│ ☑ Mon ☐ Tue ☐ Wed ☐ Thu ☑ Fri ☐ Sat ☐ Sun │
│ │
│ ○ Monthly ─────► Specific day of month │
│ Day: [1] (1st of each month) │
│ │
│ ○ Yearly ─────► Specific date each year │
│ │
└──────────────────────────────────────────────────────────────────┘

Manual Trigger from Another Script

const task = require('N/task');

const triggerScheduledScript = () => {
const scriptTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: 'customscript_daily_report_ss',
deploymentId: 'customdeploy_daily_report_ss',
params: {
'custscript_day_offset': 0 // Today
}
});

const taskId = scriptTask.submit();
log.audit('Task Submitted', `Task ID: ${taskId}`);

return taskId;
};

Script Deployment XML

<?xml version="1.0" encoding="UTF-8"?>
<scheduledscript scriptid="customscript_daily_report_ss">
<name>Daily Sales Report</name>
<scriptfile>[/SuiteScripts/ScheduledScripts/daily_report_ss.js]</scriptfile>
<description>Generates and emails daily sales report</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>

<scriptcustomfields>
<scriptcustomfield scriptid="custscript_report_recipient">
<label>Report Recipient</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
<ismandatory>T</ismandatory>
</scriptcustomfield>

<scriptcustomfield scriptid="custscript_day_offset">
<label>Day Offset</label>
<fieldtype>INTEGER</fieldtype>
<defaultvalue>1</defaultvalue>
<description>Days in the past (1 = yesterday)</description>
</scriptcustomfield>
</scriptcustomfields>

<scriptdeployments>
<scriptdeployment scriptid="customdeploy_daily_report_ss">
<status>RELEASED</status>
<title>Daily Report - 6 AM</title>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>F</allroles>
<runasrole>Administrator</runasrole>
<audslctrole>
<role>[Administrator]</role>
</audslctrole>
</scriptdeployment>
</scriptdeployments>
</scheduledscript>

Best Practices

PracticeDescription
Monitor governanceCheck remaining units in loops
Use parametersMake scripts configurable
Implement resumingSave progress for long processes
Log progressAudit log key milestones
Handle errorsWrap in try/catch, notify on failure
Test thoroughlyTest with small batches first

Governance Tips

// Good: Check before expensive operation
if (script.getRemainingUsage() < 1000) {
reschedule(lastId);
return;
}

// Bad: Check only at start
// (may run out mid-process)

// Good: Process in batches
mySearch.run().each((result) => {
processRecord(result);
return processedCount++ < 500;
});

// Bad: Load all at once
const results = mySearch.run().getRange({ start: 0, end: 10000 });

Next Steps