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 Case | Example |
|---|---|
| Batch processing | Update prices for all items |
| Report generation | Daily sales summary emails |
| Data cleanup | Archive old records |
| Integrations | Sync data with external systems |
| Notifications | Send 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
- Customization → Scripting → Script Deployments
- Find your deployment
- Click Schedule tab
- 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
| Practice | Description |
|---|---|
| Monitor governance | Check remaining units in loops |
| Use parameters | Make scripts configurable |
| Implement resuming | Save progress for long processes |
| Log progress | Audit log key milestones |
| Handle errors | Wrap in try/catch, notify on failure |
| Test thoroughly | Test 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
- Map/Reduce Script - Process large datasets in parallel
- RESTlet - Create REST APIs
- Email Notifications Scenario - Real-world example