Some projects start with a straightforward question: What tool should I use to migrate this Domino environment to Exchange Online? And some projects end up with a LotusScript agent generating JSON compatible with Microsoft Graph. This is one of those.
The migration ecosystem exists, but it doesn't always fit
Migrating from HCL Domino to Microsoft 365 isn't an unsolvable problem. There is an ecosystem of specialized tools that go far beyond a simple IMAP migration: some cover email, calendars, contacts, and even Domino applications. The market is there.
The problem isn’t a lack of options. The problem is that these options have their own take on what to migrate, how to do it, and under what conditions. When the client’s environment doesn’t fit those parameters, the tools may not meet the objectives.
In this specific case, the need was clear: to migrate calendars and contacts faithfully, with full visibility into what had been migrated and what hadn’t, and with the ability to revert specific items without relying on what the tool decided to do on its own. The tools on the market offer that control within their own parameters—the problem is that those parameters don’t always match the client’s expectations, and their cost structure may not align with the volume and context of the migration, though that’s a conversation for another department.
When that happens, it’s worth asking what you have available within your own environment.
LotusScript as a Starting Point
HCL Domino has used LotusScript as its native automation language for decades. It is a powerful tool, offering direct access to NSF databases, views, documents, and all their fields. If your goal is to extract data from Domino in the exact format you need, LotusScript is the place to start.
The question guiding the design is simple: what format does the destination expect? The Microsoft Graph API is explicit on this point. Its calendar endpoints accept JSON with a well-defined schema. If you can construct that JSON from LotusScript, the extraction and transformation problem is solved. The injection is then the responsibility of PowerShell and the Microsoft.Graph module.
The architecture: three components, each with its own responsibility
The resulting pipeline has three distinct components:
Schema discovery agent. Before building anything, we need to understand what’s inside the NSF mailboxes. This agent is cross-functional: it inspects Domino views and extracts the structure of available fields. The goal is not to migrate, but to understand. Without this step, the subsequent mapping would be guesswork.
Calendar export agent. It operates on the discovered structure and extracts calendar events, transforming them into JSON ready to be injected via Graph. It reads the mapping.csv file containing the list of users to process, iterates over their NSF mailboxes, and generates two files per user: data.json with the events in Graph format, and state.csv with the migration status of each item.
PowerShell scripts with Microsoft.Graph. They consume the data.json generated by the agent and call the Graph API to create the events in Exchange Online. Once the operation is complete, they write the ExchangeId returned by Graph to state.csv.
The state.csv file is the contract between both worlds:
|
UNID
|
Status
|
MigratedDate
|
ExchangeId
|
|
ABC123...
|
migrated
|
2026-01-22T10:30:00
|
AAMkAGI2...
|
|
DEF456...
|
pending
|
|
|
|
XYZ789...
|
error
|
2026-01-22T10:31:00
|
|
The UNID is Domino's universal identifier. The ExchangeId is the identifier returned by Graph after creating the event. Having both in the same record gives you something that third-party tools rarely provide: granular control over the process from outside the tool. You know exactly which Domino item corresponds to which item in Exchange Online, you can act on that information using your own logic, and you can undo the migration with surgical precision—removing only the items with ExchangeId—without touching anything that already existed in Exchange Online before you started.
This introduces a second feature that is important in the migration project: it enables incremental processing. Future runs of the process will respect previous entries and will only incorporate new calendar entries into Exchange Online.
The complete workflow:
1. EXPORT (LotusScript) → Generates data.json + state.csv
2. IMPORT (PowerShell) → Injects into Exchange Online, updates ExchangeId
3. VERIFY (Outlook Web) → Manual validation
4. CLEAR (if necessary) → Removes only items with ExchangeId
The tricky case: recurrences
Simple events are straightforward. The real complexity arises with recurrences, and in particular with relative recurrences: the last Friday in November, the second Tuesday of every month, the fourth Thursday in October. These are dates that aren’t fixed numbers on the calendar but rather relative positions within a period.
Before getting into mapping, there’s a conceptual element that isn’t obvious if you haven’t worked with Domino: a recurring series isn’t represented as a single document, but as two distinct types. The parent document contains the complete definition of the recurrence—the pattern, the interval, the range. The instances are individual manifestations of that series, one for each occurrence. Exporting both indiscriminately would produce duplicates and break the logic of the destination calendar. The agent must be able to identify and process only the parent, discarding the instances.
The problem isn’t that Domino lacks this information. It has it. The problem is finding it in the right place and understanding how to map it to the schema Graph expects.
Domino stores the recurrence type in the RepeatUnit field. The mapping to Graph types is as follows:
|
Domino RepeatUnit
|
Graph pattern.type
|
Description
|
|
D
|
daily
|
daily
|
|
W
|
weekly
|
weekly
|
|
MD
|
absoluteMonthly
|
A specific day of the month (e.g., the 15th)
|
|
MP
|
relativeMonthly
|
Relative day (e.g., the second Tuesday)
|
|
Y
|
absoluteYearly
|
Fixed annual date (e.g., March 15)
|
|
YD
|
relativeYearly
|
Annual date (e.g., the fourth Friday in November)
|
Mapping RepeatUnit directly to pattern.type is the easy part. The complexity lies in the additional parameters required by each recurrence type. For relativeMonthly and relativeYearly, Graph needs to know which day of the week and what position within the month or year. In Domino, that information is stored in the RepeatAdjust field, encoded as a decimal number where the integer part represents the week and the decimal part represents the day. Understanding that encoding and translating it correctly into the Graph schema is where most of the real work lies.
For weekly recurrences, RepeatHow stores the days of the week as a bitmap: 1 for Sunday, 2 for Monday, 4 for Tuesday, and so on. An event that occurs on Monday and Wednesday has RepeatHow = 10. The agent parses that bitmap to construct the daysOfWeek array that Graph expects.
In addition to the recurrence logic, there was a key design decision regarding the handling of attendees. The Graph API does not allow you to create events with attendees without triggering invitations (it only manages the ability to accept or decline, but does not suppress notifications). When migrating historical events, this is a problem: sending invitations to meetings from three years ago would generate unnecessary noise and, in some cases, result in bounces from accounts that no longer exist. The solution was to include attendees in the event body as structured text, preserving the information without triggering any notification flows:
Original attendees (migrated from Lotus Notes):
Required: María González, Carlos Rodríguez
Optional: Pedro Sánchez, Laura Fernández
Since this is a known limitation of Graph, this workaround is a design decision specific to this project that may or may not align with your objectives, but it can be reviewed without affecting the core functionality of the solution.
The model is extensible
Everything described for calendars applies directly to contacts and any other type of object stored by Domino. The pattern is the same: a discovery agent, an export agent with its field mapping, an import script via Graph, and state.csv as the control mechanism. Contacts are implemented and validated end-to-end with 40 mapped fields. Adding new object types is simply a matter of adding the corresponding agent, without modifying the existing infrastructure.
There is a way out of legacy systems—if you know where to look
This project didn’t start with LotusScript in mind. It began with a specific need, an analysis of the available options, and the conclusion that none of them met the objective. From there, the question became: What does the environment itself offer that could solve this?
The answer lay in Domino’s native capabilities. Not in a third-party tool, not in a complex integration, but in the scripting language that has been available on the platform for decades. The task was to understand what the destination required, map the structure of the source, and build the bridge between the two.
That is the principle that applies beyond this specific migration: legacy systems are rarely dead ends. They are usually environments with capabilities that haven’t been used because no one has asked whether they could solve the new problem.
And this is just one part of the process. The same line of reasoning allows us to go further: to build a synthetic injection agent that populates the lab with the edge cases you need to validate, without relying on those scenarios existing in the real data. The level of rigor you can achieve with the environment’s native tools is often surprising.
If you have an HCL Domino environment with a pending migration and standard tools don’t meet your goals, our team can help you define the right approach for your specific case.