Calling AI from SuiteScript: A Practical Guide to the N/llm Module
Calling AI from SuiteScript: A Practical Guide to the N/llm Module
You want a script to do something that needs a bit of judgement: summarise an inbound message, classify a record, draft a reply, pull the key points out of a wall of free text. Until recently that meant calling an external AI service over HTTPS, managing an API key, and owning the data-handling questions that come with sending records to a third party. The N/llm module removes most of that. It lets your SuiteScript call a language model directly, through NetSuite, with no key to manage and the request routed through Oracle's own infrastructure.
This is the practical guide to using it: the core call, how to ground answers in your own data, what it costs, and the discipline that keeps it from causing problems. It is the N/llm deep dive under the broader NetSuite AI development guide.
Before you start
Three things have to be true. N/llm is SuiteScript 2.1 only, so if your account still runs 1.0 or 2.0 scripts, that migration to 2.1 is the first step. The Server SuiteScript feature has to be enabled. And generative AI is available only for accounts in supported regions, so confirm availability before you promise a feature around it.
The core call: generateText
The method you will use most is llm.generateText(options). You give it a prompt, it returns an llm.Response, and the text is on response.text. Here is a realistic use: classifying an inbound message into a category field when a case is saved.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/llm'], (llm) => {
const beforeSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const message = context.newRecord.getValue({ fieldId: 'custbody_customer_message' });
if (!message) return;
const response = llm.generateText({
prompt: `Classify this message as one of: billing, technical, sales, other.\nReturn only the label.\n\n${message}`,
modelParameters: { maxTokens: 10, temperature: 0 }
});
context.newRecord.setValue({ fieldId: 'custbody_ai_category', value: response.text.trim() });
};
return { beforeSubmit };
});A few things are doing real work there. temperature: 0 makes the model as deterministic as it gets, which is what you want for classification rather than creative writing. maxTokens: 10 stops it writing an essay when you asked for one word. And the prompt tells it exactly what to return, because a model left to its own devices will happily add "Sure, here is the category:" in front of the answer.
The full set of options on generateText:
| Option | What it does |
|---|---|
prompt | The instruction or question. Required unless you are passing tool results. |
modelFamily | Which model to use, from llm.ModelFamily. Omit it for the default. |
modelParameters | maxTokens, temperature, topK, topP, frequencyPenalty, presencePenalty. |
documents | Source documents for grounding the answer (Cohere models). See RAG below. |
preamble | A system-style instruction that sets role and tone (Cohere models). |
chatHistory | Prior turns, when you are building something conversational. |
timeout | Milliseconds before the call gives up. Defaults to 30,000. |
ociConfig | Your own Oracle Cloud account, for unlimited usage mode. |
On models: if you do not set modelFamily, the service uses Cohere's Command model. Other families are exposed through the llm.ModelFamily enum, and the available set changes from release to release, so check the current list in Oracle's docs rather than pinning a name and hoping it survives the next upgrade.
Grounding answers in your own data: RAG
A general model does not know your returns policy, your product catalogue, or last week's release notes. Retrieval-augmented generation (RAG) fixes that by handing the model the relevant source text alongside the prompt. In N/llm you build documents with llm.createDocument(options) and pass them in the documents option.
const policy = llm.createDocument({
id: 'returns-policy',
data: 'Returns are accepted within 30 days with a receipt. Sale items are final.'
});
const response = llm.generateText({
prompt: 'Can a customer return a sale item bought three weeks ago?',
documents: [policy],
modelFamily: llm.ModelFamily.COHERE_COMMAND
});
log.debug({ title: 'Answer', details: response.text });
log.debug({ title: 'Citations', details: response.citations });Two payoffs. The answer is grounded in the text you supplied rather than the model's training data, so it reflects your actual policy. And when the model uses a document, the llm.Response comes back with a list of citations (llm.Citation objects) telling you which source it drew on, which is what lets you show a user where an answer came from instead of asking them to trust it.
In a real build the document data would come from records or files, a saved search of knowledge-base articles fed in as documents, for example, rather than hard-coded strings.
Embeddings: when you need similarity, not text
Not every AI task is about generating prose. llm.embed(options) turns text into vector embeddings, which is the right tool for semantic search, deduplication, matching, and classification by similarity. Finding the existing case that most resembles a new one is an embeddings problem, not a generateText problem. Embeddings draw on their own usage quota, separate from the generate methods, with llm.getRemainingFreeEmbedUsage() to check it.
Cost, governance, and not shooting yourself in the foot
The usage model is the part people miss, and it is the part that bites in a batch job.
Calling the model does not consume SuiteScript governance units, but the calls are metered. They draw from a monthly free usage pool: each generateText or evaluatePrompt call costs one unit, embeddings have a separate quota, and the pool refreshes monthly. NetSuite does not publish a fixed free figure and it can change, so do not design as if it were unlimited. Check llm.getRemainingFreeUsage() before a job leans on it, and never put a model call inside a tight loop in a Map/Reduce script that processes tens of thousands of records without budgeting for it first. If you genuinely need higher volume, the ociConfig option switches you to unlimited mode against your own Oracle Cloud account, which you then pay for through OCI.
The default timeout is 30 seconds. A model call is a network call to an external service, so handle the case where it is slow or fails, the same as you would for any integration.
Validate the output, always
The model is generative, which means it can be fluent and wrong in the same sentence. Oracle is explicit that you must validate AI-generated responses and that it is not responsible for how the content is used. In practice that means a model response is a suggestion to check, not a result to post. Constrain it where you can (a fixed set of categories, a low temperature, a strict prompt), validate it against rules your code owns, and keep a human in the loop for anything consequential. On data, NetSuite routes the calls through Oracle Cloud Infrastructure rather than a public API, which it positions as keeping the data inside Oracle, but confirm the current terms for your account and region before sending anything sensitive.
Used this way, N/llm is a genuinely useful addition to a SuiteScript developer's toolkit. It is at its best on summarising, classifying, extracting, and drafting, where the output is checked before it matters, and at its worst anywhere correctness has to be guaranteed without review.
Frequently asked questions
Which model does N/llm use by default?
If you do not set modelFamily, N/llm uses Cohere's Command model (llm.ModelFamily.COHERE_COMMAND). Other model families are exposed through the llm.ModelFamily enum, and the set changes by release and region, so check the current options in Oracle's docs rather than hard-coding one.
Does N/llm work in SuiteScript 1.0 or 2.0? No. N/llm is SuiteScript 2.1 only. It also needs the Server SuiteScript feature enabled and an account in a supported region. If you are still on 1.0 or 2.0, migrating to 2.1 is the prerequisite.
How is N/llm billed?
There is no separate licence fee, and the calls do not consume SuiteScript governance units. They draw from a monthly free usage pool instead: each generate or evaluate call uses one unit, embeddings have their own quota, and llm.getRemainingFreeUsage() tells you what is left. For higher volume you can switch to unlimited mode using your own OCI account, which you pay for through Oracle Cloud.
Can I trust what the model returns? Not without checking it. The output is generative and can be confidently wrong, and Oracle states it is not liable for how the content is used. Keep a human or a downstream rule in the loop for anything that has to be correct, and treat a response as a suggestion rather than a posted result.
This post covers calling AI from your own code. The other half of NetSuite's AI platform is the reverse: letting an external AI agent act in NetSuite through a tool you expose, which is the subject of building a custom tool for the AI Connector.
If you want an AI-backed feature built into NetSuite and validated properly, that is one of the core services I offer.
Have a specific problem in mind?
A 30-minute technical review call to understand what's in your codebase and whether this is the right fit.
Book a technical review