Skip to main content

Temporal evolution

The temporal evolution of an entity is its history. It holds every attribute instance the entity has had, the types it has carried, the deletions written into it, and its Scope over time. This page covers what that history holds, and how Temporal API writes affect the current entity.

The two can disagree on purpose. A backfill adds to the history and leaves the current entity as it was. The rest of the page explains where that happens and how to read the result.

Entity types​

A history carries one or more entity types, the same as the entity itself. Its type list only ever grows.

A Temporal API write adds any type it names that the history doesn't have yet. No Temporal API operation removes a type. Delete Temporal Evolution removes the whole history, and that is the only way a type leaves it.

A Core API write that narrows the current entity's types leaves the history's types alone. That covers Replace Entity, an upsert in replace mode, and a create after a delete. History recorded under a type can still be found by a temporal query on that type, even after the current entity has lost it.

Types are listed in the order they first appeared.

A temporal query by type finds an entity whose history holds the type, whether or not the current entity still has it.

How a Temporal API write changes the current entity​

A Temporal API write also updates the current entity, using the most recent instance.

  • An instance newer than what the current entity holds replaces it.
  • An older instance, such as a backfill, is recorded in the history and leaves the current entity as it was.
  • An instance without observedAt doesn't replace a current value that has one. It is recorded in the history only.
  • A type the write names that the current entity lacks is added to it. Types carry no time and can't be older. Nobody is notified, because a change of types alone changes no attribute.

When a write leaves the current entity as it was, the entity isn't stored again. Its modifiedAt stays the same, and no subscriber is notified. Every instance still goes into the history.

A Temporal API write can recreate an entity that a Core API write deleted. The history survives, because a Core API delete leaves history in place. The history keeps its types and gains the write's. The recreated current entity carries only the write's types. So a temporal query by an old type still finds the history, while a current-state query by that type doesn't find the recreated entity.

Writing a deletion into the history​

A deleted Property or Relationship appears in its history as an instance whose value is the NGSI-LD Null. For a languageMap, the value is {"@none": "urn:ngsi-ld:null"}. The instance also carries a deletedAt.

You can write one yourself among the instances of Create or Update Temporal Evolution and Add Attributes to Temporal Evolution:

{
"id": "urn:ngsi-ld:Room:1",
"type": "Room",
"temperature": [
{
"type": "Property",
"value": 21,
"observedAt": "2026-10-01T08:00:00Z"
},
{
"type": "Property",
"value": "urn:ngsi-ld:null",
"deletedAt": "2026-10-01T09:00:00Z",
"observedAt": "2026-10-01T09:00:00Z"
}
]
}

Andromeda records it like this:

  • observedAt is kept as you wrote it, so the deletion sits on the observedAt axis next to the other instances.
  • deletedAt is the time Andromeda accepted the write. A deletedAt you send is ignored, and so are createdAt and modifiedAt.
  • The instanceId is the one you gave, or one Andromeda makes up.

A written deletion comes back with its observedAt next to deletedAt. When a Core API write deleted the instance, the end of the instance carries no observedAt.

A deletion has no value, so temporal aggregation leaves it out. Modify Attribute Instance refuses a fragment containing the Null. In the current entity, a written deletion removes the instance it ends if that instance is the most recent one. Otherwise it changes nothing.

The Scope over time​

In the temporal representation, the Scope is a reified Property. Each instance gives the Scope's value and the times it was set.

How a Scope is recorded​

A Core API write that changes the entity's Scope records the new Scope, dated at the write. One that restates the Scope already in force records nothing (see Writes).

A Core API write that leaves the entity without a Scope records the removal. Deleting the entity records it too. The removal comes back as the NGSI-LD Null.

A Temporal API write can carry scope in either form. Responses always use the reified form.

  • A reified instance is recorded as given, at its own observedAt, or at the write if it has none.
  • A plain scope says the entity is in that Scope. It is recorded only if that Scope isn't already in force at the write.
  • The NGSI-LD Null records a removal.

A reified Scope instance can carry only value, observedAt, createdAt, modifiedAt and deletedAt. Anything else gets 400 BadRequestData.

A Scope is valid from when it is set until it is explicitly removed. Andromeda works that out separately on each of the four time axes, observedAt, createdAt, modifiedAt and deletedAt, so a Scope is valid from its event until the entity's next Scope event on the same axis.

Reading the Scope​

scopeQ on a temporal query selects the entities whose Scope matched at some point in the window, on the query's timeproperty, which is observedAt by default. It only selects entities. A selected entity comes back with all its attribute instances in the window, including those observed while it was in another Scope or in none.

The Scope in a temporal answer lists every Scope event whose validity overlaps the window. A Scope set before the window and still in force inside it comes back with the time it was set. The Scope history isn't paged.

A removed Scope shows only its value (the NGSI-LD Null) and deletedAt, never observedAt, even when a Temporal API write declared one. Its instant is deletedAt, and in the temporalValues representation its pair carries that instant. scopeQ and the time windows still read the observedAt that was recorded. An ordinary attribute's removal keeps the observedAt it declared.

Backfilling under a Scope​

A plain scope is dated at the write. So when a backfill sends only the plain form, its Scope lands after the history it writes. A scopeQ over the backfilled range then finds nothing.

The fix is to date the Scope once, in the reified form, at the start of the range you backfill:

{
"id": "urn:ngsi-ld:CrowdFlowObserved:CA001-L01",
"type": "CrowdFlowObserved",
"scope": {
"type": "Property",
"value": "/denhaag/scheveningen",
"observedAt": "2026-03-01T00:00:00Z"
},
"peopleCount": [ ... ]
}

Later chunks can repeat the plain scope. Each one restates a Scope that is already in force, so it records nothing.

When the history and the current entity drift apart​

  • Delete Temporal Evolution removes the Scope history along with everything else. The current entity keeps its Scope, and a Core API write restating that Scope doesn't record it again. Write it through the Temporal API to record it again.
  • A Temporal API write changes the history without touching the current entity, so the two can end up with different Scopes.
  • With temporal.history.core_writes = "not-recorded", the Scope history holds only what the Temporal API wrote.
  • Retention keeps the Scope in force at the cutoff, so a scopeQ over a window after the cutoff still sees the Scope the entity held then.
A restated Scope and a later backdated one

A plain Scope that restates the one in force records nothing. Now suppose a reified instance for a different Scope, dated earlier, is written afterwards. From the restatement's instant on the observedAt axis, the restated Scope is no longer in force.

This needs two sources that disagree about the Scope over the same period. One restates it and the other backdates a different one. Andromeda accepts the effect, because a Scope is valid from when it is set, and a restatement doesn't set it.

Where Andromeda interprets the specification​

Entity types in a history are defined by the specification. Every instance is recorded.

A deleted attribute is an instance with the NGSI-LD Null beside deletedAt. The Null is allowed in a temporal write for that purpose.

The temporal Scope is reified. The data type table and the ETSI OpenAPI type it as a string instead, which is why both forms are accepted on a write. A Scope "is considered valid from the time it has been set until the time it has been explicitly removed". An example shows a Scope set before the window served with its own time.

scopeQ is an entity selector, and the last step of a temporal query keeps the instances that meet "the temporal, query and geoquery restrictions", which leaves Scope out. The informative annex says a vehicle that entered the Scope inside the window has "only Property values ... observed after the Scope has become valid". Andromeda follows the normative text, and the annex example answers the same either way, because its vehicle has no earlier values. To get only the instances observed inside a Scope, filter them on the client by the Scope history the answer carries.