MES Integration Guide¶
The DCS gateway exposes a dedicated REST API at /api/v1/mes/ for integration with
Manufacturing Execution Systems (MES) at ISA-95 Level 3/4. This enables MES systems
such as SAP, Syncade, PharmaSuite, or Opcenter to submit work orders, poll production
data, and receive equipment status in ISA-95 terms.
Architecture¶
The MES API is a gateway module. It is not a separate service. It provides an ISA-95-shaped view of the same data managed by the DCS. No MES-specific CRDs or controllers are required.
flowchart LR
MES["MES System<br/>Level 3/4"] <-->|REST / B2MML| GW["DCS Gateway<br/>/api/v1/mes/"]
GW <-->|K8s API| CRDs[("etcd / CRDs<br/>Level 2")]
One round trip, end to end¶
The clip below is this whole guide performed by software that is not ours. Node-RED stands in for the supervisory layer: it releases an ISA-95 work order, the batch appears in the plant, an operator starts it, and the same flow pulls the finished production record back as JSON and as B2MML. Every call carries the API key and nothing else.
Two things in that clip are worth naming, because they are the answers to the questions an integrator asks first.
The API key is the only credential. There is no OIDC token and no user
session anywhere in the flow. The last beat sends the same key to a route
outside /api/v1/mes/ and is refused with 403, which is what confinement
looks like from the client's side.
The production record names both parties. The audit trail the record carries attributes the batch's creation to the MES key and its Start to the person who issued it. Nothing has to be reconciled afterwards to know which was which.
Authentication¶
MES systems authenticate using API keys (not OIDC). API keys are managed by administrators.
Create an API Key¶
dcs mes apikey create "SAP MES" --site newark
Or via REST:
curl -X POST https://gateway/api/v1/mes/apikeys \
-H "Authorization: Bearer <admin-oidc-token>" \
-H "Content-Type: application/json" \
-H "X-DCS-Nonce: $(uuidgen)" \
-d '{"name": "SAP MES", "site": "newark"}'
The response contains the plaintext key. Store it securely. It will not be shown again.
Every Write Needs a Nonce¶
The gateway requires a unique X-DCS-Nonce header on every mutating
request: POST, PUT, PATCH and DELETE. This is anti-replay protection under
IEC 62443 SR 3.8, and it is on by default (gateway.antiReplay.enabled).
A write without the header is refused before it reaches any handler:
{"error": "missing required X-DCS-Nonce header", "status": 400}
GET, HEAD and OPTIONS are exempt. Every read endpoint in this guide
works without a nonce.
What to send is up to the client. Any value unique per request will do, and a UUID is the obvious choice. Two rules matter:
- Never reuse one. A repeated nonce is rejected with
409 duplicate request nonce, which is the protection working. A client that retries a failed write must generate a fresh nonce for the retry. - The window is finite. The gateway remembers nonces for
gateway.antiReplay.window(10 minutes by default) and then forgets them, so uniqueness only has to hold within that window.
Because the nonce store is in memory and per-replica, the chart refuses to
render more than one gateway replica while anti-replay is enabled. See
docs/threat-model.md GW-T-03.
Use the API Key¶
curl https://gateway/api/v1/mes/sites/newark/production-capability \
-H "Authorization: ApiKey dcs_<key>"
List / Revoke Keys¶
dcs mes apikey list
dcs mes apikey revoke "SAP MES"
API Endpoints¶
All endpoints use the prefix /api/v1/mes/sites/{site}/.
Category 1: Product Definition (DCS → MES)¶
Returns effective MasterRecipes as ISA-95 product definitions.
| Method | Path | Description |
|---|---|---|
| GET | /product-definitions |
List all effective product definitions |
| GET | /product-definitions/{name} |
Get a single product definition |
Category 2: Production Capability (DCS → MES)¶
Returns equipment status and availability.
| Method | Path | Description |
|---|---|---|
| GET | /production-capability |
Site-level equipment overview with availability summary |
| GET | /equipment/{name}/status |
Single unit status |
Availability is derived from ISA-88 state: - available: Idle and unallocated, or Complete/Stopped/Aborted and unallocated - inUse: Running, Holding, Held, Pausing, Paused, Restarting, Resetting, or allocated - unavailable: Any unrecognized state (faulted equipment)
Category 3: Production Schedule / Work Orders (MES → DCS)¶
Submit and track work orders.
| Method | Path | Description |
|---|---|---|
| POST | /work-orders |
Submit a work order |
| GET | /work-orders |
List all work orders (one per batch in the site) |
| GET | /work-orders/{id} |
Get work order status |
| DELETE | /work-orders/{id} |
Cancel a work order that has not started |
Dispatch Modes¶
?dispatch=immediate creates a Batch now, and it is the only dispatch mode.
Send the parameter on every work order you submit. A submission that omits it,
or names any other mode, is refused with 400 Bad Request naming the parameter,
and nothing is created.
Dispatching decides when the batch is created. Whether it runs stays
with the operator. A dispatched batch lands in Pending, holding its equipment allocation request and
waiting for an operator to issue Start from the DCS. This is deliberate: an
external system can queue work, and only a person at the plant can set a vessel
running. The create response says so explicitly with
"awaitingOperatorStart": true.
That response carries no status field. The batch has been created and the batch
operator has not yet reconciled it, so there is no phase to report. Poll
GET /work-orders/{id} for the status once it exists.
A work order may carry a scheduledStartTime, and it is the time the order is
planned for. The DCS records it on the batch, reports it back on
GET /work-orders/{id}, and exports it as the B2MML ProductionRequest
StartTime. It starts nothing. A batch whose planned time has passed sits in
Pending exactly like one that named no time at all, waiting for the same
operator Start. "awaitingOperatorStart": true holds for every work order
this endpoint accepts. Until 2026-08-11 it did not: a past scheduledStartTime
started the batch on its own, which is the one thing this endpoint promises no
external system can do (ADR-0052,
#1462).
The operator finds that batch in Batch Execution for the site. The list keeps itself current while it is open, so a batch dispatched from an MES appears there without anyone reloading the page.
Work Order Request¶
{
"workOrderID": "WO-2026-001",
"productDefinition": "acetaminophen-500",
"quantity": "500",
"engineeringUnit": "kg",
"priority": 5,
"parameters": [
{"name": "batch_size", "value": "500"}
],
"requestedEquipment": {
"mixing": "reactor-1"
}
}
Submitting it, with the nonce every write requires:
curl -X POST "https://gateway/api/v1/mes/sites/newark/work-orders?dispatch=immediate" \
-H "Authorization: ApiKey dcs_<key>" \
-H "Content-Type: application/json" \
-H "X-DCS-Nonce: $(uuidgen)" \
-d @work-order.json
Resubmitting a Work Order¶
A work order ID identifies one order in a site, and the DCS enforces that. This matters most for the case the nonce rules above leave open: a retry after a response you never received carries a fresh nonce, so nothing upstream stops it from arriving as a second submission.
Submit the same workOrderID again and one of two things happens.
- The body is identical. You get the order that already exists, with
200 OK(a fresh creation returns201 Created), and no second batch is created. This is the answer a retry wants: submit, and if the answer is lost, submit the same body again until you get one. - The body differs in the product definition, parameters, requested
equipment, priority, description or scheduled start. The submission is
refused with
409 Conflictnaming the field that differs. A repeat that asks for something different is a new order under a reused ID, and the DCS will not guess which body you meant. Use a new work order ID for new work.
Distinguish the two success codes if you care whether the order was created on
this call. Both carry the same DTO, and a client that treats anything other
than 201 as a failure will retry an order that already exists.
Before this was enforced (#1460,
ADR 0051), a retry created a
second batch under the same ID, and reads then answered from whichever of the
two the API server listed first. A site that still holds such a pair resolves
every read to the older batch (the one that ran), and the extra batch is
visible in GET /work-orders.
Cancellation¶
DELETE /work-orders/{id} withdraws a work order that has not started, which
means a dispatched batch still sitting in Pending.
A batch that has begun executing is refused with 409 Conflict, whatever phase
it is in and whether or not it has finished. The Batch is the production record
that /production-performance/{batchID} serves. Deleting it would destroy
the record along with its parameters, its audit trail and its e-signature. Use a
Stop or Abort command to end a batch that is running.
curl -X DELETE https://gateway/api/v1/mes/sites/newark/work-orders/WO-2026-001 \
-H "Authorization: ApiKey dcs_<key>" \
-H "X-DCS-Nonce: $(uuidgen)"
Category 4: Production Performance (DCS → MES)¶
Returns batch production records.
| Method | Path | Description |
|---|---|---|
| GET | /production-performance |
List completed batch summaries |
| GET | /production-performance/{batchID} |
Full consolidated production record |
The full record includes batch metadata, parameters, material inputs/outputs, audit trail, and historian data (if configured).
External Results (data in from a laboratory or other system)¶
Not an ISA-95 category. This is the seam for a measurement produced outside the control system (a LIMS assay being the canonical case) reaching a phase that is waiting on it. It lives on this surface because it is the only inbound path a machine identity can call.
A chart author declares the wait in Structured Text:
titre := AWAIT_RESULT('assay-titre', 0.0, 100.0, 'g/L');
The phase parks there. Your integration then polls for what is being asked of it, and answers:
# What is waiting?
curl https://gateway/api/v1/mes/sites/newark/external-results/pending \
-H "Authorization: ApiKey dcs_<key>"
{
"items": [
{
"phaseName": "ferment-assay",
"batchID": "B-2026-0417",
"id": "a1b2c3d4e5f6a7b8",
"resultKey": "assay-titre",
"stepName": "await-assay",
"min": "0",
"max": "100",
"unit": "g/L"
}
]
}
# Deliver the measurement (this is a write — it needs a nonce, see above).
curl -X POST \
https://gateway/api/v1/mes/sites/newark/phases/ferment-assay/external-result \
-H "Authorization: ApiKey dcs_<key>" \
-H "X-DCS-Nonce: $(uuidgen)" \
-d '{
"resultID": "a1b2c3d4e5f6a7b8",
"resultKey": "assay-titre",
"value": "42.5",
"externalSystem": "LabWare LIMS",
"sampleID": "S-2026-0912"
}'
Five things to know before you build against it.
This records provenance. It is not a signature. The batch record will show that this number came from the system you named, under the key you presented, for the sample you cited, and that no person attested it. That is deliberate and it is why an API key can call this endpoint at all. Every electronic-signature endpoint in the gateway refuses a machine identity and always will.
Only an API key can call it. The delivery endpoint refuses any caller
holding a human role, with 403. An unsigned entry is defensible only while the
identity making it cannot sign, so a person is sent to the manual path below
instead. Reading the pending list is an ordinary read and is open to both.
externalSystem is required. A value whose origin the record cannot name
has no provenance, which is the only thing standing in for a signature here.
Your value is validated against the chart's declared bounds. A number
outside [min, max], or one that does not parse, returns 400 and nothing is
delivered. An operator's entry goes through the same check. A resultID that
names no pending wait, or a phase whose run has ended, returns 409.
resultKey must match. The id alone would route the value, but a laboratory
answering the wrong assay is a real failure mode. The delivery therefore states
what it measured, and the gateway checks it.
If your system cannot deliver, an operator can enter the number by hand through the normal prompt path. That is recorded as a signed operator action, and no delivered-measurement provenance appears for it. Full rationale in ADR 0055.
B2MML (XML) Format¶
All GET endpoints support B2MML XML output via content negotiation:
- Query parameter:
?format=b2mmlor?format=xml - Accept header:
Accept: application/xml
curl https://gateway/api/v1/mes/sites/newark/production-capability?format=b2mml \
-H "Authorization: ApiKey dcs_<key>"
The B2MML output uses namespace urn:wbf:b2mml:v0700 and implements a practical
subset of the full B2MML schema covering the four ISA-95 information exchange categories.
ISA-88 to ISA-95 Mapping¶
| ISA-95 Concept | DCS Implementation |
|---|---|
| Enterprise | Enterprise CRD (cluster-scoped) |
| Site | Site CRD (cluster-scoped) |
| Area | Area CRD (namespaced) |
| Work Center | ProcessCell CRD |
| Work Unit | Unit CRD |
| Product Definition | MasterRecipe (Effective lifecycle) |
| Production Order | Work Order (→ Batch) |
| Production Response | ConsolidatedBatchRecord |
| Production Capability | Unit capabilities + status |
CLI Commands¶
dcs mes apikey create <name> [--site <site>] # Create API key
dcs mes apikey list # List API keys
dcs mes apikey revoke <name> # Revoke API key
dcs mes work-orders # List work orders
dcs mes status # MES integration status