NotesLLMRequest: Call an LLM from LotusScript in 4 Lines
TL;DR
- Domino 14.5 introduced
NotesLLMRequest/NotesLLMResponse— LotusScript sends a prompt to the server-side LLM and reads the response. The call goes through in-process IPC, so no HTTP latency. - 4-line minimum:
session.CreateLlmRequest()→Completion(serverName, commandName, userPrompt)→ read.Content. - The API design separates prompt from command —
commandNamereferences a Command document pre-built by the admin indominoiq.nsf(containing the system prompt + model parameters); the app developer only supplies the user prompt. - Defensive helpers:
IsCommandAvailable(name)andGetAvailableCommands()for production sanity checks. - Streaming mode:
CompletionStream+CancelStreamfor long responses you want to surface incrementally in a UI. - Prerequisite: Domino IQ enabled on the server + at least one Command document (architecture / installation in the earlier Domino IQ intro).
4-line minimum
Once the server has Domino IQ configured and a Command document, the entire call flow is:
Dim session As New NotesSessionDim llmreq As NotesLLMRequestDim llmresp As NotesLLMResponse
Set llmreq = session.Createllmrequest()Set llmresp = llmreq.Completion(db.server, "StdReplyEmail", "Customer said: 'How do I return this?'")MsgBox llmresp.ContentThings to notice:
- Not
New NotesLLMRequest()— use thesession.Createllmrequest()factory method (same NotesSession factory pattern asCreateRichTextItem/CreateDateTime). db.serverresolves to the current database’s server name; pass a different server name string to target a remote Domino IQ server."StdReplyEmail"is one of the Commands shipped with Domino IQ — no need to create it yourself.- Synchronous call — once
Completionreturns,llmresp.Contentis ready.
The full NotesLLMRequest surface
Factory method:
Set llmreq = session.Createllmrequest()NotesLLMRequest exposes five methods:
Completion(serverName, commandName, userPrompt) → NotesLLMResponse
Synchronous chat completion request to Domino IQ. All three parameters are strings:
| Parameter | Meaning |
|---|---|
serverName | Which Domino IQ server to hit. Use db.server for the current database’s server; pass an explicit server name for remote. |
commandName | The name of a Command document pre-built in dominoiq.nsf, holding the system prompt + token cap + temperature + other model parameters. |
userPrompt | The user’s prompt text. Gets merged with the system prompt from the Command before being sent to the LLM. |
CompletionStream(...) — streaming mode
Same semantics as Completion, but the response is delivered incrementally as it’s generated — useful for “typewriter effect” UIs or long responses you don’t want users staring at a spinner for.
CancelStream()
Aborts an in-progress CompletionStream (user clicked cancel, the UI was closed, etc.).
IsCommandAvailable(commandName) → Boolean
Checks whether the named Command document exists in the current server’s dominoiq.nsf. Essential for production — avoids the app blowing up if an admin renames or deletes a command.
If Not llmreq.IsCommandAvailable("MyCustomSummary") Then Print "Command missing — cannot proceed" Exit SubEnd IfGetAvailableCommands() → Variant array
Returns an array of all Command names available on the current server. Useful for: populating a UI dropdown where the user picks a command, or doing a startup sanity check.
Dim cmds As Variantcmds = llmreq.GetAvailableCommands()ForAll c In cmds Print cEnd ForAllNotesLLMResponse — three fields
The response object is flat — just three properties:
.Content As String
The LLM’s generated response text. 90% of use cases only need this.
.FinishReason As String
Why the LLM stopped generating. Common values (aligned with the OpenAI vocabulary):
| Value | Meaning |
|---|---|
stop | Normal end (the model thought it was done) |
length | Hit the Maximum tokens cap set in the Command document, response was truncated |
content_filter | Tripped the guard model filter (if one is configured) |
Important: FinishReason = "length" means the response is incomplete — don’t treat it as a final answer. Production code should branch:
Select Case llmresp.FinishReasonCase "stop" ' OK, process normallyCase "length" Print "Warning: response truncated — bump Maximum tokens or split the prompt"Case "content_filter" Print "Guard model blocked the response"End Select.Role As String
The role of the message — almost always "assistant" when returned by Completion. Different models may vary, but 90% of cases don’t touch this field.
Why the API takes commandName instead of a raw prompt
Seeing Completion(server, commandName, userPrompt) for the first time raises a question: why can’t the system prompt just live in LotusScript? Why must a Command document be pre-built on the server?
The design deliberately separates dev and admin concerns:
| Layer | Owner | Controls |
|---|---|---|
Command document (admin / dominoiq.nsf) | Admin / Domino IQ owner | System prompt, model choice, temperature, max tokens, guard model toggle |
| App code (LotusScript) | App developer | User prompt content, when to call, how to handle the result |
Concrete payoffs:
- Tweak the prompt without changing code: an admin edits the system prompt in Notes client, the 5-minute cache expires, the new prompt is live — no app redeploy.
- Multiple apps share a command: one “customer-reply” Command serves the mail, CRM, and ticketing apps.
- Centralised governance: switching all LLM calls to a new model or enabling a guard model is a Command-document change, not an app code change.
- Separate audit trails: dev changes to user prompt logic go through the app deployment pipeline; LLM behavior changes go through Domino DB document changes — different change-tracking flows.
Practical example 1: auto-reply agent
A scheduled agent that drafts LLM replies for the inbox and saves them as drafts:
Sub Initialize Dim session As New NotesSession Dim mailDb As NotesDatabase Dim docs As NotesDocumentCollection Dim doc As NotesDocument Dim llmreq As NotesLLMRequest Dim llmresp As NotesLLMResponse
Set mailDb = session.CurrentDatabase Set llmreq = session.Createllmrequest()
' Production defense: bail if the command isn't there If Not llmreq.IsCommandAvailable("StdReplyEmail") Then Print "StdReplyEmail command not available on this server" Exit Sub End If
' Pull unprocessed inbox messages Set docs = mailDb.UnprocessedDocuments Set doc = docs.GetFirstDocument()
Do Until doc Is Nothing Set llmresp = llmreq.Completion(mailDb.Server, "StdReplyEmail", doc.Body(0))
If llmresp.FinishReason = "stop" Then doc.DraftReply = llmresp.Content Call doc.Save(True, False) Call session.UpdateProcessedDoc(doc) End If
Set doc = docs.GetNextDocument(doc) LoopEnd SubPractical example 2: conditional summarization
Summarize documents over 1000 characters; return the original otherwise:
Function SummarizeIfLong(doc As NotesDocument) As String Dim session As New NotesSession Dim llmreq As NotesLLMRequest Dim llmresp As NotesLLMResponse Dim bodyText As String
bodyText = doc.GetItemValue("Body")(0) If Len(bodyText) < 1000 Then SummarizeIfLong = bodyText ' Short enough — return as-is Exit Function End If
Set llmreq = session.Createllmrequest() Set llmresp = llmreq.Completion( _ doc.ParentDatabase.Server, _ "StdSummarize", _ bodyText _ )
Select Case llmresp.FinishReason Case "stop" SummarizeIfLong = llmresp.Content Case "length" ' Summary itself got truncated — safer to fall back to original SummarizeIfLong = bodyText Case Else SummarizeIfLong = bodyText End SelectEnd Function"StdSummarize" is one of the built-in Commands. Or build your own in dominoiq.nsf — e.g. "MyCustomTechSummary" with a specific system prompt that makes the LLM summarize in technical-article voice.
CompletionStream: when it’s worth switching
Completion blocks until the full response is ready — simple, but a long prompt may take several seconds and the UI stalls.
CompletionStream delivers chunks as they’re generated, useful for:
- Web UIs that want a “typewriter” effect
- Long-form generation (let the user see the start early and cancel if it’s heading the wrong way)
- Backend streaming pipelines forwarding to another service
The cost: LotusScript’s callback model isn’t as ergonomic as JavaScript’s — you assemble your own buffer + handle partial UI updates. Complexity is meaningfully higher than Completion. For most backend agent scenarios, you don’t need streaming — Completion is plenty.
Relationship to other IQ articles
| Topic | Article |
|---|---|
| What Domino IQ is, why local LLM, how to install | Domino IQ intro |
| Advanced: NSF as a vector source for RAG | Domino IQ RAG |
| This article: how to call it from LotusScript | You’re reading it |
Community supplement: XPagesDeveloper’s “Quick LLM Access via 4 Lines of Code” is the cleanest demo of how compact this API really is.
What about Java and SSJS?
| Language | Equivalent classes |
|---|---|
| LotusScript | NotesLLMRequest / NotesLLMResponse |
| Java | LLMReq / LLMRes (same logical surface, Java’s abbreviated naming style) |
| SSJS / XPages | No direct equivalent — invoke an LS or Java agent, or wrap it behind a REST endpoint |
SSJS has no Domino IQ class. The standard workaround is to write an LS agent that does the IQ work and trigger it from SSJS via lotusScript:, or expose the call as a Domino REST API endpoint.