Back to the blog
12 min read

Migrating data into Infor M3: the parts that bite at cutover

Isak La Fleur EngdahlBy Isak La Fleur Engdahl
Goods-in has one door. In M3 CloudSuite the API layer is that door – the only way into the system – and every record is validated on the way in. Photo: Bernd Dittrich / Unsplash.

Most articles about M3 data migration tell you to involve your key users, clean your data early, and treat migration as a fresh start. All true. All also true for migrating into any system ever built. None of it tells you what's actually different about M3, or why the project that looked fine in testing falls apart on cutover night.

So this is the other version. The one written from inside the cutover window, after a few of these.

The constraint that changes everything: you can't touch the database

If your reference point is older, on-prem M3, forget most of it. In M3 CloudSuite (multi-tenant) you cannot touch the database. No loading DB2 tables directly, no bulk loads against the file system, no clever SQL to fix a million rows after the fact. Everything goes in through the API layer.

That has one consequence you have to design around from day one: every single record is validated by M3's own business logic on the way in. An M3 API is, by Infor's own definition, a routine that validates records and creates or updates files in the database – not a raw write. So the API won't let you create an item/warehouse record before the warehouse exists. It won't let you load a sales order for a customer who isn't there yet. It checks, and it rejects.

People treat this as the annoying part. It's actually the best thing about migrating into M3. Garbage gets stopped at the door instead of sitting silently in a table until month-end close blows up. Your rejection log becomes your data-cleansing backlog, for free. But it does mean two things are now non-negotiable: load order, and error handling. More on both below.

The mechanics are the M3 API – the MI transactions – called through the ION API gateway over REST with OAuth2. Items go in through MMS200MI (the item master API – note it doesn't echo the MMS001 panel program name, one of the cases where the API and the program differ), customers through CRS610MI, suppliers through CRS620MI, and so on, each with its Add and Chg transactions. You build ETL – Python works fine – that turns your source data into those API payloads and fires them through the gateway. That's the whole game. The hard part isn't the calling. It's deciding what to call, in what order, with what data.

Master data and transactions are two different projects

The single most common mistake is treating "items, suppliers, customers, POs, sales orders" as one migration with one strategy. They're not. They split cleanly into two problems that behave nothing alike.

Master data – items, customers, suppliers, prices, structures – is stable, idempotent, and forgiving. You load it, and tomorrow it's still true. You can load it weeks early, re-run it safely, and diff it without drama.

Transactional data – open orders, balances, in-flight documents – is volatile and time-sensitive. It changes right up to the moment you go live, and a record that was correct on Tuesday is wrong by Friday. Treat it like master data and you'll spend cutover night chasing orders that shipped out from under you.

Keep them in separate mental buckets – right down to giving each its own freeze date – and the whole plan gets simpler.

Load order isn't a preference, it's structural

Because the API enforces referential integrity, sequence isn't a nicety – it's dictated by the data. You build outward from the foundations:

First the basic and configuration data – countries, currencies, units, item groups, warehouses. Then master data – item basics (MMS200MI), customers (CRS610MI) and suppliers (CRS620MI); these don't reference one another, so they load in any order. The basic item carries no supplier, so – unlike a Business Central item card – you don't have to load suppliers first. Then the objects that connect master data and therefore need their parents to exist first – item/warehouse (MMS200MI, AddItmWhs), the supplier–item link with its purchase prices (PPS040MI), bills of material. Last, the opening balances and any open transactions.

Get it wrong and the API simply rejects the dependent record. Items before structures. Warehouses before stock. Customers before orders. There's no shortcut, and trying to find one is how you end up with half-loaded objects nobody can reconcile.

Bulk load, then delta load

You don't migrate everything in one heroic overnight run. You stage it: a bulk load of each object well ahead of go-live, then delta loads that catch up whatever changed in the source since. Master data and transactions freeze on different dates, you clear open orders before the transactional cutoff, and the whole thing runs to a dated runbook – but that staging-and-freeze choreography is general to any migration, so I've pulled it into its own piece: the data object migration plan. Here I'll stay on the part that's specific to M3: how you actually compute the delta.

For master data, the delta is simple: what's new gets an Add, what changed gets a Chg. The thing to get right is how you decide a record changed – and that decision lives on the source side. The source here is your legacy system, and its data isn't in M3 yet, so M3's own metadata can't tell you what moved in it. If the source has a last-modified or audit column you can trust, use it. Often you can't – a lot of legacy systems lie, because batch jobs and direct SQL fixes don't always touch the audit column – so the dependable default is a full extract at cutover, hash-diffed against your bulk-load snapshot. For most migrations into M3, that hash-diff is the normal path, not the exception.

The exception is when the source is itself on Infor Data Fabric – an M3-to-M3 reimplementation or tenant consolidation, or another Infor app feeding the same Data Lake. Then the source rows already live in the Lake with Infor's own metadata, and the delta becomes a Compass query: everything where infor.lastmodified() is past your bulk-load cutoff, already resolved to the latest version of each object. You browse the objects in Atlas, query them in Compass, and let Purge handle retention. But mind what that timestamp is: infor.lastmodified() is the Lake's own publish marker, and it only exists for data already in the Lake. A non-Infor source migrating into M3 isn't there to query – which is exactly why, for the common case, you fall back to the source's audit column or the hash-diff.

The open-order traps (this is the real article)

Everything above is process. This is where migrations actually go wrong, and it's all in the transactional bucket.

The instinct is to migrate three years of purchase and sales orders so the new system "has the history." Don't. Closed, fully-invoiced orders are not operational data – they're reporting history, and reporting history belongs in the Data Lake, queryable through Compass, not in M3's live order tables. That's the whole point of Infor Data Fabric: M3 already publishes its data there, so three years of closed orders stay fully reportable without ever being loaded as live documents. How the data gets there is worth knowing, because for mass data movement Infor now steers you onto the Data Lake Ingestion framework rather than the older ION/BOD route. The current routes are direct: streaming ingestion (real-time, over WebSocket) for live changes, and batch ingestion (the source packages data as NDJSON and uploads it via API) for exactly this job – initial full-loads and archiving cold history out of the live ERP. On the M3 side, Data Lake Publisher controls which tables publish and lets you run an initial or partial reload on demand. So parking history in the Lake isn't a workaround; it's the supported path. Loading closed orders as live documents burns number series, creates phantom open quantities, and slows the system down for no benefit. What actually migrates as live documents is only the open orders. And open orders are exactly the volatile records that won't sit still.

Open purchase orders migrate at remaining quantity – ordered minus received – not the original quantity. The already-received goods come in through your opening inventory balance. You don't replay the receipt. The trap is the partly-received, not-yet-invoiced line: ordered 100, received 60, invoiced 40. That single line is both open-to-receive (40 still coming) and received-not-invoiced (20 sitting as a GRNI accrual on the balance sheet). If your bucket logic classifies the whole line into one state, you've already lost. And that GRNI has to reconcile against your financial opening balance exactly once – book it twice and you've doubled a liability.

Sales orders have the same problem mirrored: shipped-not-invoiced is the SO-side equivalent of GRNI. The goods left inventory, the revenue isn't booked yet, and closing the order in the legacy system doesn't make that balance disappear. It still has to land in your opening financials whether or not the order document itself migrates.

The cleanest way to migrate a hard object is to not migrate it

The hardest transactional objects are open orders. So empty them in the real world before cutover, and there's nothing to migrate.

On a recent retail cutover, the rule for sales orders was exactly this. The distribution centre took ownership of clearing the board: cancel every line that won't ship, and make sure everything shippable is actually shipped and closed a few days before go-live. By cutover night there were effectively no open sales orders left to carry across. The messy data object was solved operationally, by the warehouse, not technically, by the migration team.

That's the mindset shift. Some of your worst data problems aren't data problems. A back-order is real customer demand – you can't just "close" it in a script without either losing the demand or re-keying it natively in the new system, and that's a business decision, not an ETL rule. A future-dated order can't be shipped early to clear it. So you push those decisions to the people who own the process, early, and you shrink the technical migration down to only what genuinely has to move. The plan for this isn't a mapping spreadsheet; it's a dated cutover runbook with named owners.

Proving it: reconcile on value, test it twice

When you check a load, counting records is not enough. A purchase order line can load successfully through the API and still carry the wrong remaining quantity. So for transactional objects you reconcile on open quantity and open value, per object, every pass – source figure, loaded figure, rejected figure. The source figure is a Compass query straight against the Data Lake; the loaded figure comes back through the M3 API; the gap between them is your problem list. Row counts tell you the API didn't crash. Value reconciliation tells you the data is actually right. Those are different questions.

There's a second reconciliation that matters once you're live, and it isn't the same as the migration check. The migration check asks: did the load into M3 match the legacy source? Data Ledger asks something else: did everything M3 publishes actually arrive in the Data Lake? It's an Infor Data Fabric component built specifically for source-to-Lake delivery reconciliation – the source publishes replication statistics (data-object, instance and row counts) and Data Ledger compares them against what actually landed in the Lake, flagging any mismatch. It reconciles batch-published data on its own; for record-by-record streamed data it can't classify the delivery automatically, so you confirm it by running a Compass query from Data Ledger. You won't use it to prove your cutover. You'll use it afterwards, to trust the Lake copy that your reporting and audit now run on.

And test it twice, for real. A first migration test in a dev environment where the technical team confirms the scripts run and the objects land. Then a broader test, in a test environment, where actual business users – the people who know what the data is supposed to look like – open the migrated records and tell you what's wrong. They catch things no script ever could, because they recognise their own data. The broader test is where you find the problems that would otherwise become go-live problems.

The business takeaway

Migrating into M3 is less a data exercise than people expect and more an operational one. The API constraint forces discipline you'd want anyway. The master-versus-transactional split tells you what's hard. And the genuinely hard parts – open orders, in-flight balances, half-shipped lines – are usually best solved by the business clearing them before you ever write a line of ETL.

Decide early what is history and what is live: history goes to a data lake, only open transactions migrate as documents – and clear those open transactions in the real world, not in a script, before cutover. Do that and the technical migration shrinks to something you can actually reconcile and trust.

The teams that struggle are the ones who treat it as a pure technical lift-and-shift and discover the business questions on cutover night. The teams that sail through have already had the awkward conversations – what's the freeze date, who clears the back-orders, where does history actually live – weeks earlier, when there was still time to answer them.

Do the boring planning. The migration is the easy part.

Planning a move onto Infor M3 and want a cutover that doesn't keep you up at night? Get in touch – I'm happy to share how I work.


This article describes a general approach, not implementation advice for a specific environment. Sources: Infor M3 API overview (APIs validate records and create/update files in the database); the API repository and MI programs MRS001/MRS002; the M3 API security and multi-tenant authorisation model; the ION API gateway interface to M3; and the Infor M3 CloudSuite solution overview on the API layer and Infor Data Lake; and Infor Data Fabric – Atlas (explore), Compass (query), Purge (retention); Data Fabric Ledger for source-to-Lake delivery reconciliation; and Streaming Ingestion. Further viewing: Infor's New Features in Data Fabric webinar (April 2025 release).