Docs/Developer/Atlas API - Ontology (Entity Types, Properties, Data Types, Enumeration Types, Relationship Types, Functions)
DeveloperAPI

Atlas API - Ontology (Entity Types, Properties, Data Types, Enumeration Types, Relationship Types, Functions)

The Ontology API reads and writes the type definitions that make up an Atlas model.

Updated 5 Nov 2026Timebase Docs

Overview#

The Ontology API reads and writes the type definitions that make up an Atlas model. There are six ontology resources, and each one is mounted twice: once under a domain and once under a library.

Resource Route segment What it defines
Entity types entity-types The kinds of thing in your model (Pump, Tank, ProductionLine)
Properties properties Standalone value definitions, attached to the types that carry them
Data types data-types Compound value shapes: several named fields that travel together, such as TVQ
Enumeration types enum-types Closed lists of codes with display names, such as an asset status or a criticality level
Relationship types relationship-types Named, directed connections, with their own properties
Functions functions Reusable calculation logic

This article is written for developers building an integration against Atlas. It documents the request bodies, response bodies, verbs and status codes of all 64 ontology endpoints.

Read Atlas API Overview first. That article is the single source of truth for the base address, authentication, the RFC-9457 error envelope, pagination and the /api/v2 surface. This article does not restate any of it and links back wherever it matters. For the meaning of the words used here (domain, library, branch, ontology, compile, draft session, binding path), see Timebase Atlas Key Concepts.

The three corrections most likely to break an existing client#

  1. nodes is now entity-types and relationships is now relationship-types. Both renames were part of the /api/v1 cutover. A request to the old segment does not return 404: the single-page-application fallback answers it with 200 text/html. See the Overview article's Troubleshooting table.
  2. Update is PATCH for entity types, properties and relationship types. PUT survives only for data types and functions. Sending the wrong verb also gives you the HTML shell, not a 405.
  3. Data types are a full ontology resource. If your integration models anything historian-bound, you need them.

Scopes: every route is mounted twice#

Each of the five resources has an identical domain group and library group. The request bodies, response bodies and status codes are the same; only the scope segment differs.

/api/v1/branches/{branch}/domains/{domain}/{resource}
/api/v1/branches/{branch}/libraries/{library}/{resource}

{branch} is always required and is a Git branch name, typically main. {domain} and {library} are the lowercased, hyphenated internal names, not display labels.

Every example in this article uses the domain scope. To use the library scope, swap domains/{domain} for libraries/{library} and change nothing else.

Note

Libraries hold types only. They have no instances, no compiled World and no compile step, so an ontology write to a library is complete once it commits. A domain still needs PUT /api/v1/branches/{branch}/domains/{domain}/world before the change is queryable.

Authentication#

/api/v1 requires a Pulse-issued bearer token whose aud claim includes Atlas (Authentication is required on all ten ontology route groups). A browser session cookie is not accepted on this plane. Every example below sends the header; do not omit it.

Authorization: Bearer <your-pulse-token>
Note

Git commit authorship comes from the authenticated caller's sub and name claims, defaulting to Anonymous when Pulse:Auth:Enabled is false.

Serialization#

Responses use camelCase property names, omit null members entirely rather than emitting null, and serialize enums as camelCase strings, so NodeKind.Entity arrives as "entity". A strongly typed client that expects every declared member to be present will see gaps. The response samples in this article show the fields as they appear when populated.

201 Created responses carry the created resource in the body. Ontology creates do not set a Location header (only domain, library and branch creation do), so do not build a client that follows it.

Naming: one URN namespace for all six kinds#

Every entity type, property, data type, enumeration type, relationship type and function is identified by a URN with exactly one shape:

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

{scope} is the domain or library name. {LocalName} is the name you supplied on create. A Plant entity type in the lonestar-plant scope is:

urn:atlas:ontology:lonestar-plant:Plant

There are no per-kind prefixes. urn:atlas:class:, urn:atlas:prop:, urn:atlas:rel: and urn:atlas:fn: do not exist and never did on this surface. A Cypher query or binding path built on one matches nothing. In the shipped libraries every @id resolves to urn:atlas:ontology:..., for example urn:atlas:ontology:atlas-common:TVQ.

Two adjacent namespaces you will also encounter in responses:

  • urn:atlas:vocab:... for the Atlas framework vocabulary, such as urn:atlas:vocab:NodeBase, urn:atlas:vocab:ContainsBase and urn:atlas:vocab:RelationshipBase.
  • urn:atlas:instance:... for individual instances in the compiled World.

{name} in a route is the local name#

Every single-item route addresses the resource by its local name, not its URI. GET .../entity-types/Pump, never GET .../entity-types/urn:atlas:ontology:brewery:Pump. The server mints the URI from the scope and the name.

A name collision across kinds is rejected#

Because all six kinds mint from the same namespace, a name is unique across the whole scope, not just within its own kind. Creating a property called Tank in a scope that already has an entity type called Tank fails with 409 Conflict:

{
  "type": "https://atlas.timebase.io/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Cannot create Property 'Tank': the key 'urn:atlas:ontology:brewery:Tank' is already used by a Type. All element kinds (Types, Properties, Relationships, Data Types, Functions) share one URN namespace per domain."
}

This is a different failure from a same-kind duplicate, which reports Property 'Tank' already exists. The cross-kind case cannot be fixed by switching to an update: pick a different local name. The occupying kind is named in the detail string as one of Type, Property, Relationship, Data Type, Enumeration Type or Function. The list of kinds at the end of the message does not mention enumeration types, but they share the namespace too.

What a successful write actually means#

This is the single most consequential behaviour on the ontology surface, and it changed. Read it before you write anything.

A save that fails validation still returns 2xx. When an ontology write leaves the scope with outstanding error-severity violations, the content is committed to the caller's private draft session and the target branch does not advance. You still receive 201 Created or 200 OK with the resource in the body. Nobody else sees the change.

A valid save commits straight through. It runs in a throwaway worktree, fast-forwards the target branch and leaves nothing behind. There is no session to publish and nothing else to do.

So a draft session is not a mode you opt into. Having one means "I have unpublished work that does not validate".

The only signal is a response header#

Atlas-Validation-Violations: 3

The header carries the count of outstanding error-severity violations in the state the write just persisted. It is absent on a clean save, not 0. Check for its presence on every write.

The header carries only a count. To get the actionable list, read the violations resource:

GET /api/v1/branches/{branch}/domains/{domain}/validation
GET /api/v1/branches/{branch}/libraries/{library}/validation

To promote your draft onto the branch:

POST /api/v1/branches/{branch}/draft/publish

Rule IDs in the violations you get back are O-A* and O-B* for ontology problems and I-A* and I-B* for instance-map problems. See the Validation Rules Reference for what each one means and how to clear it.

Important

Function creates and updates are the one exception to "saves return 2xx". They run a synchronous pre-save validator and reject with 422 before anything is persisted. See Function validation.

rdfs:domain on a property is now a validation error#

If you migrate hand-written JSON-LD into Atlas, note that property-to-type attachment moved onto the entity class as an owl:Restriction. An rdfs:domain on a property definition is reported as O-A12. Atlas framework vocabulary under urn:atlas:vocab: and the atlas: CURIE are exempt; your own subjects never are.

Compile is a separate action with a different failure shape#

PUT /api/v1/branches/{branch}/domains/{domain}/world returns 422 with a violations[] array and builds no World when validation fails. Do not conflate the two: saves return 2xx and route to a draft; compile returns 422 and aborts. Full detail is in the Overview article.

Note

when the caller is draft-routed, the compile builds your own copy and leaves the shared world in place. Combined with a draft-routed save, a write and a compile can both succeed while the branch is unchanged. Check Atlas-Validation-Violations, then GET /api/v1/branches/{branch}/draft, and publish.


Entity types#

An entity type is a kind of thing. It may inherit from a parent entity type and it carries a set of attached properties.

Routes#

Operation Method and path Success
List GET .../entity-types 200
Get one GET .../entity-types/{name} 200
Create POST .../entity-types 201
Update PATCH .../entity-types/{name} 200
Delete DELETE .../entity-types/{name} 204
Attach a property PUT .../entity-types/{name}/properties/{property} 204
Detach a property DELETE .../entity-types/{name}/properties/{property} 204

Declared (domain) and (library).

List all entity types#

GET /api/v1/branches/main/domains/brewery/entity-types
Authorization: Bearer <your-pulse-token>

Response - 200 OK

[
  {
    "uri": "urn:atlas:ontology:brewery:Pump",
    "label": "Pump",
    "comment": "Centrifugal pump",
    "parentType": "urn:atlas:ontology:isa95-common:Equipment",
    "kind": "entity",
    "properties": [
      {
        "uri": "urn:atlas:ontology:brewery:Speed",
        "label": "Speed",
        "attachedTypes": ["urn:atlas:ontology:brewery:Pump"],
        "range": "http://www.w3.org/2001/XMLSchema#double",
        "isObjectProperty": false
      }
    ]
  }
]
Read field Type Notes
uri string Full URN. Minted from scope plus local name
label string Display name. Omitted when null
comment string Description. Omitted when null
parentType string Full URI of the rdfs:subClassOf parent. Omitted when the type has no parent
properties array The full property read models the type carries, declared and inherited
sourceLibrary string Short name of the imported library that declared this type. Omitted for types declared in the scope you are reading
kind string "entity" or "dataType". A camelCase enum string, never 0 or 1
propertyPins object[] The effective pins of the type on Defer properties it uses, its own and inherited. Each entry has propertyUri, declaredOnUri, fieldTypeOverrides and, when an enumeration is bound, fieldEnumOverrides. Empty when the type pins nothing. See Pin a property's leaf on a type
timeAxisField string Set only when kind is "dataType". The bare local name of the compound's time-axis field
Note

The entity-types list is a read over all classes, so compound data types appear here too, discriminated by kind: "dataType". Filter on kind if you want entities only. Data types are authored through the data-types resource, never through this one.

Get a single entity type#

GET /api/v1/branches/main/domains/brewery/entity-types/Pump
Authorization: Bearer <your-pulse-token>

{name} is the local name. Returns 200 with a single object in the shape above, or 404 when no entity type of that name exists in the scope.

Create an entity type#

POST /api/v1/branches/main/domains/brewery/entity-types
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "Compressor",
  "label": "Gas Compressor",
  "comment": "Reciprocating gas compressor",
  "parentType": "Equipment",
  "properties": ["Speed", "Description"]
}
Field Type Required Description
name string Yes Local name, PascalCase. Must be unique across all five kinds in this scope
label string No Display name
comment string No Description
parentType string No Local name or full URI of the parent type. May reference a type from an imported library
properties string[] No Local names or full URIs of existing properties to attach in the same atomic commit
propertyPins object No Pins on the open leaves of Defer properties the new type attaches or inherits, { "Running": { "Value": "xsd:boolean" } }. See Pin a property's leaf on a type
propertyEnumPins object No Enumeration types bound to pinned leaves, same shape. API only

Response - 201 Created, body is the created entity type read model.

properties is the atomic create-plus-attach path (AddEntityTypeRequest). Every entry is resolved before anything is written, so one unresolvable reference fails the whole create and the type is never half-created. That failure is 400 Bad Request with the detail Cannot attach property '<ref>' - no such property in scope..

parentType resolution searches the current scope first, then the Atlas framework vocabulary roots, then every loaded entity by local-name suffix. If nothing matches, it falls back to a scope-local URI, and whole-scope validation reports it.

Update an entity type#

PATCH /api/v1/branches/main/domains/brewery/entity-types/Compressor
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "label": "Reciprocating Compressor",
  "properties": ["Speed", "Description", "UOM"]
}

PATCH is a partial merge. Fields you do not send are preserved.

Field Type Semantics
label string null or omitted leaves it unchanged
comment string null or omitted leaves it unchanged
parentType string null or omitted leaves it unchanged
properties string[] Full replacement of the type's declared attachments. null or omitted leaves them untouched; [] detaches every declared attachment
propertyPins object Full replacement of the type's declared pins. null or omitted leaves them untouched; an object, even {}, replaces them. See Pin a property's leaf on a type
propertyEnumPins object Enumeration types bound to pinned leaves, same shape and the same replacement rule. API only

Response - 200 OK, body is the updated read model. 404 when the name does not exist.

The properties replacement is all-or-nothing and lands as one commit with the metadata edit, so a metadata change never persists alone when an attachment reference is bad. Inherited attachments, declared on an ancestor type, are never affected by this list.

Defer properties and rule O-B23#

A property is Defer when its type is not yet fixed: its own range is a union of scalars, or its data type has a union-typed field, such as TVQ.Value (xsd:double, xsd:string or xsd:boolean), that fieldTypeOverrides has not pinned. Each such place is an unresolved leaf, and the read model reports them in openLeaves.

A Defer property can be attached to an entity type or a relationship type, and nothing refuses the write. Validation rule O-B23, an Error that is not fatal, reports every concrete type that uses a property with a leaf that is still open after the type's own pins and its ancestors' pins, for example Type 'Pump' uses property 'FlowRate' with an unresolved type (Value is one of double, string, boolean), so 'Pump' cannot compile. Pin it on 'Pump' or on 'FlowRate'. Abstract types are skipped. The domain still loads and every save still lands, but compile refuses until every leaf used by a type is pinned. A property that comes from a library adds a clause to the message, for example 'FlowRate' comes from library 'isa95-common'; pin it on 'Pump' here, or in 'isa95-common'. See the Validation Rules Reference.

There are two places to pin a leaf:

  • On the property, with fieldTypeOverrides, for example {"Value": "xsd:double"}. Every type that uses the property gets that type. Use it when the shape is the same everywhere.
  • On the type, with propertyPins, described next. Only that type and its subtypes get the type. Use it when the shape depends on the type.

Pin a property's leaf on a type#

propertyPins pins an open leaf of a Defer property that a type uses. The type can attach the property itself or inherit it from an ancestor. The same two fields exist on the four requests that write a type: create and update of an entity type, and create and update of a relationship type.

PATCH /api/v1/branches/main/domains/plant/entity-types/Pump
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "propertyPins": {
    "Running": { "Value": "xsd:boolean" },
    "Mode": { "": "xsd:string" }
  }
}
Field Type Description
propertyPins object Property reference to a map of leaf path to XSD type. The property reference is a local name or a full URI. The path is the dotted field path of the open leaf, such as Value or Data.Value, and is "" for a property whose own range is the union. The type is an XSD CURIE such as xsd:boolean, or a full XSD URI
propertyEnumPins object The same shape, but each value is the name or URI of an enumeration type to bind to the pinned leaf. Every path needs a matching entry in propertyPins, and the pinned type must equal the enumeration's key type. API only: the Atlas UI and Gage do not expose it

Where the fields exist:

Request Create Update
Entity type POST .../entity-types (AddEntityTypeRequest) PATCH .../entity-types/{name} (UpdateNodeApiRequest)
Relationship type POST .../relationship-types (AddRelationshipRequest) PATCH .../relationship-types/{name} (UpdateRelationshipApiRequest)

On update, an omitted or null propertyPins keeps the type's declared pins. An object, even {}, is a full replacement of the type's declared pins, so send every pin you want to keep. Inherited pins are never affected. The pins land in the same commit as the rest of the write.

A pin follows three rules, and a write that breaks one is refused before anything is saved:

  • A property pin is final. A type cannot pin a leaf that the property already fixed in fieldTypeOverrides.
  • A subtype only pins a leaf its ancestors left open. A leaf that an ancestor type pinned cannot be pinned again further down.
  • The pinned type must be a member of the leaf's union. Value on a TVQ accepts xsd:double, xsd:string or xsd:boolean.

The refusals are 400 invalid-request, and each carries an error key. For a relationship type the key starts with Relationship. instead of EntityType., and the message opens with Relationship instead of Type.

Error key Raised when Detail
EntityType.PropertyNotFound A key in propertyPins or propertyEnumPins names a property that does not exist in scope Type 'Pump' pins property 'Runing', which does not exist in scope.
EntityType.PinOnUnusedProperty The type neither attaches the property nor inherits it Type 'Pump' pins property 'Running', which it does not use. Attach 'Running' to 'Pump' or an ancestor first.
EntityType.PinPathUnknown The path is not a field of the property's data type, the path is "" but the property is not a union, or one property mixes "" with field paths Type 'Pump' pins 'FlowRate.Valu', which is not a field of 'TVQ'.
EntityType.PinNotAMember The pinned type is not one of the leaf's members Type 'Pump' pins 'Running.Value' to 'xsd:integer', which is not one of double, string, boolean.
EntityType.PinAlreadyFixed The property or an ancestor type already fixed the leaf 'Running.Value' is already fixed to 'xsd:double' by property 'Running'. A property pin is final. or 'Running.Value' is already fixed to 'xsd:double' by ancestor type 'Equipment'. A subtype only pins a leaf its ancestors left open.
EntityType.PinEnumUnknown propertyEnumPins names an enumeration type that does not exist Type 'Pump' binds 'Mode' to 'Modes', which is not a known enumeration type. Define the enumeration type first.
EntityType.PinEnumWithoutType A path in propertyEnumPins has no entry in propertyPins Type 'Pump' binds 'Mode' to enumeration type 'RunModes' without pinning that leaf's type. Pin 'Mode' to 'xsd:string', the enumeration's key type.
EntityType.PinEnumKeyMismatch The pinned type differs from the enumeration's key type Type 'Pump' pins 'Mode' to 'xsd:integer' but binds it to enumeration type 'RunModes', whose key type is 'xsd:string'. The pinned type and the key type must agree.

The same cases, when found in an existing file, are reported by validation rule O-B24.

Reading pins. The entity type and relationship type read models carry propertyPins, the effective pins: the type's own plus the ones it inherits.

"propertyPins": [
  {
    "propertyUri": "urn:atlas:ontology:plant:Running",
    "declaredOnUri": "urn:atlas:ontology:plant:Pump",
    "fieldTypeOverrides": { "Value": "http://www.w3.org/2001/XMLSchema#boolean" }
  }
]

declaredOnUri names the type that wrote the pin, so a pin inherited from Equipment reads declaredOnUri of Equipment. fieldEnumOverrides, a path to enumeration URI map, is present only when an enumeration is bound.

A property can resolve to a different leaf type on different types, even in one compiled World. The compiled columns are typed for each type. A Cypher query that reads such a property across types is held to a rule: see the openCypher Query Reference.

Delete an entity type#

DELETE /api/v1/branches/main/domains/brewery/entity-types/Compressor
Authorization: Bearer <your-pulse-token>

Response - 204 No Content. 404 when the name does not exist.

Entity types carry no per-item dependents guard, so check for references before you delete one. Unlike properties, data types and functions, a delete that leaves a dangling reference is not blocked with a 409. It is caught by whole-scope re-validation instead, which aborts the write. Practically that means the delete either succeeds cleanly or routes to your draft session with an Atlas-Validation-Violations header.

Attach and detach a property#

This is the primary day-to-day ontology edit and it is a sub-resource of the entity type, not of the property.

PUT    /api/v1/branches/main/domains/brewery/entity-types/Pump/properties/Speed
DELETE /api/v1/branches/main/domains/brewery/entity-types/Pump/properties/Speed
Authorization: Bearer <your-pulse-token>

Both return 204 No Content with no body. Neither takes a request body.

Behaviour Attach (PUT) Detach (DELETE)
Already in the target state Idempotent no-op success 404, the property is not among the type's declared attachments
Unknown entity type 404 404
Unknown property 404 404
Entity type declared by an imported library 400 400

The write subject is the entity class, which is what makes cross-scope attachment work: a property imported from a library attaches to a local type by construction, and the change persists only in the local type's file. The reverse is not allowed. Attaching onto a type that an import declared would be a phantom write, validated and read back but dropped by the scope-filtered save, so it is rejected with 400 and the detail Entity type '<name>' is declared by an import ('<uri>') and cannot be modified from this scope. Attach properties to it from its declaring library instead.

The attach route accepts a Defer property. See Defer properties and rule O-B23.

Detach is surgical: it retracts exactly one restriction and only from the type's declared attachments. An attachment inherited from an ancestor cannot be detached from a subtype.

Note

attachment is addressed under the entity type. If your client called GET, PUT or DELETE on .../properties/{name}/attached-types/..., move it to the route above. See the Overview article's Troubleshooting table.


Properties#

A property is a standalone value definition. It is not owned by a type. The attachment link lives on the type, and the same property can be attached to any number of types.

Routes#

Operation Method and path Success
List GET .../properties 200
Get one GET .../properties/{name} 200
Create POST .../properties 201
Update PATCH .../properties/{name} 200
Delete DELETE .../properties/{name} 204, or 409 when blocked

Declared (domain) and (library).

List all properties#

GET /api/v1/branches/main/domains/brewery/properties
Authorization: Bearer <your-pulse-token>

Response - 200 OK

[
  {
    "uri": "urn:atlas:ontology:brewery:Speed",
    "label": "Speed",
    "comment": "Shaft speed in RPM",
    "attachedTypes": [
      "urn:atlas:ontology:brewery:Pump",
      "urn:atlas:ontology:brewery:Compressor"
    ],
    "range": "http://www.w3.org/2001/XMLSchema#double",
    "isObjectProperty": false
  }
]
Read field Type Notes
uri string Full URN
label, comment string Omitted when null
attachedTypes string[] Full URIs of the types this property is declared on, not inherited attachments. Unioned with any compound data types that link it as a field
range string The value type URI. Omitted when the property carries a compound or union range instead
isObjectProperty boolean true when the property's range is a data type
defaultConfig object The class-level default sourcing strategy, in the same flat shape you post. Omitted when none is set
enumType string Full URI of the enumeration type the value is bound to. Omitted when none
compoundRange object Present when the range is a compound with pinned field types. Carries fieldEnumOverrides when a pinned field is bound to an enumeration type
subPropertyOf string Full URI of the first asserted rdfs:subPropertyOf. Omitted when none
unionMembers string[] Read-only projection of an owl:unionOf range. Omitted for a single-URI range
rangeUnion string[] The write-side union carrier
openLeaves object[] The places where the property's type is still open, which make it Defer. Each entry has a path and the members it could be, for example [{"path": "Value", "members": ["xsd:double", "xsd:string", "xsd:boolean"]}]. path is "" when the property's own range is the union, otherwise the dotted field path (Value, Data.Value). null when the type is fixed
sourceLibrary string Short name of the imported library that declared it. Omitted for locally declared properties
Note

the field is attachedTypes and it is an array. There is no domain field on a property; in Atlas the word means the workspace. A client reading body.domain gets undefined, and the singular string it expected could never represent a property attached to several types anyway.

An empty attachedTypes is legal. A property attached to nothing stays available until you attach it, and is not reached by any type read.

Get a single property#

GET /api/v1/branches/main/domains/brewery/properties/Speed
Authorization: Bearer <your-pulse-token>

200 with a single object, or 404.

Create a property#

POST /api/v1/branches/main/domains/brewery/properties
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "TargetSpeed",
  "label": "Target Speed",
  "comment": "Setpoint speed in RPM",
  "attachToTypes": ["Pump"],
  "range": "xsd:double",
  "isObjectProperty": false
}
Field Type Required Description
name string Yes Local name. Must be unique across all five kinds in this scope
label string No Display name
comment string No Description
attachToTypes string[] Yes Local names or full URIs of the entity types to attach this property to, in the same commit. One class-side owl:Restriction per entry. Send [] to create the property unattached
range string No The value type. An XSD CURIE (xsd:double, xsd:string, xsd:boolean, xsd:dateTime, xsd:integer), or a data type local name such as TVQ. An entity (or enumeration) type is refused with Value type '<name>' is an entity type. A property's value type is a primitive or a data type; to source a value's fields independently, make it a data type and use the Composed strategy.
isObjectProperty boolean No Defaults to false
defaultConfig object No A class-level default sourcing strategy for this property. StaticStrategy, BoundStrategy or CalculatedStrategy; ReferenceStrategy is rejected. See Property Sourcing Strategies
fieldTypeOverrides object No Pins individual fields of a compound range to an XSD CURIE, for example {"Value": "xsd:double"}. When set, range is the base compound local name. Mutually exclusive with rangeUnion. Pin every union-typed field (Data.Value for a Tag) so that types using the property can compile
subPropertyOf string No Local name of the rdfs:subPropertyOf base property. Organizational metadata only
rangeUnion string[] No XSD CURIE members of an authored value-type union. Two or more members produce an owl:unionOf range; exactly one collapses to a plain single-URI range
enumType string No Binds the value to an enumeration type's list of codes. The local name, CURIE or full URI of an existing enumeration type. range stays a plain scalar and must equal the enumeration's key type. For a scalar property only. See Bind a property to an enumeration type
fieldEnumOverrides object No The data-type form of enumType. Keyed by the same field path as fieldTypeOverrides ("Value", "Data.Value") and valued with an enumeration type. Every key must also be pinned in fieldTypeOverrides, to the enumeration's key type. Cannot be combined with enumType

Response - 201 Created, body is the created property read model.

Note the asymmetry that catches people out: you send attachToTypes and you read back attachedTypes.

range values carrying a reserved CURIE prefix (xsd:, rdf:, rdfs:, owl:) pass through unscoped and are expanded to a full URI downstream. Anything else is resolved against the loaded ontology.

Bind a property to an enumeration type#

Use enumType when a property holds one of a fixed set of codes, such as a status or a criticality. Create the enumeration type first, then give the property a plain scalar range equal to the enumeration's key type and name the enumeration in enumType:

POST /api/v1/branches/main/libraries/plant-assets/properties
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "Criticality",
  "label": "Criticality",
  "attachToTypes": ["Asset"],
  "range": "xsd:string",
  "enumType": "CriticalityLevel"
}

What the binding does, and what it does not:

  • The stored value is the member's key, unchanged. Instance maps, the value hub, instance reads and queries all carry the key ("A", "3", "true"). Nothing on the value path translates it.
  • The Atlas UI shows the member's label in place of the key, falling back to the member's name when no label was written. A key that is not in the list is shown as the raw key, never as an error.
  • Atlas does not reject a value outside the list. An instance map value is checked against the property's range (rule I-B4), not against the enumeration's members. A code nobody has modelled yet is treated as real data. If your integration needs a closed list, check membership before you write.
  • Bridge type descriptions carry the binding, so a downstream system receives the enumeration with the column it types. See Exporting Data with Bridge.

The checks, and how each one fails:

Problem Result
range does not equal the enumeration's key type, or range is omitted The write returns 2xx, goes to your draft session and reports O-B16. See What a successful write actually means
enumType names something that is not an enumeration type, or nothing at all The same, reporting O-B15
A fieldEnumOverrides key is not pinned in fieldTypeOverrides 400 with the error key Property.InvalidEnumBinding
range itself names an enumeration type 400. An enumeration type is never a value type. Use range plus enumType

For a property whose value type is still open (a Defer property), the entity type that uses it can bind an enumeration to the leaf it pins, with propertyEnumPins. See Pin a property's leaf on a type.

Update a property#

PATCH /api/v1/branches/main/domains/brewery/properties/TargetSpeed
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "label": "Speed Setpoint",
  "attachToTypes": ["Pump", "Compressor"]
}

PATCH is a partial merge; unsent fields are preserved. Every field is independently nullable.

Field Semantics
label, comment null or omitted leaves unchanged
attachToTypes Full replacement of the declared attachment set. null leaves it untouched; [] detaches every declared attachment
range, isObjectProperty null or omitted leaves unchanged
fieldTypeOverrides null or empty leaves the existing range untouched, plain or compound
subPropertyOf null leaves unchanged; "" clears it
rangeUnion null leaves the existing range untouched
enumType Three-state. null or omitted leaves the binding unchanged; "" removes it; a name or URI binds or rebinds. Sending a new range without enumType also removes the binding, because a code list keyed to the old value type no longer fits
fieldEnumOverrides Send it together with fieldTypeOverrides. When fieldTypeOverrides is null, the existing range and its field bindings are left untouched
defaultConfig null or omitted leaves the default unchanged; an object replaces it
clearDefaultConfig true removes the default, whatever defaultConfig says

Response - 200 OK, or 404 when the name does not exist. Updating a library's property by name from a domain is refused with 400 and Property.NotWritableInScope; it was a plain 404 before.

Delete a property#

DELETE /api/v1/branches/main/domains/brewery/properties/TargetSpeed
Authorization: Bearer <your-pulse-token>

Response - 204 No Content when nothing references it.

409 Conflict when a type still declares it, or a data type links it as a field. The body is an RFC-9457 problem with target and dependents extensions:

{
  "type": "https://atlas.timebase.io/errors/delete-blocked",
  "title": "Delete Blocked",
  "status": 409,
  "detail": "Cannot delete 'TargetSpeed' — it is declared on 2 types. Detach from those types first.",
  "target": { "name": "TargetSpeed", "kind": "property" },
  "dependents": [
    { "id": "Pump", "name": "Pump", "kind": "entity", "referenceType": "type-declares-property" },
    { "id": "Compressor", "name": "Compressor", "kind": "entity", "referenceType": "type-declares-property" }
  ]
}

The dependents list is never truncated. Note that the dependent kind is renamed from type to entity on this surface, to match the entity-types naming. Clear the block by detaching from each listed type first, using DELETE .../entity-types/{type}/properties/{name}.


Data types#

A data type is a compound: one property value made of several named fields that always travel together. This is the fifth ontology resource, a peer of the other four, and it shares their URN namespace.

The canonical example ships in the atlas-common library as TVQ, for Timestamp-Value-Quality, and it is how nearly every measurement in Atlas is shaped.

Routes#

Operation Method and path Success
List GET .../data-types 200
Get one GET .../data-types/{name} 200
Create POST .../data-types 201
Update PUT .../data-types/{name} 200
Delete DELETE .../data-types/{name} 204, or 409 when blocked

Declared (domain) and (library).

The library scope gained PUT and DELETE in this cutover. ### List all data types

GET /api/v1/branches/main/domains/brewery/data-types
Authorization: Bearer <your-pulse-token>

Response - 200 OK

[
  {
    "uri": "urn:atlas:ontology:atlas-common:TVQ",
    "sourceLibrary": "atlas-common",
    "fields": [
      { "name": "Timestamp", "typeUri": "urn:atlas:ontology:atlas-common:Timestamp", "isTimeAxis": true },
      { "name": "Value",     "typeUri": "urn:atlas:ontology:atlas-common:Value",     "isTimeAxis": false },
      { "name": "Quality",   "typeUri": "urn:atlas:ontology:atlas-common:Quality",   "isTimeAxis": false }
    ]
  }
]
Read field Type Notes
uri string Full URN
fields array Already sorted by the persisted atlas:fieldOrder, so the array order is the author's intended field order
fields[].name string Bare local name of the linked property
fields[].typeUri string Full URI of the property that supplies this field
fields[].isTimeAxis boolean true on at most one field
sourceLibrary string Short name of the imported library that declared it. Omitted for locally declared data types
Important

The data-type read model carries only uri, fields and sourceLibrary. It does not return label or comment, even though the create and update bodies accept both. Do not build a client that round-trips a data type by reading it and sending it back: the label and comment are cleared. Read the label from the entity-types list instead, where the same subject appears with kind: "dataType" and carries label, comment and timeAxisField.

Get a single data type#

GET /api/v1/branches/main/domains/brewery/data-types/TVQ
Authorization: Bearer <your-pulse-token>

200 with a single object, or 404.

Create a data type#

POST /api/v1/branches/main/domains/brewery/data-types
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "TVQR",
  "label": "TVQ with Rate",
  "comment": "Measurement tuple carrying a derived rate",
  "fields": ["Timestamp", "Value", "Quality", "Rate"],
  "timeAxisField": "Timestamp"
}
Field Type Required Description
name string Yes Local name. Must be unique across all five kinds in this scope
label string No Display name. Persisted but not returned by the data-type read
comment string No Description. Persisted but not returned by the data-type read
fields string[] Yes Local names or full URIs of existing first-class properties to link as fields, in field order. The array index is the fieldOrder. Selection only, never inline definitions
timeAxisField string No Which entry of fields is the time axis. Must resolve to one of fields and must be a dateTime-ranged property. At most one per data type. null means no axis

Response - 201 Created, body is the created data-type read model.

Three rules bite here:

  • Fields are selected, not defined. Each entry must already exist as a property in the scope or in an imported library. The write mints only the field-membership edge; the linked property already carries its own range.
  • No field may resolve to a property declared by an import when the compound itself is local, because the scope-filtered save would drop that back-pointer. This is rejected rather than allowed to become a phantom write.
  • The array is a complete roster. There is no per-field add or remove operation.

An unresolvable timeAxisField fails the write with DataType.UnknownTimeAxisField.

Note

The time axis is what tells Atlas which field means "when this happened". Time-range queries and point normalization both depend on it. A compound with no time axis still compiles but cannot participate in either.

Update a data type#

Update is PUT, not PATCH. The backend retracts the prior compound and re-emits the class plus every field edge in one commit, so relabelling, adding, removing and reordering fields all land together.

PUT /api/v1/branches/main/domains/brewery/data-types/TVQR
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "label": "TVQ with Rate",
  "comment": "Measurement tuple carrying a derived rate",
  "fields": ["Timestamp", "Value", "Quality", "Rate", "Confidence"],
  "timeAxisField": "Timestamp"
}
Field Type Semantics
label, comment string Sent value replaces the stored value. Omitting them clears them
fields string[] Full replacement of the roster, in field order. An empty list clears every field
timeAxisField string Full replacement. null or omitted clears any previously declared axis

Response - 200 OK, body is the updated read model. 404 when the name does not exist.

Important

PUT here is a genuine replacement, not a merge. Because the read model does not return label, comment or timeAxisField, and because omitting them clears them, a read-modify-write needs all three fields sent back. Always send the complete intended body. The URI key itself is immutable: to rename a data type, create a new one and delete the old.

Delete a data type#

DELETE /api/v1/branches/main/domains/brewery/data-types/TVQR
Authorization: Bearer <your-pulse-token>

Response - 204 No Content, or 409 Conflict with target and dependents when something still references it.

Two reference shapes block the delete:

Dependent kind referenceType Meaning
property property-ranges-over-data-type A property whose range is this data type
dataType data-type-nests-data-type Another data type that links a property whose range is this data type
{
  "type": "https://atlas.timebase.io/errors/delete-blocked",
  "title": "Delete Blocked",
  "status": 409,
  "detail": "Cannot delete 'TVQR' — it is referenced by 1 dependent.",
  "target": { "name": "TVQR", "kind": "dataType" },
  "dependents": [
    { "id": "Flow", "name": "Flow", "kind": "property", "referenceType": "property-ranges-over-data-type" }
  ]
}

Enumeration types#

An enumeration type is a closed list of codes, each with an identifier and a display label: an asset status, a criticality level, a machine state. It holds no values itself. A property points at it with enumType, as described in Bind a property to an enumeration type.

Routes#

Operation Method and path Success
List GET .../enum-types 200
Get one GET .../enum-types/{name} 200
Create POST .../enum-types 201
Update PUT .../enum-types/{name} 200
Delete DELETE .../enum-types/{name} 204, or 409 when blocked

Declared (domain) and (library).

Create an enumeration type#

POST /api/v1/branches/main/libraries/plant-assets/enum-types
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "CriticalityLevel",
  "label": "Criticality Level",
  "comment": "How much an asset failure affects production",
  "keyType": "string",
  "members": [
    { "key": "A", "name": "High",   "label": "A - production stops" },
    { "key": "B", "name": "Medium", "label": "B - production slows" },
    { "key": "C", "name": "Low",    "label": "C - no production impact" }
  ]
}
Field Type Required Description
name string Yes Local name. Must be unique across all six kinds in this scope
label string No Display name
comment string No Description
keyType string Yes The value type of every key: integer, long, float, double, boolean or string. The bare word, the xsd: CURIE and the full XML Schema URI are all accepted. A property bound to this enumeration must use the same type as its range
members object[] Yes The codes, in order. The array order is the order pickers and the catalog show. [] is allowed: an enumeration with no members yet
members[].key string Yes The value as it is stored and sent. Always a JSON string, whatever keyType is: "0", "true", "Running". It must be a valid value of keyType, and it is stored in canonical form, so "007" becomes "7" and "TRUE" becomes "true". Unique within the enumeration
members[].name string Yes An identifier: a letter or underscore, then letters, digits or underscores. Unique within the enumeration. It is not derived from the label
members[].label string No Any display text

Response - 201 Created, body is the read model:

{
  "uri": "urn:atlas:ontology:plant-assets:CriticalityLevel",
  "label": "Criticality Level",
  "comment": "How much an asset failure affects production",
  "keyType": "http://www.w3.org/2001/XMLSchema#string",
  "members": [
    { "key": "A", "name": "High",   "label": "A - production stops" },
    { "key": "B", "name": "Medium", "label": "B - production slows" },
    { "key": "C", "name": "Low",    "label": "C - no production impact" }
  ]
}
Read field Type Notes
uri string Full URN
label, comment string Omitted when null
keyType string Always the full XML Schema URI, whichever spelling you sent
members object[] In authored order. A member's label is omitted when none was written
sourceLibrary string Short name of the imported library that declared it. Omitted for locally declared enumeration types

An unsupported key type, a key that is not a valid value of the key type, an invalid member name, or a duplicate key or name fails with 400 and the error key EnumType.Invalid. The detail names the offending member. A same-kind duplicate fails with 409 and EnumType.AlreadyExists.

Update an enumeration type#

Update is PUT, and members is a full replacement, so adding, removing and reordering members land together:

PUT /api/v1/branches/main/libraries/plant-assets/enum-types/CriticalityLevel
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "label": "Criticality Level",
  "comment": "How much an asset failure affects production",
  "keyType": "string",
  "members": [
    { "key": "A", "name": "High",   "label": "A - production stops" },
    { "key": "B", "name": "Medium", "label": "B - production slows" },
    { "key": "C", "name": "Low",    "label": "C - no production impact" },
    { "key": "X", "name": "Retired", "label": "Retired asset" }
  ]
}
Field Semantics
label, comment Sent value replaces the stored value
keyType Required. Every key is re-checked against it
members Full replacement of the list, in order

The name in the route cannot change. Response - 200 OK, or 404 when the name does not exist.

Important

Changing keyType on an enumeration that properties already use makes each of those properties report O-B16 until its range is changed to match.

Delete an enumeration type#

DELETE /api/v1/branches/main/libraries/plant-assets/enum-types/CriticalityLevel
Authorization: Bearer <your-pulse-token>

Response - 204 No Content, or 409 Conflict when something still uses it. The body has the same shape as the data-type delete conflict, with a target of kind enumType and the full dependents list:

Dependent kind referenceType Meaning
property property-typed-by-enum A property bound with enumType
property property-pins-field-to-enum A property that binds one of its data-type fields with fieldEnumOverrides
type type-pins-property-to-enum An entity or relationship type that binds a pinned leaf with propertyEnumPins

Remove each binding, then delete.


Relationship types#

A relationship type is a named, directed connection with a Source end and a Target end. In Atlas a relationship is a thing, not a line: it is reified as an instance in its own right, it carries its own property values, and it is created by its own instance map with top-level source and target templates.

Important

Because relationships are first-class and carry properties natively, the old advice to promote a data-bearing association into an intermediate entity type is now strictly worse than the native shape. Model it as a relationship type with properties and give it its own instance map.

Important

author each connection as its own instance map definition. The inline relationships[] form is not part of the current shape, and an unrecognised JSON property is ignored, so sending it returns 201 with no edges created.

Routes#

Operation Method and path Success
List GET .../relationship-types 200
Get one GET .../relationship-types/{name} 200
Create POST .../relationship-types 201
Update PATCH .../relationship-types/{name} 200
Delete DELETE .../relationship-types/{name} 204

Declared (domain) and (library).

Note

/relationships at instance scope is a different resource. The ontology resource is relationship-types.

List all relationship types#

GET /api/v1/branches/main/domains/brewery/relationship-types
Authorization: Bearer <your-pulse-token>

Response - 200 OK

[
  {
    "uri": "urn:atlas:ontology:brewery:LineContainsPump",
    "label": "Line Contains Pump",
    "comment": "Physical containment of a pump within a brewing line",
    "baseType": "urn:atlas:vocab:ContainsBase",
    "sourceNodeType": "urn:atlas:ontology:brewery:BrewingLine",
    "targetNodeType": "urn:atlas:ontology:brewery:Pump",
    "properties": ["urn:atlas:ontology:brewery:CommissionedOn"],
    "isContainment": true
  }
]
Read field Type Notes
uri string Full URN
label, comment string Omitted when null
baseType string The framework root as a full URI, urn:atlas:vocab:ContainsBase or urn:atlas:vocab:RelationshipBase, or the URI of the relationship type this one narrows
parent string The immediate user-defined rdfs:subClassOf super-relationship-type. Distinct from baseType. Omitted when there is none
sourceNodeType string Full URI of the entity type allowed at the Source end. Omitted when the end is unconstrained ("any node")
targetNodeType string Full URI of the entity type allowed at the Target end
sourceUnion, targetUnion string[] Full URIs of the entity types allowed at that end, in the order you sent them. Present only when an end allows two or more types, and the matching sourceNodeType or targetNodeType is then omitted
inverseName string The name of the reverse direction. Omitted when none
isAbstract boolean true when the type is derivation-only
properties string[] Full URIs of the edge properties attached to this relationship class, merged across the parent chain. Excludes the reserved Source and Target members
isContainment boolean true when the type is rooted at ContainsBase, directly or transitively. Drives hierarchy views
propertyPins object[] The effective pins of the relationship on its Defer edge properties, own and inherited, in the same shape as on an entity type
sourceLibrary string Short name of the imported library that declared it. Omitted for locally declared types

Create a relationship type#

POST /api/v1/branches/main/domains/brewery/relationship-types
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "LineContainsPump",
  "baseType": "ContainsBase",
  "label": "Line Contains Pump",
  "comment": "Physical containment of a pump within a brewing line",
  "sourceNodeType": "BrewingLine",
  "targetNodeType": "Pump",
  "properties": ["CommissionedOn"]
}
Field Type Required Description
name string Yes Local name, PascalCase. Must be unique across all five kinds in this scope
baseType string Yes ContainsBase for containment, RelationshipBase for a peer association, or the full URI of an existing relationship type to narrow. See the warning below
label string No Display name
comment string No Description, stored as rdfs:comment
sourceNodeType string No Local name or full URI of the entity type at the Source end. Use NodeBase for "any node". Omit for unconstrained
targetNodeType string No Local name or full URI of the entity type at the Target end
sourceUnion string[] No Two or more entity types, any of which may sit at the Source end. Local names or full URIs of existing entity types. Takes precedence over a sourceNodeType sent on the same request. A single entry behaves exactly like sourceNodeType. See Allow several entity types at one end
targetUnion string[] No The same, for the Target end
inverseName string No A name for the same connection read from the Target end, such as FedBy for Feeds. See Name the reverse direction
isAbstract boolean No Defaults to false. true makes the type derivation-only: subtypes inherit its ends and properties, but no instance map can create a connection of this type itself. See Abstract types
properties string[] No Local names or full URIs of existing properties to attach as first-class edge data
propertyPins object No Pins on the open leaves of Defer edge properties, same shape as on an entity type. See Pin a property's leaf on a type
propertyEnumPins object No Enumeration types bound to pinned leaves. API only

Response - 201 Created, body is the created read model with parent and merged inherited constraints populated.

Important

baseType is resolved with the URN resolver, which prefixes any value that is not already a full URI with urn:atlas:vocab:. So "ContainsBase" correctly becomes urn:atlas:vocab:ContainsBase, but a bare local name such as "Feeds" becomes urn:atlas:vocab:Feeds, which is not a class that exists. To narrow an existing relationship type, send its full URI (urn:atlas:ontology:{scope}:Feeds), not its local name. A local name here fails later at whole-scope validation, not at the create.

Edge properties

properties is how a relationship carries data of its own: a flow rate on a feeds, a commissioning date on a contains. Each entry becomes an owl:Restriction on the relationship class, the same shape the Source and Target ends use, so no new vocabulary is involved.

A Defer property can be an edge property. The relationship type then reports O-B23 and cannot compile until the property is pinned, on the property or on the relationship type with propertyPins. See Defer properties and rule O-B23.

Primitive-ranged edge properties compile end to end. Give a relationship property a scalar value type such as xsd:double, or carry a compound on one of the connected entity types.

Allow several entity types at one end#

A single sourceNodeType allows one entity type and its subtypes. When a connection can start or end at unrelated types, list them in sourceUnion or targetUnion instead of leaving the end unconstrained:

POST /api/v1/branches/main/libraries/plant-assets/relationship-types
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "Feeds",
  "baseType": "RelationshipBase",
  "label": "Feeds",
  "sourceUnion": ["Equipment", "Stockpile"],
  "targetUnion": ["Equipment", "Stockpile"],
  "inverseName": "FedBy",
  "properties": ["SplitPercent"]
}
  • Each entry must be an existing entity type. An unknown name reports O-B1, and something that is not an entity type reports O-B4.
  • Subtypes count. An instance of any listed type, or of a subtype of one, satisfies the end. A connection whose end is of another type reports I-B7 when the instance map is validated.
  • The read model reports what is stored. Two or more types read back as sourceUnion, with sourceNodeType omitted. A one-entry list is stored as a plain single-type end and reads back as sourceNodeType.

Name the reverse direction#

inverseName names the same connection as seen from the Target end. It adds no second connection and stores nothing per instance: it is another way to read the one you already have.

Where it is used:

  • In Cypher. The inverse name is accepted as a relationship type and matches the connection backwards. With "inverseName": "FedBy" on Feeds, these two queries return the same rows:

    MATCH (a)-[:FedBy]->(b) RETURN a, b
    MATCH (b)-[:Feeds]->(a) RETURN a, b
    

    If a relationship type's own name is the same as another type's inverse name, the forward type wins.

  • In the i3X API, as the reverse direction of the relationship type.

The rules, all checked when the scope is validated:

  • It shares the URN namespace. The inverse direction is identified as urn:atlas:ontology:{scope}:{inverseName}, so it must not equal the relationship type's own name, another relationship type's inverse name in the same scope, or the name of any other element in the scope. Each case reports O-B12.
  • Reusing an ancestor's inverse name is a warning, O-B13. It is allowed, but the same inverse name then means two different things in one hierarchy.

Update a relationship type#

PATCH /api/v1/branches/main/domains/brewery/relationship-types/LineContainsPump
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "label": "Brewing Line Contains Pump",
  "targetNodeType": "CentrifugalPump",
  "properties": []
}

PATCH is a partial merge. Note that name and baseType are not updatable.

Field Semantics
label null or omitted leaves unchanged
comment null or omitted leaves unchanged. Any non-null value, including "", replaces it
sourceNodeType Three-state. null or omitted leaves the end unchanged; "" clears it back to "any node" and drops the restriction; a name or URI replaces it
targetNodeType Three-state, exactly as sourceNodeType
sourceUnion, targetUnion Three-state. null or omitted leaves the end unchanged; a non-empty array replaces the end, including one set with sourceNodeType or targetNodeType; [] clears it back to "any node". When one request sends both a single type and a union for the same end, the union wins
inverseName null or omitted leaves it unchanged. Any non-null value replaces it, so "" removes it
isAbstract null or omitted leaves it unchanged; true or false sets it
properties Full replacement of the edge properties. null leaves them untouched; [] clears them all
propertyPins Full replacement of the relationship's declared pins. null or omitted leaves them untouched; an object, even {}, replaces them
propertyEnumPins Enumeration types bound to pinned leaves, same shape and the same replacement rule. API only

Response - 200 OK, or 404 when the name does not exist.

The ""-to-clear sentinel on the endpoints is undiscoverable from the wire and there is no other way to relax a constrained end back to "any node". Note the asymmetry: on comment, "" sets an empty comment; on the two endpoints, "" is a clear sentinel.

Delete a relationship type#

DELETE /api/v1/branches/main/domains/brewery/relationship-types/LineContainsPump
Authorization: Bearer <your-pulse-token>

Response - 204 No Content. 404 when the name does not exist.

Like entity types, relationship types have no per-item dependents guard. A dangling reference is caught by whole-scope re-validation, which aborts the write.


Functions#

A function is reusable calculation logic with named parameters, one structured output, an inline C# body, and optionally a normalization anchor. Functions have full CRUD on both scopes. They are not read-only and they are not written for you by the instance-map layer.

A function has no schedule and no trigger settings. A calculated value is worked out when something reads it.

Routes#

Operation Method and path Success
List GET .../functions 200
Get one GET .../functions/{name} 200
Create POST .../functions 201, or 422 on validation
Update PUT .../functions/{name} 200, or 422 on validation
Delete DELETE .../functions/{name} 204, or 409 when blocked

Declared (domain) and (library).

List all functions#

GET /api/v1/branches/main/domains/brewery/functions
Authorization: Bearer <your-pulse-token>

Response - 200 OK

[
  {
    "uri": "urn:atlas:ontology:brewery:ConvertFlowRate",
    "label": "Convert Flow Rate",
    "comment": "Scales a raw flow reading by a calibration coefficient",
    "parameters": [
      {
        "name": "rawFlow",
        "type": "http://www.w3.org/2001/XMLSchema#double",
        "predicateUri": "urn:atlas:ontology:brewery:param/rawFlow",
        "required": true
      },
      {
        "name": "coefficient",
        "type": "http://www.w3.org/2001/XMLSchema#double",
        "predicateUri": "urn:atlas:ontology:brewery:param/coefficient",
        "required": true
      }
    ],
    "output": {
      "name": "flow",
      "type": "http://www.w3.org/2001/XMLSchema#double",
      "predicateUri": "urn:atlas:ontology:brewery:output/flow"
    },
    "returnType": "http://www.w3.org/2001/XMLSchema#double",
    "implementation": {
      "body": "return rawFlow * coefficient;"
    }
  }
]
Read field Type Notes
uri string Full URN
label, comment string Omitted when null
library string Optional library marker on the function
parameters array Ordered input list
parameters[].name string The local C# identifier used in the body
parameters[].type string The value type URI
parameters[].predicateUri string Non-nullable. The full predicate URI used as the arguments-dictionary key at execution time, and the key an instance map's Calculated strategy binds against
parameters[].required boolean Defaults to true
parameters[].label, .comment string Label and comment on the shared predicate declaration
output object The single structured output: name, type, predicateUri, label
returnType string Computed convenience only. It is a projection of output.type and cannot be set independently
implementation.body string C# method-body statements only. No signature, no braces. The compiler wraps it in a scaffold
anchor object Present only on multi-row functions. See below
Note

returnType appears on the wire as a read-only view over output.type. Author a function by sending the structured output object.

Create a function#

POST /api/v1/branches/main/domains/brewery/functions
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "name": "ConvertFlowRate",
  "label": "Convert Flow Rate",
  "comment": "Scales a raw flow reading by a calibration coefficient",
  "parameters": [
    {
      "name": "rawFlow",
      "type": "xsd:double",
      "predicateUri": "urn:atlas:ontology:brewery:param/rawFlow",
      "required": true
    },
    {
      "name": "coefficient",
      "type": "xsd:double",
      "predicateUri": "urn:atlas:ontology:brewery:param/coefficient",
      "required": true
    }
  ],
  "output": {
    "name": "flow",
    "type": "xsd:double",
    "predicateUri": "urn:atlas:ontology:brewery:output/flow"
  },
  "implementation": {
    "body": "return rawFlow * coefficient;"
  }
}
Field Type Required Description
name string Yes Local name. Must be unique across all five kinds in this scope
label, comment string No Display name and description
library string No Library marker. Must be an absolute URI or null; anything else is refused with 400
parameters array Yes Ordered inputs. Each entry needs name, predicateUri, and normally type
output object Yes in practice Exactly one output is required by validation. name, type, predicateUri
implementation object Yes in practice { "body": "..." }. A non-empty body is required by validation
anchor object No Normalization-anchor spec. null means single-row

Response - 201 Created. The body is a save envelope, not the bare read model:

{
  "function": { "uri": "urn:atlas:ontology:brewery:ConvertFlowRate", "...": "..." },
  "compileDiagnostics": [
    {
      "id": "CS0019",
      "severity": "Error",
      "message": "Operator '*' cannot be applied to operands of type 'string' and 'double'",
      "line": 1,
      "column": 8
    }
  ]
}

Atlas compiles the body as part of the save and returns the compiler's diagnostics in compileDiagnostics, empty when the body is clean. line and column are 1-based and relative to the first line of the body you sent, not to a file.

The save itself is not blocked by a diagnostic: a body that does not compile is still stored, and the function is set aside at compile time until it does. Treat a non-empty compileDiagnostics as work to finish rather than as a failed write. A 422 remains the validation path, for the O-F rules.

The predicate URIs are domain-shared and reused across functions. They follow the convention {scope-urn}:param/{name} and {scope-urn}:output/{name}. They are what an instance map's Calculated strategy binds arguments against, so they are part of your model's contract, not an implementation detail.

Anchors and multi-row normalization

An anchor turns a scalar converter into one that aligns two or more time series onto a common instant before calculating.

{
  "anchor": {
    "outputField": "Timestamp",
    "inputs": [
      { "parameterName": "rawFlow",     "inputField": "Timestamp" },
      { "parameterName": "coefficient", "inputField": "Timestamp" }
    ]
  }
}
Field Description
outputField The datetime field on the output compound that the anchor resolves to
inputs[].parameterName Matches a parameters[].name
inputs[].inputField The datetime field on that input's compound whose sample timestamps drive the anchor grid

When anchor is set, the compiler runs the function multi-row and auto-injects a single anchor local fed by the union of the contributing inputs. null or an empty inputs array means single-row, and no provenance nodes are emitted.

Point normalization needs both halves: the data type must declare a time axis, and the function must declare contributing anchor inputs. Without both, the calculation returns a number built from each input at its latest known value, which is the ordinary behaviour.

Update a function#

Update is PUT. The backend retracts the prior fno:Function subgraph and re-asserts a complete one, so relabelling and signature, output, implementation and anchor changes all land in one commit.

PUT /api/v1/branches/main/domains/brewery/functions/ConvertFlowRate
Authorization: Bearer <your-pulse-token>
Content-Type: application/json

{
  "label": "Convert Flow Rate (calibrated)",
  "implementation": {
    "body": "return rawFlow * coefficient * 1.02;"
  }
}

Despite the PUT verb, each top-level field is independently nullable and null means "leave unchanged" (UpdateFunctionRequest):

Field Semantics
label, comment, library null or omitted leaves unchanged. A library that is sent must be an absolute URI
parameters null leaves the existing signature untouched. A sent list replaces it wholesale
output null leaves the existing output untouched
implementation null leaves the existing body untouched
anchor null leaves the existing anchor untouched. A spec with an empty inputs array clears the anchor back to single-row. A populated spec replaces it

Response - 200 OK, or 404 when the name does not exist, or 422 on validation.

Malformed URIs: 400#

Before the validator runs, a create or update is refused with 400 and the error key below when a URI field is malformed. This applies to both /app and /api/v1, on create and update.

Error key When Message
Function.InvalidLibrary library is not null and not an absolute URI The 'library' field must be an absolute URI or null, but was 'fnlib'.
Function.InvalidPredicateUri parameters[i].predicateUri is missing or not an absolute URI The 'parameters[0].predicateUri' field (input 'value') must be an absolute URI.
Function.InvalidPredicateUri output.predicateUri is missing or not an absolute URI The 'output.predicateUri' field (output 'result') must be an absolute URI.

Function validation: O-F1 to O-F6#

Functions are the one ontology resource with a synchronous pre-save validator. A rejected create or update returns 422 Unprocessable Content as application/problem+json and nothing is persisted, so the draft-session behaviour described earlier does not apply.

Rule What it checks
O-F1 Identity. The name must be non-empty and its URI must not already be taken
O-F2 Type resolvability. Every input and output type must resolve to a primitive or a known compound
O-F3 Predicate clash. A shared parameter predicate reused with a different type than an existing declaration
O-F4 Shape. Exactly one output, and a non-empty implementation body
O-F5 Anchor consistency. The output field and every contributing input field must be valid datetime fields on the right compounds
O-F6 Name format. An input name must be a C# identifier, not a reserved C# keyword and not anchor. An output name can only contain letters, digits, underscores and hyphens. One violation per bad name

O-F6 checks only names that are new or changed on an update, so a function saved under the older, looser rule stays editable. An input named anchor on an anchored function is refused on every save. The messages are:

  • Input 'flow rate' has an invalid name. An input name can only contain letters, digits and underscores (_), and cannot start with a digit.
  • Input 'class' has an invalid name: 'class' is a reserved word in C#. Choose another name.
  • Input 'anchor' has an invalid name: 'anchor' is reserved for the anchor of an anchored function. Choose another name.
  • Output 'flow rate' has an invalid name. An output name can only contain letters, digits, underscores (_) and hyphens (-).

The 422 body:

{
  "type": "https://atlas.timebase.io/errors/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "1 violation(s) found in brewery.",
  "violations": [
    {
      "ruleId": "O-F4",
      "severity": "Error",
      "filePath": "ontology.jsonld",
      "message": "A function must declare exactly one output."
    }
  ]
}

jsonPointer, instanceId and suggestion also appear on each violation when populated, and occurrences says how many violations the entry stands for. It is almost always 1; the count in detail sums it rather than counting array entries.

Delete a function#

DELETE /api/v1/branches/main/domains/brewery/functions/ConvertFlowRate
Authorization: Bearer <your-pulse-token>

Response - 204 No Content, or 409 Conflict when an instance-map Calculated strategy still references it.

{
  "type": "https://atlas.timebase.io/errors/delete-blocked",
  "title": "Delete Blocked",
  "status": 409,
  "detail": "Cannot delete 'ConvertFlowRate' — it is referenced by 1 dependent.",
  "target": { "name": "ConvertFlowRate", "kind": "function" },
  "dependents": [
    { "id": "pumps", "name": "pumps", "kind": "instanceMap", "referenceType": "calculated-strategy" }
  ]
}

Clear the block by changing the strategy on each listed instance map first.

A delete also removes each input's shared property when no other function uses it and nothing else uses it as a property. An input property that is still in use stays. Renaming or removing an input on an update does the same for the property it leaves behind. The in-use check is scoped to the Domain or Library that owns the function: a Domain that imports a Library and has attached one of the Library's input properties to its own types is not consulted, and the property is still removed with the Library function.


Errors on this surface#

All failures are RFC-9457 application/problem+json. The full type-URI table lives in the Atlas API Overview. These are the shapes specific to the ontology resources:

Status Type URI token When
400 .../errors/invalid-request A function's library or a predicateUri is not an absolute URI (see Malformed URIs); an attachment reference does not resolve; a write targets a subject declared by an import; a pin is refused (see Pin a property's leaf on a type); a domain writes to a property declared by one of its libraries (below)
404 .../errors/not-found The scope, branch, or named resource does not exist; a detach targets a property that is not declared on the type
409 .../errors/conflict A same-kind duplicate name, or a cross-kind key collision. Read the detail to tell them apart
409 .../errors/delete-blocked Properties, data types and functions only. Adds target and dependents
422 .../errors/validation-failed Function create and update only, on this surface. Adds violations[]

A write from a domain to a property that one of its libraries declares is refused with 400 invalid-request, and the detail carries the message:

Refusal Raised when Detail
Property.NotWritableInScope A domain tries to update a library's property by name, for example to pin it. Pin the leaf on the domain's own type instead Property 'FlowRate' is declared by 'process-signals' and cannot be changed from 'brewery'. Pin the open leaf on the type that uses it, or pin 'FlowRate' in 'process-signals'.
Note

There is no universal code field. The generic error path emits only type, title, status and detail, plus the framework's traceId. Branch your client on the type URI. On the ontology surface no endpoint emits a code extension.

The /app plane is not an alternative#

Every route in this article has a twin under /app/branches/{branch}/... that dispatches the same commands and returns the same wire shapes. It is private glue for the Atlas single-page application, excluded from the published API documents, gated by a session cookie for reads and an inline admin check for writes. A bearer token does not clear it.

Browser code inside Atlas uses /app. Machine integrations use /api/v1. Picking the wrong one is a guaranteed 401. Do not build against /app.

Troubleshooting#

Symptom Cause What to do
A request returns 200 text/html instead of JSON The path matched no route and the single-page-application fallback answered it. Almost always nodes instead of entity-types, relationships instead of relationship-types, /api/v2 instead of /api/v1, or PUT where PATCH is expected Check Content-Type before parsing. Then check the segment and the verb against the route tables above
An update returns no change and no 405 The verb does not match a route on that group Entity types, properties and relationship types use PATCH. Data types and functions use PUT
A write returned 201 or 200 but the change is not on the branch The save carried outstanding validation errors and was routed to your private draft session Check for the Atlas-Validation-Violations response header, read GET .../validation for the list, fix, then POST /api/v1/branches/{branch}/draft/publish
You saved, you compiled, and nothing changed Both of the above at once. A draft-routed compile does not overwrite the shared World cache Check GET /api/v1/branches/{branch}/draft, publish, then compile again
409 on create and your handler says "already exists", but the name is free for that kind A cross-kind key collision. All five kinds share one URN namespace per scope Read the detail, which names the occupying kind. Pick a different local name. An update cannot fix this
Your client reads property.domain and gets undefined The field is attachedTypes on read, and it is an array Read attachedTypes. Write attachToTypes
A property you created never appears on the type you meant attachToTypes was empty or omitted. An unattached property is valid and is simply never reached by a type read Attach it with PUT .../entity-types/{type}/properties/{name}
You cannot attach a property to a type imported from a library The write subject must live in the save scope. Attaching onto an imported class would be dropped by the scope-filtered save Attach it from the declaring library's scope, or create a local subtype and attach there
Detach returns 404 for a property you can see on the type The attachment is inherited from an ancestor, not declared on this type. Detach only touches declared attachments Detach from the ancestor that declares it, or accept the inheritance
A data-type round trip cleared the label and comment The data-type read model does not return label or comment, and PUT is a full replacement Always send the complete intended body on PUT .../data-types/{name}
A relationship type you narrowed does not resolve baseType prefixes any non-URI value with urn:atlas:vocab:, so a bare local name becomes a vocabulary class that does not exist Send the full URI urn:atlas:ontology:{scope}:{Name} to narrow an existing relationship type
You cannot relax a relationship end back to "any node" The clear sentinel is an empty string, not null. null means "leave unchanged" PATCH with "sourceNodeType": ""
An instance map created a relationship but no edges exist The inline relationships[] form is not part of the current shape, and an unrecognised property is ignored Author the relationship as its own instance map whose type is the relationship archetype, with top-level source and target
POST .../functions returns 422 and the save never lands Functions run a synchronous pre-save validator Read the violations[] array and fix the O-F1 to O-F6 rule reported
A calculated property is always 0 A function parameter was left unbound on the instance map. That is rule I-B22, which is only a warning and never blocks a compile Check the warning count on GET .../validation
A 409 on delete that your handler reports as "name already exists" A blocked delete uses the distinct type .../errors/delete-blocked and carries target and dependents Branch on the type URI, not the status code, and show the dependents list
Migrating hand-written JSON-LD fails with O-A12 The file carries rdfs:domain on a property, the retired attachment shape Remove it. Express attachment as an owl:Restriction on the entity class, or by sending attachToTypes
Deleted type still appears in query results The domain has not been recompiled PUT /api/v1/branches/{branch}/domains/{domain}/world. GET .../world reports isStale without recompiling
401 on every call /api/v1 needs a Pulse bearer whose aud includes Atlas. A cookie is not accepted See Authentication in the Atlas API Overview
esc
↑↓ navigate · ↵ open · ⌘K anywhere