Workflow Action Script Development
Workflow Action Scripts provide custom functionality within NetSuite workflows, allowing complex business logic that isn't possible with built-in workflow actions.
When to Use Workflow Action Scripts
| Use Case | Example |
|---|---|
| Complex calculations | Calculate discount based on multiple factors |
| External API calls | Send data to third-party system |
| Record creation | Create related records |
| Custom validations | Complex validation beyond workflow conditions |
| Data transformation | Format or transform field values |
Workflow Integration Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ WORKFLOW ACTION SCRIPT IN CONTEXT │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ WORKFLOW │
│ (Invoice Approval Workflow) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ STATE: Pending Approval │
│ ────────────────────────────────────────────────────────────────│
│ Entry Actions: │
│ • Set status field │
│ • Send notification │
└──────────────────────────────┬───────────────────────────────────┘
│
Transition: "Approve"
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ TRANSITION ACTIONS │
│ ────────────────────────────────────────────────────────────────│
│ 1. [Built-in] Set Field Value │
│ 2. [Custom Script] ◄── WORKFLOW ACTION SCRIPT │
│ • Calculate commission │
│ • Create approval record │
│ • Call external API │
│ 3. [Built-in] Send Email │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ STATE: Approved │
│ ────────────────────────────────────────────────────────────────│
│ Entry Actions: │
│ • Update status │
│ • [Custom Script] Post to accounting system │
└──────────────────────────────────────────────────────────────────┘
Script Execution Points
┌─────────────────────────────────────────────────────────────────────────────┐
│ WHERE WORKFLOW ACTIONS RUN │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ON STATE ENTRY │
│ ───────────────────────────────────────────────────────────────────│
│ When workflow enters a state │
│ │
│ ┌─────────┐ ┌─────────────────┐ │
│ │ State A │────────►│ State B │ │
│ └─────────┘ │ ┌───────────┐ │ │
│ │ │ Entry │ │ ◄── Script runs here │
│ │ │ Actions │ │ │
│ │ └───────────┘ │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ON STATE EXIT │
│ ───────────────────────────────────────────────────────────────────│
│ When workflow leaves a state │
│ │
│ ┌─────────────────┐ ┌─────────┐ │
│ │ State A │────────►│ State B │ │
│ │ ┌───────────┐ │ │
│ │ │ Exit │ │ ◄── Script runs here │
│ │ │ Actions │ │ │
│ │ └───────────┘ │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ON TRANSITION │
│ ───────────────────────────────────────────────────────────────────│
│ During state transition │
│ │
│ ┌─────────┐ Transition ┌─────────┐ │
│ │ State A │─────────────►│ State B │ │
│ └─────────┘ ▲ └─────────┘ │
│ │ │
│ ┌──────────────┐ │
│ │ Transition │ ◄── Script runs here │
│ │ Actions │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ON BUTTON ACTION │
│ ───────────────────────────────────────────────────────────────────│
│ When user clicks custom workflow button │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Record Form │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ [Approve] [Reject] [Custom Action] │ │ │
│ │ └────────────────────────────▲────────────────────────┘ │ │
│ └───────────────────────────────│─────────────────────────────┘ │
│ │ │
│ Script runs when clicked │
└─────────────────────────────────────────────────────────────────────┘
Basic Workflow Action Script Structure
/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
* @NModuleScope SameAccount
*/
define(['N/record', 'N/search', 'N/runtime', 'N/log'],
(record, search, runtime, log) => {
/**
* Entry point for workflow action
* @param {Object} context
* @param {Record} context.newRecord - The record being processed
* @param {Record} context.oldRecord - Previous state (if applicable)
* @param {string} context.workflowId - Internal ID of the workflow
* @param {string} context.type - Trigger type
* @returns {string|number|null} - Value to store in workflow field
*/
const onAction = (context) => {
log.debug('Workflow Action', 'Starting execution');
try {
const rec = context.newRecord;
const recordId = rec.id;
const recordType = rec.type;
log.debug('Context', {
recordId: recordId,
recordType: recordType,
workflowId: context.workflowId
});
// Your custom logic here
const result = processRecord(rec);
log.debug('Workflow Action', `Completed with result: ${result}`);
// Return value is stored in workflow result field
return result;
} catch (e) {
log.error('Workflow Action Error', e.message);
throw e; // Re-throw to fail the workflow action
}
};
/**
* Custom processing logic
*/
const processRecord = (rec) => {
// Add your business logic
return 'SUCCESS';
};
return { onAction };
});
Reading Workflow Parameters
Workflows can pass parameters to scripts:
/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
*/
define(['N/runtime', 'N/log'],
(runtime, log) => {
const onAction = (context) => {
const script = runtime.getCurrentScript();
// Get workflow-defined parameters
const approverEmail = script.getParameter({
name: 'custscript_approver_email'
});
const notificationTemplate = script.getParameter({
name: 'custscript_email_template'
});
const threshold = script.getParameter({
name: 'custscript_amount_threshold'
}) || 1000;
log.debug('Parameters', {
approverEmail,
notificationTemplate,
threshold
});
// Use parameters in logic
const amount = context.newRecord.getValue({ fieldId: 'total' });
if (amount > threshold) {
// Handle high-value transaction
}
return 'PROCESSED';
};
return { onAction };
});
Return Values
┌─────────────────────────────────────────────────────────────────────────────┐
│ RETURN VALUE BEHAVIOR │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Return Value → Stored in Workflow Result Field │
│ ────────────────────────────────────────────────────────────────│
│ │
│ return "APPROVED"; // String value │
│ return 12345; // Number (e.g., record ID) │
│ return true; // Boolean │
│ return null; // No value (action completed) │
│ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ USE IN WORKFLOW CONDITIONS │
│ ────────────────────────────────────────────────────────────────│
│ │
│ Condition: {workflow.result} = "APPROVED" │
│ │ │
│ └──► Transition to "Approved" state │
│ │
│ Condition: {workflow.result} = "REJECTED" │
│ │ │
│ └──► Transition to "Rejected" state │
│ │
└──────────────────────────────────────────────────────────────────┘
Complete Example: Approval Workflow Action
/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
* @NModuleScope SameAccount
* @description Handles invoice approval with validation and notifications
*/
define(['N/record', 'N/search', 'N/email', 'N/runtime', 'N/url', 'N/log', 'N/error'],
(record, search, email, runtime, url, log, error) => {
// Configuration
const CONFIG = {
approvalRecordType: 'customrecord_invoice_approval',
statusField: 'custrecord_ia_status',
invoiceField: 'custrecord_ia_invoice',
approverField: 'custrecord_ia_approver',
notesField: 'custrecord_ia_notes',
amountField: 'custrecord_ia_amount',
dateField: 'custrecord_ia_date'
};
/**
* Main workflow action entry point
*/
const onAction = (context) => {
log.audit('Approval Action', 'Starting');
try {
const invoice = context.newRecord;
const invoiceId = invoice.id;
// Get script parameters
const script = runtime.getCurrentScript();
const actionType = script.getParameter({
name: 'custscript_action_type'
}) || 'approve';
log.debug('Parameters', {
invoiceId: invoiceId,
actionType: actionType
});
// Execute based on action type
let result;
switch (actionType.toLowerCase()) {
case 'submit':
result = submitForApproval(invoice);
break;
case 'approve':
result = approveInvoice(invoice);
break;
case 'reject':
result = rejectInvoice(invoice);
break;
default:
throw error.create({
name: 'INVALID_ACTION',
message: `Unknown action type: ${actionType}`
});
}
log.audit('Approval Action', `Completed: ${result}`);
return result;
} catch (e) {
log.error('Approval Action Error', e.message);
throw e;
}
};
/**
* Submit invoice for approval
*/
const submitForApproval = (invoice) => {
const invoiceId = invoice.id;
const amount = parseFloat(invoice.getValue({ fieldId: 'total' })) || 0;
// Find appropriate approver based on amount
const approver = findApprover(amount, invoice);
if (!approver) {
throw error.create({
name: 'NO_APPROVER',
message: 'No approver found for this invoice amount'
});
}
// Create approval record
const approvalRecord = record.create({
type: CONFIG.approvalRecordType
});
approvalRecord.setValue({ fieldId: CONFIG.invoiceField, value: invoiceId });
approvalRecord.setValue({ fieldId: CONFIG.approverField, value: approver.id });
approvalRecord.setValue({ fieldId: CONFIG.amountField, value: amount });
approvalRecord.setValue({ fieldId: CONFIG.statusField, value: 1 }); // Pending
approvalRecord.setValue({ fieldId: CONFIG.dateField, value: new Date() });
const approvalId = approvalRecord.save();
// Send notification to approver
sendApprovalNotification(invoice, approver, approvalId);
// Update invoice status
record.submitFields({
type: invoice.type,
id: invoiceId,
values: {
'custbody_approval_status': 'pending',
'custbody_pending_approver': approver.id
}
});
log.audit('Submit for Approval', `Approval record ${approvalId} created`);
return 'PENDING';
};
/**
* Approve the invoice
*/
const approveInvoice = (invoice) => {
const invoiceId = invoice.id;
const currentUser = runtime.getCurrentUser();
// Find and update approval record
const approvalId = findApprovalRecord(invoiceId);
if (approvalId) {
record.submitFields({
type: CONFIG.approvalRecordType,
id: approvalId,
values: {
[CONFIG.statusField]: 2, // Approved
'custrecord_ia_approved_by': currentUser.id,
'custrecord_ia_approved_date': new Date()
}
});
}
// Update invoice
record.submitFields({
type: invoice.type,
id: invoiceId,
values: {
'custbody_approval_status': 'approved',
'custbody_approved_by': currentUser.id,
'custbody_approved_date': new Date()
}
});
// Send confirmation
sendApprovalConfirmation(invoice, 'approved');
log.audit('Approve Invoice', `Invoice ${invoiceId} approved`);
return 'APPROVED';
};
/**
* Reject the invoice
*/
const rejectInvoice = (invoice) => {
const invoiceId = invoice.id;
const currentUser = runtime.getCurrentUser();
// Get rejection reason from script parameter
const script = runtime.getCurrentScript();
const rejectionReason = script.getParameter({
name: 'custscript_rejection_reason'
}) || 'Not specified';
// Find and update approval record
const approvalId = findApprovalRecord(invoiceId);
if (approvalId) {
record.submitFields({
type: CONFIG.approvalRecordType,
id: approvalId,
values: {
[CONFIG.statusField]: 3, // Rejected
[CONFIG.notesField]: rejectionReason,
'custrecord_ia_rejected_by': currentUser.id,
'custrecord_ia_rejected_date': new Date()
}
});
}
// Update invoice
record.submitFields({
type: invoice.type,
id: invoiceId,
values: {
'custbody_approval_status': 'rejected',
'custbody_rejection_reason': rejectionReason
}
});
// Send rejection notification
sendApprovalConfirmation(invoice, 'rejected', rejectionReason);
log.audit('Reject Invoice', `Invoice ${invoiceId} rejected: ${rejectionReason}`);
return 'REJECTED';
};
/**
* Find appropriate approver based on amount thresholds
*/
const findApprover = (amount, invoice) => {
// Get subsidiary for approver lookup
const subsidiary = invoice.getValue({ fieldId: 'subsidiary' });
// Search for approval thresholds
const thresholdSearch = search.create({
type: 'customrecord_approval_threshold',
filters: [
['custrecord_at_subsidiary', 'anyof', subsidiary],
'AND',
['custrecord_at_min_amount', 'lessthanorequalto', amount],
'AND',
['isinactive', 'is', 'F']
],
columns: [
'custrecord_at_approver',
search.createColumn({
name: 'custrecord_at_min_amount',
sort: search.Sort.DESC
})
]
});
let approver = null;
thresholdSearch.run().each((result) => {
approver = {
id: result.getValue('custrecord_at_approver'),
name: result.getText('custrecord_at_approver')
};
return false; // Get first (highest threshold match)
});
// Fallback to default approver
if (!approver) {
const defaultApprover = runtime.getCurrentScript().getParameter({
name: 'custscript_default_approver'
});
if (defaultApprover) {
approver = {
id: defaultApprover,
name: 'Default Approver'
};
}
}
return approver;
};
/**
* Find existing approval record for invoice
*/
const findApprovalRecord = (invoiceId) => {
const approvalSearch = search.create({
type: CONFIG.approvalRecordType,
filters: [
[CONFIG.invoiceField, 'anyof', invoiceId],
'AND',
[CONFIG.statusField, 'anyof', 1] // Pending
],
columns: ['internalid']
});
let approvalId = null;
approvalSearch.run().each((result) => {
approvalId = result.id;
return false;
});
return approvalId;
};
/**
* Send approval request notification
*/
const sendApprovalNotification = (invoice, approver, approvalId) => {
const invoiceNumber = invoice.getValue({ fieldId: 'tranid' });
const amount = invoice.getValue({ fieldId: 'total' });
const vendor = invoice.getText({ fieldId: 'entity' });
// Build approval URL
const approvalUrl = url.resolveRecord({
recordType: invoice.type,
recordId: invoice.id,
isEditMode: false
});
const emailBody = `
<html>
<body>
<h2>Invoice Approval Required</h2>
<p>An invoice requires your approval:</p>
<table style="border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Invoice #:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${invoiceNumber}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Vendor:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${vendor}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Amount:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$${parseFloat(amount).toFixed(2)}</td>
</tr>
</table>
<p><a href="${approvalUrl}" style="background: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">Review Invoice</a></p>
</body>
</html>
`;
email.send({
author: runtime.getCurrentUser().id,
recipients: [approver.id],
subject: `Approval Required: Invoice ${invoiceNumber}`,
body: emailBody
});
log.debug('Notification Sent', `Email sent to ${approver.name}`);
};
/**
* Send approval/rejection confirmation
*/
const sendApprovalConfirmation = (invoice, status, reason = '') => {
const invoiceNumber = invoice.getValue({ fieldId: 'tranid' });
const createdBy = invoice.getValue({ fieldId: 'createdby' });
let subject, body;
if (status === 'approved') {
subject = `Invoice ${invoiceNumber} Approved`;
body = `<p>Your invoice ${invoiceNumber} has been approved.</p>`;
} else {
subject = `Invoice ${invoiceNumber} Rejected`;
body = `<p>Your invoice ${invoiceNumber} has been rejected.</p>
<p><strong>Reason:</strong> ${reason}</p>`;
}
email.send({
author: runtime.getCurrentUser().id,
recipients: [createdBy],
subject: subject,
body: `<html><body>${body}</body></html>`
});
};
return { onAction };
});
Script Deployment XML
<?xml version="1.0" encoding="UTF-8"?>
<workflowactionscript scriptid="customscript_invoice_approval_wa">
<name>Invoice Approval Action</name>
<scriptfile>[/SuiteScripts/WorkflowActions/invoice_approval_wa.js]</scriptfile>
<description>Handles invoice approval workflow actions</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>
<scriptcustomfields>
<scriptcustomfield scriptid="custscript_action_type">
<label>Action Type</label>
<fieldtype>TEXT</fieldtype>
<description>submit, approve, or reject</description>
</scriptcustomfield>
<scriptcustomfield scriptid="custscript_rejection_reason">
<label>Rejection Reason</label>
<fieldtype>TEXTAREA</fieldtype>
</scriptcustomfield>
<scriptcustomfield scriptid="custscript_default_approver">
<label>Default Approver</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</scriptcustomfield>
</scriptcustomfields>
<scriptdeployments>
<scriptdeployment scriptid="customdeploy_invoice_approval_wa">
<status>RELEASED</status>
<title>Invoice Approval Action</title>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<recordtype>vendorbill</recordtype>
</scriptdeployment>
</scriptdeployments>
</workflowactionscript>
Workflow XML Configuration
When using workflow action scripts in workflows:
<?xml version="1.0" encoding="UTF-8"?>
<workflow scriptid="custworkflow_invoice_approval">
<name>Invoice Approval Workflow</name>
<recordtype>vendorbill</recordtype>
<workflowstates>
<workflowstate scriptid="workflowstate_pending">
<name>Pending Approval</name>
<workflowactions>
<triggertype>ENTRY</triggertype>
<setfieldvalueaction>
<field>STDBODYAPPROVALSTATUS</field>
<value>Pending Approval</value>
</setfieldvalueaction>
<!-- Custom Script Action -->
<customaction>
<scriptid>customscript_invoice_approval_wa</scriptid>
<scriptdeploymentid>customdeploy_invoice_approval_wa</scriptdeploymentid>
<parameters>
<parameter>
<name>custscript_action_type</name>
<value>submit</value>
</parameter>
</parameters>
</customaction>
</workflowactions>
</workflowstate>
</workflowstates>
</workflow>
Adding Workflow Action to Workflow (UI)
┌─────────────────────────────────────────────────────────────────────────────┐
│ ADDING SCRIPT ACTION IN UI │
└─────────────────────────────────────────────────────────────────────────────┘
1. Navigate: Customization → Workflow → Workflows
2. Select your workflow
3. Click on state where action should run
4. Add new action:
┌──────────────────────────────────────────────────────────────────┐
│ Add Action │
│ ────────────────────────────────────────────────────────────────│
│ │
│ Action Type: [Custom Action ▼] │
│ │
│ Script: [Invoice Approval Action ▼] │
│ │
│ Deployment: [Invoice Approval Action ▼] │
│ │
│ Parameters: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Action Type: [approve ] │ │
│ │ Rejection Reason: [ ] │ │
│ │ Default Approver: [John Smith ▼] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Result Field: [custbody_workflow_result ▼] │
│ (Stores the return value from script) │
│ │
│ [Cancel] [Save] │
└──────────────────────────────────────────────────────────────────┘
5. Use result in conditions:
Condition: {custbody_workflow_result} = "APPROVED"
→ Transition to Approved state
Best Practices
| Practice | Description |
|---|---|
| Keep actions focused | One action per script |
| Return meaningful values | Use for workflow branching |
| Handle errors | Log and re-throw for visibility |
| Use parameters | Make scripts reusable |
| Test thoroughly | Test all workflow paths |
| Log key events | Audit log for troubleshooting |
Governance Limits
┌─────────────────────────────────────────────────────────────────────────────┐
│ WORKFLOW ACTION SCRIPT LIMITS │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Governance Units: 1,000 per execution │
│ ────────────────────────────────────────────────────────────────│
│ • Keep actions lightweight │
│ • For heavy processing, trigger scheduled script │
│ • Monitor usage with runtime.getCurrentScript() │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ If Need More Processing: │
│ ────────────────────────────────────────────────────────────────│
│ • Trigger scheduled script from workflow action │
│ • Use Map/Reduce for bulk operations │
│ • Queue work for async processing │
└──────────────────────────────────────────────────────────────────┘
Triggering Other Scripts
const task = require('N/task');
const onAction = (context) => {
// For heavy processing, trigger scheduled script
const scriptTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: 'customscript_heavy_processing',
deploymentId: 'customdeploy_heavy_processing',
params: {
'custscript_record_id': context.newRecord.id
}
});
scriptTask.submit();
return 'QUEUED';
};
Next Steps
- Invoice Approval Scenario - Complete workflow example
- Custom Records - Create approval tracking records
- Workflows - Configure workflow definitions