JSON-LD and the Atlas Data Model
Atlas stores ontologies as JSON-LD files in a Git-backed repository.
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 underscripts/, and it is not what the runtime loads. It predates several vocabulary additions, carries the earlier function aliases, and includes anisa95-common.jsonldthat Atlas does not ship. Only the copy in your ownlibraries/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-coreis always imported first, and imports are de-duplicated, so selectingatlas-coreexplicitly does not write it twice.- From a domain file the reference is
../../libraries/{name}.jsonld. From a library file, which already sits inlibraries/, 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
@typeisfno:Function, not a property kind, so the kind gate never fires. Functions legitimately keeprdfs:domainas 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:Parameterandatlas:contributesToNormalizationAnchorall carryrdfs:domaininatlas-core.jsonldas 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:integeris recognised and maps toint, the same asxsd:int. This matters becauseatlas-common:Qualityisxsd:integer.xsd:dateTimeis 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 toDateTimefor consumers.- An unrecognised URI falls back to
stringin the column view but returnsnullin 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:
isTimeAxisappears as the bare alias with a string value.atlas-core's@contextdeclares"isTimeAxis": {"@id": "atlas:isTimeAxis", "@type": "xsd:boolean"}, so"true"in the file is a typed boolean literal, not a string. The reader parses it withbool.TryParse.atlas:fieldOrderis written as a plain JSON number, not as a{"@type","@value"}value object. The serialiser collapsesxsd:int/xsd:integervalue objects back to plain numbers to match hand-authored files.fieldOrderis a sort key only, and is not persisted on the field record. The read model ispublic sealed record CompoundField(string Name, string TypeUri, bool IsTimeAxis = false). The store readsatlas:fieldOrderoff theHasFieldBaseedge, uses it to order the list, then discards it. Fields with nofieldOrdersort after all ordered fields.IsTimeAxis, by contrast, is a member ofCompoundField. 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:ContainsBaseoratlas:RelationshipBase.Parent: the immediate user-defined super-relationship-type, ornull.
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:
- The
owl:Ontologydeclaration is pinned first, then the remaining entries sort ordinal-alphabetically by@id. Ordinal, not culture-aware. - 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 thefno:terms. Anything unlisted is appended after those, alphabetically (KeyOrderandKeyRank,). The raw dotNetRDF writer alphabetises; this post-pass undoes that. - Blank nodes are inlined. No
_:auto123identifiers reach the file; everyowl:Restrictionappears inline in its parent'srdfs:subClassOfarray (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.
Related articles#
- 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.