Docs/Developer/JSON-LD and the Atlas Data Model
DeveloperReference

JSON-LD and the Atlas Data Model

Atlas stores ontologies as JSON-LD files in a Git-backed repository.

Updated 9 Nov 2026Timebase Docs

Overview#

Atlas stores ontologies as JSON-LD files in a Git-backed repository. Every entity type, property, data type, relationship type and function you author through the API is serialised into one of those files. When you compile, Atlas parses them, builds an in-memory knowledge graph, and generates a .NET assembly that powers live queries.

This chapter owns the on-disk shapes. It is the reference for what a .jsonld file in the Atlas repository actually contains: which predicates carry which meaning, what the writer emits, and what the format validator rejects. It does not restate two things covered in elsewhere:

  • What the words mean. Domain, library, branch, entity type, data type, World, compile, draft session, binding path: see Timebase Atlas Key Concepts.
  • The HTTP surface. Routes, request bodies, response bodies, verbs, status codes and error envelopes: see Atlas API - Ontology.

Read this chapter when you are generating JSON-LD yourself, reviewing an ontology diff, writing a round-trip test, or trying to understand why a file the API produced does not look like the one you hand-wrote.

One thing to know before you write any JSON-LD#

Property-to-type attachment moved. It used to be rdfs:domain on the property. It is now a bare owl:Restriction on the entity class. rdfs:domain on a property definition is a hard validation error (rule O-A12) with no dual-read path. If you carry a pre-cutover file forward, this is the change that will stop you. See Attachment lives on the class, not on the property.

A write that fails validation does not reach the branch#

An ontology save whose content fails validation is held in the caller's private draft session; the target branch does not advance. The only signal on the response is the Atlas-Validation-Violations header. So a 2xx on a save does not mean the JSON-LD is committed. Publish explicitly with POST /api/v1/branches/{branch}/draft/publish. Full treatment in Atlas API - Ontology, section "What a successful write actually means".


Where the files live#

Path What it is
domains/{domain}/ontology.jsonld A domain's entry-point ontology file
libraries/{library}.jsonld A library, one file at the repository root

The file is ontology.jsonld, not schema.jsonld.

ontology.jsonld is the entry point, not necessarily the whole domain. A domain may be split across several .jsonld files under domains/{domain}/, reached through @context references. The delete guard and the dependency resolver both scan *.jsonld recursively under the domain subtree rather than assuming a single file.

One library ships with Atlas and is force-upgraded on every startup: atlas-core. Every other library Atlas ships, atlas-common and isa95-common among them, is opt-in: they are copied into your repository only when you ask for them, and are yours to edit afterwards.

Use the copy in your own libraries/ folder as your reference. A second copy of the seed libraries exists under scripts/, and it is not what the runtime loads. It predates several vocabulary additions, carries the earlier function aliases, and includes an isa95-common.jsonld that Atlas does not ship. Only the copy in your own libraries/ folder is authoritative.


The @context block#

The @context is where library imports are declared. That is the most consequential thing in the block, and it is why the shape matters.

Domain and library files: an array#

A domain or library file's @context is an array: one string per imported library, in import order, followed by a single object declaring that file's own prefix.

A domain named plant-model created with the atlas-common library selected gets:

{
  "@context": [
    "../../libraries/atlas-core.jsonld",
    "../../libraries/atlas-common.jsonld",
    {
      "plant-model": "urn:atlas:ontology:plant-model:"
    }
  ],
  "@graph": [
    {
      "@id": "urn:atlas:ontology:plant-model",
      "@type": "owl:Ontology",
      "rdfs:label": "Plant Model Domain"
    }
  ]
}

Rules the writer follows:

  • atlas-core is always imported first, and imports are de-duplicated, so selecting atlas-core explicitly does not write it twice.
  • From a domain file the reference is ../../libraries/{name}.jsonld. From a library file, which already sits in libraries/, it is the bare {name}.jsonld.
  • The trailing object has exactly one entry: the file's own prefix.

A library file looks the same with the shorter relative path:

"@context": [
  "atlas-core.jsonld",
  {
    "atlas-common": "urn:atlas:ontology:atlas-common:"
  }
]

isa95-common.jsonld layers on top of atlas-common the same way.

The root owl:Ontology node also records the import in RDF, with owl:imports. The @context reference is what the loader resolves; owl:imports is the declarative record.

atlas-core.jsonld is the exception: a flat prefix object#

Only atlas-core declares raw prefixes, because it is the bottom of the stack:

"@context": {
  "@version": 1.1,
  "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
  "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
  "owl": "http://www.w3.org/2002/07/owl#",
  "xsd": "http://www.w3.org/2001/XMLSchema#",
  "fno": "https://w3id.org/function/ontology#",
  "atlas": "urn:atlas:vocab:",

  "hasPropertyConfig": {"@id": "atlas:hasPropertyConfig", "@container": "@set"},
  "configuresProperty": {"@id": "atlas:configuresProperty", "@type": "@id"},
  "strategy": "atlas:strategy",
  "bindingPath": "atlas:bindingPath",
  "function": {"@id": "atlas:function", "@type": "@id"},
  "arguments": {"@id": "atlas:arguments", "@container": "@list"},

  "body": "atlas:body",

  "contributesToNormalizationAnchor": { "@id": "atlas:contributesToNormalizationAnchor", "@type": "xsd:boolean" },
  "isTimeAxis": { "@id": "atlas:isTimeAxis", "@type": "xsd:boolean" }
}

The nine term-level aliases matter when you read a file: isTimeAxis in a shipped library is written as the bare alias, not as atlas:isTimeAxis, and it expands to the same predicate. Same for bindingPath, strategy, body and the rest.

You do not normally hand-write @context. Atlas manages it when a domain or library is created, and rewrites it on every save from the entry-point file's original context.


Namespaces#

Prefix Namespace URI Purpose
rdf http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF core vocabulary
rdfs http://www.w3.org/2000/01/rdf-schema# Labels, comments, ranges, subclass chains
owl http://www.w3.org/2002/07/owl# Class, property and restriction declarations
xsd http://www.w3.org/2001/XMLSchema# Scalar data types
fno https://w3id.org/function/ontology# Function Ontology, used by the function subgraph
atlas urn:atlas:vocab: Atlas framework vocabulary (NodeBase, EdgeBase, CompoundPropertyType, ...)
(domain) urn:atlas:ontology:{domain}: Types authored in a domain
(library) urn:atlas:ontology:{library}: Types authored in a library
(instances) urn:atlas:instance: The A-Box namespace, minted for instances
seed/libraries/atlas-core.jsonld.

One URN namespace for all five kinds#

There is no urn:atlas:class:, urn:atlas:prop:, urn:atlas:rel: or urn:atlas:fn:. Entity types, properties, data types, relationship types and functions all mint URIs from the same scope namespace:

urn:atlas:ontology:{scope}:{LocalName}

UrnHelper.BuildUri(scope, localName) => $"{OntologyBase}{scope}:{localName}". The practical consequence is that a name used by one kind is unavailable to the other four in the same scope; the collision guard blocks the write.

Use lowercase-hyphen scope names#

Domain and library names are lowercased and hyphenated when the prefix is generated: request.DomainName.ToLower(CultureInfo.InvariantCulture).Replace(' ', '-'), so a domain named Plant Model gets the prefix plant-model and the namespace urn:atlas:ontology:plant-model:. Entity URIs, however, are minted from the route's {domain} value verbatim (calling UrnHelper.BuildUri). Every shipped library uses a lowercase-hyphen name (atlas-common, isa95-common), and so should yours.


How ontology concepts map to JSON-LD#

Entity types: owl:Class#

An entity type is an owl:Class that is a subclass of atlas:NodeBase, directly or through another entity type.

{
  "@id": "atlas-common:Folder",
  "@type": "owl:Class",
  "rdfs:label": "Folder",
  "rdfs:comment": "Generic organizational grouping node with no domain-specific meaning.",
  "rdfs:subClassOf": {"@id": "atlas:NodeBase"}
}

(examples/Standard Libraries/atlas-common/atlas-common.jsonld.)

When you supply a ParentType, rdfs:subClassOf points at that parent instead of directly at atlas:NodeBase. Inheritance is that chain. The compiler walks the whole chain and unions the attached properties with de-duplication, so a child type gets its parent's properties without any of them being restated in the file.

When a type carries attached properties, rdfs:subClassOf becomes an array: the parent first, then one bare owl:Restriction per attached property. See the next section.

Properties: owl:DatatypeProperty and owl:ObjectProperty#

A property is a standalone subject. It declares its own identity, label, comment and range, and nothing about which types carry it.

{
  "@id": "atlas-common:Description",
  "@type": "owl:DatatypeProperty",
  "rdfs:label": "Description",
  "rdfs:comment": "Description property",
  "rdfs:range": {"@id": "xsd:string"}
}

(examples/Standard Libraries/atlas-common/atlas-common.jsonld.)

An owl:ObjectProperty has the same shape with a class URI as its range:

{
  "@id": "atlas-common:Data",
  "@type": "owl:ObjectProperty",
  "rdfs:label": "Data",
  "rdfs:comment": "TVQ binding",
  "rdfs:range": {"@id": "atlas-common:TVQ"}
}

(examples/Standard Libraries/atlas-common/atlas-common.jsonld.)

Note what that range is. atlas-common:TVQ is not an entity type; it is a data type (a compound). An object property's range is a data type, never an entity type: an entity is not a property's value, and a property ranged over one fails validation with rule O-B22. The single most-used object property in the shipped libraries ranges over a compound. PropertyDefinition.CompoundRange exists precisely for this case. See Data types (compounds).

The optional rdfs:subPropertyOf records an organisational parent property. It is metadata only and has no effect on the compiled World.

Attachment lives on the class, not on the property#

rdfs:domain on a property is the earlier attachment shape, and attachment now lives on the entity class. The persisted contract is now:

<EntityClass> rdfs:subClassOf [ a owl:Restriction ; owl:onProperty <Property> ] .

The attachment restriction carries no type information: at most an owl:minCardinality of 0, which marks the property optional. Writers never emit owl:hasValue or owl:allValuesFrom on it. A type's pin on a Defer property's open leaf is a separate sibling restriction on the class, not a facet of the attachment: see Pinning an open leaf on a type. This was a hard cutover with no dual-read: a file that still uses rdfs:domain does not fall back, it fails.

Real shape, atlas-common:AnalogTag (abridged: UOM and Formatted are attached the same way as Format):

{
  "@id": "atlas-common:AnalogTag",
  "@type": "owl:Class",
  "rdfs:label": "Analog Tag",
  "rdfs:subClassOf": [
    {"@id": "atlas-common:Tag"},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas-common:Format"},
     "owl:minCardinality": {"@type": "xsd:nonNegativeInteger", "@value": "0"}},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas-common:Data"},
     "owl:minCardinality": {"@type": "xsd:nonNegativeInteger", "@value": "0"}},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas-common:Data"},
     "owl:allValuesFrom": {"@type": "owl:Class", "owl:intersectionOf": {"@list": [
       {"@id": "atlas-common:TVQ"},
       {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas-common:Value"},
        "owl:allValuesFrom": {"@id": "xsd:double"}}
     ]}}}
  ]
}

One attachment entry per attached property, alongside the parent-class entry. The property's type comes from the property's own rdfs:range; the attachment carries no type information. The second Data restriction, the one with owl:allValuesFrom, is not an attachment: it is the type's pin of the open TVQ.Value leaf to xsd:double, as described in the linked section. Tag itself is abstract ("isAbstract": true) and attaches only Description, UOM and Format.

The error you will hit if you get this wrong. Rule O-A12, ValidationSeverity.Error. The JsonPointer on the violation points at /@graph/{index}/rdfs:domain, and the message is:

Property '{id}' declares 'rdfs:domain' - the retired property-owns-attachment shape.
Property↔type attachment now lives on the entity TYPE as an owl:Restriction (attach the
property from the type); rdfs:domain on a property definition is no longer read. Remove it
and attach the property to the entity type(s) that use it.

Two exemptions are built into the rule, and both are visible in the shipped libraries:

  • Functions. A function's @type is fno:Function, not a property kind, so the kind gate never fires. Functions legitimately keep rdfs:domain as their library marker (AssertUriTriple(function.Uri, RdfsDomain, function.Library)).
  • The atlas: framework vocabulary. atlas:Source, atlas:Target, atlas:fieldOrder, atlas:isTimeAxis, atlas:asymmetric, atlas:body, atlas:Parameter and atlas:contributesToNormalizationAnchor all carry rdfs:domain in atlas-core.jsonld as self-documentation, and are exempt by namespace.

Neither exemption applies to anything you author in a domain or a user library.

XSD to CLR type mapping#

is the single source of truth. It exposes two views: the column view (physical storage) and the public view (descriptors and generated record structs).

XSD type Public CLR type Column CLR type
xsd:string string string
xsd:double double double
xsd:float float float
xsd:integer int int
xsd:int int int
xsd:long long long
xsd:boolean bool bool
xsd:dateTime System.DateTime long (UTC ticks)

Two things worth knowing:

  • xsd:integer is recognised and maps to int, the same as xsd:int. This matters because atlas-common:Quality is xsd:integer.
  • xsd:dateTime is the one divergence between the two views. Columns hold 64-bit ticks so the storage stays cache-dense and SIMD-friendly; a boundary coercion converts to DateTime for consumers.
  • An unrecognised URI falls back to string in the column view but returns null in the public view, which is intended: codegen reports an explicit error instead of mistyping the field.

Value-type unions on a property range#

A property may declare a union of scalar types. Two or more distinct members produce an anonymous rdfs:Datatype carrying an owl:unionOf list; exactly one member collapses to a plain single-URI range, byte-identical to a non-union write.

atlas-common:Value is the shipped example:

{
  "@id": "atlas-common:Value",
  "@type": "owl:DatatypeProperty",
  "rdfs:label": "Value",
  "rdfs:comment": "The measured value",
  "rdfs:range": {
    "@type": "rdfs:Datatype",
    "owl:unionOf": {
      "@list": [{"@id": "xsd:double"}, {"@id": "xsd:string"}, {"@id": "xsd:boolean"}]
    }
  }
}

Union members are declared in authoring order and preserved in that order.


Data types (compounds)#

A data type is a named value shape whose fields travel together. atlas-common:TVQ (Timestamp / Value / Quality) is the single most common thing in a shipped library, and it is the piece most likely to be unfamiliar if you learned the model from the entity/property/relationship triad alone.

The vocabulary is documented in and declared in atlas-core.jsonld:

Term Declared at Meaning
atlas:CompoundPropertyType atlas-core.jsonld Marker class. Subclassing it flags a class as a compound.
atlas:HasFieldBase atlas-core.jsonld Base class for the reified compound-to-field edge. subClassOf atlas:EdgeBase.
atlas:fieldOrder atlas-core.jsonld xsd:int ordinal position of a field within its compound.
atlas:isTimeAxis atlas-core.jsonld xsd:boolean. Marks the field that is the compound's time axis. At most one.

The shape#

A compound type is an owl:Class subclassing both atlas:NodeBase and atlas:CompoundPropertyType. Each field is a separate reified class subclassing atlas:HasFieldBase, with a Source restriction naming the compound and a Target restriction naming an existing owl:DatatypeProperty. The field's name is the local name of that target property and its type is that property's rdfs:range.

The atlas-common:TVQ block, verbatim:

{
  "@id": "atlas-common:TVQ",
  "@type": "owl:Class",
  "rdfs:label": "TVQ",
  "rdfs:comment": "Timestamp-Value-Quality measurement tuple - standard measurement representation",
  "rdfs:subClassOf": [{"@id": "atlas:NodeBase"}, {"@id": "atlas:CompoundPropertyType"}]
},
{
  "@id": "atlas-common:TVQContainsQuality",
  "@type": "owl:Class",
  "rdfs:label": "TVQ contains Quality",
  "rdfs:subClassOf": [
    {"@id": "atlas:HasFieldBase"},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Source"}, "owl:allValuesFrom": {"@id": "atlas-common:TVQ"}},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Target"}, "owl:allValuesFrom": {"@id": "atlas-common:Quality"}}
  ],
  "atlas:fieldOrder": 2
},
{
  "@id": "atlas-common:TVQContainsTimestamp",
  "@type": "owl:Class",
  "rdfs:label": "TVQ contains Timestamp",
  "rdfs:subClassOf": [
    {"@id": "atlas:HasFieldBase"},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Source"}, "owl:allValuesFrom": {"@id": "atlas-common:TVQ"}},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Target"}, "owl:allValuesFrom": {"@id": "atlas-common:Timestamp"}}
  ],
  "atlas:fieldOrder": 0,
  "isTimeAxis": "true"
},
{
  "@id": "atlas-common:TVQContainsValue",
  "@type": "owl:Class",
  "rdfs:label": "TVQ contains Value",
  "rdfs:subClassOf": [
    {"@id": "atlas:HasFieldBase"},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Source"}, "owl:allValuesFrom": {"@id": "atlas-common:TVQ"}},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Target"}, "owl:allValuesFrom": {"@id": "atlas-common:Value"}}
  ],
  "atlas:fieldOrder": 1
}

The three target properties (atlas-common:Timestamp, atlas-common:Value, atlas-common:Quality) are ordinary first-class properties declared elsewhere in the same file. Field membership is selection only: a data type never inlines a field definition.

Reading the block#

Things worth noticing, because they trip people up:

  • isTimeAxis appears as the bare alias with a string value. atlas-core's @context declares "isTimeAxis": {"@id": "atlas:isTimeAxis", "@type": "xsd:boolean"}, so "true" in the file is a typed boolean literal, not a string. The reader parses it with bool.TryParse.
  • atlas:fieldOrder is written as a plain JSON number, not as a {"@type","@value"} value object. The serialiser collapses xsd:int/xsd:integer value objects back to plain numbers to match hand-authored files.
  • fieldOrder is a sort key only, and is not persisted on the field record. The read model is public sealed record CompoundField(string Name, string TypeUri, bool IsTimeAxis = false). The store reads atlas:fieldOrder off the HasFieldBase edge, uses it to order the list, then discards it. Fields with no fieldOrder sort after all ordered fields. IsTimeAxis, by contrast, is a member of CompoundField. If you are diffing the read model against the file, do not expect a field order to survive the round trip through the record; the list order is the only carrier.
  • At most one time axis. Enforced across own and inherited fields, and it throws on build: "Compound type '{subjectUri}' declares more than one atlas:isTimeAxis field; at most one is allowed.". A compound with no time axis is atomic and not time-segmentable.
  • Field inheritance exists. A compound may subclass another compound; the parent's fields are prepended and own fields override inherited ones by name, cycle-guarded.

Data types show up in a naive type listing#

NodeKind { Entity = 0, DataType = 1 } is derived on read from the compound-marker subclass test, never authored. Because a compound is an owl:Class subclassing atlas:NodeBase, it appears in any listing that only asks "is this a class under NodeBase" unless you filter on NodeKind. That filter is what keeps compounds out of the entity-parent picker and entities out of the value-type picker.

Pinning a field's type at the point of use#

A property whose range is a compound can pin individual fields to concrete XSD types, so a generic TVQ becomes a TVQ whose Value is a double. The writer emits an anonymous owl:Class with an owl:intersectionOf list: the base compound URI first, then one owl:Restriction per pinned field carrying owl:onProperty plus owl:allValuesFrom. The read model is CompoundRangeSpec(string BaseCompoundUri, IReadOnlyDictionary<string,string> FieldTypeOverrides). Override keys are dotted field paths relative to the base compound, so Inner.Value descends through a nested compound.

This is the one place owl:allValuesFrom legitimately appears on a property range.

{
  "@id": "urn:atlas:ontology:plant-model:FlowReading",
  "@type": "owl:ObjectProperty",
  "rdfs:label": "Flow Reading",
  "rdfs:range": {
    "@type": "owl:Class",
    "owl:intersectionOf": {
      "@list": [
        {"@id": "atlas-common:TVQ"},
        {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas-common:Value"}, "owl:allValuesFrom": {"@id": "xsd:double"}}
      ]
    }
  }
}

No shipped seed library contains owl:intersectionOf. The compacted rendering above comes from the property writer, not from seed data.


Relationship types#

Relationship types are reified: the relationship type is itself an owl:Class, and its endpoints are declared as restrictions on atlas:Source and atlas:Target.

atlas:EdgeBase is the root#

The chapter's older framing treated ContainsBase and RelationshipBase as unrelated peers. They are not. Every reified edge class descends from atlas:EdgeBase:

Class Parent Used for
atlas:ContainsBase atlas:EdgeBase Containment relationship types
atlas:RelationshipBase atlas:EdgeBase Non-containment relationship types
atlas:HasFieldBase atlas:EdgeBase Compound-to-field edges (see data types)
atlas:OutputFieldLinksToInputField atlas:EdgeBase Function field-provenance edges

The last two are edge classes whose endpoints are property URIs rather than NodeBase-rooted classes, which is why they hang off EdgeBase directly rather than off RelationshipBase. Only the first two are relationship types in the product sense.

The endpoint shape#

atlas-common:contains, verbatim:

{
  "@id": "atlas-common:contains",
  "@type": "owl:Class",
  "rdfs:label": "contains",
  "rdfs:comment": "Generic physical-containment relationship. ...",
  "rdfs:subClassOf": [
    {"@id": "atlas:ContainsBase"},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Source"}, "owl:allValuesFrom": {"@id": "atlas:NodeBase"}},
    {"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas:Target"}, "owl:allValuesFrom": {"@id": "atlas:NodeBase"}}
  ],
  "atlas:asymmetric": "containedIn"
}

A non-containment relationship type is identical but subclasses atlas:RelationshipBase (atlas-common:relatesTo, :sameAs, :connectsTo).

rdfs:comment on a relationship class is supported and is read back even when it was authored directly in JSON-LD rather than through the API.

"Unconstrained" is atlas:NodeBase, never a missing block#

A relationship class rooted directly at ContainsBase or RelationshipBase must declare both a Source and a Target restriction. "Any node to any node" is expressed as owl:allValuesFrom atlas:NodeBase, which is exactly what all four shipped atlas-common relationships do. An absent block is rejected by the format validator (rule O-A8,); the writer substitutes atlas:NodeBase rather than omitting the block.

The exception: a relationship type whose base is another relationship type may omit a block and inherit its parent's constraint. That is the narrowing hierarchy.

Relationship types have a real inheritance hierarchy#

RelationshipDefinition carries both:

  • BaseType: the framework root, atlas:ContainsBase or atlas:RelationshipBase.
  • Parent: the immediate user-defined super-relationship-type, or null.

plus Properties (own and inherited, merged server-side), IsContainment (transitive rooting at ContainsBase), Comment and SourceLibrary. Narrowing is enforced: a child may tighten its endpoints, not widen them.

Relationship properties are bare restrictions#

Edge data attaches to a relationship class exactly the way a property attaches to an entity class: one bare owl:Restriction per property, appended to the same rdfs:subClassOf array. The writer's own comment says it plainly: "one owl:Restriction per attached property URI, identical shape to Source/Target. The property's own URI plays the role of owl:onProperty; no new vocabulary term."

atlas-common:connectsTo carries one:

{"@type": "owl:Restriction", "owl:onProperty": {"@id": "atlas-common:Description"}}

No owl:allValuesFrom, and no other facet. The edge property's type comes from the linked property's own rdfs:range, not from the restriction. Contrast this with the Source/Target restrictions in the same array, which do carry allValuesFrom. Facets written on an edge-property restriction are tolerated and ignored, which means they are carried on the wire without being applied.

Primitive-valued edge properties compile end to end. Data-type-valued (compound) edge properties persist but are skipped at compile.

atlas:asymmetric#

atlas:asymmetric is declared in atlas-core.jsonld as an owl:DatatypeProperty with rdfs:domain atlas:EdgeBase and rdfs:range xsd:string. Its documented meaning: "Marks a relationship type as directional and supplies the label for its reverse direction (e.g. contains -> containedIn). Presence indicates the relationship is asymmetric; absence indicates it is symmetric or has no meaningful direction." It is used once in the shipped libraries, on atlas-common:contains.

atlas:asymmetric is reserved vocabulary. Atlas does not act on it.

Relationships at the instance level: the T-Box here is unchanged#

Relationship reification shipped for the A-Box. The T-Box shape documented above is unaffected: the T-Box already reifies relationships, and the A-Box is the asymmetric layer.

What changed is on the instance side, and it matters if you are writing instance maps: InstanceMapDefinition.Relationships[] is superseded: a relationship instance is now an instance map whose type is a relationship archetype, and atlas:Source / atlas:Target are two reserved reference members. The inline relationships[] form is superseded, and because System.Text.Json ignores the unknown property, sending it returns 201 with zero edges created. See the instance-maps chapter for the current shape.


Functions#

Functions are the fifth kind in a @graph, and they use a different vocabulary: the golden fno:Function subgraph. A function node carries an ordered fno:expects parameter list, a single predicate-backed fno:Output under fno:returns, an fno:Mapping and an atlas:CSharpFunctionImplementation whose C# body sits in atlas:body. Anchor machinery adds atlas:OutputFieldLinksToInputField, atlas:Parameter, atlas:contributesToNormalizationAnchor and atlas:NormalizationAnchor.

The one thing to carry over from this chapter: a function's rdfs:domain is legal. It is the function's library marker, and O-A12 exempts it because a function's @type is fno:Function, not a property kind. If you see rdfs:domain in a real file, check the subject's @type before concluding the file is stale.

Function authoring, the O-F1 to O-F5 synchronous pre-save validators, and the function request bodies are covered in Atlas API - Ontology.


Determinism and Git diffs#

Atlas rewrites the whole file on every save, so determinism is what keeps diffs readable. Three rules, all in OntologyStore.Save and its canonicalisation helpers:

  1. The owl:Ontology declaration is pinned first, then the remaining entries sort ordinal-alphabetically by @id. Ordinal, not culture-aware.
  2. Keys within an entry follow a fixed authoring order, not alphabetical order. The order is @id, @type, @value, @language, @list, @set, rdfs:label, rdfs:comment, rdfs:domain, rdfs:range, rdfs:subClassOf, rdfs:subPropertyOf, owl:onProperty, owl:allValuesFrom, owl:someValuesFrom, owl:intersectionOf, owl:unionOf, atlas:fieldOrder, then the fno: terms. Anything unlisted is appended after those, alphabetically (KeyOrder and KeyRank,). The raw dotNetRDF writer alphabetises; this post-pass undoes that.
  3. Blank nodes are inlined. No _:auto123 identifiers reach the file; every owl:Restriction appears inline in its parent's rdfs:subClassOf array (InlineBlankNodes,).

Plus the value-object collapse noted earlier: xsd:int / xsd:integer literals such as atlas:fieldOrder are written as plain JSON numbers.

If you are writing a round-trip test, assert authoring key order, not alphabetical. That is the assertion most likely to be wrong in a test written from the old documentation.

Determinism is why a single property addition produces one clean diff block instead of reordering the file. The persistence format was not changed by the libgit2 read-path work: reads moved to an in-process Git library behind a content-source abstraction, but the write path, the worktree model and OntologyStore.Save are explicitly out of scope . Git is still the source of truth and JSON-LD on disk is still the format.


The compile pipeline#

Compile is:

PUT /api/v1/branches/{branch}/domains/{domain}/world

(.) The legacy POST .../compile was outside the API surface, so Atlas answers it with the single-page-application shell as 200 text/html, so a client following old documentation receives an HTML blob rather than an error it can act on.

GET .../world returns the cached status without recompiling, including isStale and compiledAtSha; it is 404 when the domain has never been compiled.

The pipeline itself is unchanged and every stage is still named this:

JSON-LD files (Git)
        |
dotNetRDF JsonLdParser  ->  OntologyGraph (in-memory RDF graph)
        |
OntologyStore extracts NodeDefinitions, PropertyDefinitions,
CompoundPropertyTypes, RelationshipDefinitions, FunctionDefinitions
        |
WorldBuilder assigns archetypes - each type gets a column per effective property
        |
WorldEmitter generates C# factory code
        |
Atlas compiles the C# into an assembly

What compile returns#

CompileResult:

Field Type Meaning
entityCount int Entity instances in the built World
edgeCount int Edges in the built World
archetypeCount int Distinct archetypes assigned
compiledAt DateTime When the build completed
functionErrors FunctionCompilationError[] Soft function-compilation errors

There is no tripleCount in the compile response. A field by that name survives only on the domain status response, and it does not count triples: it is isLoaded ? (int?)_persistence.Ontology.GetNodeTypes().Count(): null, a count of node types. Despite the name, it is not a triple count. Read it as a node-type count regardless of what the field is called.

Compile aborts, it does not degrade#

Error-severity violations no longer exclude the affected types from the World. Compile throws, returns 422 RFC-9457 with a violations[] array, and builds nothing. Note the difference from a save: a save with outstanding violations returns 2xx and is held in a draft session, while a compile returns 422 and leaves the previous World in place.

The compiled .dll is what queries run against. openCypher executes over the in-memory World reconstructed by IWorldFactory, never against the raw JSON-LD, which is why an ontology or instance-map change is invisible to queries until the domain is recompiled. Atlas does recompile every recorded (branch, domain) pair at startup, so "always recompile after a restart" is not a rule.


Query layer: openCypher, not SPARQL#

The storage is RDF; the query language is not. Atlas exposes no SPARQL endpoint (zero case-insensitive sparql hits across). All queries use a read-only openCypher subset parsed by ANTLR (Antlr4.Runtime.Standard 4.13.1) and compiled to expressions over the compiled World. CREATE, MERGE, DELETE, SET and REMOVE all throw NotSupportedException.

Supported syntax is in the openCypher query reference chapter.

If you need SPARQL-queryable output you would have to re-serialise from the JSON-LD source files with an external RDF tool; Atlas does not do it for you.


Quick reference: API fields to JSON-LD#

Rebuilt. The wire keys are camelCase; the record members are shown here.

AddEntityTypeRequest (resource: entity-types)#

Request field JSON-LD result
Name @id, minted as urn:atlas:ontology:{scope}:{Name}
Label rdfs:label
Comment rdfs:comment
ParentType rdfs:subClassOf entry naming the parent class (else atlas:NodeBase)
Properties One bare owl:Restriction per property in rdfs:subClassOf, atomically

AddEntityTypeRequest is a superset of the older AddNodeRequest; Properties is what makes the multi-attach a single server-side commit.

AddPropertyRequest (resource: properties)#

Request field JSON-LD result
Name @id
Label / Comment rdfs:label / rdfs:comment
IsObjectProperty: false @type: owl:DatatypeProperty
IsObjectProperty: true @type: owl:ObjectProperty
Range rdfs:range, a single URI (XSD type, entity class, or base compound)
AttachToTypes One bare owl:Restriction on each named entity class. Wire key attachToTypes; reads back as attachedTypes
RangeUnion Two or more members: anonymous rdfs:Datatype + owl:unionOf list. Exactly one: collapses to a plain single-URI range
FieldTypeOverrides Anonymous owl:Class + owl:intersectionOf pin on the compound range. Mutually exclusive with RangeUnion
SubPropertyOf rdfs:subPropertyOf
DefaultConfig Default property configuration (strategy vocabulary; see the instance-maps chapter)

Nothing in this table writes rdfs:domain.

AddDataTypeRequest (resource: data-types)#

Request field JSON-LD result
Name @id of an owl:Class with rdfs:subClassOf [atlas:NodeBase, atlas:CompoundPropertyType]
Label / Comment rdfs:label / rdfs:comment
Fields One HasFieldBase edge class per entry, with Source = the compound and Target = the linked property. The array index is the atlas:fieldOrder
TimeAxisField atlas:isTimeAxis on that field's HasFieldBase edge. Must resolve to one of Fields and be xsd:dateTime-ranged; at most one

Fields entries are existing first-class properties, referenced by local name, CURIE or full URI. Selection only, never inline. UpdateDataTypeApiRequest replaces the whole roster, so adds, removes and reorders land in one commit.

AddRelationshipRequest (resource: relationship-types)#

Request field JSON-LD result
Name @id of an owl:Class
BaseType rdfs:subClassOf atlas:ContainsBase or atlas:RelationshipBase
Label rdfs:label
Comment rdfs:comment
SourceNodeType owl:Restriction on atlas:Source with owl:allValuesFrom. Omitted on a root-based relationship: defaults to atlas:NodeBase, never an absent block
TargetNodeType owl:Restriction on atlas:Target with owl:allValuesFrom. Same defaulting
Properties One bare owl:Restriction per edge property. No allValuesFrom

On UpdateRelationshipApiRequest, an endpoint is three-state: omitted leaves it unchanged, "" clears it back to "any node", and a name or URI replaces it. Properties is a full replacement: null leaves the existing set alone, [] clears it.


  • Timebase Atlas Key Concepts - the authoritative definitions of domain, library, branch, entity type, data type, World, compile, draft session and binding path.
  • Atlas API - Ontology - routes, request and response bodies, verbs, status codes, and the full validation-rule surface for all five ontology resources.
  • Atlas API Overview - base address, authentication, the RFC-9457 error envelope, pagination.
  • Atlas Developer Overview - which of the eight Atlas surfaces you should be building against.
esc
↑↓ navigate · ↵ open · ⌘K anywhere