---
question: How do I write Turtle so onto2py accepts it?
kind: task
via_web: soon
via_cli: available
via_python: available
via_api: na
via_config: na
ref_web: File › New Class
ref_cli: abi new module --in-engine
ref_python: triple_store.load_schema
prereq: You need an in-engine module listed under `modules:` in `config.yaml`. The engine loads ontology files only from in-engine modules. See [Creating a Module](/user-guide/build/modules/creating-a-module#remote-or-in-engine).
next:
  - /capabilities/build/knowledge-graph/generate-python-classes
  - /capabilities/build/knowledge-graph/enable-ontology
  - /capabilities/build/knowledge-graph/creating-a-pipeline
---

# Create an ontology

Ontologies are the schema layer of ABI's knowledge graph. They define classes, properties, and axioms in OWL Turtle format. `onto2py` then generates typed Python classes from them.

An ontology is a `.ttl` file in a module's `ontologies/` folder. The engine loads every `*.ttl` under it into the triple store when it loads the module. There is no `abi new ontology` command and no REST call that writes the file: you write Turtle.

## Design before you code

Before writing Turtle, answer these questions:

1. **What entities do you model?** (Continuants: organizations, persons, products)
2. **What events/processes do you model?** (Occurrents: acts of partnership, employment, purchase)
3. **What properties link them?** (Object properties: `hasOwner`, `isPartOf`)
4. **What data properties do they carry?** (Datatype properties: `name`, `url`, `externalId`)
5. **What upper ontology classes do they extend?** (ABI's `abi:` classes, CCO classes, BFO categories)

Do not create a class that already exists in BFO or CCO. Look it up first, and keep a module-local name with `owl:equivalentClass` or `rdfs:subClassOf` pointing at the CCO class, so your pipelines keep stable Python names.

## The seven buckets

Every class must trace to one of the seven BFO buckets, or `onto2py` refuses to generate code for it. Subclass the matching `abi:` class and the trace comes with it:

| Bucket | Question | Subclass |
|---|---|---|
| WHAT | what is happening | `abi:Process` |
| WHEN | over what interval | `abi:TemporalRegion` |
| WHO | which participant | `abi:MaterialEntity`, `abi:Person` |
| WHERE | at what place | `abi:Site` |
| HOW WE KNOW | which record | `abi:GenericallyDependentContinuant` |
| HOW IT IS | which quality | `abi:Quality` |
| WHY | which role or disposition | `abi:Role`, `abi:Disposition` |

Qualities, roles and dispositions must carry an `abi:inheresIn` restriction that names their bearer; the validator rejects them otherwise. Use the `abi:` properties (`abi:hasParticipant`, `abi:occursIn`, `abi:concretizes`, `abi:inheresIn`, ...) rather than raw `bfo:` ones, so their inverses carry over.

## Ontology file structure

`abi new module <name> --in-engine` scaffolds the layout, with a starter file in each folder:

```text
src/my_module/ontologies/
├── modules/
│   └── MyModuleOntology.ttl      # shared classes and properties; owl:imports each process
├── processes/
│   └── MyModuleProcess.ttl       # one BFO process slice per file
└── queries/
    └── MyModuleSparqlQueries.ttl # templatable SPARQL queries, turned into agent tools
```

`onto2py` writes the generated Python next to each Turtle file (`MyModuleOntology.py`), and action-class stubs under `classes/`. Commit them; do not edit them by hand. Anything in these files lands in the live triple store, so keep named individuals out: they belong in pipeline output.

## Web UI

Coming soon. The workspace Ontology screen has **New Class** and **New Object Property** in its File menu, but both are disabled: "Creation is unavailable until changes can be saved to an ontology file." Write the Turtle with the CLI or Python steps on this page. To bring in an existing file, see [Import an ontology](/user-guide/build/knowledge-graph/import-an-ontology).

## CLI

### Scaffold the files

There is no `abi new ontology` command. An in-engine module comes with starter ontology files. From the project root:

```bash
abi new module my_module --in-engine
```

It writes `ontologies/modules/MyModuleOntology.ttl`, `ontologies/processes/MyModuleProcess.ttl` and `ontologies/queries/MyModuleSparqlQueries.ttl`, with an `AGENTS.md` that states the rules. Add the module under `modules:` in `config.yaml`. The command is [`abi new module`](/user-guide/reference/cli#abi-new-module).

### Write the classes

Edit the starter files. The shape of a module ontology is under [Writing a Turtle file](#writing-a-turtle-file) below.

### Generate the Python classes

```bash
uv run python -m naas_abi_core.utils.onto2py --module src/my_module
```

It generates the process files, merges them into the module ontology, then generates that, and skips files that have not changed. Details are on [Generate Python classes](/user-guide/build/knowledge-graph/generate-python-classes).

### Restart and check

Restart the engine, then query the schema graph for your classes. With `abi dev up`, `abi dev ports` prints the triple store's port (`oxigraph`):

```bash
curl -s -X POST "http://localhost:<oxigraph-port>/query" \
  -H "Content-Type: application/sparql-query" \
  -H "Accept: application/sparql-results+json" \
  --data 'SELECT ?class WHERE { GRAPH <http://ontology.naas.ai/graph/schema> {
      ?class a <http://www.w3.org/2002/07/owl#Class>
      FILTER(STRSTARTS(STR(?class), "http://ontology.naas.ai/my_module/")) } }'
```

Your new class should appear. With the Docker stack the triple store is Fuseki (http://localhost:3030); the Nexus Ontology screen shows the same classes once the file is [enabled in the workspace](/user-guide/build/knowledge-graph/enable-ontology).

## Python

### Write the Turtle file

Save it under the module's `ontologies/` folder, for example `src/my_module/ontologies/modules/MyModuleOntology.ttl`. The shape is under [Writing a Turtle file](#writing-a-turtle-file) below.

### Generate the Python classes

```python title="scripts/generate_ontology.py"
from naas_abi_core.utils.onto2py.module_codegen import onto2py_module

report = onto2py_module("src/my_module")
assert report.ok, report.errors
```

`onto2py_module` generates the process files, merges them into the module ontology, then generates that, and skips files that have not changed. See [Generate Python classes](/user-guide/build/knowledge-graph/generate-python-classes?via=python).

### Load it without a restart

At startup the engine loads every `.ttl` of the module. To load an edited file into a running engine, call the triple store service. The module must list `TripleStoreService` in its `dependencies`:

```python title="scripts/load_ontology.py"
from pathlib import Path

from my_module import ABIModule

ttl = Path("src/my_module/ontologies/modules/MyModuleOntology.ttl").resolve()
triple_store = ABIModule.get_instance().engine.services.triple_store
triple_store.load_schema(str(ttl))

rows = triple_store.query("""
    SELECT ?class WHERE { GRAPH <http://ontology.naas.ai/graph/schema> {
        ?class a <http://www.w3.org/2002/07/owl#Class>
        FILTER(STRSTARTS(STR(?class), "http://ontology.naas.ai/my_module/")) } }
""")
print([str(row[0]) for row in rows])
```

`abi run script scripts/load_ontology.py` loads the engine, then runs the script. Pass the absolute path: the engine records each file by the path it loaded it from. `load_schema` skips a file whose content has not changed, and applies the difference when it has.

A new file loaded this way is in the schema graph, but not yet in the workspace catalog: Nexus reads each module's list of ontology files once, when the module loads. Restart the engine before you [enable it in a workspace](/user-guide/build/knowledge-graph/enable-ontology); until then Settings › Ontologies does not list it.

## REST API

Not applicable. There is no REST endpoint that writes an ontology file. The Nexus `/api/ontology` routes read the module files; none writes one. Write Turtle with the CLI or Python steps, or [import a file in Nexus](/user-guide/build/knowledge-graph/import-an-ontology). The process list is the [API](/api).

## Config

Not applicable. No `config.yaml` key declares classes. The engine loads a module's `.ttl` files because the module is listed under `modules:`. `global_config.skip_ontology_loading: true` (or `ABI_SKIP_ONTOLOGY_LOADING=1`) turns that loading off for every module.

## Writing a Turtle file

A module ontology declares itself, imports the ABI ontology, and then defines properties and classes. This is the shape of the scaffolded `modules/MyModuleOntology.ttl` (abridged):

```turtle title="ontologies/modules/MyModuleOntology.ttl"
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix abi:  <http://ontology.naas.ai/abi/> .
@prefix my_module: <http://ontology.naas.ai/my_module/> .

my_module:MyModuleOntology a owl:Ontology ;
  # One owl:imports statement per IRI, never a comma-separated list.
  owl:imports <http://ontology.naas.ai/abi/Ontology> ;
  owl:imports <http://ontology.naas.ai/abi/PersonOntology> ;
  # Locator annotations: other modules resolve this ontology to Python through them.
  abi:pythonPackage "my_module" ;
  abi:ontologyResource "ontologies/modules/MyModuleOntology.ttl" ;
  abi:pythonResource "ontologies/modules/MyModuleOntology.py" .

# Pair every relation with its inverse, and subclass an abi: relation.
my_module:hasRecord a owl:ObjectProperty ;
    rdfs:subPropertyOf abi:isCarrierOf ;
    owl:inverseOf my_module:isRecordOf ;
    rdfs:label "has record"@en ;
    skos:definition "Relates a person to a record that generically depends on them."@en ;
    rdfs:domain abi:Person ;
    rdfs:range my_module:MyModuleRecord .

my_module:record_id a owl:DatatypeProperty ;
    rdfs:label "record id"@en ;
    skos:definition "Identifier of the record in the source system."@en ;
    rdfs:domain my_module:MyModuleRecord ;
    rdfs:range xsd:string .

# HOW WE KNOW: a record about something
my_module:MyModuleRecord a owl:Class ;
    rdfs:subClassOf abi:GenericallyDependentContinuant ,
      [ a owl:Restriction ; owl:onProperty my_module:isRecordOf ; owl:someValuesFrom abi:Person ] ;
    rdfs:label "My Module Record"@en ;
    skos:definition "An information content entity that documents something about a person."@en ;
    skos:example "A record with the identifier 12345 in the source system."@en .
```

A process file in `processes/` imports the module ontology (never the reverse), defines only its process class, process-only properties and restrictions on shared IRIs, and gives the process one restriction per bucket it touches. Open the scaffolded `MyModuleProcess.ttl` for the pattern.

The generated classes and their `rdf()` method are on [Generate Python classes](/user-guide/build/knowledge-graph/generate-python-classes). A pipeline that uses them is on [Create a pipeline](/user-guide/build/knowledge-graph/creating-a-pipeline#using-onto2py-generated-classes).

## Ontology best practices

1. **Start with existing classes**: reuse BFO, CCO and `abi:` classes before minting your own.
2. **One concept per class**: keep classes focused; avoid "God classes" with many unrelated properties.
3. **Document everything**: `rdfs:label`, `skos:definition`, and `skos:example` for every class and property. Agents use these descriptions.
4. **Pair every relation with its inverse**, and subclass an `abi:` relation.
5. **Stable IRIs** under `http://ontology.naas.ai/<module>/`.
6. **Separate shared vocabulary from processes**: shared classes in `modules/`, one process per file in `processes/`.
7. **Backward compatible updates**: adding new classes/properties is safe. Removing or renaming requires a migration.
