Skip to main content

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 CaseExample
Complex calculationsCalculate discount based on multiple factors
External API callsSend data to third-party system
Record creationCreate related records
Custom validationsComplex validation beyond workflow conditions
Data transformationFormat 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

PracticeDescription
Keep actions focusedOne action per script
Return meaningful valuesUse for workflow branching
Handle errorsLog and re-throw for visibility
Use parametersMake scripts reusable
Test thoroughlyTest all workflow paths
Log key eventsAudit 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