# OWL — API: OWL Over HTTP: Export, Import, Map, Replay

Two questions decide whether a format is really supported: can the system read it, and can the system write it? For the `owl` format key in CoreModels the answer is yes to both, and this article is the proof - four routes, real request bodies, real responses, and the direction rules stated without hedging.

# OWL Over HTTP: Export, Import, Map, Replay

Two questions decide whether a format is really supported: can the system read it, and can the system write it? For the `owl` format key in CoreModels the answer is yes to both, and this article is the proof - four routes, real request bodies, real responses, and the direction rules stated without hedging.

The honest headline first. The transform surface publishes two lists, and they are not identical:

- decode (import / source): `jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm`
- encode (export / target): `jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse`

Exactly two keys are one-way. `odm` decodes only - it is authored documentation, so there is nothing to encode back to. `synapse` encodes only - its output is plain draft-07 JSON Schema, which you re-import as `jsonschema`. `owl` is on neither list of exceptions. It appears in both, and everything below follows from that.

## The surface

All routes live under `{host}/graph/transform/` with no `api/` prefix, require authentication, and are project-scoped and role-checked like every other CoreModels graph API. We use `https://coremodels.example.com` as the host placeholder and `$TOKEN` for the bearer token.

| Purpose | Route | Role |
|---|---|---|
| Export project schema as Turtle | `POST graph/transform/schema/export/{projectId}` | Viewer |
| Import Turtle into a project | `POST graph/transform/schema/import/{projectId}` | Admin |
| Map a schema through the engine (stateless) | `POST graph/transform/schema/map/{projectId}` | Viewer (`ai` mapping: Editor) |
| Replay a stored plan (stateless) | `POST graph/transform/plan/execute/{projectId}` | Viewer |

Every one of them answers with the same envelope:

```jsonc
{
  "success": true,     // it ran; false means it could not proceed - read "errors"
  "lossiness": [],     // what the target could not hold exactly; can be non-empty on success
  "errors": [],        // on failure: [{ "path": "…", "message": "…" }]
  "schema": "…"        // or "projectId", plus "plan" on the mapping routes
}
```

Two request options exist on this surface that never apply to Turtle: `vendor` (SQL output dialect) and the `synapseOrg`/`synapseName`/`synapseVersion` trio (the Synapse registered-schema `$id`). Formats ignore options they do not own, so passing them is harmless - but for `owl` there are no format-specific knobs at all. The optional `spaces` array on the import and export routes works as it does everywhere else: empty means the project's main space.

## 1. Export - the project as an ontology

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "format": "owl" }' | jq -r '.schema'
```

The `schema` field is a complete Turtle document. For a governed model holding one `Product` type with a required `sku` and an optional `list_price`, the encoder emits a sorted prefix header, an `owl:Ontology` subject carrying the schema's label, then one block per class and property:

```turtle
@prefix ex: <https://coremodels.example.com/ns/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<https://coremodels.example.com/ns/ProductModel> a owl:Ontology ;
    rdfs:label "Product Model" .

ex:Product a owl:Class ;
    rdfs:label "Product" ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:Sku ; owl:minCardinality "1"^^xsd:nonNegativeInteger ] ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:Sku ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ] ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:ListPrice ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ] .

ex:Sku a owl:DatatypeProperty ;
    rdfs:label "sku" ;
    rdfs:domain ex:Product ;
    rdfs:range xsd:string .

ex:ListPrice a owl:DatatypeProperty ;
    rdfs:label "list_price" ;
    rdfs:domain ex:Product ;
    rdfs:range xsd:double .
```

Read the restriction blocks carefully, because they are the point. Required and single-valued are not stated in a CoreModels-specific annotation; they are expressed in OWL's own cardinality vocabulary, so any consumer that understands OWL understands the constraint without understanding us. Entity IRIs are minted under the `ex:` namespace from each node's label; the original label rides along as `rdfs:label`, so `list_price` survives even though the IRI is `ex:ListPrice`.

Export is a read: `lossiness` on this route reports what the *project* held that OWL could not carry. A curated component view comes back as `StructuralDrop`; a custom relation comes back as `SemanticNarrowing` because it is emitted as the weaker `rdfs:seeAlso`. Both arrive alongside `"success": true`, and both are true at once.

## 2. Import - an ontology as a project

Import is the only route here that writes, hence Admin. The body is `format` plus the Turtle text; rather than hand-escaping a document into a JSON string, let `jq` do it.

```bash
jq -n --rawfile ttl ontology.ttl '{ format: "owl", schema: $ttl }' \
| curl -sS -X POST \
    "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    --data @-
```

For a well-formed ontology the response is the minimal envelope with the project id echoed back:

```json
{ "success": true, "lossiness": [], "errors": [], "projectId": "…" }
```

If you prefer a one-liner to test the route, this body is small enough to paste directly:

```json
{ "format": "owl", "schema": "@prefix : <https://example.org/onto#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n:Person a owl:Class .\n:name a owl:DatatypeProperty ; rdfs:domain :Person ; rdfs:range xsd:string .\n" }
```

Two decoder behaviors are worth knowing before you point this at an ontology you did not write.

**Statements outside the schema vocabulary are lossiness, never errors.** Real Turtle files carry Dublin Core dates, versioning metadata, annotation properties. The decoder counts every well-formed triple it did not consume and reports one summarized record - for example `"2 triples outside the OWL schema vocabulary were ignored."` Your import does not fail because someone added `dcterms:created`. Language-tagged labels behave the same way: `rdfs:label "Person"@en , "Personne"@fr` decodes to the first lexical form and records `SemanticNarrowing` for the rest.

**Structurally broken Turtle fails with a line number.** That is a different category, and it stops the run:

```json
{
  "success": false,
  "lossiness": [],
  "errors": [ { "path": "$", "message": "Line 1: Unclosed IRI reference (missing '>')." } ],
  "ir": null
}
```

The parser handles the Turtle you actually meet in the wild: `@prefix` and SPARQL-style `PREFIX` directives, full IRIs and prefixed names, datatyped and language-tagged literals, `;` and `,` continuations, blank nodes, collections, and `#` comments.

## 3. Map - anything in, Turtle out

`schema/map` is the mapping engine's stateless front door: decode the source, produce a plan against a target hint, validate the plan through the universal gate, execute, encode. Nothing touches the project, so every call is inherently a dry run. Because `owl` encodes, it works just as well as a target - here a warehouse table becomes an ontology.

```bash
DDL='CREATE TABLE Product (
  sku VARCHAR(64) NOT NULL,
  list_price NUMERIC,
  launched_on TIMESTAMP
);'

jq -n --arg ddl "$DDL" '{
  sourceFormat:     "sql",
  sourceSchema:     $ddl,
  targetFormat:     "owl",
  targetHintFormat: "sql",
  targetHintSchema: $ddl,
  mapping:          { kind: "inferred" }
}' | curl -sS -X POST \
      "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      --data @-
```

The response carries the Turtle as a string in `schema` (JSON-shaped targets return an object instead), an empty ledger for this input, and the executed plan:

```jsonc
{
  "success": true,
  "lossiness": [],
  "errors": [],
  "schema": "@prefix ex: <https://coremodels.example.com/ns/> .\n…",
  "plan": {
    "operations": [
      { "kind": "TypeMapping", "origin": "Inferred",
        "sourceTypeId": "Product", "targetTypeId": "Product", "targetLabel": "Product" },
      { "kind": "ElementMapping", "origin": "Inferred",
        "sourceElementIds": ["ProductSku"], "targetElementIds": ["ProductSku"] },
      { "kind": "ElementMapping", "origin": "Inferred",
        "sourceElementIds": ["ProductListPrice"], "targetElementIds": ["ProductListPrice"] },
      { "kind": "ElementMapping", "origin": "Inferred",
        "sourceElementIds": ["ProductLaunchedOn"], "targetElementIds": ["ProductLaunchedOn"] }
    ]
  }
}
```

The `NOT NULL` on `sku` arrived as a `minCardinality 1` restriction plus a `maxCardinality 1` restriction on `ex:Product`. Three request details matter here: `targetHintSchema` is what inferred mapping aligns *toward* (set `"useProjectAsTargetHint": true` to use the project's own schema instead, which reads but never writes the project); `mapping.kind` is `inferred`, `explicit`, or `ai`; and `sourceFormat` and `targetFormat` are independent, so `owl` on either side - or both - is equally valid.

## 4. Replay - the plan as an artifact

The `plan` object is not a log. It is the input to `plan/execute`. Store it next to the source file and the transformation becomes reproducible under review. Note that `plan` travels as a **string**, which is exactly what `--rawfile` gives you.

```bash
jq -n --rawfile src product.sql --rawfile plan plans/product.plan.json '{
  sourceFormat: "sql",
  sourceSchema: $src,
  plan:         $plan,
  targetFormat: "owl"
}' | curl -sS -X POST \
      "https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      --data @-
```

The parsed plan passes through the same validation gate as a freshly produced one - a stored plan earns no shortcut - and then executes. Same plan plus same source produces the same output, and no target hint is needed on replay: the plan already says what to do. The response is the standard envelope with `schema`.

## Failure modes worth recognizing

- A bad format key returns `success: false` with `errors[0].message` reading `Unknown schema format 'ttl'. Use: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm.`
- Missing body keys are rejected before any work: `Body must include 'format' and 'schema'.` on import, `Body must include 'sourceFormat' and 'sourceSchema'.` on the mapping routes, and `Body must include 'plan' (the plan JSON produced by a mapping call).` on replay.
- Turtle syntax problems come back on path `$` with a line number, as shown above.

Whatever the route, the reading order is the same: check `success`; if false, `errors[].path` and `errors[].message` pinpoint the problem; if true, read `lossiness` before you celebrate. A non-empty ledger on a successful call is not a bug - it is the API declining to pretend that OWL and, say, SQL have identical expressive power.

For ready-to-paste bodies covering the other formats on this surface, see the Transform section of the CoreModels documentation.
