Written by Daniel Ashcraft — 12+ years building industrial software including SCADA modernization, IoT data pipelines, and WebGL 3D visualization for manufacturing and energy companies.
MES integration with ERP follows the ISA-95 split: ERP sends released production orders, material definitions, routings and BOMs to the MES, and the MES returns confirmations, consumption and scrap, often as B2MML messages. The MES reads PLCs over OPC UA, Modbus TCP, EtherNet/IP or a serial gateway, starting read-only so production keeps running.
Done well, the integration replaces end-of-shift paperwork with confirmations posted from machine counts and gives you lot genealogy you can query. This post covers the integration patterns; whether to modernize at all is covered in our risk-aware guide to legacy modernization for manufacturers.
What do the ISA-95 levels mean for MES, ERP and PLC integration?
ISA-95 splits a plant into five functional levels and puts the MES at Level 3, between ERP at Level 4 and the control systems below it. According to ISA's summary of the standard, "The ISA-95 standard primarily deals with the interface between levels 3 and 4," which is why most of its object models describe what ERP and MES exchange.
ISA's series runs to eight parts, the earlier ones also published internationally as IEC 62264. Parts 2 and 4 define the objects (materials, equipment, personnel, schedules, performance) and Part 5 the business-to-manufacturing transactions.
| ISA-95 level | Function | Typical systems in a legacy plant | What crosses the boundary above it |
|---|---|---|---|
| Level 4 | Business planning and logistics | SAP ECC or S/4HANA, Epicor, JobBOSS, Infor, an AS/400 package | (top of the stack) |
| Level 3 | Manufacturing operations management | MES, historian, quality system, WMS | Orders, material definitions, routings and BOMs down; performance, consumption, scrap and genealogy up |
| Level 2 | Monitoring and supervisory control | SCADA, HMI, batch servers, OPC servers such as Kepware | Tag values, alarms, counts, job status |
| Level 1 | Sensing and manipulating the process | PLCs, I/O, sensors, drives | I/O values, registers, tags |
| Level 0 | The physical process | Motors, valves, presses, molds | Signals |
The levels describe functions rather than products, so plant diagrams disagree on whether a given PLC sits at Level 1 or Level 2. The integration rule: ERP does not talk to a PLC directly, and each record has one authoritative owner. ERP owns the item master and the MES may keep a read-only copy. When two systems can edit the same record, nobody can say which copy is right.
How should ERP send work orders and BOMs to the MES?
Send released production orders, material definitions, routings and BOMs from ERP to the MES as ISA-95 objects, and send confirmations back as performance records. ISA-95 defines the objects but no wire format. B2MML, the XML schema set that implements ISA-95, defines a message for each one, which makes it a sensible default when you write the contract yourself.
B2MML is a set of XSD schemas published by MESA International. The schema files are marked Version 0701. A B2MML-JSON schema ships too, but the README says "There has been no checking of this file," so treat XML as the reference. MESA ceased operations on 30 June 2026, with ISA taking over its educational programs and content. The schemas stay on GitHub (last commit July 2023), so pin the exact commit in your interface agreement.
Every B2MML message is a verb plus a noun. A Process message asks the receiver to act and can request an Acknowledge. A Get asks for data and expects a Show. A Sync publishes a change with no reply. Versions 5 and 6 had separate ProductionSchedule and ProductionPerformance schemas; version 7 drops them, and a production order is now an OperationsSchedule with OperationsType set to Production.
| ERP object | ISA-95 / B2MML V07 object | Message | Direction | Notes |
|---|---|---|---|---|
| Released production order | OperationsSchedule / OperationsRequest | ProcessOperationsSchedule, answered by AcknowledgeOperationsSchedule | ERP to MES | Send on release. Edits use ChangeOperationsSchedule, withdrawals CancelOperationsSchedule. |
| Material master | MaterialDefinition | SyncMaterialDefinition | ERP to MES | ERP is authoritative. MES keeps a read-only copy. |
| BOM (components and quantities) | OperationsMaterialBill with OperationsMaterialBillItem | SyncOperationsMaterialBill | ERP to MES | Items carry quantity and effectivity dates. Send the version the order was released with. |
| Routing | OperationsDefinition (references the bill by BillOfMaterialsID) | SyncOperationsDefinition | ERP to MES | Version it, and define which revision a running order uses after an engineering change. |
| Material lot or batch | MaterialLot | SyncMaterialLot | Both | Decide once which side creates lot numbers. |
| Confirmations, goods issue, scrap | OperationsPerformance / SegmentResponse with MaterialActual | ProcessOperationsPerformance or SyncOperationsPerformance | MES to ERP | Aggregate per operation or shift. If ERP backflushes, agree whether MES sends actual consumption. |
Here is a released order for one shift on a molding line as a ProcessOperationsSchedule message. We validated it with xmllint --schema AllSchemas.xsd against the B2MML repository at commit 65cc665.
<ProcessOperationsSchedule xmlns="http://www.mesa.org/xml/B2MML" releaseID="7.0">
<ApplicationArea>
<CreationDateTime>2026-10-26T05:40:00-05:00</CreationDateTime>
</ApplicationArea>
<DataArea>
<Process/>
<OperationsSchedule>
<ID>SCHED-2026-10-26-L3</ID>
<OperationsType>Production</OperationsType>
<OperationsRequest>
<ID>1000457</ID>
<StartTime>2026-10-26T06:00:00-05:00</StartTime>
<EndTime>2026-10-26T14:00:00-05:00</EndTime>
<HierarchyScope>
<EquipmentID>PLANT1.MOLDING.LINE3</EquipmentID>
<EquipmentLevel>ProductionLine</EquipmentLevel>
</HierarchyScope>
<OperationsType>Production</OperationsType>
<SegmentRequirement>
<ID>0010</ID>
<ProcessSegmentID>MOLD-PA66</ProcessSegmentID>
<OperationsDefinitionID>BRKT-4410-R03</OperationsDefinitionID>
<OperationsSegmentID>0010-MOLD</OperationsSegmentID>
<MaterialRequirement>
<ID>OUT-1</ID>
<MaterialDefinitionID>BRKT-4410</MaterialDefinitionID>
<MaterialUse>Produced</MaterialUse>
<Quantity><QuantityString>12000</QuantityString><UnitOfMeasure>EA</UnitOfMeasure></Quantity>
</MaterialRequirement>
<MaterialRequirement>
<ID>IN-1</ID>
<MaterialDefinitionID>RESIN-PA66-GF30</MaterialDefinitionID>
<MaterialUse>Consumed</MaterialUse>
<Quantity><QuantityString>310</QuantityString><UnitOfMeasure>KG</UnitOfMeasure></Quantity>
</MaterialRequirement>
</SegmentRequirement>
</OperationsRequest>
</OperationsSchedule>
</DataArea>
</ProcessOperationsSchedule>
Element order matters: OperationsType inside an OperationsRequest must come after HierarchyScope, and a segment requirement must carry ProcessSegmentID, OperationsDefinitionID and OperationsSegmentID. In the current OperationsPerformance schema the top-level end time element is spelled Endtime, while OperationsSchedule uses EndTime. Validate every outbound message against the exact XSD version both sides agreed on. On the undocumented ISP CRM API we reverse engineered, schema validation caught three vendor-side changes before they reached production.
The integration layer translates from whatever the ERP speaks. On SAP ECC, SAP's MES integration documentation sends released production orders as IDoc LOIPRO03 through the data replication framework and posts production confirmations back through a BAPI. If you are on ECC and facing the 2027 maintenance deadline, writing the MES contract in B2MML instead of raw IDocs lets it survive the ERP change. On S/4HANA, the exchange can use OData APIs such as API_PRODUCTION_ORDER_2_SRV for orders and API_PROD_ORDER_CONFIRMATION_2_SRV for confirmations (see this connectivity vendor's S/4HANA guide). For ERPs with no outbound messaging, the translator polls the order tables.
How do you keep ERP and MES postings from duplicating or going missing?
Give every message a unique ID and make the receiver ignore IDs it has already processed. Queue outbound messages durably so an ERP outage only delays postings, and count a confirmation as posted when ERP returns a business acknowledgment with a document number.
B2MML has a slot for this: the ApplicationArea carries an optional BODID. Use it as the idempotency key, because the case to design for is a confirmation ERP posted whose response never came back; a blind retry posts the goods issue twice. You also need:
- A per-order message log: what was sent, the acknowledgment, and the ERP document number it produced.
- A dead-letter queue for messages ERP rejects (closed period, missing material, locked order), cleared daily by a named person.
- In-sequence processing per order, so a confirmation never posts before its release.
- A defined message and test case for changes and cancellations after release, partial completions, rework and reversals.
- A daily report comparing MES totals with ERP postings per order, with PLC, MES and ERP clocks synced to one time source so shift boundaries line up.
- Agreed state mapping. ERP may close an order on quantity while the MES still holds parts for quality disposition, so define which MES state triggers the final confirmation.
How does an MES connect to PLCs in a legacy plant?
Use whatever protocol each controller already speaks: OPC UA where it exists, EtherNet/IP for Rockwell, S7 or OPC UA for Siemens, Modbus TCP for many drives, meters and skids, and a driver or gateway for serial-only PLCs. Many plants put an OPC server at Level 2 so the MES reads one OPC UA address space.
Fill in this map per line during discovery (ports from the IANA registry).
| Controller family | Protocol | Port | How data is addressed | Keeping MES access read-only | What bites you |
|---|---|---|---|---|---|
| OPC servers (Kepware, Ignition) and embedded UA servers | OPC UA binary, opc.tcp:// | TCP 4840 registered; many servers use another port | Browsable nodes with types and engineering units | A dedicated MES user with read-only roles on the server | Certificate trust setup, and security mode left at None |
| Rockwell ControlLogix, CompactLogix | EtherNet/IP (CIP) | TCP 44818 messaging, UDP 2222 I/O | Symbolic tag names, UDTs, arrays | Read-only access at the OPC server, or External Access on telemetry-only tags | External Access applies to every external client, including HMIs |
| Siemens S7-300/400/1200/1500 | S7comm over ISO-on-TCP, or the S7-1500's built-in OPC UA server | TCP 102 (S7), 4840 (UA) | DB number plus byte offset (classic S7), or symbols (UA and symbolic drivers) | Read-only items in the OPC server or collector, plus firewall rules | Classic absolute access to S7-1200/1500 needs PUT/GET enabled and non-optimized DBs |
| Modicon, drives, meters, many OEM skids | Modbus TCP | TCP 502 (802 for Modbus/TCP Security) | Register numbers, zero-based on the wire | Collector issues only function codes 01 to 04; a function-code-aware gateway or firewall blocks writes | Off-by-one addresses and multi-register word order |
| SLC 500, PLC-5, MicroLogix on serial | DF1 (RS-232/485), DH+ | None (serial) | Data files such as N7:0, F8:3 | Read-only items in a DF1 driver such as Kepware's, or a DF1/DH+ gateway | If the HMI owns the only serial port, the gateway becomes a single point of failure |
Every extra client adds communication load on the controller, read-only or not. Baseline each controller's comms load and connection count, start the collector slow on a few tags, and raise the rate in steps while watching scan time, against stop criteria the controls engineer sets.
Modernizing Manufacturing Systems?
Get a free architecture review for your industrial software. IoT integration and legacy migration expertise.
Download: Manufacturing Legacy Modernization Checklist
Zero-downtime migration playbook for SCADA, MES & ERP systems. Includes system assessment, risk matrix, ROI calculator, and phased cutover plan.
On OPC UA, the specification defines three security modes: None, Sign and SignAndEncrypt. Move servers left at None to Sign or SignAndEncrypt before go-live. For pushing job orders to machines, the OPC Foundation's ISA-95 Job Control companion specification (OPC 10031-4) defines methods such as StoreAndStart, Pause and Abort; version 2.00 (2024) is incompatible with 1.0, so ask which one your machine builder implemented. For the wider protocol choice, see our comparison of OPC UA and MQTT on the factory floor.
On Rockwell, Studio 5000 gives every tag an External Access setting of Read/Write, Read Only or None, and newer versions add a separate OPC UA Access property. External Access covers all external applications, so Read Only also stops a PanelView from writing the tag; lock down only tags nothing external writes. For a custom collector, libplctag reads ControlLogix tags and PLC-5, SLC 500 and MicroLogix data.
On Siemens, the route depends on the driver. Classic absolute-address reads from an S7-1200 or S7-1500 need "Permit access with PUT/GET communication from remote partner" ticked in the CPU protection settings and, on the S7-1500, data blocks with standard (non-optimized) access, as this integration vendor's S7 setup guide documents. Siemens describes the S7-1500's built-in OPC UA server as offering symbolic access to PLC data. Either route is a PLC project change that goes through the controls engineer.
On Modbus, the Modbus Application Protocol Specification V1.1b3 makes discrete inputs and input registers read-only and uses 16-bit registers. Function codes 03 and 04 read 1 to 125 registers per request. The spec numbers registers from 1 in the data model but addresses them from 0 in the PDU, so "register 40101" in a manual is address 100 on the wire with function code 03. A 32-bit value takes two registers and a 64-bit value four, in an order the device documentation defines.
How do you reverse engineer an undocumented PLC data map without touching production?
Capture the traffic that already flows between the PLC and its HMI or SCADA, decode it, and build a candidate tag map from what the existing systems read. Confirm each tag with slow, read-only polling while a controls tech watches the HMI. Nothing writes to the controller during discovery.
On a line with no usable documentation, we work in this order:
- Get the PLC program if one exists. Even an uncommented
.ACDor.RSSfile gives you names; check its revision against the running controller. - Mirror the HMI's switch port to a capture laptop. Per the Wireshark capture setup guide, use a SPAN port on a managed switch or a network tap, and make the monitoring port at least as fast as the mirrored one or you lose packets. Inserting a tap breaks the link while you cable it, so do it during a planned stop.
- Capture across a full cycle, a changeover and at least one fault. Wireshark ships dissectors for Modbus/TCP, EtherNet/IP and S7comm, so requests show up as register ranges, tag names or DB offsets.
- Build the candidate map: address, type guess, observed range and HMI label. Counters climb, states sit in small integer sets, temperatures drift.
- Confirm candidates with read-only polling and mark each tag confirmed, suspected or unknown. Watch for HMI-side scaling: a temperature showing 123.4 on screen may be the integer 1234 in the register.
- Commit the confirmed map to version control with the PLC program revision it matches, and re-verify it after every program download.
The capture only shows what something already asks for; data no HMI reads has to come from the program file or the OEM. OPC UA traffic with SignAndEncrypt cannot be decoded from a capture, and serial DF1 links need a serial port monitor or tap instead of a SPAN port.
This snippet decodes two captured Modbus holding registers as an IEEE 754 float in both word orders:
import struct
def regs_to_float(w0, w1, word_swap=False):
hi, lo = (w1, w0) if word_swap else (w0, w1)
return struct.unpack(">f", struct.pack(">HH", hi, lo))[0]
# FC03 response for registers 40101-40102 (PDU address 100, count 2)
regs = [0x42F6, 0xE979]
print(regs_to_float(*regs)) # 123.45600128173828 (HMI shows 123.5 deg C)
print(regs_to_float(*regs, word_swap=True)) # -1.8833671448184637e+25
The wrong order is not always this obvious, and the value may be a scaled integer, so check several known operating values before trusting a decode.
On OEM equipment with its own software license, check those terms before decoding anything beyond the plant's own PLC program. This is not legal advice; review your license agreement and MSA with counsel. Our post on whether it is legal to reverse engineer software covers the general rules.
Where should the integration layer sit on the network?
Put the MES and its translators on the plant side, put anything the business network touches in a DMZ, and allow no direct connection from the corporate network to a PLC. NIST's OT security guide, SP 800-82 Rev. 3, recommends a DMZ to "prevent direct traffic between the corporate and OT networks" and says communications between the enterprise level and the operations management level "are required to go through services within the DMZ."
ERP-to-MES traffic lands on a broker in the DMZ and the MES pulls from it; PLC traffic stays on the plant network. ANSI/ISA-62443-3-2 groups assets into zones and defines a conduit as a "logical grouping of communication channels that share common security requirements connecting two or more zones." The firewall rules should be exactly the conduit list. Plain Modbus TCP has no authentication (the Modbus Security protocol adds TLS on port 802), and a port rule cannot tell a read from a write, so blocking Modbus writes takes a device that inspects function codes.
What does a low-risk MES rollout sequence look like?
Roll out in read-only stages and cut over one line at a time: shadow mode, a parallel run against the current paperwork, then cutover at a planned changeover with a written rollback. Writes to PLCs come last, if at all. Any step that needs a stop goes into an existing changeover or maintenance window.
| Stage | What runs | Exit criteria | Rollback |
|---|---|---|---|
| 1. Discovery | Capture, program review, integration map, conduits, baseline controller load | Every tag the MES needs is confirmed or explicitly out of scope | Nothing to roll back |
| 2. Shadow mode | MES reads PLCs read-only and receives ERP orders; nobody acts on its output | Counts match PLC and HMI totals for two weeks, within a tolerance written down beforehand (exact match per shift is a fair target for discrete parts); controller load within stop criteria | Turn the collector off |
| 3. Parallel run | Operators use the MES and the old method side by side on one line | Daily reconciliation of good parts, scrap and consumption shows no unexplained gaps | Operators drop the MES screen; the old method never stopped |
| 4. Line cutover | MES posts to ERP for that line; the old paper record continues as a fallback for the first weeks | One full production week with clean ERP postings and no manual corrections | Stop posting at a shift boundary, hold the queue, reconcile ERP postings by message ID, resume from the fallback record |
| 5. Repeat per line | Same steps, usually shorter each time | Same | Same, per line |
| 6. Writes (optional) | MES sends recipes or job orders via OPC UA Job Control or a handshake tag set | Controls team signs off the handshake, tested on a non-production controller | PLC ignores MES writes when a local "MES enable" bit is off |
Cut over at a changeover or shift boundary, and let the order already running finish on the old method so no order is split. The line's production supervisor reviews and signs off the stage 3 reconciliation every morning. The "MES enable" bit belongs to the controls engineer, so the plant can disconnect the MES from a machine without touching the MES.
A plant historian gives shadow mode weeks of stored counter values to compare against; see our post on extracting data from Wonderware Historian and OSIsoft PI.
Which integration pattern should a small or mid-size plant pick?
Use a vendor connector when one exists and you can inspect its output, and a small ERP-to-B2MML translator when there is none. Add an OPC server at Level 2 once you have two controller families, and a message broker or iPaaS once several systems share the same messages. Avoid a mesh of scripts writing to each other's tables; our guide to legacy system integration makes the case for one documented contract per boundary.
To start, inventory every PLC in scope with model, firmware and program revision, every place counts or material usage get written down today, and the ERP transactions the plant posts. If you want a second set of eyes on the resulting integration map and conduit list, our industrial IoT development team can review them; send them over.
Download: Manufacturing Legacy Modernization Checklist
Zero-downtime migration playbook for SCADA, MES & ERP systems. Includes system assessment, risk matrix, ROI calculator, and phased cutover plan.