Industrial Intelligence Architecture

Using Neo4j to Build an Ontology for Manufacturing Operations

We take the nine-concept core manufacturing ontology from the previous articles and implement it in Neo4j, representing classes and relationships as graph nodes.

Kudzai Manditereza
Kudzai Manditereza
Aug 18, 2026
·

In the previous articles, we defined a core manufacturing ontology around nine concepts:

  • Process
  • PhysicalEntity
  • OperationalEntity
  • Agent
  • Location
  • Capability
  • Role
  • Event
  • State

We also defined the core relationships that connect these concepts and give the ontology its semantic structure.

The next step is to turn that conceptual specification into a machine-readable model, an ontology, that applications and AI agents can query.

Choosing the Technology for Your Ontology

There are several established formats for expressing ontologies and semantic models, including RDF, OWL, JSON-LD, JSON, and YAML.

File-based formats such as JSON or YAML can be simple and quick to work with. RDF and OWL provide richer semantic modelling capabilities and can support querying and reasoning when used with an appropriate semantic database or triple store.

The challenge arises when the ontology is stored separately from the operational data it describes.

For example, your ontology may exist in an RDF, JSON, or YAML file while your operational data lives in another database. In that case, applications must load, interpret, and connect the two representations themselves. You cannot automatically query the ontology and the operational data together unless the surrounding technology has been designed to support it.

An alternative is a graph-database-native approach.

With this approach, you represent the ontology as a metagraph inside a graph database such as Neo4j. The concepts, properties, and relationships that define the ontology exist in the same graph environment as the real operational entities that instantiate them.

The advantage is that you can navigate the ontology and the operational data using the same query language.

For example, a question such as:

Give me all the rotating equipment located under Site A.

can be expressed as a Cypher query that traverses from the definition of Rotating Equipment, through its subclasses, to the real equipment instances deployed at Site A.

This is the approach we will use.

We will define our ontology as a metagraph in Neo4j. Later, we will populate the database with operational data and link each real-world entity to the relevant concepts in that metagraph to create a Knowledge Graph.

Before doing that, however, we need to understand the data model used by modern graph databases such as Neo4j. That model will provide the structure through which we represent both the ontology and the operational data beneath it.

The Property Graph Data Model

Modern graph databases such as Neo4j use what is known as the property graph data model.

At its core, this model is built from two main elements: nodes and relationships.

Nodes represent entities within a domain. In manufacturing, these might include an asset, machine, production line, material, work order, operator, or site.

Each node can contain zero or more properties. Properties are key-value pairs that store information about the entity, such as its name, serial number, status, installation date, or unique identifier.

Nodes can also have one or more labels. Labels act as classifications that describe what the node represents. For example, a node might be labelled Asset, Machine, or Site. A single node can have multiple labels where necessary.

Relationships describe how nodes are connected.

Every relationship has a type, such as BOUGHT, MEASURES, FEEDS, or SUBCLASS_OF. The relationship type gives the connection its meaning.

Relationships are also directional. They run from a start node to an end node, although the relationship can still be queried in either direction. A relationship may also connect a node back to itself.

Like nodes, relationships can carry properties. These might include a timestamp, duration, distance, confidence score, sequence number, or any other information that describes the connection itself.

For example, a FEEDS relationship between two process units might contain properties describing the material being transferred, the normal flow rate, or the date on which the connection became active.

Using just these basic building blocks, nodes, labels, properties, and relationships, you can represent detailed, interconnected operational data in a graph database.

The same building blocks can be used to model the ontology itself, not just the operational data underneath it. That's what we mean by ontology as a meta graph: a graph whose nodes and edges describe the shape of another graph.

Creating a Basic Ontology in Neo4j Cypher

Below is a simple Cypher query that creates a small ontology containing three classes: Asset, Compressor, and Sensor.

It also defines two relationships:

  • A Compressor is a subclass of an Asset.
  • A Sensor measures a Compressor.
CREATE (asset:Class {name: 'Asset'})CREATE (compressor:Class {name: 'Compressor'})CREATE (sensor:Class {name: 'Sensor'})CREATE (compressor)-[:SUBCLASS_OF]->(asset)CREATE (sensor)-[:MEASURES]->(compressor)

Each ontology class, the abstract concept of an Asset, Compressor, or Sensor, is represented as a node.

We give each of these nodes the label Class so that we can distinguish ontology definitions from the operational data that will eventually instantiate them.

The relationships permitted by the ontology are represented as typed relationships between these Class nodes. In this example, SUBCLASS_OF expresses that every compressor is a type of asset, while MEASURES expresses that a sensor can measure a compressor.

The actual operational entities, such as Compressor07, the Pressure_Sensor will exist as a separate layer of nodes. These instance nodes can then be linked to their class definitions through relationships such as INSTANCE_OF.

Although this is a small example, it demonstrates the central idea: the graph is not only storing operational data. It is also storing the definitions, classifications, relationships, and rules that give that data meaning.

What we have built so far is intentionally simple. Now, let’s use those mechanics to create an ontology that is genuinely useful for manufacturing operations.

Creating the core ontology concepts in Neo4j

The first section creates the nine core ontology concepts.

Process

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Process'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Process',
    c.description = 'Work that happens over time and creates, transforms, moves, inspects, maintains, or otherwise affects something. A process may contain subprocesses and may be executed manually, automatically, or through a combination of people, equipment and software.',
    c.examples = [
        'Mixing',
        'Welding',
        'Packaging',
        'Inspection',
        'Cleaning',
        'Material transfer'
    ],
    c.version = '1.0.0';

This creates Process as the general concept for work that unfolds over time.

PhysicalEntity

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/PhysicalEntity'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'PhysicalEntity',
    c.description = 'A physical entity that exists within the manufacturing environment. Describes what something IS, not the role it plays in a particular process.',
    c.examples = [
        'Material',
        'Product',
        'Equipment',
        'Tool',
        'Container',
        'Sample'
    ],
    c.version = '1.0.0';

PhysicalEntity describes physical things that exist in the manufacturing environment.

OperationalEntity

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/OperationalEntity'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'OperationalEntity',
    c.description = 'An informational object that directs, constrains, describes, or records operational work. Divides broadly into entities that describe intended work and entities that provide evidence of completed work.',
    c.examples = [
        'Order',
        'Schedule',
        'Recipe',
        'Procedure',
        'Specification',
        'Work instruction',
        'Production record',
        'Inspection result'
    ],
    c.version = '1.0.0';

An OperationalEntity is an informational object involved in manufacturing operations.

Agent

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Agent'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Agent',
    c.description = 'Something capable of participating in, controlling, or making decisions about a process. It may supervise, coordinate, control, approve, or decide rather than perform the physical transformation itself.',
    c.examples = [
        'Personnel',
        'Team',
        'Equipment controller',
        'Software application',
        'AI agent'
    ],
    c.version = '1.0.0';

An Agent is anything capable of intentional, autonomous, or delegated participation in operational work.

An agent does not necessarily perform the physical transformation itself.

It may:

  • Supervise a process
  • Coordinate resources
  • Control equipment
  • Approve work
  • Observe execution
  • Recommend an action
  • Make a decision

Location

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Location'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Location',
    c.description = 'Where an entity, agent, or process exists or occurs. The ISA-95 equipment hierarchy extends this concept.',
    c.examples = [
        'Enterprise',
        'Site',
        'Area',
        'Work center',
        'Work unit'
    ],
    c.version = '1.0.0';

Location provides the spatial context in which entities exist and processes occur.

Capability

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Capability'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Capability',
    c.description = 'What an entity, agent, or organizational unit is able to do. Separates what a resource CAN DO from the process it is currently executing; may carry constraints or parameters.',
    c.examples = [
        'Mixing capability',
        'Filling capability',
        'Temperature-measurement capability',
        'Sterilization capability',
        'Inspection capability'
    ],
    c.version = '1.0.0';

A capability describes what something can do independently of what it is currently doing.

For example:

Mixer101 HAS_CAPABILITY MixingCapability

This statement may remain true even while Mixer101 is idle.

Separating capability from current execution enables several operational use cases:

  • Resource selection
  • Scheduling
  • Substitution
  • Qualification checking
  • Capacity planning
  • AI-assisted work assignment

A process can state what capability it requires without referring to a particular resource:

MixingProcess REQUIRES_CAPABILITY MixingCapability

The knowledge graph can then be queried for entities that possess that capability.

Role

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Role'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Role',
    c.description = 'How an entity participates in a particular process or relationship. The same entity may play different roles in different contexts, so a role is interpreted within a context rather than as a permanent classification.',
    c.examples = [
        'Input',
        'Output',
        'Performer',
        'Observer',
        'Controller',
        'Consumer',
        'Producer'
    ],
    c.version = '1.0.0';

Role captures contextual participation. The same entity may play different roles in different processes.

Event

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Event'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Event',
    c.description = 'Something operationally significant that occurred at a particular point or interval in time. Provides the time-based operational evidence needed for analytics, traceability, and agentic decision-making.',
    c.examples = [
        'Process started',
        'Order released',
        'Material consumed',
        'Alarm activated',
        'Inspection completed',
        'Equipment failed'
    ],
    c.version = '1.0.0';

Events provide the time-based evidence of manufacturing operations.

They allow us to represent not only static facts but what occurred:

MixingStartedEvent
MaterialConsumedEvent
EquipmentFailureEvent
InspectionCompletedEvent
BatchReleasedEvent

Events are particularly important for analytics, traceability, root-cause investigation, and AI-agent reasoning because they connect operational facts to time.

State

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/State'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'State',
    c.description = 'The condition of an entity or process during a period of time. Events usually cause or indicate state changes.',
    c.examples = [
        'Running',
        'Idle',
        'Failed',
        'Available',
        'In production',
        'Awaiting inspection',
        'Released',
        'Quarantined'
    ],
    c.version = '1.0.0';

A state describes a condition that holds during a period of time.

Creating subclasses of the core ontology concepts

The nine concepts are deliberately abstract. They are not the vocabulary you use to describe a real plant, nobody commissions a "PhysicalEntity" or schedules work at a "Location".

That is by design. The ontology grows by subclassing the nine concepts rather than by adding new ones. SUBCLASS_OF is the mechanism, and keeping the concept count fixed is what stops the core from sprawling as new domains arrive.

Most of that subclassing happens later, in the domain extensions: a production ontology adds Filler and RawMaterial, a maintenance ontology adds ConditionSensor, and each attaches to a core concept with SUBCLASS_OF.

But two sets of subclasses belong in the core itself, because every domain needs them:

  • The ISA-95 equipment hierarchy model, so every domain locates work against the same spatial vocabulary.
  • The physical entity tier, so every domain classifies physical things the same way.

Each is created the same way: a Class node, then a SUBCLASS_OF edge to its parent concept.

The ISA-95 Hierarchy Model

Location on its own is too coarse to schedule or report against. Every manufacturing domain needs the same spatial vocabulary, and ISA-95 already provides it: an enterprise owns sites, a site divides into areas, an area contains work centres.

These four levels are created as subclasses of Location:

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Enterprise'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Enterprise',
    c.description = 'The organization or business entity that owns and operates one or more sites. The top of the ISA-95 spatial hierarchy.',
    c.examples = ['Global Industries'],
    c.version = '1.0.0';

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Site'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Site',
    c.description = 'A physical location where operations are conducted, such as a manufacturing plant. Belongs to an Enterprise and is divided into Areas.',
    c.examples = [
        'Munich Plant',
        'Dallas Plant'
    ],
    c.version = '1.0.0';

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Area'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Area',
    c.description = 'A functional division of a site that groups the work centres serving a common purpose. Domain extensions specialise it (ProductionArea, PackagingArea, UtilitiesArea, Warehouse).',
    c.examples = [
        'Production Area',
        'Packaging Area',
        'Utilities Area',
        'Raw Material Storage Area'
    ],
    c.version = '1.0.0';

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/WorkCenter'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'WorkCenter',
    c.description = 'A location within an area where work is performed — a production line, packaging line, storage zone, or utility system. The finest shared spatial grain; equipment is located at a work centre. Domain extensions specialise it (ProductionLine, PackagingLine, StorageZone).',
    c.examples = [
        'Production Line 1',
        'Packaging Line 1',
        'Dry Ingredient Storage',
        'Steam System'
    ],
    c.version = '1.0.0';

All four attach to Location in one statement:

MATCH (loc:Class {name: 'Location'})
MATCH (c:Class)
WHERE c.name IN ['Enterprise', 'Site', 'Area', 'WorkCenter']
MERGE (c)-[:SUBCLASS_OF]->(loc);

Two decisions here are worth stating explicitly, because both are easy to get wrong.

These are levels, not containers. It is tempting to model the hierarchy by subclassing — to say a Site is a subclass of an Enterprise. That would be wrong. A site is not a kind of enterprise; it is a thing contained by one. Subclassing describes what something IS, while containment describes how instances nest. So the four are siblings under Location, and the actual nesting is asserted between instances using the CONTAINS relationship:

GlobalIndustries CONTAINS MunichPlant
MunichPlant      CONTAINS ProductionArea
ProductionArea   CONTAINS ProductionLine1

Work Unit is deliberately omitted. ISA-95 defines a fifth level below the work centre, but in practice a physical asset fills that role, the filler on a packaging line is the work unit. Adding a separate WorkUnit level would create a redundant one-to-one location node above every asset. Instead, equipment attaches directly to its work centre through IS_LOCATED_AT.

Domain extensions then specialise Area and WorkCenter into concrete types, ProductionArea, PackagingArea, Warehouse, ProductionLine, PackagingLine, StorageZone, each with its own SUBCLASS_OF edge to the core level it refines.

The physical entity tier

PhysicalEntity needs the same treatment for the same reason. On its own it groups everything physical in the plant into one undifferentiated bucket, from a boiler to a bottle cap.

Every physical thing divides on a single question: does the plant operate it, or does it flow through? A filler, a boiler and a pH meter are installed, operated and maintained. A raw ingredient, a closure and a sample are consumed, transformed or dispatched. That distinction gives two subclasses:

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Equipment'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Equipment',
    c.description = 'A physical asset the plant installs, operates and maintains in order to carry out work — it transforms, moves, packages, measures, stores or services something, rather than being consumed by the process itself. Located at a Work Center, and the entity that equipment states, modes, metrics and maintenance are asserted against. Domain extensions specialise it into concrete asset types.',
    c.examples = [
        'Filler',
        'Cook Kettle',
        'Boiler',
        'Energy Meter',
        'pH Meter',
        'Condition Sensor',
        'Storage Tank',
        'Torque Wrench'
    ],
    c.version = '1.0.0';

MERGE (c:Class {
    uri: 'http://industry40.tv/ontology/core/Material'
})
ON CREATE SET c.created_at = datetime()
SET c:Core,
    c.name = 'Material',
    c.description = 'A kind of physical stuff that flows through the plant — consumed as input, carried through the process, packaged into the product, drawn off for test, or held to service an asset. Material is a DEFINITION, not a quantity: it answers what kind of stuff this is, while a tracked quantity of it is a MaterialLot related by IS_QUANTITY_OF. Domain extensions specialise it into concrete material types.',
    c.examples = [
        'Raw Material',
        'Intermediate Product',
        'Finished Product',
        'Primary Container',
        'Closure',
        'Label',
        'Shipping Case',
        'Sample',
        'Spare Part'
    ],
    c.version = '1.0.0';

MATCH (pe:Class {name: 'PhysicalEntity'})
MATCH (c:Class)
WHERE c.name IN ['Equipment', 'Material']
MERGE (c)-[:SUBCLASS_OF]->(pe);

Note the symmetry with the location tier. Location gains structural levels; PhysicalEntity gains a physical classification. In both cases the core supplies a shared vocabulary and the domain extensions specialise it, Filler and Boiler under Equipment, RawMaterial and Closure under Material, exactly as ProductionArea sits under Area.

Not everything physical is Equipment or Material, and forcing it to be would break the model. Three kinds of thing sit alongside the two subclasses, as direct children of PhysicalEntity:

  • EnergyCarrier: steam and compressed air behave like materials, but electricity is not a substance. Modelling the carriers as Material would assert something physically false, so they stay separate.
  • MaterialLot: a lot is a tracked quantity of a material, not a kind of material. RawMaterial answers "what kind of stuff is this?"; a lot answers "which specific quantity, under which lot code?". These are two different tiers, and subclassing one under the other collapses the ISA-95 material-definition / material-lot split. A lot reaches its material through a dedicated IS_QUANTITY_OF relationship instead.
  • UnitLoad: a palletised grouping of material, which is again a grouping rather than a kind.

The general rule: use SUBCLASS_OF only when the child genuinely IS a kind of the parent. When the connection is "quantity of", "grouping of", or "contained by", it belongs in a relationship.

The state scopes

State needs dividing for the same reason Location and PhysicalEntity do. Left flat, a vocabulary of conditions cannot say whose lifecycle a state belongs to, so nothing prevents a work order being Running or a filler being Completed.

The core therefore divides it five ways:

EquipmentState    the condition of a physical asset
ProcessState      the condition of a process execution
OrderState        an order, as the planning system sees it
WorkOrderState    a work order, as the execution system sees it
MaterialState     the disposition and availability of material
MATCH (s:Class {name: 'State'})
MATCH (c:Class)
WHERE c.name IN
  ['EquipmentState', 'ProcessState', 'OrderState', 'WorkOrderState', 'MaterialState']
MERGE (c)-[:SUBCLASS_OF]->(s);

OrderState and WorkOrderState are deliberately separate even though both track something order-shaped. Planning releases an order; execution receives, dispatches and activates a work order. They are two systems' views of the same work, and merging them would lose that.

Domain extensions then hang concrete conditions under the scopes, and every state class ends in StateRunningState, HeldState, OrderClosedState. Classes share one namespace, and the natural name for a state is also the natural name for the event that causes it: BatchCompleted the event against BatchCompleted the state. The suffix keeps them apart and makes the kind of a class readable without traversing to its parent.

Where a word means different things in different scopes it is qualified further. An order is released by planning, meaning authorised to run; material is released by the laboratory, meaning approved for dispatch. OrderReleasedState and MaterialReleasedState are not the same condition, and a single Released would let an ERP authorisation be mistaken for a quality decision.

Not everything fits a scope. A tariff period is a condition of time rather than of any equipment, process, order or material, so the energy extension attaches its two period states directly to State. That is a deliberate exception, not an oversight.

The resulting hierarchy

After this section the core contains twenty classes: the nine concepts, the four location levels, the two physical entity subclasses, and the five state scopes, with the subclassing forming three small trees:

PhysicalEntity
├── Equipment
└── Material

Location
├── Enterprise
├── Site
├── Area
└── WorkCenter

State
├── EquipmentState
├── ProcessState
├── OrderState
├── WorkOrderState
└── MaterialState

The remaining six concepts, Process, OperationalEntity, Agent, Capability, Role and Event, have no core subclasses. They are specialised directly by the domain extensions.

Everything a domain extension adds from here attaches to one of these twenty nodes.

Creating the core relationship definitions

The next section creates the relationships that connect the core concepts.

Each relationship is represented as a reified Relationship node.

For example:

MERGE (r:Relationship {
    uri: 'http://industry40.tv/ontology/core/rel/PARTICIPATES_IN'
})
ON CREATE SET r.created_at = datetime()
SET r:Core,
    r.name = 'PARTICIPATES_IN',
    r.description = 'An entity is involved in the execution of a process. Participation alone does not explain HOW the entity is involved — that is expressed through a Role.',
    r.examples = [
        'Mixer PARTICIPATES_IN MixingExecution',
        'Operator PARTICIPATES_IN MixingExecution',
        'MaterialLot PARTICIPATES_IN MixingExecution'
    ],
    r.version = '1.0.0';

The core defines the following relationship vocabulary:

AFFECTS
PARTICIPATES_IN
PLAYS_ROLE
HAS_CAPABILITY
REQUIRES_CAPABILITY
IS_GUIDED_BY
PRODUCES
OCCURS_AT
IS_LOCATED_AT
OCCURS_DURING
CHANGES_STATE
TRANSITIONS_TO
TRANSITIONS_FROM
HAS_STATE
HAS_SUBPROCESS
CONTAINS

How state is asserted

Four of those relationships work together, and the way they combine is a deliberate design decision rather than an obvious consequence of the model.

An entity carries exactly one HAS_STATE edge, pointing at its current state. When the state changes the edge is replaced, not added to. History is not accumulated on the entity.

Instead, each event records the state it produced:

(workOrder)-[:HAS_STATE]->(ActiveState)              current condition, one hop
(event)-[:CHANGES_STATE]->(workOrder)                what changed
(event)-[:TRANSITIONS_TO]->(ActiveState)             what it became
(event)-[:TRANSITIONS_FROM]->(DispatchedState)       what it left

CHANGES_STATE names the thing whose condition changed; TRANSITIONS_TO names the state it changed to. They are complementary rather than overlapping, which is why their ranges differ — one points at an entity, the other at a state.

Because every event carries a timestamp, an ordered walk of the events reconstructs the full history without storing it on the entity:

EVT-1  t=1000   —              → ReceivedState
EVT-2  t=2000   ReceivedState   → DispatchedState
EVT-3  t=3000   DispatchedState → ActiveState

Asking what state something was in at a past moment becomes a matter of taking the latest event before that moment, rather than a single indexed lookup — the cost of this design, and the reason the alternative below exists.

Why not a state-assertion node? The previous article describes a more precise model in which a reified StateAssertion carries VALID_FROM, VALID_UNTIL and provenance. That remains the right escalation where a regulated audit trail is required. It is not the default here for one reason: the event node already carries everything such an assertion would hold — the timestamp, the source system, the gateway, the originating system id, and the data quality. Introducing a separate assertion node would duplicate all of it and add nothing but VALID_UNTIL, which is the next event's timestamp.

TRANSITIONS_FROM is optional. It is absent on the first event of a lifecycle, and is derivable from the preceding event — but recording it makes a transition queryable without ordering.

Durations and time-in-state are deliberately not stored in the graph at all. They belong in the historian, for the same reason metric values do: the graph holds structure and current condition, the time-series store holds the series.

These relationships are deliberately general. A production ontology later specializes them:

TRANSFORMS SPECIALIZES AFFECTS
CONSUMES   SPECIALIZES AFFECTS
YIELDS     SPECIALIZES AFFECTS
EXECUTES   SPECIALIZES IS_GUIDED_BY
FULFILLS   SPECIALIZES IS_GUIDED_BY

Note that CONSUMES specializes AFFECTS rather than PARTICIPATES_IN. Consuming a material changes it — the process acts on the thing — which is what AFFECTS means. PARTICIPATES_IN runs the other way: it says an entity took part in a process, without saying the process did anything to it. A specialization has to preserve the direction and meaning of its parent.

Distinguishing physical and informational production

The definition of PRODUCES deserves special attention:

MERGE (r:Relationship {
    uri: 'http://industry40.tv/ontology/core/rel/PRODUCES'
})
ON CREATE SET r.created_at = datetime()
SET r:Core,
    r.name = 'PRODUCES',
    r.description = 'An informational object created as a result of executing a process — the operational evidence of the work (records, results, decisions), as distinct from any physical entity produced.',
    r.examples = [
        'ProductionProcess PRODUCES ProductionRecord',
        'InspectionProcess PRODUCES InspectionResult'
    ],
    r.version = '1.0.0';

In this core model, PRODUCES specifically points to an OperationalEntity.

For example:

InspectionProcess PRODUCES InspectionResult

It does not represent the physical output of a process:

MixingProcess PRODUCES MixedMaterial

Physical transformation is currently covered more generally by AFFECTS, with a domain extension expected to introduce a more precise relationship such as:

PRODUCES_MATERIAL
TRANSFORMS
CREATES_PHYSICAL_OUTPUT

This distinction avoids giving the same relationship two different ranges and meanings.

Defining relationship domains

Now that we have defined the relationships in our ontology, we need to specify how each one may be used. We begin with the domain, which identifies the kinds of concepts that may appear on the starting side of a relationship. For example, defining Process as the domain of OCCURS_AT tells us that it is a process, not a state, capability, or operational record, that may occur at a location.

For example:

MATCH
    (r:Relationship {name: 'OCCURS_AT'}),
    (c:Class {name: 'Process'})
MERGE (r)-[:HAS_DOMAIN]->(c);

This creates:

OCCURS_AT HAS_DOMAIN Process

The ontology therefore says that a process is the kind of thing that occurs at a location.

Some relationships support more than one domain.

For example, both a process and an event can affect an entity:

MATCH
    (r:Relationship {name: 'AFFECTS'}),
    (c:Class {name: 'Process'})
MERGE (r)-[:HAS_DOMAIN]->(c);

MATCH
    (r:Relationship {name: 'AFFECTS'}),
    (c:Class {name: 'Event'})
MERGE (r)-[:HAS_DOMAIN]->(c);

The resulting definition is:

AFFECTS HAS_DOMAIN Process
AFFECTS HAS_DOMAIN Event

Multiple HAS_DOMAIN relationships are interpreted here as an allowed union: the source may be any one of the connected classes.

HAS_STATE, for instance, may originate from a process or from any of the three concepts that describe things, so it names all four:

HAS_STATE HAS_DOMAIN PhysicalEntity
HAS_STATE HAS_DOMAIN Agent
HAS_STATE HAS_DOMAIN OperationalEntity
HAS_STATE HAS_DOMAIN Process

Naming each class rather than grouping them under a shared parent is what lets each domain be exactly as wide as the relationship needs. HAS_CAPABILITY, for example, accepts only two of the four:

HAS_CAPABILITY HAS_DOMAIN PhysicalEntity
HAS_CAPABILITY HAS_DOMAIN Agent

A mixer has a capability and so does an operator, but a work order does not. The same applies to IS_LOCATED_AT, PARTICIPATES_IN and PLAYS_ROLE: they describe things that physically take part in work, which an informational object does not do. Informational objects relate to work through IS_GUIDED_BY and PRODUCES instead.

Naming each class costs a few more edges than a shared parent would, but it keeps every domain exactly as wide as the relationship it describes.

Defining relationship ranges

After defining where each relationship may start, we need to define where it may point. The range identifies the kinds of concepts permitted on the target side of a relationship. For example, defining Location as the range of OCCURS_AT completes the statement that a Process OCCURS_AT Location. Together, the domain and range give the relationship its complete semantic structure.

For example:

MATCH 
(r:Relationship {name: 'OCCURS_AT'}), 
(c:Class {name: 'Location'}) 
MERGE (r)-[:HAS_RANGE]->(c);

This creates:

OCCURS_AT HAS_RANGE Location

In the previous section, we defined Process as the domain of OCCURS_AT. Combining that domain with the range now gives us the complete semantic signature:

Process   OCCURS_AT   Location

The ontology therefore defines both where the relationship may start and where it may point: a process may occur at a location.

The same pattern is used for the rest of the relationship vocabulary. Where a side lists more than one class, each is a separate HAS_DOMAIN or HAS_RANGE edge and any one of them is permitted:

PhysicalEntity, Agent                              PLAYS_ROLE           Role
PhysicalEntity, Agent                              HAS_CAPABILITY       Capability
PhysicalEntity, Agent                              IS_LOCATED_AT        Location

Process                                            REQUIRES_CAPABILITY  Capability
Process                                            IS_GUIDED_BY         OperationalEntity
Process                                            PRODUCES             OperationalEntity
Process                                            OCCURS_AT            Location
Process                                            HAS_SUBPROCESS       Process

Process, Event                                     AFFECTS              PhysicalEntity, Agent, OperationalEntity

Event                                              OCCURS_DURING        Process
Event                                              CHANGES_STATE        PhysicalEntity, Agent, OperationalEntity, Process
Event                                              TRANSITIONS_TO       State
Event                                              TRANSITIONS_FROM     State

PhysicalEntity, Agent, OperationalEntity, Process  HAS_STATE            State

Location                                           CONTAINS             Location

Notice that the four "taking part in work" relationships, PARTICIPATES_IN, PLAYS_ROLE, HAS_CAPABILITY, IS_LOCATED_AT, accept PhysicalEntity and Agent but not OperationalEntity, while HAS_STATE, AFFECTS and CHANGES_STATE accept all three. That asymmetry is the point of declaring domains per relationship: each one is exactly as wide as the statement it licenses.

These signatures become semantic guidance for applications that create or validate the knowledge graph.

Domain and range are descriptive

It is important to understand what the domain and range model does, and what it does not do.

Creating:

PARTICIPATES_IN HAS_DOMAIN PhysicalEntity
PARTICIPATES_IN HAS_RANGE Process

does not automatically prevent someone from creating an invalid operational relationship such as:

LocationA PARTICIPATES_IN StateB

The domain and range are represented as ontology data.

They describe the valid use of the relationship, but they are not automatically enforced against every arbitrary relationship written to the database.

The application or ingestion layer should therefore consult the ontology before creating instance relationships.

Conceptually, the ingestion flow becomes:

Requested relationship
        ↓
Resolve source instance class
        ↓
Resolve target instance class
        ↓
Find relationship definition
        ↓
Check HAS_DOMAIN and HAS_RANGE
        ↓
Create or reject relationship

This is also valuable for AI agents.

Before generating or proposing a graph update, an agent can retrieve the relationship definition and verify that the proposed source and target classes are semantically compatible.

Inspecting the resulting ontology

Once the script has been executed, we can query the ontology to confirm what was created.

List the core classes

MATCH (c:Class:Core)
RETURN
    c.name AS concept,
    c.abstract AS abstract,
    c.description AS description,
    c.examples AS examples,
    c.version AS version
ORDER BY c.name;

This should return twenty classes: the nine core concepts, the four ISA-95 location levels, the two physical entity subclasses, and the five state scopes.

View the class hierarchy

MATCH (child:Class:Core)-[:SUBCLASS_OF]->(parent:Class)
RETURN
    child.name AS child,
    parent.name AS parent
ORDER BY parent.name, child.name;

Expected result:

Area              Location
Enterprise        Location
Site              Location
WorkCenter        Location
Equipment         PhysicalEntity
Material          PhysicalEntity
EquipmentState    State
MaterialState     State
OrderState        State
ProcessState      State
WorkOrderState    State

Dropping the :Core label from the match returns the same eleven rows now, but once the domain extensions are loaded it also returns every domain subclass — which is the query you want when checking that an extension attached where you expected.

List the relationship vocabulary

MATCH (r:Relationship:Core)
RETURN
    r.name AS relationship,
    r.description AS description,
    r.examples AS examples,
    r.version AS version
ORDER BY r.name;

Display domains and ranges

MATCH (domain:Class)<-[:HAS_DOMAIN]-(r:Relationship)
MATCH (r)-[:HAS_RANGE]->(range:Class)
RETURN
    domain.name AS domain,
    r.name AS relationship,
    range.name AS range
ORDER BY relationship, domain, range;

The result provides a readable ontology relationship catalogue. Because each accepted class is named individually, a relationship that accepts several appears several times, one row per permitted combination:

Event              AFFECTS              Agent
Event              AFFECTS              OperationalEntity
Event              AFFECTS              PhysicalEntity
Process            AFFECTS              Agent
Process            AFFECTS              OperationalEntity
Process            AFFECTS              PhysicalEntity
Event              CHANGES_STATE        Agent
Event              CHANGES_STATE        OperationalEntity
Event              CHANGES_STATE        PhysicalEntity
Event              CHANGES_STATE        Process
Location           CONTAINS             Location
Agent              HAS_CAPABILITY       Capability
PhysicalEntity     HAS_CAPABILITY       Capability
Agent              HAS_STATE            State
OperationalEntity  HAS_STATE            State
PhysicalEntity     HAS_STATE            State
Process            HAS_STATE            State
Process            HAS_SUBPROCESS       Process
Process            IS_GUIDED_BY         OperationalEntity
Agent              IS_LOCATED_AT        Location
PhysicalEntity     IS_LOCATED_AT        Location
Process            OCCURS_AT            Location
Event              OCCURS_DURING        Process
Agent              PARTICIPATES_IN      Process
PhysicalEntity     PARTICIPATES_IN      Process
Agent              PLAYS_ROLE           Role
PhysicalEntity     PLAYS_ROLE           Role
Process            PRODUCES             OperationalEntity
Process            REQUIRES_CAPABILITY  Capability
Event              TRANSITIONS_FROM     State
Event              TRANSITIONS_TO       State

Thirty-one rows for sixteen relationships, and every row is a statement worth defending. OperationalEntity appears once as a domain, an order has a state, and as the range of AFFECTS, CHANGES_STATE, IS_GUIDED_BY and PRODUCES. It is absent from HAS_CAPABILITY, IS_LOCATED_AT, PARTICIPATES_IN and PLAYS_ROLE, because a work order has no capability and a recipe is not located anywhere.

Reading the catalogue is the fastest way to audit an ontology. A row that looks wrong here is a relationship whose domain or range is wrong, and a wrong signature is worse than a missing one: an application or agent that consults the ontology before writing to the graph will treat it as permission.

Visualize the ontology graph

The entire core model can be returned as a graph:

MATCH path =
    (domain:Class)<-[:HAS_DOMAIN]-
    (relationship:Relationship)
    -[:HAS_RANGE]->(range:Class)
RETURN path;

A separate query can return the concept hierarchy:

MATCH path = (:Class)-[:SUBCLASS_OF]->(:Class)
RETURN path;

This gives us a visual model consisting of:

  • Class nodes
  • Relationship-definition nodes
  • Domain connections
  • Range connections
  • Subclass connections

Conclusion

The implementation shown in this article turns the Core Manufacturing Ontology from a conceptual specification into a machine-readable Neo4j graph.

The resulting model is small enough to remain understandable and governable, but expressive enough to provide a shared semantic foundation across production, maintenance, quality, inventory, and other manufacturing domains.

More importantly, it creates a formal structure that people, applications, analytics, and AI agents can use to interpret operational data consistently.

In the next step, we can extend this core with a production ontology and use it to create an actual manufacturing knowledge graph containing equipment, processes, material lots, recipes, observations, events, and state changes.

Index

Get the Next Guide

New architecture guides, implementation tutorials and use-case blueprints, delivered as they’re published.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

More guides

All guides →
Connecting Industrial Assets to a Knowledge Graph Using MQTT, Node-RED, and Neo4j
Semantic Data Layer
We build a small, complete pipeline that takes an MQTT message from a shop-floor system, transforms it into a Cypher write, and updates the knowledge graph in real time.
Kudzai Manditereza
Kudzai Manditereza
Aug 18, 2026
Connecting Industrial Assets to a Knowledge Graph Using MQTT, Node-RED, and Neo4j
How to Build a Knowledge Graph for Manufacturing Operations
Semantic Data Layer
The core and domain ontologies define what may exist in a manufacturing environment. Here we use them to build a knowledge graph of what actually exists.
Kudzai Manditereza
Kudzai Manditereza
Aug 18, 2026
How to Build a Knowledge Graph for Manufacturing Operations
Building an Energy Operations Domain Ontology in Manufacturing
Semantic Data Layer
The core manufacturing ontology already gives us shared concepts. Here we extend it with the classes and relationships needed to describe energy and plant utilities.
Kudzai Manditereza
Kudzai Manditereza
Aug 18, 2026
Building an Energy Operations Domain Ontology in Manufacturing