# LinkML — Quickstart: LinkML to Postgres in One Call: A CoreModels Quickstart

Ten minutes, one YAML file, one HTTP call. By the end of this you will have converted a LinkML schema into Postgres DDL through CoreModels, read the machine-readable ledger of everything the conversion could not carry exactly, and sent the same schema back out as LinkML unchanged.

# LinkML to Postgres in One Call: A CoreModels Quickstart

Ten minutes, one YAML file, one HTTP call. By the end of this you will have converted a LinkML schema into Postgres DDL through CoreModels, read the machine-readable ledger of everything the conversion could not carry exactly, and sent the same schema back out as LinkML unchanged.

LinkML is a YAML schema language: classes, slots, enums, and CURIEs that point at real ontology terms. CoreModels (by ARAMAI) implements it as a first-class transform format under the key `linkml`, and it works in **both directions** - we decode LinkML YAML into our neutral model and we encode that model back to LinkML YAML. Everything below is the encode/decode pair doing real work.

## Before you start

- An API host. We write `https://coremodels.example.com` throughout - substitute your deployment.
- A bearer token in `$TOKEN`. The call in this quickstart is stateless and needs only **Viewer** on the scoping project.
- A project id in `$PROJECT_ID`. It scopes authorization; nothing is written to it by the first call.

Transform routes live under `graph/transform/...` with no `api/` prefix, and every response uses the same envelope: `success`, `lossiness`, `errors`, plus the payload key for that route.

## The schema

Save this as `sensor.yaml`. It is small but not a toy: two classes, a class-to-class reference, a multivalued slot, an enum with ontology `meaning` CURIEs, a `class_uri`, a `slot_uri`, and a constraint facet.

```yaml
id: https://example.org/sensor-registry
name: sensor_registry
prefixes:
  linkml: https://w3id.org/linkml/
  schema: https://schema.org/
  qudt: http://qudt.org/schema/qudt/
imports:
  - linkml:types
default_range: string

classes:
  Sensor:
    description: A deployed measurement device.
    class_uri: schema:Thing
    attributes:
      sensor_id:
        description: Stable identifier for the device.
        identifier: true
        required: true
      name:
        slot_uri: schema:name
        required: true
      installed_on:
        range: datetime
      sampling_interval_s:
        range: integer
        minimum_value: 1
      unit:
        range: UnitEnum
      readings:
        range: Reading
        multivalued: true
  Reading:
    description: One measurement emitted by a sensor.
    attributes:
      taken_at:
        range: datetime
        required: true
      value:
        range: float
        required: true
      qc_flag:
        range: boolean

enums:
  UnitEnum:
    description: Units the registry accepts.
    permissible_values:
      CELSIUS:
        description: Degrees Celsius.
        meaning: qudt:DEG_C
      PASCAL:
        meaning: qudt:PA
      PERCENT:
```

## The call

`schema/map` is the stateless conversion route: it decodes your source, runs it through the mapping engine, and encodes the result into the target format. Nothing touches the project.

One thing to understand before you run it: the engine always works from a **plan**, and the default strategy (`inferred`) builds that plan by matching labels against a **target hint** - the vocabulary you are mapping toward. When all you want is a format conversion, the source schema's own labels *are* the target vocabulary, so pass the same document as the hint. (Skip the hint and the call fails honestly with `The inference resolver requires a target IR to match against.`)

```bash
jq -n --rawfile s sensor.yaml \
  '{sourceFormat:"linkml", sourceSchema:$s,
    targetFormat:"sql", vendor:"postgres",
    targetHintFormat:"linkml", targetHintSchema:$s,
    mapping:{kind:"inferred"}}' \
| curl -s "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d @-
```

The `jq -n --rawfile` step exists only to JSON-escape the YAML; the body it produces is ordinary JSON. Add `| jq -r '.schema'` to the end and you get the DDL straight out:

```sql
CREATE TABLE "Sensor" (
  "sensor_id" VARCHAR(255) NOT NULL,
  "name" VARCHAR(255) NOT NULL,
  "installed_on" TIMESTAMP,
  "sampling_interval_s" INTEGER,
  "unit" VARCHAR(255),
  "readings" INTEGER REFERENCES "Reading"
);

CREATE TABLE "Reading" (
  "taken_at" TIMESTAMP NOT NULL,
  "value" NUMERIC NOT NULL,
  "qc_flag" BOOLEAN
);

COMMENT ON COLUMN Sensor.name IS '{"x-maps-to":{"schema":"https://schema.org/name"}}';
```

Look at what came across without you configuring anything. `required: true` became `NOT NULL`. `range: datetime` became `TIMESTAMP`, `range: float` became `NUMERIC`, `range: integer` became `INTEGER`, `range: boolean` became `BOOLEAN`. The class-to-class range became a foreign-key reference. And `slot_uri: schema:name` - a CURIE - was expanded through your `prefixes:` block into the full IRI `https://schema.org/name` and emitted as a column comment. Meaning travels; it does not evaporate at the format boundary.

## The ledger

The same response carries a `lossiness` array. For this input it has exactly one entry:

```json
{
  "kind": "ConstraintRelaxation",
  "path": "Element[SensorUnit]",
  "explanation": "Postgres has no inline enum; emitted as VARCHAR (the allowed-value constraint is not enforced)."
}
```

That is the habit worth building on call one: **`success: true` means the call ran, not that nothing changed.** The ledger is the exact, reviewable list of what the target format could not hold. Here it is telling you something you would otherwise discover in production - the `UnitEnum` values are still documented in your LinkML, but the Postgres column will accept any string.

Every record has one of four kinds, and they mean precisely what they say:

- **StructuralDrop** - something had no home in the target and was left out.
- **TypeApproximation** - a value or type was represented by a close-but-not-exact target type.
- **ConstraintRelaxation** - a rule (required, max length, enum) could not be enforced and was relaxed.
- **SemanticNarrowing** - meaning was narrowed or guessed.

An empty array is a clean conversion. Anything else is a checklist. Change `"vendor":"postgres"` to `"vendor":"mysql"` in the body above and that single record disappears, because MySQL *does* have an inline enum and the coder emits one.

The response also carries a third key you should not ignore: `plan`. It is the executed mapping as a JSON artifact - a list of operations, each stamped with its `kind` and its `origin` (`Inferred` here) - and it can be stored, reviewed, and replayed later against the same source for a byte-identical result.

## Send it back out as LinkML

Change one field and the same call round-trips the schema:

```bash
jq -n --rawfile s sensor.yaml \
  '{sourceFormat:"linkml", sourceSchema:$s,
    targetFormat:"linkml",
    targetHintFormat:"linkml", targetHintSchema:$s,
    mapping:{kind:"inferred"}}' \
| curl -s "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d @- | jq -r '.schema'
```

What comes back is your document, with every fact intact: the schema `id`, the prefixes, `imports`, `default_range`, both descriptions, `class_uri: schema:Thing`, `slot_uri: schema:name`, `identifier: true`, `required: true`, `multivalued: true`, `range: datetime`, `minimum_value: 1`, the enum with `meaning: qudt:DEG_C`, and the valueless `PERCENT:` entry. Two cosmetic differences are worth knowing about because they are deliberate: the `prefixes:` block comes back sorted (`linkml`, `qudt`, `schema`) and classes are separated by a blank line. Our encoder writes YAML by hand with a fixed key order and two-space indent, so the same input always produces the same bytes - which is what makes exported LinkML safe to commit and diff.

Note also that `range: datetime` came back as `datetime`, not as some normalized spelling. LinkML has several temporal built-ins (`date`, `datetime`, `time`, `date_or_datetime`) that all collapse onto one internal type; the coder keeps the verbatim wire range alongside so re-export is exact rather than merely equivalent.

## Putting it in a project

The stateless route is the fastest path to a conversion. When you want the schema to *live* in CoreModels - governed, searchable, exportable to every other format we encode - import it:

```bash
jq -n --rawfile s sensor.yaml '{format:"linkml", schema:$s}' \
| curl -s "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d @-
```

That route writes, so it requires **Admin** on the project, and it answers `{ "success": true, "lossiness": [], "errors": [], "projectId": "..." }`. Each LinkML class becomes a Type, each attribute an Element, each enum a Taxonomy whose permissible values are its terms - and the `class_uri`, `slot_uri`, and `meaning` CURIEs become the same `mapsTo` meaning links every other CoreModels export reads. One naming note so nothing surprises you when you open the project: Element **labels** keep your original names (`sensor_id`), while internal node **ids** are camelCase composites of class and slot (`SensorSensorId`), because CoreModels node ids must be alphanumeric.

Going the other way is `POST graph/transform/schema/export/{projectId}` with the body `{ "format": "linkml" }` - a **Viewer**-role read that never writes, safe to hand to an analyst account or a build agent.

## Where to go next

You have now seen the whole contract in miniature: a real conversion, a plan you can keep, and an honest account of the difference. The same source document can be pointed at any format we encode - JSON Schema, Avro, ShEx, JSON-LD, OWL, an Apache Ossie semantic model, a Bitol ODCS contract, proto3, or a Synapse-ready draft-07 schema - by changing `targetFormat` alone. Every route, role, and per-format option is written up in the Schema Transformation API reference in the CoreModels docs.
