Hooks are custom code that can run at different stages during the execution of a flow to modify the behavior of export and import processes.
Tip
Press the Home key to return to the top of this article (Fn + Left Arrow for Mac).
Hooks must be created in an integration tile
Any hook that calls an export, lookup, or import must be in an integration tile rather than a standalone flow. You cannot call a resource inside a hook if your hook is in a standalone flow. To call a resource inside a hook, create a new integration tile and then add your flow to the tile.
SuiteScript hooks are functions executed during a flow within NetSuite using scripts (RESTlets, Scheduled or MapReduce depending upon the hook type and NetSuite version). Hooks should be configured in a JavaScript file in the NetSuite File Cabinet as a global function. A single file can support multiple functions for multiple hooks. When the flow runs, the Celigo platform loads and executes the hooks. NetSuite has SuiteScript available in two versions, SuiteScript 1.0 and SuiteScript 2.0, both of which are supported in the Celigo platform.
You can leverage full SuiteScript capabilities in both exports and imports to customize data. Some simple examples of what you can do with a SuiteScript hook include performing a search in NetSuite or loading a record via the SuiteScript APIs within NetSuite to add additional data or make a decision before exporting or importing a record. Using NetSuite hooks during a flow saves external API calls needed from outside of NetSuite to achieve the same functionality, thereby optimizing the flow's performance.
In a NetSuite export, you can use the pre send hook, which is invoked after the data is collected but before it’s sent to the Celigo platform. In a NetSuite import, the data is sent from the Celigo platform to NetSuite to create or update records. The data goes through several stages, during which you can use the pre map, post map, and post submit hooks to configure your data.
Learn more about SuiteScript 2.0 and the integrator.io SuiteApp.
-
Click the Hooks icon in Flow builder on your export or import.
-
In the SuiteScript hooks section, provide the Function and the File internal id for the SuiteScript hook you are linking to.
Before you add a SuiteScript hook to an export or import you must find the File internal ID.
Note
The File internal id (or file path) is the ID of the script file stored in the NetSuite File Cabinet. It must be accessible using the role you used to configure your NetSuite connection.
-
Navigate to NetSuite → Documents → Files → File Cabinet.
-
Find the correct folder or file.
-
The file’s internal ID is located next to the filename. A file path can be constructed using the folder path and filename.
The pre send hook is invoked before records leave NetSuite. Use this hook when gathering, modifying, or manipulating data within NetSuite outside of what the standard search has collected. This hook works within NetSuite, which helps to optimize the network API calls and NetSuite concurrency limits, making it the best choice for data modification before it's sent to the Celigo platform.
In a scheduled export, the hook is invoked for each batch of records based on the batch size set in the export, or based on the default batch size (1000). The function is executed as a RESTlet.
In real-time exports, SuiteApp SuiteScript 1.0 versions are executed as user events, SuiteApp SuiteScript 2.0 versions are executed as Map/Reduce scripts, and SuiteBundle SuiteScript 1.0 versions are executed as scheduled scripts.
This hook is helpful when:
-
Attaching more data by running searches in NetSuite via SuiteScript
-
Modifying/creating NetSuite records before they are exported
-
Fetching lookup fields from a parent (sales order) record and appending the input data
This is a function stub for a scheduled export.
/* * SCHEDULED EXPORT PRESEND STUB * The function will be passed one 'options' argument that has the following fields: * 'data' - an array of records representing one page of data * 'recordType': the recordType on current running NS Export * 'searchId': the searchId on current running NS Export * 'skipGrouping': Boolean flag based on current running export * 'delta': delta exports only. * 'once': once export field only * '_exportId' - the _exportId currently running. * '_connectionId' - the _connectionId currently running. * '_flowId' - the _flowId currently running. * '_integrationId' - the _integrationId currently running. * 'settings' - all custom settings in scope for the export currently running. * 'testMode' - Boolean flag that executes script only on test mode and preview/send actions. * * The function needs to return an object that has the following fields: * 'data' - your modified data. * 'errors' - your modified errors. * 'errorsAndRetryData' - return brand new errors linked to retry data: [{retryData: <record>, errors: [<error>]}]. * Throwing an exception will signal a fatal error and stop the flow. */ function preSendHookScheduledExport (options) { var responseData = { data: [], errors: [] } responseData.data = options.data return responseData }
Scheduled export example: This code fetches the email from customer lookup fields from a parent (sales order) record and appends the input data.
define(['N/search'], function (NSearch) { var preSend = function (options) { _.each(options.data, function (salesOrder, index) { var lookupfieldValue = NSearch.lookupFields({ type: 'customer', id: salesOrder.entity.internalid, columns: ['email'] }); salesOrder.customerEmail = lookupfieldValue['email'] }) return { data: options.data, errors: options.errors, } } return { preSend: preSend } })
Example for a scheduled export.
function preSendHookScheduledExport (options) { var responseData = { data: [], errors: [] } // record will be in an array var records = options.data for (var i = 0; i < records.length; i++) { var customerId = records[i].entity && records[i].entity.internalid if (customerId) { var customerDetails = nlapiLookupField('customer', customerId, ['email', 'subsidiary']) records[i].customerInformation = customerDetails } response.data.push(records[i]); } return responseData }
Note
In the above example, the nlapilookupfield
will be executed for every record to get customer IDs. To optimize governance points, you could execute nlapisearchrecord
once instead of the nlapilookupfield
multiple times.
This is the function stub for a real-time export.
/* * REALTIME EXPORT PRESEND STUB * The function will be passed one 'options' argument that has the following fields: * 'data' - an array of records representing one page of data. * '_exportId' - the _exportId currently running. * 'settings' - all custom settings in scope for the export currently running. * * The function needs to return an object that has the following fields: * 'data' - your modified data. * 'errors' - your modified errors. ****************************************************************************** */ function preSendHookRealTimeExport (options) { var responseData = { data: [], errors: [] } responseData.data.push(options.data) return responseData }
Note
For SuiteScript 1.0, input data will be an object, whereas for SuiteScript 2.0 input data will be an array for real-time exports.
function preSendHookRealTimeExport (options) { var responseData = { data: [], errors: [] } //record will be in object for RT Export SS1.0 and array for SS2.0 var record = options.data var customerId = record.entity && record.entity.internalid if (customerId) { var customerDetails = nlapiLookupField('customer', customerId, ['email', 'subsidiary']) record.customerInformation = customerDetails } responseData.data.push(record) return responseData }
The pre map hook is invoked before source records are mapped to NetSuite records. It is invoked on each batch of records based on the batch size. This hook can reformat the record's fields before mapping, or apply logic to the data within an import before the mapping process. Changes made to source records in this hook will persist only for the duration of the import, and will not carry over to downstream applications in your flow. This hook is a great place to execute logic on batches of records to optimize the mapping experience in the Celigo platform.
Note
This hook is available only in SuiteApp SuiteScript 1.0 and SuiteBundle SuiteScript 1.0.
This hook is helpful when:
-
Reformatting fields or object structures to avoid complex mappings.
-
You can use NetSuite features and settings to provide a better experience.
-
-
Performing calculations on lists to avoid tedious mapping expressions.
-
You can use NetSuite for a better mapping experience. For example, making mapping decisions for inventory transactions based on the item type and using SuiteScript to search for item inventory details like bin, serial, and lot.
-
-
Loading the destination record to pre-populate data needed by mapping logic.
-
You can fetch data from within NetSuite, avoiding additional API calls.
-
/* * IMPORT PREMAP STUB * The name of the function can be changed to anything you like. * The function will be passed one ‘options’ argument that has the following fields: * ‘data’ - an array of records representing the page of data before it has been mapped. A record can be an object {} or array [] depending on the data source. * '_importId' - the _importId currently running. * '_connectionId' - the _connectionId currently running. * '_flowId' - the _flowId currently running. * '_integrationId' - the _integrationId currently running. * 'settings' - all custom settings in scope for the import currently running. * 'testMode' - Boolean flag that executes script only on test mode and preview/send actions. * The function needs to return an array, and the length MUST match the options.data array length. * Each element in the array represents the actions that should be taken on the record at that index. * Each element in the array should have the following fields: * 'data' - the modified/unmodified record that should be passed along for processing. * 'errors' - used to report one or more errors for the specific record. Each error must have the following structure: {code: '', message: '', source: ‘’ } * Returning an empty object {} for a specific record will indicate that the record should be ignored. * Returning both 'data' and 'errors' for a specific record will indicate that the record should be processed but errors should also be logged. * Throwing an exception will fail the entire page of records. */ function preMapHook(options) { var preMapResponse = [] for (var i = 0; i < options.data.length; i++) { preMapResponse.push({ data: options.data[i] }) } return preMapResponse }
function preMapHook(options) { var preMapResponse = [] for (var i = 0; i < options.data.length; i++) { //Reformatting fields or object structures to avoid complex mappings for each record. var record = options.data[i] // Initialize total to 0 for each record var totalAmount = 0; // Check if the record has items and iterate over them if (record.items && Array.isArray(record.items)) { record.items.forEach(item => { // Calculate total based on item properties, e.g., quantity and price totalAmount += (item.quantity || 0) * (item.price || 0); }); } record.totalAmount = totalAmount; // Return the modified record preMapResponse.push({ data: record }) } return preMapResponse }
The post map hook is invoked after source records are mapped to NetSuite records. This hook is invoked on a batch of records based on the batch size. It can validate, update, or ignore records before submitting them to NetSuite. Changes made to source records in this hook will only persist for the duration of the import, and will not carry over to downstream applications in your flow. This hook is a great place to execute logic on batches of records to optimize the final payload-building experience in the Celigo platform. This hook is helpful when:
-
Removing line items if the item lookup is not found before submitting the record into NetSuite.
-
Performing custom logic based on NetSuite setup and input data. For example, while importing transactions, use this hook to perform tax calculations, handle special promotions and discounts, and clean up line items by looking or searching for data within NetSuite.
-
Reformatting fields or object structures to avoid complex handlebars expressions.
-
Performing calculations on lists to avoid tedious handlebars expressions.
-
Loading the destination record to dynamically change the fields being submitted.
Note
The Celigo platform’s post map hook is unavailable for NetSuite imports. You should only use the SuiteScript post map hook for NetSuite imports.
/* IMPORT POSTMAP STUB * The name of the function can be changed to anything you like. * The function will be passed one argument ‘options’ that has the following fields: * ‘preMapData’ - an array of records representing the page of data before it was mapped. A record can be an object {} or array [] depending on the data source. * ‘postMapData’ - an array of records representing the page of data after it was mapped. A record can be an object {} or array [] depending on the data source. * '_importId' - the _importId currently running. * '_connectionId' - the _connectionId currently running. * '_flowId' - the _flowId currently running. * '_integrationId' - the _integrationId currently running. * 'settings' - all custom settings in scope for the import currently running. * 'testMode' - Boolean flag that executes script only on test mode and preview/send actions. * The function needs to return an array, and the length MUST match the options.data array length. * Each element in the array represents the actions that should be taken on the record at that index. * Each element in the array should have the following fields: * 'data' - the modified/unmodified record that should be passed along for processing. * 'errors' - used to report one or more errors for the specific record. Each error must have the following structure: {code: '', message: '', source: ‘’ } * Returning an empty object {} for a specific record will indicate that the record should be ignored. * Returning both 'data' and 'errors' for a specific record will indicate that the record should be processed but errors should also be logged. * Throwing an exception will fail the entire page of records. */ function postMap (options) { var postMapResponse = [] for (var i = 0; i < options.postMapData.length; i++) { // sample code to modify postMapResponse.push({ data: options.postMapData[i] }) } return postMapResponse }
The examples below demonstrate how to remove line items from a record when the item lookup returns null, ensuring that only valid items are submitted into NetSuite for SuiteScript 1.0 and 2.0.
Internally, Celigo has a framework that converts the mapped data into an intermediary object, which Celigo calls $R. This object contains nested JSON objects for NetSuite fields, sublists and some additional options. Finally, Celigo converts this object into SuiteScript API calls (such as nlapiCreateRecord and nlapiSetFieldValue,) and submits the record to NetSuite. This allows users to easily deal with JSON objects rather than using NetSuite APIs. The following example is of a customer $R record, which is the output of a post map hook.
new $R({ nlobjRecordType: "customer", nlobjFieldIds: { isperson: true, firstname: "TESTING", lastname: "LASTNAME", entitystatus: 15 }, nlobjSublistIds: { addressbook: [{ city: "City1", state: "CA", country: "US", defaultbilling: true, defaultshipping: false }, { city: "City2", state: "OH", country: "US", defaultbilling: false, defaultshipping: true }], partners: [{ partner: 201, contribution: '25%' }, { partner: 217, contribution: '75%' }] }, submitRecordOptions: { doSourcing: true, ignoreMandatoryFields: true, reloadAfterSubmit: true } });
define(['N/log'], function (NLog) { var postMap = function (options) { var postMapResponse = [] _.each(options.postMapData, function (record, index) { var newLines = [] if (record.nlobjSublistIds && record.nlobjSublistIds.item) { NLog.debug({ title: 'removeEmptyLineItem: before', details: record.nlobjSublistIds.item }) _.each(record.nlobjSublistIds.item, function (line, index) { if (line.item) { newLines.push(line); } }) NLog.debug({ title: 'removeEmptyLineItem: after', details: newLines }) } record.nlobjSublistIds.item = newLines postMapResponse.push({ data: record }) }) return postMapResponse } return { postMap: postMap } })
function postMap (options) { var postMapResponse = [] for (var i = 0; i < options.postMapData.length; i++) { var record = options.postMapData[i] var newLines = [] if (record.nlobjSublistIds && record.nlobjSublistIds.item && record.nlobjSublistIds.item.lines) { nlapiLogExecution('DEBUG', 'removeEmptyLineItem Before: ', record.nlobjSublistIds.item.lines); for (var j = 0; j < record.nlobjSublistIds.item.lines.length; j++) { if (record.nlobjSublistIds.item.lines[j].item) { newLines.push(record.nlobjSublistIds.item.lines[j]); } } } record.nlobjSublistIds.item.lines = newLines; nlapiLogExecution('DEBUG', 'removeEmptyLineItem after: ', record.nlobjSublistIds.item.lines); postMapResponse.push({ data: record }) } return postMapResponse }
The post submit hook is invoked after the records are submitted to NetSuite. This hook is invoked on a batch of records based on the batch size. It can enhance error messages and/or modify the response objects returned by NetSuite. Changes made to the response object are localized and must be mapped back into the source record using response mapping to be visible in the flow. This hook is a great place to execute logic on batches of records to mitigate errors and to optimize response structures needed by subsequent steps in the flow.
This hook is helpful when:
-
Reviewing a record that was created or updated and taking corrective actions.
-
For example, order import variance calculation, where the external order total may be $10.27, but when saved in NetSuite it becomes $10.20 due to NetSuite rolling issues or tax rules being applied differently. You can use this hook to update the record with the actual delta/variance, by using nlapiSubmitField for example, and reporting such orders.
-
-
Loading additional data from NetSuite and appending it to the response. For example, while saving an order, you can return its internalId, tranId, and status.
/* * IMPORT POSTSUBMIT STUB * The name of the function can be changed to anything you like. * * The function will be passed one ‘options’ argument that has the following fields: * ‘preMapData’ - an array of records representing the page of data before it was mapped. A record can be an object {} or array [] depending on the data source. * ‘postMapData’ - an array of records representing the page of data after it was mapped. A record can be an object {} or array [] depending on the data source. * ‘responseData’ - an array of responses for the page of data that was submitted to the import application. An individual response will have the following fields: * ‘statusCode’ - 200 is a success. 422 is a data error. 403 means the connection went offline. * ‘errors’ - [{code: '', message: '', source: ‘’}] * ‘ignored’ - true if the record was filtered/skipped, false otherwise. * ‘id’ - the id from the import application response. * ‘_json’ - the complete response data from the import application. * ‘dataURI’ - if possible, a URI for the data in the import application (populated only for errored records). * '_importId' - the _importId currently running. * '_connectionId' - the _connectionId currently running. * '_flowId' - the _flowId currently running. * '_integrationId' - the _integrationId currently running. * 'settings' - all custom settings in scope for the import currently running. * 'testMode' - Boolean flag that executes script only on test mode and preview/send actions. * * The function needs to return the responseData array provided by options.responseData. The length of the responseData array MUST remain unchanged. Elements within the responseData array can be modified to enhance error messages, modify the complete _json response data, etc... * Throwing an exception will fail the entire page of records. */ function postSubmit (options) { return options.responseData }
The examples below retrieve the tranid
and status
from a sales order and appends this information to the _json
object for SuiteScript 1.0 and 2.0.
define(['N/search'], function (NSearch) { var postSubmit = function (options) { _.each(options.responseData, function (response, index) { if (response.statusCode == 200) { var lookupfieldValue = NSearch.lookupFields({ type: 'salesorder', id: response.id, columns: ['tranid', 'status'] }) var nsResponse = response._json || {} nsResponse.tranid = lookupfieldValue.tranid nsResponse.status = lookupfieldValue.status && lookupfieldValue.status[0].value response._json = nsResponse } }) return options.responseData } return { postSubmit: postSubmit } }) */
function postSubmit(options) { for (var i = 0; i < options.responseData.length; i++) { if (options.responseData[i].statusCode == 200) { var nsResponse = options.responseData[i]._json || {} var salesOrderDetails = nlapiLookupField('salesorder', options.responseData[i].id, ['tranid']) nsResponse.tranid = salesOrderDetails.tranid options.responseData[i]._json = nsResponse } } return options.responseData }
Yes, you can use both of these hooks in conjunction. Depending on the Suitescript version and whether you have JavaScript hooks also configured, the workflow to process data is as follows:
-
pre map (NetSuite SuiteScript 1.0 only)
-
post map (NetSuite)
-
post submit (NetSuite)
Currently, the Celigo platform recommends writing SuiteScript 2.0 hooks because it is the latest SuiteScript major version. While the platform also supports SuiteScript 1.0, we recommend moving to SuiteScript 2.0 because NetSuite no longer updates SuiteScript 1.0.
Warning
Your NetSuite flow exports, imports, and hooks should use the same SuiteScript version. The pre map hook is not supported in Suite Script 2.0.
SuiteScript hooks run on RESTlets, so they follow the same NetSuite governance limits. This governance is shared with the Celigo platform execution as well, so make sure that you plan how many governance points (the number of API operations allowed) your hooks will consume.
What should I do:
-
If an import hook is invoked with one record, a NetSuite RESTlet has 5000 governance points, integrator.io will also consume points for all the lookups and creating/submitting the record in NetSuite.
-
If you are running out of points, try adjusting your flow slightly. For example, if you have complicated logic for your NetSuite import and your hooks are consuming a lot of points, you can reduce the pageSize of the corresponding export to have less data passed to the hook for each page.
-
In addition to the limits NetSuite puts on points (number of API operations allowed), it also puts a limit on execution time. Since a RESTlet has a 5-minute time limit, make sure that all the processing in your hooks is completed within that limit.
The Celigo platform allows you to attach a single file for a hook. You can build common libraries and closures into a single file to share common utils across hooks. To make it easier to write clean code, the Underscore.js JavaScript library is available when the hooks are invoked in NetSuite.
To view your export and import logs:
-
Navigate to ScriptPath → Customization → Scripting → Scripts → RESTlet:
-
Celigo IO Restlet Runner for SuiteApp SuiteScript 2.0.
-
Celigo SA Import Restlet Runner for SuiteApp SuiteScript 1.0.
-
Celigo Realtime Import Restlet Runner for the SuiteBundle SuiteScript 1.0.
-
To view real-time export logs:
-
Navigate to Customization → Scripting → Scripts:
-
Map/Reduce → Celigo Consumer Runner 2 for SuiteApp SuiteScript 2.0.
-
User Event → Celigo SS1.0 Presend Hook Processor for SuiteApp SuiteScript 1.0.
-
Scheduled → Celigo Consumer Runner for SuiteBundle SuiteScript 1.0.
-
Comments
Article is closed for comments.