Skip to main content

Bridging to DCS, MES & ERP: DeltaV, Siemens, SAP

📍 Where we are: Part IV · Meeting Reality — having bridged the validated historian, we now wire our open-source stack to the three systems it will never replace: the DCS that runs the plant, the MES that executes the recipe, and the ERP that owns the materials and orders.

The simple version

Picture a hospital. The operating room (the DCS) controls the patient minute by minute; the surgical workflow board (the MES) says which procedure happens in which room and signs that each step was done; the hospital's billing and supply office (the ERP, i.e. SAP) owns the inventory, the bed assignments, and the paperwork. You would not let a clever new app run the anesthesia, sign the surgical record, or issue the supply orders — those are life-and-records-critical and already accountable. What a new app can do is listen (read what the OR is doing), mirror the workflow board so analytics can see it, and exchange notes with the supply office through the official mail slot. This chapter builds those three listening posts and mail slots — and is blunt about the one thing open source genuinely cannot give you here: a credible GxP MES (GxP = the umbrella of Good-x-Practice regulations — GMP for manufacturing, GLP for labs, GCP for clinical — that govern medicine-making).

What this chapter covers

For seventeen chapters we built a complete open-source platform and, last chapter, taught it to live politely beside a commercial historian. The historian was the friendly commercial system: it speaks open protocols and trades in time-series we already understand. This chapter walks into harder country, where the honest-hybrid boundary is drawn not by taste but by necessity:

  • Reading DeltaV and Siemens control data into the open-source (OSS) stack over OPC UA (an open, vendor-neutral industrial protocol), without touching the validated control loop — including how DeltaV actually connects (a wrapper server for live values, alarms, and history — DA/A&E/HDA), where it stores data (its three historians, the databases that keep its time-series, alarms, and batch records), and how a read-only NAMUR Open Architecture (NOA) seam shares it.
  • The blunt verdict that there is no credible open-source GxP MES, and what that means for your architecture.
  • Exchanging materials, lots, and work orders with SAP/ERP using B2MML/ISA-95 messages over IDoc and OData.
  • Why every one of these exchanges must be idempotent and reconcilable, and how the ISA-88/95 model (the industry-standard way of describing recipes, equipment, and batches — ISA-88 for the procedure, ISA-95 for the enterprise integration) we already built in PostgreSQL is the landing pad for all of it.

The thread tying it together is a single sentence you can take into any design review: the validated DCS, MES, and ERP remain the systems of record; the open-source layer mirrors them, it does not replace them [12].

Three systems, three different doors

The historian had two doors. DCS, MES, and ERP each have their own, and they are not interchangeable. The trap is to treat them as one "integration" problem. They are three problems with three contracts, three trust levels, and three honest verdicts.

The Purdue/ISA-95 model — the reference standard that stacks a plant into numbered levels — makes the direction obvious. Level 1-2 is the equipment and its real-time control, Level 3 is execution (running the recipe), and Level 4 is the business (orders and materials), with each higher number sitting "above" as the more abstract, more authoritative system of record. The DCS sits at Levels 1-2, the MES at Level 3, the ERP at Level 4 [1]. Data, and trust, flow down from those validated systems into our analytics mirror — almost never the other way. That single design rule keeps us out of trouble.

DeltaV and Siemens: read the control layer over OPC UA

A distributed control system (DCS) is the validated brain of a bioreactor suite — it holds the control loops (the automatic feedback that nudges a knob to hold a measurement at its target) that keep BR101 (the running example's bioreactor vessel) at 37 °C, pH 7.0, dissolved oxygen near 40 %sat (percent of saturation), and agitation in band. You do not reimplement those loops in open source, and you do not write setpoints (the commanded targets) from a Python script into a GMP (Good Manufacturing Practice) control system. What you do is read.

Both major vendors hand you a clean read path. Emerson's DeltaV natively exposes runtime parameters, alarms and events, and historical data over an OPC UA server, so any standard OPC UA client can browse the DeltaV address space and subscribe to values without altering the control system [5]. Siemens SIMATIC controllers act as OPC UA servers in exactly the same way, offering a manufacturer- and platform-independent, secured channel up to higher layers [6]. OPC UA itself is the standard that makes this portable: a platform-independent, service-oriented, secure architecture standardized as IEC 62541 [4].

Because our bioreactor already speaks OPC UA — Chapter 7 built the opcua-collector against the opcua-server — a DCS bridge costs us almost nothing on the client side. The open-source SDKs are mature: node-opcua (MIT) can browse, read, write, and subscribe to a DCS/PLC OPC UA server [8], and the Python asyncua library we already use does the same. The bridge is the same shape as every capture chapter: subscribe to a node, receive value + quality + timestamp, write a row.

Since neither a DeltaV nor a SIMATIC server runs on a laptop, the companion repo follows the same honest-hybrid pattern it uses for PI and SAP: a small dcs-mock — an asyncua server exposing DeltaV/PCS7-style nodes (the control hierarchy BR101 > TIC-101 > PID1 > PV), serving the golden batch (the reference run whose values every test asserts against) over both live DA (Data Access, live values) and historical HDA (Historical Data Access), behind the commercial profile — now ships alongside the pi-web-api-stub, so the bridge is developed and contract-tested with no licensed hardware in reach. The production swap is a server URL and a certificate, not a code change. A DeltaV value, read over OPC UA, arrives in exactly the shape our historian stores (the read result the dcs-mock actually serves, and the contract a real DeltaV Edge server honors):

{ "NodeId": "ns=2;s=BR101/TIC-101/PID1/PV", "DisplayName": "BR101.Temp.PV",
"Value": 37.02, "StatusCode": "Good", "SourceTimestamp": "2026-01-12T08:30:00Z",
"Unit": "degC" }

That maps, one-to-one, onto the ts.sensor_reading row contract you saw the loader use last chapter (ts, tag, value, unit, quality, batch_id). StatusCode "Good" becomes OPC quality 192 (the standard numeric code for a good-quality OPC point — the field-by-field walk below shows where 192/64/0 come from); the DCS tag BR101.Temp.PV is already a name our Chapter 5 tag dictionary recognizes. The DCS bridge is, deliberately, the least exciting code in the book — and that is the point.

Anatomy of a DeltaV OPC UA read result (field by field)

That JSON deserves a slow read, because it is the actual artifact the bridge consumes — not an illustration. The dcs-mock service (examples/services/dcs-mock/app.py) builds an asyncua server whose LOOPS table maps a DeltaV browse path to a canonical tag, engineering unit, and range, then serves each PV as a real DataValue carrying a Value, a StatusCode, a SourceTimestamp, and an EngineeringUnits property; examples/tests/test_opcua_bridges.py reads it back and asserts the rows match the golden batch to the digit. Dissecting one DataValue field by field shows there are no spare parts — every field becomes one column of the ts.sensor_reading six-tuple (the six-column row — ts, tag, value, unit, quality, batch_id — that the ts time-series schema stores each reading as), and the one that decides whether the point is trustworthy is the StatusCode.

Identity-card diagram dissecting one DeltaV OPC UA DataValue. The header reads DeltaV DataValue for BR101.Temp.PV mapping to ts.sensor_reading. Rows map DisplayName to the tag column, Value 37.02 to the value column as a Double, SourceTimestamp to the ts column, EngineeringUnits degC to the unit column, a green StatusCode block showing Good maps to quality 192 with severity bits Good 192, Uncertain 64, Bad 0 and a note that a Bad point is recorded with quality 0 rather than dropped, and batch_id supplied by the bridge as the GMP join key. A violet panel decodes the NodeId browse path ns=2 s=BR101 slash TIC-101 slash PID1 slash PV into module, function block, and parameter, with a red note that the read-only NOA seam lets data flow out only and never writes back. One DeltaV DataValue dissected: the DisplayName carries the canonical tag, Value/SourceTimestamp/EngineeringUnits fill value/ts/unit, the StatusCode becomes the 192/64/0 quality flag, and the string NodeId is the DeltaV browse path — read across the one-way NOA seam. Original diagram by the authors, created with AI assistance.

Walk the fields in the order the loader cares about. The DisplayName is not decoration — the dcs-mock writes the canonical tag (BR101.Temp.PV) into it precisely so the bridge can discover the DeltaV-path-to-tag mapping by browsing, never a pre-shared list; opcua_dcs_bridge.discover_pv() recurses the address space and _tag_and_unit() reads exactly this attribute. The Value is a Double; the collector's to_sample() rounds it to four decimals — the same precision the PI path carries (the pi-web-api-stub serves PI values pre-rounded to 4 dp), so the OSS copy and the DCS original agree to the digit. The SourceTimestamp is the moment the DCS sampled the loop, not the moment we read it — the distinction that keeps the mirror contemporaneous. EngineeringUnits travels with the value so 37.02 is never a bare number; the dcs-mock attaches it (with a UNECE namespace URI) as an OPC UA property, exactly as a real DeltaV PV does.

The load-bearing field is the StatusCode. The collector's quality_code() takes the top two severity bits of the 32-bit status word — 00 Good, 01 Uncertain, 10/11 Bad — and collapses them to our smallint 192/64/0, the OPC-native twin of the PI bridge's Good/Questionable mapping. A Bad read carries no value (asyncua nulls it), so to_sample() passes None through and the point is recorded with quality 0, never dropped — an investigator must see the gap, not infer it. Finally batch_id is the one field the message does not carry: the bridge supplies it from the run it is backfilling, and it is the join key (the shared value that links a row in one table to its match in another) from a flat sensor row — a single timestamped measurement with no batch context of its own — to the GMP batch record in s88.batch. The string NodeId (ns=2;s=BR101/TIC-101/PID1/PV) is the DeltaV browse path — module, function block, parameter — and the .PV suffix matters: it is the process value (evidence), not the .SP setpoint (the recipe), and the whole exchange runs across a read-only NOA seam that lets data flow out and nothing in.

Where this DataValue comes from — the trilogy back-trace

This single OPC UA reading completes a thread that runs through all three books. The temperature it carries is generated by a real piece of equipment: Book 1 walks the floor where that loop physically lives, in the production bioreactor holding BR101 at 37 °C. Book 2 then frames the same reading as a data-point and names the open problem this code answers — how a DCS tag travels up the stack over open protocols in Connectivity & Integration Standards, and how control and historian data earn their meaning in Automation & Control Data. The bridge in this chapter is the open-source implementation of that journey: the physical PV becomes one StatusCode-checked row in ts.sensor_reading.

Inside DeltaV: how it connects, stores, and shares

It is worth opening the DeltaV side of that contract, because "DeltaV exposes data over OPC UA" quietly answers three different questions: how does the data leave the DCS, where does DeltaV keep it, and how does our stack take it without disturbing the control loop?

How it connects. DeltaV's OPC UA face is, in most plants, a server hosted on the ProfessionalPLUS and Application Station workstations — and it is a wrapper: it converts DeltaV's older OPC Classic interfaces into OPC UA, presenting one common endpoint for three kinds of data — DA (live process values), A&E (alarms and events), and HDA (historical values) [13]. A PK or PK Flex controller can also embed its own OPC UA server, but that one serves live Data Access only — no alarms, no history. The address space is the DeltaV control hierarchy laid bare: you browse control strategies → area → module → function block → parameter → field (a function block being one reusable control element — a PID controller, say — wired into a module), so the process value of a loop is reached at a path like … > BR101 > TIC-101 > PID1 > PV > CV, where the final CV (current value) is the live field under the PV parameter. The illustrative read result above writes that destination as a single string NodeId for brevity; what Emerson actually documents is the browse path — and, exactly as with our own server, a client discovers it by browsing rather than memorizing it. On the wire it is ordinary OPC UA: an opc.tcp endpoint, 128- or 256-bit session encryption with message signing, and username/password (or a deliberately limited anonymous) login.

How it stores data. DeltaV does not keep everything in one place; it keeps three stores for three kinds of record, and knowing which is which tells you where to read [14]:

  • the Continuous Historian holds the time-series — analog, discrete, and text parameters with their quality — collected on configurable deadbands so an unchanged value is not re-logged every scan; it captures up to roughly 250 parameters on a standard workstation and scales to tens of thousands on an Application Station;
  • the Event Chronicle holds alarms and events — process, system, operator, safety, and sequence-of-events — in a Microsoft SQL Server database;
  • the Batch Historian holds the batch and recipe execution records, aggregated in SQL Server from the Batch Executive and the Event Chronicle.

History reaches the OPC UA wrapper through the DeltaV OPC History Server, which fronts the Continuous Historian with an HDA interface — which is why a client can HistoryRead a parameter back over the very connection it subscribes on. (We deliberately quote no fixed retention here: DeltaV's retention is a function of configured archive size and export policy, not a headline number.)

How it connects to the rest. Here the honest-hybrid discipline bites. We take a strictly read-only path: the collector subscribes to live parameters and reads history, and never writes a setpoint. That is not a restriction the DeltaV server imposes — it happily accepts writes — it is our architectural choice, and it has a name: the NAMUR Open Architecture (NOA, NE 175). NOA sanctions a second channel for monitoring and optimization in which data flows out of the validated control core only — a "data-diode" direction of travel — with OPC UA named as the line to the outside world [15]. Our open-source collector (asyncua or node-opcua — our tooling, not Emerson's) lands those reads exactly where the rest of the book expects them: ts.sensor_reading rows in TimescaleDB and the ISA-88/95 batch and genealogy tables in PostgreSQL. The DCS keeps controlling; we keep mirroring.

A three-zone diagram of DeltaV feeding an open-source mirror. On the left, the DeltaV DCS validated core: BR101 control modules carrying live PV, SP, OUT and MODE parameters, historized into three stores — a Continuous Historian for time-series values, an Event Chronicle for alarms and events in SQL Server, and a Batch Historian for batch records built from the Batch Executive and Event Chronicle; a note records that the PK controller embeds its own live-data-only OPC UA server. In the middle, an OPC UA wrapper server on the ProfessionalPLUS or Application Station exposing DA for live values, alarms-and-events for events, and HDA for history. On the right, across a read-only NAMUR Open Architecture seam that carries data out only with no setpoint writes back, an open-source collector subscribes and reads history into TimescaleDB sensor_reading rows and a PostgreSQL ISA-88/95 batch and genealogy store. DeltaV exposes one OPC UA wrapper server (DA, alarms-and-events, HDA) over three internal historians; the open-source layer reads it across a strictly read-only NOA seam and mirrors it into TimescaleDB and PostgreSQL — never writing back. Original diagram by the authors, created with AI assistance.

The bridge code already exists — as a working template. You do not have to imagine the DCS bridge: it is a near-clone of the PI bridge we shipped and tested in the last chapter — the bridge to the commercial PI System process historian (OSIsoft/AVEVA), the long-term database many plants use to archive their time-series. That bridge lives in examples/chapters/17-bridge-pi-historian/pi_bridge.py, and it is exercised by examples/tests/test_bridges.py against the pi-web-api-stub. Its two load-bearing functions are the exact shape a DCS bridge needs — map a vendor's quality flag to an OPC quality code, then emit the canonical row tuple:

# examples/chapters/17-bridge-pi-historian/pi_bridge.py
def quality_code(item: dict) -> int:
"""PI Good/Questionable/Substituted -> OPC UA quality code."""
if item.get("Questionable"):
return 64 # Uncertain
return 192 if item.get("Good", True) else 0 # Good / Bad


def to_sensor_rows(items: list[dict], tag: str, batch_id: str) -> list[tuple]:
"""Map PI recorded values to ts.sensor_reading rows (ts, tag, value, unit, quality, batch_id)."""
return [(it["Timestamp"], tag, it["Value"], it.get("UnitsAbbreviation"),
quality_code(it), batch_id) for it in items]

The DCS bridge clones this exactly: where pi_bridge.read_recorded() does client.get(".../streams/{wid}/recorded") against PI Web API, the DCS version (opcua_dcs_bridge.py) HistoryReads an OPC UA node with asyncua — or subscribes to its live DA value — and receives value + StatusCode + SourceTimestamp; quality_code() maps the OPC UA StatusCode severity to 192/64/0 instead of PI's flag, and the Chapter 7 opcua-collector's to_sensor_rows() — the OPC-native twin of the PI function above, with the tag carried per sample — is reused verbatim. This is now committed and tested, not a sketch: examples/chapters/18-bridge-dcs-mes-erp/opcua_dcs_bridge.py against the dcs-mock service, plus the Chapter 7 opcua-collector that already streams the flat opcua-server into ts.sensor_reading (compose capture profile) — and examples/tests/test_opcua_bridges.py asserts the OPC UA rows match the golden batch, and the PI rows, to the digit and the quality flag. The DCS port is just a different transport call wrapped around the same quality_code / to_sensor_rows core; make dcs-backfill lands a window from the dcs-mock into the historian.

When OPC UA is not on offer. Older Siemens skids (self-contained, pre-assembled process units that arrive on a frame) sometimes expose only the native S7 protocol (Siemens' proprietary PLC communication language, from its Step7 toolset), with no OPC UA server in front. Here the open-source answer is Apache PLC4X (Apache 2.0), a vendor-neutral library whose S7 (Step7) driver reads and writes Siemens S7 PLCs directly [7]. It speaks dozens of legacy protocols, so it is the bridge of last resort for the brownfield skids (existing, older installations rather than new "greenfield" builds) OPC UA forgot. Chapter 11 used the same read-a-tag-write-a-row pattern for legacy Modbus skids (with pymodbus, in examples/chapters/09-legacy-skids-modbus-s7/modbus_reader.py); the DCS case applies that pattern through PLC4X instead, pointed at a control system rather than a standalone skid.

One honesty note carried over from the connectivity chapters: a read-only OPC UA channel up to a monitoring layer is precisely the NAMUR Open Architecture idea — a second, read-mostly data path for analytics that leaves the validated core untouched. It is the architecturally sanctioned way to get DCS data without re-validating the DCS.

The MES: the honest verdict is "no credible OSS GxP option"

Now the hard sentence. A Manufacturing Execution System (MES) at ISA-95 Level 3 is what turns a master recipe into an executed, electronically signed batch record: it dispenses materials against a work order, enforces the order of operations, captures operator e-signatures, and produces the reviewed electronic batch record (EBR) that a quality unit releases against. It is the most heavily validated (carrying documented, risk-based evidence that the system does exactly what it is specified to do, kept current across its whole life), most Part-11-saturated system on the floor — Part 11 being FDA 21 CFR Part 11, the US rule that makes electronic records and e-signatures legally as trustworthy as paper and ink.

There is no open-source product that credibly fills this slot for GMP biomanufacturing. This is not an oversight in the book's tool survey; it is the survey's finding. You can assemble pieces — a workflow engine here, an eLN (electronic lab notebook) there, our own ISA-88/95 model in PostgreSQL — but none of them carries the validated, vendor-accountable, Part-11-complete execution-and-e-signature package a commercial MES (or a validated paper-on-glass system, where a tablet replaces the paper batch sheet) provides. GAMP 5 (the industry's standard guide for validating computerized systems) — in its second edition — is explicit that open source can be used in GxP, but only inside a validated lifecycle with risk-based, critical-thinking assurance proportionate to use [12] — and assembling a homemade MES and validating it to that bar is a multi-year program no analytics team should pretend to win on the side.

So the architecture follows the verdict. The commercial MES (or paper-on-glass) stays the system of record for execution. Our open-source layer does two legitimate things instead:

  1. It mirrors the MES's structural output — the recipe, the operations and phases, the batch and its actual phase windows — into the relational model we already built, so analytics and dashboards have context.
  2. It never originates the execution record. No e-signature, no material disposition, no release decision lives in the OSS layer.

The good news is that the mirror is already built. The MES's world is ISA-88/95, and we modeled ISA-88/95 in PostgreSQL back in Chapter 4. Here is the actual backbone, from examples/platform/db/10-isa88-95.sql — the equipment hierarchy and the procedural model an MES exports map straight onto these tables:

-- 10-isa88-95.sql — the relational backbone (Chapter 4).
CREATE TABLE s88.unit ( -- the equipment a phase runs on
unit_id text PRIMARY KEY, -- e.g. BR101
area_id text NOT NULL REFERENCES s88.area,
name text NOT NULL,
unit_type text NOT NULL, -- bioreactor | chromatography | tff | fill_line ...
vendor text,
model text
);

CREATE TABLE s88.operation ( -- an ordered step of the recipe
operation_id text PRIMARY KEY,
recipe_id text NOT NULL REFERENCES s88.recipe,
seq_no int NOT NULL,
name text NOT NULL, -- Inoculation | Fed-batch | Harvest | ProteinA ...
unit_type text NOT NULL
);

CREATE TABLE s88.phase ( -- the smallest procedural element
phase_id text PRIMARY KEY,
operation_id text NOT NULL REFERENCES s88.operation,
seq_no int NOT NULL,
name text NOT NULL
);

And the batch — the run a work order produces — with the genealogy that lets us trace a finished lot back to its seed train, exactly as an MES would record it (also from examples/platform/db/10-isa88-95.sql):

CREATE TABLE s88.batch (
batch_id text PRIMARY KEY,
product_id text NOT NULL,
recipe_id text NOT NULL REFERENCES s88.recipe,
unit_id text NOT NULL REFERENCES s88.unit,
lot text,
status text NOT NULL DEFAULT 'in_progress', -- in_progress | complete | released | rejected
start_ts timestamptz NOT NULL,
end_ts timestamptz
);

-- lot genealogy: directed edges child -> parent (seed -> bioreactor -> pool -> DS -> DP)
CREATE TABLE s88.genealogy (
batch_id text REFERENCES s88.batch,
child text NOT NULL,
child_type text NOT NULL,
parent text NOT NULL,
parent_type text NOT NULL,
PRIMARY KEY (child, parent)
);

This is the receiving end of every MES and ERP message in the chapter. When SAP sends a production order, it becomes a row in s88.batch. When the MES reports the actual phase windows, they land in s88.batch_phase. When the ERP reconciles which component lots fed which product lot, those edges land in s88.genealogy. Our seed data already populates this for the running case — the ACME Biologics enterprise, the Newark DE Plant site, BR101 (a Sartorius Biostat STR 50), the CHO-MAB-001 fed-batch recipe (a monoclonal-antibody process grown in Chinese-hamster-ovary cells, fed nutrients over the run; trimmed here to upstream-plus-capture — its operations stop at ProteinA, the affinity-chromatography step that first captures the antibody; a full train adds polish, viral, UF/DF and fill operations on the seeded TFF01 and fill line — the downstream purification steps Book 1 walks through, from capture chromatography to fill-finish), and six campaign batches including the deliberately rejected BATCH-2026-004. The mirror is real and queryable today; what it must not do is become the place the batch is executed and signed.

Three lanes feeding one open-source mirror. The OT lane reads DeltaV and Siemens over OPC UA into TimescaleDB; the MES Level-3 lane exports batch structure as B2MML into a PostgreSQL ISA-88/95 model, with a red dashed barrier marking no e-signature and no release in open source; the ERP Level-4 lane exchanges SAP materials and orders via IDoc and OData. Every arrow points into the mirror.

The honest-hybrid boundary at Levels 1-4: open source reads the DCS over OPC UA, mirrors MES batch structure as B2MML, and exchanges ERP messages — but the validated systems keep execution, signatures, and disposition. The OSS layer is the mirror, never the original. Original diagram by the authors, created with AI assistance.

SAP/ERP: exchange materials, lots, and work orders with B2MML

The ERP — in biopharma, overwhelmingly SAP S/4HANA (SAP is the dominant enterprise-software vendor; S/4HANA is its current ERP product) — owns the business truth: which materials exist, which lots are released or quarantined, and which work order authorizes which batch. The OSS layer needs that context to make a sensor reading meaningful, and occasionally needs to report a result back. The standard, vendor-neutral way to do this is ISA-95 / IEC 62264 object models serialized as B2MML (Business To Manufacturing Markup Language), an XML implementation of ISA-95 that any company may use royalty-free with attribution to MESA (MESA International, the industry body that publishes the B2MML schema) [1][2][3]. The literature backs the pattern: peer-reviewed ERP↔MES integrations use ISA-95 object models implemented as B2MML XML transaction messages [11]. And the "mirror MES batch structure as B2MML" claim is not just described here: the companion repo ships two real-shaped B2MML documents (examples/platform/ontology/b2mml/{master-recipe.xml,batch-production-record.xml}) and a runnable b2mml_to_rdf.py crosswalk (a converter that maps one data format's fields onto another's) that reads them and emits graph triples (subject-predicate-object statements, the building block of a knowledge graph) — the same exchange, taken one step further in the ontology book's shop-floor and digital-twins chapter.

SAP itself offers two concrete doors. The classic one is the IDoc (Intermediate Document), SAP's structured message for exchanging materials, lots, and order data, which integration layers convert to and from B2MML [10]. The modern one is OData: SAP S/4HANA exposes production orders and related material data through documented OData APIs — for example API_PRODUCTION_ORDER_2_SRV — that an OSS client consumes to mirror ERP state [9]. As with PI and the DCS, the companion repo's plan is a sap-mock (a FastAPI service offering an OData endpoint plus an IDoc XML drop folder, behind the commercial profile) so the exchange is built and contract-tested with no SAP license; it is on the roadmap, not yet shipped, so the snippets here are the target contract.

A production order, pulled from the OData door, would look like this (illustrative SAP OData JSON matching the documented API_PRODUCTION_ORDER_2_SRV shape):

{
"d": {
"ManufacturingOrder": "1000004711",
"Material": "MAB-001",
"ProductionPlant": "NEWARK",
"MfgOrderPlannedTotalQty": "1",
"ProductionUnit": "EA",
"MfgOrderScheduledStartDate": "2026-01-05T00:00:00Z",
"OrderIsReleased": true
}
}

The bridge's job is to land that in our model without ever pretending to be SAP. The mapping is small and explicit — Materials88.batch.product_id, ProductionPlant → the site, and the order ID deterministically resolved to a stable batch_id (the bridge maps ManufacturingOrder 1000004711 to the same BATCH-2026-007 every time) and also kept as a reference so the mirror can be reconciled back to SAP:

-- Mirror a released SAP production order into the ISA-88/95 batch table.
-- SAP stays the system of record; this row is a faithful copy keyed to the order.
INSERT INTO s88.batch (batch_id, product_id, recipe_id, unit_id, lot, status, start_ts)
VALUES ('BATCH-2026-007', 'MAB-001', 'CHO-MAB-001', 'BR101', 'L26007',
'in_progress', '2026-01-05T00:00:00Z')
ON CONFLICT (batch_id) DO UPDATE
SET status = EXCLUDED.status,
product_id = EXCLUDED.product_id; -- idempotent: re-applying the same order is a no-op

Anatomy of a SAP production-order message (field by field)

The OData side deserves the same dissection the DeltaV DataValue got — with one honesty flag. The sap-mock is on the roadmap, not yet shipped, so the JSON above is the target contract (the documented API_PRODUCTION_ORDER_2_SRV shape [9]), but the landing side is fully real: every field maps to a column of s88.batch, the table seeded in examples/platform/db/seed/seed_cho_line.sql and consumed by the INSERT … ON CONFLICT above. Reading the order field by field shows which slot each value fills, and which single field is the idempotency key.

Identity-card diagram dissecting one SAP OData production-order record. The header reads SAP OData order 1000004711 mapping to s88.batch. An amber highlighted block shows ManufacturingOrder, labelled the idempotency key, kept as the reconciliation reference and used as the ON CONFLICT batch_id key so re-applying the same order is a no-op. Rows below map Material MAB-001 to product_id, ProductionPlant NEWARK to the site, MfgOrderPlannedTotalQty 1 plus ProductionUnit EA to planned quantity and unit of measure, MfgOrderScheduledStartDate to start_ts, and OrderIsReleased true to status in_progress. A cyan footer panel states SAP stays the system of record, that recipe_id and unit_id are resolved from plant and material, that the B2MML ISA-95 envelope carries the same fields whether OData JSON or IDoc, that a MaterialLot reply lands lineage as genealogy edges, and a red note that the row is never written back to SAP as an authoritative record. One SAP order dissected against s88.batch: Material/ProductionPlant/MfgOrderScheduledStartDate/OrderIsReleased fill product, site, start and status, while ManufacturingOrder is held as the reconciliation reference and the ON CONFLICT idempotency key. Original diagram by the authors, created with AI assistance.

Field by field: Material ("MAB-001") is the product, and it is the seed line's real product_id on the CHO-MAB-001 recipe — so the bridge resolves recipe_id and unit_id (BR101) from the plant-plus-material context rather than trusting the message to carry them. ProductionPlant ("NEWARK") is the s88.site already seeded as the Newark DE Plant. MfgOrderPlannedTotalQty ("1") with ProductionUnit ("EA") is the planned quantity and its unit of measure — one campaign batch each. MfgOrderScheduledStartDate becomes start_ts. OrderIsReleased: true is the gate: only a released order is mirrored, landing as status = 'in_progress' (the seed batches show the downstream states this can reach — released, complete, and the deliberately rejected BATCH-2026-004). The decisive field is ManufacturingOrder ("1000004711"): the bridge deterministically derives the stable batch_id (BATCH-2026-007) from it, so the same order always lands on the same batch_id; the order ID itself is not copied into a data column but kept as the reconciliation reference. Because the conflict key batch_id is derived this way, the ON CONFLICT (batch_id) clause is what makes re-delivering the same order a no-op instead of a duplicate batch. Whether the order arrives as this OData JSON or as a classic IDoc, an integration layer converts both to the same B2MML/ISA-95 envelope, so the mapping above is the same either way [2][3].

The production-order exchange, step by step

Putting the two anatomies together, the SAP-to-OSS handshake is a short, idempotent loop with no surprises:

  1. SAP releases the order. OrderIsReleased flips to true; SAP remains the system of record for the order itself.
  2. The OSS bridge pulls it. It polls the documented OData endpoint (or receives the IDoc drop), reads the seven fields, and resolves recipe_id/unit_id from the plant and material.
  3. It lands one idempotent row. The INSERT … ON CONFLICT (batch_id) DO UPDATE writes (or updates) exactly one s88.batch row, on the batch_id deterministically derived from ManufacturingOrder.
  4. It reconciles, never overwrites. A periodic check asserts every released SAP order has exactly one mirrored batch; a mismatch becomes a data-integrity event, not a silent fix.
  5. Lineage flows back the same way. When the ERP confirms which component lots fed which product lot, the MaterialLot/MaterialDefinition objects land as s88.genealogy edges — the mirror records the lineage, SAP keeps the authority.

Two doors, two trust contracts

It is worth naming why the DCS and ERP exchanges look alike — both land a vendor message as a canonical row — yet sit on opposite sides of the trust boundary. They are two doors with two contracts. The DCS door (DA/A&E/HDA over OPC UA) is a read contract: the data is real-time process evidence, the direction is strictly out-of-the-core across the NOA seam, and the idempotency mechanism is delete-window-then-insert on a keyless time-series hypertable (TimescaleDB's auto-partitioned table for time-series, which here carries no unique constraint per reading). The ERP door (IDoc/OData carrying B2MML) is an exchange contract: the data is business truth (orders, materials, lots), the direction is mostly down from Level 4 but occasionally a reported result goes back, and idempotency comes from a primary key plus ON CONFLICT on the relational batch table. Same honest-hybrid rule — mirror, do not replace — but the OPC UA door trusts a StatusCode while the SAP door trusts a ManufacturingOrder, and confusing the two is how a mirror quietly becomes a shadow.

The same B2MML envelope carries material-lot information the other direction. ISA-95's MaterialLot and MaterialDefinition objects map onto our s88.genealogy edges, so when the ERP confirms that capture pool PApool-007 came from bioreactor batch BATCH-2026-007, the mirror records the lineage exactly as the seed data already does for the golden batch:

batch_id,child,child_type,parent,parent_type
BATCH-2026-001,SEED-001,seed_train,WCB-CHO-001,wcb
BATCH-2026-001,BATCH-2026-001,bioreactor,SEED-001,seed_train
BATCH-2026-001,PApool-001,capture_pool,BATCH-2026-001,bioreactor
BATCH-2026-001,DS-001,drug_substance,PApool-001,capture_pool
BATCH-2026-001,DP-001,drug_product,DS-001,drug_substance

That is real data from examples/datasets/lot_genealogy.csv — the full working-cell-bank → seed-train → bioreactor → capture-pool → drug-substance → drug-product chain for one lot, the exact lineage an ERP-driven genealogy exchange would reconstruct. Each hop is a real downstream unit operation Book 1 walks through, and the genealogy edge is the record that the material crossed it: the capture pool is the low-pH Protein A eluate from affinity capture (which doubles as the start of low-pH viral inactivation); the drug substance is what survives polishing chromatography, viral filtration, and final UF/DF concentration and buffer exchange. The edge says nothing about how each step purifies — that is Book 1's subject — but it is the spine that lets a release investigation or a recall scope back from a failed drug-product vial to the exact pool, bioreactor batch, and cell-bank vial it came from, which is precisely why the ERP keeps it authoritative and the mirror only copies it.

Idempotency and reconciliation: the rule that keeps a mirror honest

Every exchange in this chapter is a copy, and copies drift. The discipline that keeps a mirror trustworthy is the same one the historian chapter introduced: make every write idempotent, and reconcile rather than overwrite.

Idempotent means re-applying the same message changes nothing. SAP redelivers IDocs; OData polls overlap; a DCS subscription replays after a reconnect. If "production order 1000004711 is released" arrives three times, you must end with one batch row, not three. The ON CONFLICT (batch_id) DO UPDATE above is precisely that guarantee — because the same order deterministically derives the same batch_id, re-applying it is a no-op. (Note the asymmetry with time-series: the historian's hypertable carries no unique key on (tag, ts), so a DCS backfill earns idempotency by delete-window-then-insert, exactly as Chapter 20 described, while the relational batch/genealogy tables earn it through primary keys and ON CONFLICT.)

Reconciliation means you periodically ask both systems the same question and assert they agree. Does every released SAP order have exactly one mirrored batch? Does every s88.genealogy edge trace to an ERP-confirmed material lot? A divergence is logged as a data-integrity event, never silently fixed — because a silent fix in a mirror is how a shadow record is born, and a shadow record that disagrees with the validated system of record is exactly what an inspector hunts for. The rule, stated once: the OSS layer must sync faithfully and never become a parallel record of authority.

What the field record shows: DCS-MES-ERP integration failures

The "shadow record" warning is not theoretical hand-waving — it is the specific failure mode the inspectorates (the regulators who audit a plant) name. The UK MHRA's (the Medicines and Healthcare products Regulatory Agency, Britain's drug regulator) GXP Data Integrity Guidance is explicit that all data, including data held in unofficial or interim copies, falls under GMP scrutiny, and that uncontrolled parallel copies of a regulated record are a recognized integrity risk precisely because they can be selectively kept, discarded, or made to disagree with the system of record [16]. A mirror that silently "fixes" a divergence is, by that definition, manufacturing exactly the kind of unofficial record the guidance warns against. This is why the reconciliation step logs the divergence instead of healing it: the divergence is the record an investigator needs to see.

The integration literature points at the same fault line from the engineering side. The peer-reviewed ERP-to-MES case study this chapter already cites found that the hard part of enterprise-to-control integration is not the transport but the semantic mapping — making one system's "order," "material," and "lot" mean the same object on both sides — and that ISA-95 object models serialized as B2MML exist precisely to pin that mapping down so the two records cannot drift apart [11]. Read together, the two sources frame the realistic field failure: the bridge is easy to write and easy to get subtly wrong, and the way it goes wrong is a quiet semantic mismatch that ages into a shadow record. Three concrete shapes recur:

  • The non-idempotent re-delivery. SAP redelivers an IDoc after a timeout; without the ON CONFLICT key on the batch_id derived from ManufacturingOrder, the mirror now holds two batches for one order — a count that will not reconcile against SAP at release review.
  • The unit-of-measure drop. A DataValue arrives with its EngineeringUnits unread, or a ProductionUnit is ignored; 37.02 or a planned quantity lands as a bare number, and the mirror silently disagrees with the source on what the value means.
  • The swallowed Bad point. A DCS read returns a Bad StatusCode; a naive bridge drops it as "no value," so the historian shows an unbroken trace where the validated system recorded a gap — the mirror now tells a more flattering story than the original, which is the worst possible direction for a shadow record to lean.

Each is prevented by a single discipline already in the code: key the relational write, carry the unit, and record the Bad point with quality 0. The bridge is the least exciting code in the book; the integrity of the bridge is among the most consequential.

The same exchange, expressed as a graph the consumers can trust

The relational landing pad is one face of the mirror; the knowledge graph two chapters back is the other, and the two carry the identical facts these bridges deliver. It is worth saying explicitly how the bridge's central artifacts become semantics, because the downstream consumers — a SPARQL digital-thread query, a SHACL release gate, an ML soft sensor — all read from that graph rather than from the wire. A SAP-confirmed lineage edge is one RDF triple: bp:PApool-007 bp:derivedFrom bp:BATCH-2026-007 — a subject IRI, the predicate the ontology book conceptualizes as a transitive derivedFrom spine, and an object IRI you can walk. A DataValue is a typed-literal triple instead: bp:BATCH-2026-007 bp:temperature "37.02"^^xsd:float, the float datatype tag carrying the same precision the OPC read did. The genealogy backbone the ERP reconciles is the answer to a competency question — "what did this lot derive from, all the way back?" — that a SPARQL (bp:derivedFrom)+ property path resolves in one statement, exactly the walk SQL needs a recursive CTE for. And the reconciliation rule the previous section made non-negotiable has a formal twin: a SHACL shape — the same Shapes Constraint Language the ontology book's release gate uses — asserts closed-world that every released bp:Batch carries exactly one derivedFrom parent and a present, in-range CQA, so a dropped lineage edge or a missing unit fails as a now error instead of aging silently into a shadow record. The bridge guarantees the triples are emitted with their StatusCode and unit intact; SHACL guarantees the graph cannot claim release with a hole in it. The two anatomies in this chapter — the DeltaV DataValue and the SAP order — are, read this way, just two triples waiting for a subject IRI.

Why a faithful mirror is the precondition for any learning model

The closing claim of this chapter — that the mirror is where SPC and soft-sensing become possible — has a sharper edge once you take the ML lifecycle seriously, and it is the reason a contextualized mirror, not a raw tag stream, is what Book 5 builds on. Three model-side disciplines depend directly on the genealogy and batch keys these bridges land:

  • Leakage-free, batch-grouped validation. A soft sensor's honest test error is only measured by grouped cross-validation — holding out whole batches, never random rows, so a measurement and its near-twin from the same run cannot sit on both sides of the split. That grouping is impossible without the batch_id join key the DCS bridge supplies and the s88.genealogy spine the ERP reconciles; the models-and-validation chapter shows the leak-free, batch-honest split is what separates a real R² from a flattering one. The bridge's quietest field is the one that makes the model's headline number trustworthy.
  • Applicability domain and drift, distinguished from process drift. A model is only trustworthy inside the input region it was calibrated on; the StatusCode-checked, unit-carrying reading is what lets a monitor tell covariate shift (the inputs moved — a probe fouled, a new raw-material lot) from genuine process drift (the living culture itself changed), the distinction the MLOps chapter builds two separate detectors for. A swallowed Bad point or a dropped unit does not just dent a dashboard — it silently moves a model's applicability domain and makes its drift alarm lie.
  • Model lineage as a first-class record. The same genealogy discipline that traces a vial back to its cell bank traces a prediction back to the exact data, model version, and batch it was made from — the lineage MLOps and lifecycle treats as a GMP requirement. The mirror is the substrate that record hangs on, which is why a hybrid model or digital twin is only as auditable as the bridge feeding it.

The through-line is blunt: garbage-in is not merely garbage-out for a learning model — it is a validated model quietly going wrong while still passing its monitors. Every discipline this chapter imposes on the mirror — idempotency, the unit, the Bad-point-as-quality-0 — is, downstream, a precondition for a model anyone may trust with a decision about a medicine.

Why it matters

Get these three bridges wrong and you fail in one of two recognizable ways. Either you over-reach — you let the open-source layer write setpoints to the DCS, sign batch records, or dispose material — and you inherit a validation and Part-11 burden the OSS tools cannot carry, for no patient benefit and considerable regulatory peril. Or you under-engineer the exchange — non-idempotent writes, no reconciliation — and your mirror quietly drifts from SAP and the MES until a dashboard contradicts the official record during a release review.

Get them right and the division of labor is clean and liberating. The DCS keeps controlling the process; the MES keeps executing and signing the batch; SAP keeps owning materials and orders. The open-source layer reads all three through standard doors, lands everything in one ISA-88/95 model, and becomes the fast, cheap, unconstrained place to do contextualization, SPC, and soft-sensing on top of trustworthy context (built in-stack in Process Analytics: SPC, MVDA & Soft Sensors, and modeled end-to-end in Book 5's ML & AI chapters). Each commercial system stays the system of record for what it is accountable for [12]; the standards — OPC UA for control [4], B2MML/ISA-95 for business exchange [1] — are the seams that make the mirror faithful.

In the real world

Walk into an approved-product mAb plant and this is the topology you find: a DeltaV or Siemens PCS 7 DCS running the suites — Siemens' newer PCS neo turns up on greenfield lines, but PCS 7 remains the dominant installed base — a commercial MES (Werum PAS-X, Körber, Tulip-on-glass, or similar) holding the electronic batch record, and SAP at the top owning everything financial and material. Integration teams spend their careers on exactly the doors this chapter describes — OPC UA off the DCS, B2MML/IDoc/OData to and from SAP — and they spend it precisely because nobody is allowed to replace the validated systems underneath.

The honest OSS-vs-commercial verdict for this layer is the bluntest in the book. For the DCS read path, open source is excellent: node-opcua [8], asyncua, and Apache PLC4X [7] give you everything you need to mirror control data, and the read-only NOA pattern means you do it without re-validating anything. For ERP exchange, open source is fully capable: B2MML is a free, royalty-free schema [3], and a Python OData/IDoc client is a weekend, not a year. But for the MES slot, there is simply no credible open-source GxP product, and pretending otherwise would be the most dangerous overstatement this book could make. Pure open source gets you roughly 80% of the platform; the MES is one of the places the last GxP mile is not hybrid but firmly, honestly commercial.

The intensified/continuous variant of our process — perfusion (continuously feeding and harvesting the bioreactor instead of the single fed-batch above) with multi-column capture — only multiplies the tags and the orders flowing across these seams, which makes a disciplined, idempotent, reconcilable mirror more valuable, not less.

Key terms

  • GxP / GMP — GxP is the family of Good-x-Practice regulations that govern medicine-making (GMP for manufacturing, GLP for labs, GCP for clinical); GMP (Good Manufacturing Practice) is the manufacturing member of that family, so the two are related but not synonyms — an OSS tool used on the floor must fit inside this framework.
  • Part 11 (21 CFR Part 11) — the US FDA rule that makes electronic records and electronic signatures legally as trustworthy as paper and ink; the e-signature and EBR burden it imposes is why the MES slot stays commercial.
  • Validation / CSV — documented, risk-based evidence that a computerized system does exactly what it is specified to do, maintained across its whole life; validating a homemade MES to the regulated bar is a multi-year program.
  • DCS (Distributed Control System) — the validated control layer (Emerson DeltaV, Siemens PCS7/SIMATIC) that runs the process loops at ISA-95 Levels 1-2; the OSS layer reads it over OPC UA [5][6], never writes setpoints to it.
  • DeltaV OPC UA wrapper (DA / A&E / HDA) — the server on the ProfessionalPLUS / Application Station that converts DeltaV's OPC Classic interfaces into one OPC UA endpoint exposing live Data Access, Alarms & Events, and Historical Data Access [13].
  • Continuous Historian / Event Chronicle / Batch Historian — DeltaV's three internal stores: time-series process values (deadband-collected), alarms/events (SQL Server), and batch/recipe records (SQL Server, from the Batch Executive) respectively [14].
  • MES (Manufacturing Execution System) — the Level-3 system that executes the recipe, captures e-signatures, and produces the electronic batch record; there is no credible open-source GxP option, so it stays commercial [12].
  • ERP (Enterprise Resource Planning) — the Level-4 business system (SAP S/4HANA) owning materials, lots, and work orders, exchanged via IDoc and OData [9][10].
  • OPC UA — IEC 62541; the platform-independent, secure protocol over which DCS/PLC control data is read into the OSS stack [4].
  • Apache PLC4X — vendor-neutral open-source library (Apache 2.0) that reads Siemens S7 and many legacy PLC protocols when no OPC UA server is on offer [7].
  • node-opcua — MIT-licensed Node.js OPC UA SDK used to browse/read/subscribe to DCS OPC UA servers [8].
  • B2MML — Business To Manufacturing Markup Language; the royalty-free XML implementation of ISA-95 used for materials/lots/work-order exchange [2][3].
  • ISA-95 / IEC 62264 — the standard models and terminology for enterprise-to-control integration that B2MML serializes and our PostgreSQL model implements [1][11].
  • IDoc / OData — SAP's classic message format and modern REST API for exchanging order and material data [9][10].
  • System of record — the authoritative, validated source for a kind of data; here, the DCS for control, the MES for execution, SAP for materials. The OSS layer is a mirror, never the record [12].
  • Idempotent — a write safe to repeat; relational mirrors earn it with primary keys and ON CONFLICT, time-series with delete-window-then-insert.
  • Shadow record — an uncontrolled parallel copy that diverges from the validated system of record; the failure mode reconciliation exists to prevent.
  • NAMUR Open Architecture (NOA) — a sanctioned read-mostly second channel that feeds analytics without altering the validated control core.
  • OPC UA DataValue — the structure a DCS returns per read: a Value, a StatusCode (which becomes our 192/64/0 quality), a SourceTimestamp, and an EngineeringUnits property; each field fills one column of the ts.sensor_reading six-tuple [4].
  • ManufacturingOrder (the idempotency key) — the SAP production-order ID held as the reconciliation reference; the bridge deterministically derives the stable batch_id from it, so the ON CONFLICT (batch_id) clause makes re-delivering the same order a no-op rather than a duplicate batch [9].
  • RDF triple — a subject-predicate-object fact (a lineage edge bp:PApool-007 bp:derivedFrom bp:BATCH-2026-007, or a typed-literal reading); the graph form of the same facts the bridges land in the relational model, walked by SPARQL and gated by SHACL (built in Semantics & the Digital Thread).
  • SHACL release gate — a closed-world Shapes Constraint Language shape that asserts every released batch carries its required lineage and a present, in-range CQA, so a dropped edge or missing unit fails now rather than aging into a shadow record (the ontology book's release gate).
  • Grouped (leave-one-batch-out) cross-validation — the leakage-free way to measure a soft sensor's honest error, holding out whole batches rather than random rows; it depends on the batch_id join key the DCS bridge supplies and the genealogy spine the ERP reconciles (Models and Validation).
  • Applicability domain / drift — the input region a model was calibrated on, and the StatusCode-checked, unit-carrying reading that lets a monitor tell input drift (a fouled probe, a new lot) from genuine living-culture process drift (MLOps and Lifecycle).

Where this leads

Control and business data now mirror faithfully into our stack, but one system of record still stands outside: the laboratory. Release testing — the assays that decide whether a lot ships — often lives in a commercial LIMS, and the result it signs is the one that matters most. The next chapter, Bridging to Commercial & Open-Source LIMS, builds the sample-and-certificate-of-analysis exchange to that world, with the honest note that LabKey's Part 11 features are paywalled and that SENAITE and openBIS fit the QC and process-development slots respectively — the same mirror-not-replace discipline, applied where the stakes are highest.