Fix NetSuite Workflow Action Script Email Failures
Troubleshoot NetSuite Workflow Action Script Email Errors: Fix Silent Failures & Surface Unhandled Exceptions in 3 Steps.

On this page
The script deploys without errors. The workflow triggers. But no email arrives, and the execution log shows nothing, not even the debug statements you sprinkled throughout. This silent failure is the hallmark of a Workflow Action Script that hits an unhandled exception before log.debug can flush, or a deployment configuration that suppresses output entirely. Here is how to surface the real error and fix the three most common causes.
Why the Log Shows Nothing
Workflow Action Scripts run in the workflow engine's execution context, not a standard script deployment. If the script deployment's Log Level is set to Error, log.debug calls never write to the execution log. Open the script deployment record (Customization > Scripting > Script Deployments > [Your Deployment]) and set Log Level to Debug. Re-trigger the workflow, then check Customization > Scripting > Script Execution Logs filtered by your script ID.
The first debugging step is always verifying the deployment log level. A script that "runs but logs nothing" is usually just muted.
If logs appear but stop at Workflow Action Started, the exception occurs before the next log.debug. Wrap the entire onAction body in a try/catch that writes log.error with err.stack, the stack trace reveals the exact line. For deeper visibility into what changed on the record, you can also review System Notes (Setup > Company > System Notes) which capture field-level changes across transactions.
The getValue Trap in Search Results
The posted code uses result.getValue('email'). That works on a record.Record object, but search.Result requires the options object:
const emails = results.map(result => result.getValue({ name: 'email' }));Using the shorthand string argument returns undefined for every row, so recipients becomes an array of undefined values. email.send then fails silently because the recipient list is invalid. This is the single most common reason the script "runs" but sends nothing.
Sender Must Be an Internal ID, Not an Email Address
The author parameter in email.send expects the internal ID of an employee or user record, not an email string. If your script parameter custscript_se_sender stores [email protected], the call throws INVALID_EMAIL_AUTHOR. Fix it in one of two ways:
- Store the employee internal ID in the script parameter (preferred).
- Resolve the ID at runtime if you only have the email:
const empSearch = search.create({
type: search.Type.EMPLOYEE,
filters: [['email', 'is', senderEmail]],
columns: ['internalid']
});
const result = empSearch.run().getRange({ start: 0, end: 1 });
const senderId = result[0]?.getValue({ name: 'internalid' });Better: create a List/Record script parameter pointing to the Employee record. The UI forces a valid internal ID, eliminating typos.
Role Filter Returns Zero Rows When Employees Have Multiple Roles
The filter ['role', 'anyof', roleId] matches employees who have that role assigned directly. It does not match employees who inherit the role through a role hierarchy or group membership. If your target recipients receive the role via a parent role, the search returns empty.
Workaround: search the role record for all subordinate role IDs, then filter employees against that full set:
function getSubordinateRoleIds(parentRoleId) {
const roleSearch = search.create({
type: search.Type.ROLE,
filters: [['isinactive', 'is', 'F'], 'AND', ['parent', 'anyof', parentRoleId]],
columns: ['internalid']
});
const ids = [parentRoleId];
roleSearch.run().each(r => { ids.push(r.id); return true; });
return ids;
}
// Then in getEmailAddForRole:
const roleIds = getSubordinateRoleIds(role);
const empSearch = search.create({
type: search.Type.EMPLOYEE,
filters: [
['email', 'isnotempty', ''],
'AND',
['role', 'anyof', roleIds]
],
columns: ['email']
});Parameter Values Can Be Strings, Not Arrays
Script parameters of type Free-Form Text return a single string. If you enter multiple emails as [email protected], [email protected], recipients becomes that string. email.send accepts a string or an array, but your role-lookup function returns an array. Normalize early:
let recipients = scriptObj.getParameter('custscript_se_recipients');
if (typeof recipients === 'string') {
recipients = recipients.split(',').map(e => e.trim()).filter(Boolean);
}Complete Hardened Template
Replace your onAction with this version. It forces debug logging, validates every input, resolves the sender ID, handles role inheritance, and surfaces every error with a stack trace.
/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
*/
define(['N/runtime', 'N/search', 'N/url', 'N/email', 'N/log'],
(runtime, search, url, email, log) => {
const onAction = (scriptContext) => {
try {
log.debug({ title: 'Workflow Action Started', details: 'Execution has begun.' });
const scriptObj = runtime.getCurrentScript();
const suppressError = !!scriptObj.getParameter({ name: 'custscript_se_suppresserror' });
const senderParam = scriptObj.getParameter({ name: 'custscript_se_sender' });
const recipientRole = scriptObj.getParameter({ name: 'custscript_se_recipientrole' });
let recipients = scriptObj.getParameter({ name: 'custscript_se_recipients' });
let body = scriptObj.getParameter({ name: 'custscript_se_body' });
const subject = scriptObj.getParameter({ name: 'custscript_se_subject' });
const recordType = scriptObj.getParameter({ name: 'custscript_se_recordtype' });
const recordId = scriptObj.getParameter({ name: 'custscript_se_recordid' });
// Normalize recipients parameter
if (typeof recipients === 'string') {
recipients = recipients.split(',').map(e => e.trim()).filter(Boolean);
}
// Resolve sender internal ID
let senderId = senderParam;
if (isNaN(senderParam)) {
// Assume it's an email; look up employee by email
const empSearch = search.create({
type: search.Type.EMPLOYEE,
filters: [['email', 'is', senderParam]],
columns: ['internalid']
});
const result = empSearch.run().getRange({ start: 0, end: 1 });
if (result.length === 0) {
throw error.create({
name: 'INVALID_SENDER',
message: `No employee found with email: ${senderParam}`
});
}
senderId = result[0].getValue({ name: 'internalid' });
}
// Fetch recipients by role (with hierarchy)
if (recipientRole) {
recipients = getEmailAddForRole(recipientRole);
}
if (!recipients || recipients.length === 0) {
log.error({ title: 'No Recipients', details: 'Email will not be sent.' });
return;
}
if (!body || !subject) {
log.error({ title: 'Missing Email Details', details: 'Body or subject is missing.' });
return;
}
// Append record link
if (recordType && recordId) {
try {
const viewRecord = url.resolveRecord({
recordType,
recordId,
isEditMode: false
});
body += `<br/><a href="${viewRecord}">View Record</a>`;
} catch (err) {
log.error({ title: 'Error Resolving Record URL', details: err.message });
}
}
log.debug({ title: 'Sending Email', details: { sender: senderId, recipients, subject } });
email.send({
author: senderId,
recipients,
subject,
body
});
log.debug({ title: 'Email Sent Successfully', details: { sender: senderId, recipients } });
} catch (err) {
log.error({ title: 'Unexpected Error', details: { name: err.name, message: err.message, stack: err.stack } });
if (!suppressError) throw err;
}
};
function getEmailAddForRole(roleId) {
try {
// Collect role ID + all subordinate roles
const roleIds = [roleId];
const roleSearch = search.create({
type: search.Type.ROLE,
filters: [
['isinactive', 'is', 'F'],
'AND',
['parent', 'anyof', roleId]
],
columns: ['internalid']
});
roleSearch.run().each(r => { roleIds.push(r.id); return true; });
const empSearch = search.create({
type: search.Type.EMPLOYEE,
filters: [
['email', 'isnotempty', ''],
'AND',
['role', 'anyof', roleIds]
],
columns: ['email']
});
const results = empSearch.run().getRange({ start: 0, end: 1000 });
if (results.length === 0) {
log.error({ title: 'No Employees Found', details: `No employees with role ${roleId} or its subordinates.` });
return [];
}
return results.map(r => r.getValue({ name: 'email' }));
} catch (err) {
log.error({ title: 'Error in getEmailAddForRole', details: { name: err.name, message: err.message, stack: err.stack } });
return [];
}
}
return { onAction };
});Deployment Checklist Before You Test Again
| Setting | Required Value |
|---|---|
| Script Deployment > Log Level | Debug |
Script Parameter custscript_se_sender | List/Record → Employee (not Free-Form Text) |
Script Parameter custscript_se_recipientrole | List/Record → Role |
Script Parameter custscript_se_recipients | Free-Form Text (comma-separated) or leave blank if using role |
| Workflow Action > Trigger On | After Submit (or your chosen event) |
| Workflow > Release Status | Released |
Trigger the workflow, then open Script Execution Logs filtered by your script deployment. You will now see either Email Sent Successfully or a precise Unexpected Error entry with a stack trace pointing to the exact line.
If the log still shows nothing, the workflow action isn't firing, verify the workflow's Initiation and Condition settings, and confirm the record type matches the workflow's base record type. That configuration issue is outside the script, but it's the next place to look when the script itself is clean.


