Time: 45 minutes | Level: Intermediate
You will learn: How to build a /ns command that looks up an order from Slack.
You will need: A sandbox, the signing secret, and a Suitelet you can make available without login.
Imagine a customer service rep is on a call and needs the status of an order. Instead of logging in, searching and clicking through tabs, they type this in Slack:
/ns SO10482
…and a second later, they see the customer, the status, the total and a link to the order. That is a slash command, and it is one of the most loved features of a NetSuite and Slack setup, because it saves small interruptions all day.
How it works
- A person types
/ns SO10482in any Slack channel or direct message. - Slack sends the command to a Suitelet URL that you provide.
- The Suitelet checks the request is genuinely from Slack, searches NetSuite, and replies with a formatted message.
- Only the person who typed the command sees the answer (an “ephemeral” reply), so nothing sensitive is dropped into a public channel.
Part A: Create the command in Slack
- Open your app at api.slack.com/apps and choose Slash Commands.
- Click Create New Command.
- Fill in:
- Command:
/ns - Request URL: the external URL of the Suitelet from Part B (you can come back and add it)
- Short Description: Look up a NetSuite record
- Usage Hint:
SO12345
- Command:
- Save, then Reinstall to Workspace if Slack asks you to (new features often require it).
Make sure the commands scope from Step 1 is in place.
Part B: The Suitelet
Slack’s important rule: it expects an answer within 3 seconds. Keep the lookup light: one search, a few fields, then reply.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*
* Handles the /ns slash command: looks up a sales order by number.
*/
define(['N/crypto', 'N/encode', 'N/search', 'N/url', 'N/log'],
(crypto, encode, search, url, log) => {
// Same signature check as the approvals page.
const isFromSlack = (request) => {
const signature = request.headers['X-Slack-Signature'] || request.headers['x-slack-signature'];
const timestamp = request.headers['X-Slack-Request-Timestamp'] || request.headers['x-slack-request-timestamp'];
if (!signature || !timestamp) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
const key = crypto.createSecretKey({ secret: 'custsecret_slack_signing', encoding: encode.Encoding.UTF_8 });
const hmac = crypto.createHmac({ algorithm: crypto.HashAlg.SHA256, key: key });
hmac.update({ input: 'v0:' + timestamp + ':' + request.body, inputEncoding: encode.Encoding.UTF_8 });
return ('v0=' + hmac.digest({ outputEncoding: encode.Encoding.HEX })) === String(signature).toLowerCase();
};
const reply = (response, payload) => {
response.setHeader({ name: 'Content-Type', value: 'application/json' });
response.write(JSON.stringify(payload));
};
const onRequest = (context) => {
const { request, response } = context;
try {
if (request.method !== 'POST' || !isFromSlack(request)) {
response.write('Unauthorized');
return;
}
// Slack sends slash commands as normal form fields.
const text = (request.parameters.text || '').trim().toUpperCase();
if (!text) {
return reply(response, {
response_type: 'ephemeral',
text: 'Try `/ns SO10482` to look up a sales order.'
});
}
// Search for the sales order by its number.
const results = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'], 'AND',
['tranid', 'is', text]
],
columns: ['tranid', 'entity', 'status', 'total', 'shipdate']
}).run().getRange({ start: 0, end: 1 });
if (!results.length) {
return reply(response, {
response_type: 'ephemeral',
text: ':mag: I could not find a sales order called *' + text + '*.'
});
}
const r = results[0];
const domain = url.resolveDomain({ hostType: url.HostType.APPLICATION });
const link = 'https://' + domain + url.resolveRecord({ recordType: 'salesorder', recordId: r.id });
return reply(response, {
response_type: 'ephemeral',
blocks: [
{ type: 'section', text: { type: 'mrkdwn', text: '*Sales order <' + link + '|' + r.getValue('tranid') + '>*' } },
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: '*Customer*\n' + r.getText('entity') },
{ type: 'mrkdwn', text: '*Status*\n' + r.getText('status') },
{ type: 'mrkdwn', text: '*Total*\n$' + Number(r.getValue('total')).toFixed(2) },
{ type: 'mrkdwn', text: '*Ship date*\n' + (r.getValue('shipdate') || '-') }
]
}
]
});
} catch (e) {
log.error('Slash command failed', e.name + ': ' + e.message);
return reply(response, { response_type: 'ephemeral', text: 'Something went wrong. Please try again in a moment.' });
}
};
return { onRequest };
});
Deploy the Suitelet
- Upload the file and create a script record (Customization > Scripting > Scripts > New).
- Create a deployment. Set the status to Released (or Testing while you are developing).
- Tick Available Without Login.
- Save and copy the External URL.
- Paste that URL into the slash command’s Request URL in Slack.
Because this deployment is reachable from the internet, the signature check is essential: it is what keeps out everyone who is not Slack.
Part C: Try it
- In Slack, type
/nsfollowed by a real sandbox order number. - You should see a neat card with the order details.
- Try a number that does not exist, and an empty
/ns, to check the friendly messages.
Important: who is allowed to see what?
The Suitelet runs with the permissions of its owner, so it can see everything that owner can see, even if the Slack user could not see it in NetSuite. That is a real design decision, not a technicality.
Sensible ways to handle it:
- Keep replies minimal: status, customer name, ship date, link. Leave out margins, costs and personal data.
- Restrict who can use it. Match the Slack user’s email to a NetSuite employee (as on the approvals page) and only answer for allowed roles or departments.
- Restrict where it works. Use the
channel_idSlack sends to limit the command to specific channels if you wish. - Keep replies ephemeral so results are not broadcast to the channel.
Ideas for more commands
| Command | What it could do |
|---|---|
/ns inv INV-2031 |
Invoice status and amount due |
/ns cust Acme |
Customer contact, balance and credit hold flag |
/ns item ABC-100 |
Quantity available by location |
/ns po PO-778 |
PO status and expected receipt date |
/ns help |
A short list of the commands |
A tidy approach is to read the first word after /ns (for example inv, cust, item) and route to a small function for each. Keep each lookup to one search so you stay under Slack’s three-second limit.
What is next
People can now ask NetSuite questions in Slack. Next, let NetSuite talk first every morning with a daily digest.