NotesDocumentCollection: Complete Guide to Document Collections in LotusScript
Almost every Domino scheduled agent ends with the same shape: Set docs = db.FTSearch(...) or Set docs = db.Search(...), then a Do Until doc Is Nothing loop, save, done.
The object carrying all those documents is NotesDocumentCollection. Virtually every “fetch a set of documents” operation in Domino returns one — but most code only uses the most basic traversal pattern and leaves the rest of the API untouched.
This article covers the full class: seven ways to obtain a collection, five navigation methods, the three set operations, batch operations (StampAll / RemoveAll), and a few traps that tend to cost half a day’s debugging.
TL;DR
NotesDocumentCollectionis a snapshot — a list of documents matching some criteria at the moment of creation, not a live query- Seven creation entry points:
db.AllDocuments,db.Search,db.FTSearch,db.GetModifiedDocuments,db.GetAllReadDocuments,db.GetAllUnreadDocuments,db.GetProfileDocCollection - Five navigation methods:
GetFirstDocument,GetNextDocument,GetPrevDocument,GetLastDocument,GetNthDocument(n) - All three set operations mutate the original collection in place:
Intersect(keep intersection),Merge(add union),Subtract(remove difference) — none of them return a new collection StampAllbatch-writes a field value to every document in the collection — much faster than a manual loopRemoveAll’sforceparameter controls concurrency:Truedeletes even if another user modified the document;Falseskips those- Use a NoteID array when you need to modify documents while iterating — same trap as db.Search
Seven ways to get a collection
| Entry point | Description |
|---|---|
db.AllDocuments | Every document in the database (read-only property) |
db.Search(formula, date, max) | @Formula predicate filter (no FT index needed) |
db.FTSearch(query, max, sort, opts) | Full-text indexed search |
db.GetModifiedDocuments(since, noteClass) | Documents modified after a given date |
db.GetAllReadDocuments(user) | Documents a specific user has read |
db.GetAllUnreadDocuments(user) | Documents a specific user has not read |
db.GetProfileDocCollection(name) | Profile documents matching a given name |
All return the same NotesDocumentCollection type; the API is identical regardless of how you obtained the collection.
Five navigation methods
GetFirstDocument + GetNextDocument is the most common pattern, but the others have their uses:
Dim docs As NotesDocumentCollectionSet docs = db.Search("Type = ""Order""", Nothing, 0)
' Forward traversal (most common)Dim doc As NotesDocumentSet doc = docs.GetFirstDocument()Do Until doc Is Nothing Print doc.GetItemValue("Subject")(0) Set doc = docs.GetNextDocument(doc)Loop
' Reverse traversalSet doc = docs.GetLastDocument()Do Until doc Is Nothing Print doc.GetItemValue("Subject")(0) Set doc = docs.GetPrevDocument(doc)Loop
' Random access (1-based index)Set doc = docs.GetNthDocument(1) ' first documentSet doc = docs.GetNthDocument(docs.Count) ' last documentGetNthDocument is O(N) for large collections — only use it on small sets or for one-off position lookups. For heavy random access, freeze the NoteIDs into an array first.
Two key properties
Print "Document count: " & docs.Count ' Integer, read-onlyPrint "Sorted: " & docs.IsSorted ' Boolean, read-onlyPrint "Source query: " & docs.Query ' String, read-onlyIsSorted is True only for collections from FTSearch (sorted by relevance or date). Collections from db.Search are unsorted — traversal order is not guaranteed.
The three set operations
All three mutate the original collection in place — they don’t return a new one:
Dim pending As NotesDocumentCollectionSet pending = db.Search("Status = ""Pending""", Nothing, 0)
Dim urgent As NotesDocumentCollectionSet urgent = db.Search("Priority = ""High""", Nothing, 0)
' Intersect: keep only documents in both collections (AND)pending.Intersect(urgent)' pending now contains only documents that are both Pending AND Urgent
' Merge: add documents from urgent that aren't already in pendingpending.Merge(urgent)' equivalent to union
' Subtract: remove from pending any documents in urgentpending.Subtract(urgent)' equivalent to set differenceAll three accept NotesDocumentCollection, NotesDocument, NotesViewEntry, or NotesViewEntryCollection as input, but both collections must be from the same database — cross-database operations throw an error.
In practice: FTSearch + set operations chained
' Start with structured criteriaDim orders As NotesDocumentCollectionSet orders = db.Search("Type = ""Order"" & Status = ""Open""", Nothing, 0)
' Narrow further with a text search inside the first resultsCall orders.FTSearch("urgent", 0)
' Remove already-flagged documentsDim flagged As NotesDocumentCollectionSet flagged = db.Search("Flagged = ""1""", Nothing, 0)orders.Subtract(flagged)
Print "Final urgent orders: " & orders.CountFor more on the FTSearch layer see the FTSearch three-tier API article. Note that collection.FTSearch only takes two parameters (query and maxDocs) — no sortopt or options flags.
StampAll — batch-write a field value
StampAll writes the same value to a named item on every document in the collection — much cleaner than a manual loop:
' ❌ Verbose, slowDim doc As NotesDocumentSet doc = docs.GetFirstDocument()Do Until doc Is Nothing doc.Status = "Processed" Call doc.Save(True, False) Set doc = docs.GetNextDocument(doc)Loop
' ✅ One lineCall docs.StampAll("Status", "Processed")StampAll(itemName, value) auto-saves every document — no separate doc.Save() needed.
For multiple fields at once, use StampAllMulti(itemNames(), values()):
Dim fields(1) As StringDim vals(1) As Stringfields(0) = "Status"fields(1) = "ProcessedBy"vals(0) = "Processed"vals(1) = "agent/ACME"Call docs.StampAllMulti(fields, vals)StampAll trap: collection is a snapshot
After StampAll completes, the collection’s Count is unchanged and GetFirstDocument returns documents with the new stamped values — but the original filter condition no longer holds:
Set docs = db.Search("Status = ""Pending""", Nothing, 0)Print docs.Count ' say, 50 documents
Call docs.StampAll("Status", "Processed")
' Re-search to see how many are still PendingDim newDocs As NotesDocumentCollectionSet newDocs = db.Search("Status = ""Pending""", Nothing, 0)Print newDocs.Count ' 0 (we just stamped them all)Print docs.Count ' still 50 (snapshot unchanged)See the snapshot trap section in the db.Search article for more on this.
RemoveAll — bulk delete documents from disk
RemoveAll(force) permanently deletes every document in the collection from the database — this is not just removing them from the collection object:
force value | Behavior |
|---|---|
True | Delete even if another user modified the document after it was retrieved |
False | Skip any document that was modified by someone else during the operation |
Dim toDelete As NotesDocumentCollectionSet toDelete = db.Search("Status = ""Expired"" & @Created < [01/01/2020]", Nothing, 0)
Print "About to delete: " & toDelete.Count & " documents"
' Use force=False in production: safer under concurrent accessCall toDelete.RemoveAll(False)⚠️ This is irreversible (without a backup or DAOS). Production code should log the count before deletion and ideally log NoteIDs.
Other useful methods
| Method | Description |
|---|---|
AddDocument(doc) | Add a document to the collection (disk unchanged) |
DeleteDocument(doc) | Remove a document from the collection (not from disk) |
Clone() | Copy the collection to a new object (also a snapshot) |
Contains(doc) | Check if a document is in this collection |
PutAllInFolder(name) | Add every document to a folder (creates folder if missing) |
RemoveAllFromFolder(name) | Remove every document from a folder |
MarkAllRead() | Mark all documents as read |
MarkAllUnread() | Mark all documents as unread |
UpdateAll() | Mark all documents as processed by an agent |
Safe iteration when modifying documents
Same principle as db.Search — walking the collection while modifying documents can cause skipped or double-processed entries. Freeze NoteIDs first:
' Freeze NoteID listDim noteIds() As StringReDim noteIds(docs.Count - 1)Dim doc As NotesDocumentSet doc = docs.GetFirstDocument()Dim i As Integeri = 0Do Until doc Is Nothing noteIds(i) = doc.NoteID i = i + 1 Set doc = docs.GetNextDocument(doc)Loop
' Iterate the array, not the collectionFor i = 0 To UBound(noteIds) Set doc = db.GetDocumentByID(noteIds(i)) If Not (doc Is Nothing) Then doc.ReplaceItemValue "Status", "Processed" Call doc.Save(True, False) End IfNextDeletion stubs
After you obtain a collection, if a document gets deleted or you lose read access to it before you finish iterating, the traversal still returns a deletion stub object. Check for it:
Set doc = docs.GetFirstDocument()Do Until doc Is Nothing If doc.IsDeleted Then ' Skip deletion stub Else Print doc.GetItemValue("Subject")(0) End If Set doc = docs.GetNextDocument(doc)LoopLong-running agents processing large collections are most likely to encounter this — other users may delete documents while the agent is still running.
NotesDocumentCollection vs NotesNoteCollection
Two collection classes that often get confused:
NotesDocumentCollection | NotesNoteCollection | |
|---|---|---|
| Member type | NotesDocument only | Any note type (documents, design elements, config) |
| How obtained | db.Search, db.FTSearch, etc. | db.CreateNoteCollection |
| Primary use | Business document processing | DXL export, design element management |
| Navigation | GetFirstDocument, etc. | GetFirstNoteID, etc. |
Day-to-day business logic almost always uses NotesDocumentCollection. NotesNoteCollection comes up when automating maintenance tasks (see the NotesNoteCollection article).
What about Java and SSJS?
| Language | Counterpart |
|---|---|
| LotusScript | NotesDocumentCollection |
| Java | lotus.domino.DocumentCollection (drop the Notes prefix; remember .recycle()) |
| SSJS | documentCollection (directly callable in XPages; same API surface) |