LinkML logoAPI

The LinkML HTTP Surface: Import, Export, Map, Replay

There are ten routes on the CoreModels transform API. Five of them matter if LinkML is your source or your target, and the first thing to settle is which one you actually want — because the difference between them is not the format, it is whether anything gets written and who decides what passes through.

The LinkML HTTP Surface: Import, Export, Map, Replay

There are ten routes on the CoreModels transform API. Five of them matter if LinkML is your source or your target, and the first thing to settle is which one you actually want — because the difference between them is not the format, it is whether anything gets written and who decides what passes through.

This article is the reference for that decision: the routes, their roles, real request and response bodies, and the errors you will hit if you get an argument wrong.

Direction: both, without asterisks

Two format lists govern the whole surface. Decode (the formats you can bring in) is jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm. Encode (the formats you can produce) is jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse.

linkml appears in both, which is worth stating plainly because two neighbours on those lists do not: odm decodes only (ODM entities are authored documentation, not a generated artifact), and synapse encodes only — attempt to decode it and you get the honest reply "'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema — decode it with the 'jsonschema' format." LinkML carries no such caveat. Anywhere a route says source format or target format, linkml is legal.

The five routes

All are POST, all are authorized and project-scoped, and none of them uses an api/ prefix.

RouteWhat it does with LinkMLRole
graph/transform/schema/import/{projectId}writes a LinkML schema into the projectAdmin
graph/transform/schema/export/{projectId}emits the project's schema as LinkMLViewer
graph/transform/schema/map/{projectId}LinkML in or out, through the mapping engine; nothing writtenViewer (ai: Editor)
graph/transform/schema/mapImport/{projectId}maps LinkML onto the project's existing schemaAdmin (dry run: Viewer)
graph/transform/plan/execute/{projectId}replays a stored plan against the same sourceViewer

Every response shares one envelope: success (false only when it could not proceed — read errors), lossiness (an array of {kind, path, explanation} records whose kinds are StructuralDrop, TypeApproximation, ConstraintRelaxation, SemanticNarrowing), errors, and the payload key for that route — projectId, schema, or summary, plus plan on the mapping routes and dryRun on map-import.

Import: LinkML → project

Body: format, schema (the document text), optional spaces (target space ids; empty means the project's main space).

curl -s "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "linkml",
    "schema": "id: https://example.org/person\nname: person\nclasses:\n  Person:\n    attributes:\n      name:\n        range: string\n        required: true\n      age:\n        range: integer\n"
  }'
{ "success": true, "lossiness": [], "errors": [], "projectId": "..." }

Classes become Types, attributes become Elements, enums become Taxonomies, and any class_uri / slot_uri / meaning CURIE is expanded through the document's prefixes: map into a full IRI and attached as a mapsTo meaning link. Omit format or schema and the call fails before it touches the graph: Body must include 'format' and 'schema'.

Export: project → LinkML

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

The schema key holds the LinkML as a string. Export is a Viewer read that never writes, so it is safe to hand to a build agent or an analyst account. vendor is ignored here — it only applies to sql output — as are the synapseOrg / synapseName / synapseVersion keys. LinkML export takes no format-specific options at all; optional spaces narrows it to particular spaces.

Map: the engine surface

schema/map is where LinkML gets interesting, and where the mental model has to change. This route does not "convert a document"; it executes a plan. The plan is produced by one of three strategies, validated by a single gate, executed deterministically, and returned to you next to the result.

That has a consequence people meet on their first call: whatever the plan does not cover does not appear in the output. Here it is, concretely. The source — a four-slot LinkML class:

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

classes:
  Sensor:
    class_uri: schema:Thing
    attributes:
      sensor_id:
        identifier: true
        required: true
      name:
        slot_uri: schema:name
        required: true
      installed_on:
        range: datetime
      sampling_interval_s:
        range: integer

The target hint — the shape we are mapping toward, which deliberately knows about only two of those slots:

jq -n --rawfile s sensor.yaml --arg hint '{
  "$id": "Sensor", "type": "object", "title": "Sensor",
  "properties": {
    "sensor_id": { "type": "string" },
    "name": { "type": "string" }
  },
  "required": ["sensor_id", "name"]
}' '{sourceFormat:"linkml", sourceSchema:$s,
     targetFormat:"linkml",
     targetHintFormat:"jsonschema", targetHintSchema:$hint,
     mapping:{kind:"inferred", caseInsensitive:true}}' \
| curl -s "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d @-

The plan in the response has three operations:

{
  "operations": [
    { "kind": "TypeMapping", "origin": "Inferred",
      "sourceTypeId": "Sensor", "targetTypeId": "Sensor", "targetLabel": "Sensor" },
    { "kind": "ElementMapping", "origin": "Inferred",
      "sourceElementIds": ["SensorSensorId"], "targetElementIds": ["Sensor::sensor_id"] },
    { "kind": "ElementMapping", "origin": "Inferred",
      "sourceElementIds": ["SensorName"], "targetElementIds": ["Sensor::name"] }
  ]
}

The lossiness array accounts for the rest, by name:

[
  { "kind": "StructuralDrop", "path": "Type[Sensor].SensorInstalledOn",
    "explanation": "Element is a member of the mapped type but no operation maps or drops it." },
  { "kind": "StructuralDrop", "path": "Type[Sensor].SensorSamplingIntervalS",
    "explanation": "Element is a member of the mapped type but no operation maps or drops it." }
]

And schema holds LinkML containing exactly what the plan carried — meaning links included, because they ride along on the mapped nodes:

classes:
  Sensor:
    class_uri: schema:Thing
    attributes:
      sensor_id:
        identifier: true
        required: true
      name:
        slot_uri: schema:name
        required: true

Two practical rules follow. First: if you want a straight format conversion rather than a projection, hand the route the source document as its own target hint (targetHintFormat: "linkml" with the same text) — every label matches, the plan covers everything, and nothing is dropped. Second: mapping is optional and defaults to {"kind":"inferred"}, but inference is not optional about its hint. With neither targetHintSchema nor useProjectAsTargetHint: true, the call fails with The inference resolver requires a target IR to match against.

Setting useProjectAsTargetHint: true swaps the inline hint for the project's own schema — the way to align an incoming LinkML file against a model you already govern without writing anything.

Replay: the plan as an artifact

The plan you got back is not a log line. Send it to plan/execute with the same source and you get the same output, every time; the parsed plan goes through the identical validation gate first, because a stored plan earns no shortcut. Note that plan here is a string, not an object:

jq -n --rawfile s sensor.yaml --rawfile p plan.json \
  '{sourceFormat:"linkml", sourceSchema:$s, plan:$p, targetFormat:"sql", vendor:"postgres"}' \
| curl -s "https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d @- | jq -r '.schema'

plan/execute answers with the standard envelope plus schema, and it is a Viewer call — which is the point. Review and approve the plan once; let a low-privilege pipeline replay it forever after.

Map-import: onto a model you already have

schema/mapImport is the writing cousin of schema/map: the project's current schema is the mapping target, and the transformed result is written back. Dry-run first — with "dryRun": true the call needs only Viewer and returns summary (counts of types, elements, taxonomies, components, relations), the plan, and the ledger, while writing nothing:

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

Read the lossiness, review the plan, then repeat with "dryRun": false (which requires Admin) to commit.

The errors you will actually see

Every failure carries a path and a message, and the LinkML decoder is deliberately hard to break — unknown keys ride through as preserved data rather than errors. What genuinely fails:

  • A document that is not YAML: The document is not valid YAML: ..., with the parser's complaint appended.
  • Valid YAML that is not a schema: The document is valid YAML but is not a LinkML schema (none of id, name, prefixes, classes, slots, or enums is present). — or, for a document that is not a mapping at all, ... (expected a mapping at the top level).
  • An empty payload: The LinkML YAML document is empty.
  • A bad format key: Unknown schema format '...'. Use: ..., listing the legal values.
  • A mapping-kind typo: Unknown mapping kind '...'. Use: explicit | inferred | ai.
  • kind: "explicit" without a guide: An explicit mapping requires a 'guide' object (see the SIA mapping-guide wire format).
  • A typo inside an explicit guide — rejected, never silently ignored: Unknown mapping-guide key 'fieldMapping'. Known keys: autoMatchByMapsTo, fieldMappings, taxonomyDirectives, drops.

One role note to file away: mapping.kind: "ai" raises the requirement on any of these routes to Editor or Admin membership, because it sends your schema content to the Anthropic API server-side. inferred and explicit stay at the route's baseline role and never leave the server.

For the complete ten-route catalog, the record-level routes, and ready-to-paste bodies for every format, see the Schema Transformation API reference in the CoreModels docs.