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​