Skip to main content

Temporal

Andromeda keeps the history of every attribute, each value it held, with when it was observed, in the same PostgreSQL database as the current entities. You read that history under /ngsi-ld/v1/temporal/entities, for one entity or many, as raw instances or summarised per period. This guide shows how to write history, read it back, and narrow it to the window you want.

A Core API write adds to the history, so the history can hold more than you wrote through the Temporal API. Reads cover both.

In Rust the window is a typed temporal_query, so a bad time or a window that contradicts the aggregation is refused before any request leaves the machine.

Writing history​

Core API writes record history on their own, so an update to speed adds an instance to the history of speed. Writes says exactly what a write records.

To load a series, or values observed in the past, write them through the Temporal API. Each attribute holds an array of instances.

POST /ngsi-ld/v1/temporal/entities
Content-Type: application/json

{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 52,
"observedAt": "2026-10-24T08:00:00Z"
},
{
"type": "Property",
"value": 61,
"observedAt": "2026-10-24T08:05:00Z"
},
{
"type": "Property",
"value": 58,
"observedAt": "2026-10-24T08:10:00Z"
}
]
}
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Vehicle:A4567

The answer is 201 when the entity is new, and 204 when you add to an existing history.

Give every instance an observedAt. Reads go by observedAt unless you ask otherwise, so an instance without one doesn't appear on them.

A Temporal API write also updates the current entity, but only with instances newer than what it holds. A backfill goes into the history alone. Temporal evolution covers how the two interact.

Reading one entity's history​

GET /ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Vehicle:A4567?attrs=speed&timerel=after&timeAt=2026-10-24T08:00:00Z
{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 52,
"observedAt": "2026-10-24T08:00:00Z",
"instanceId": "urn:ngsi-ld:..."
},
{
"type": "Property",
"value": 61,
"observedAt": "2026-10-24T08:05:00Z",
"instanceId": "urn:ngsi-ld:..."
},
{
"type": "Property",
"value": 58,
"observedAt": "2026-10-24T08:10:00Z",
"instanceId": "urn:ngsi-ld:..."
}
]
}

Each instance carries an instanceId, which you use to change or delete that one instance. Without a window, Retrieve returns the whole history, page by page.

For a more compact answer, ask for options=temporalValues. Each attribute then carries its values as pairs of value and time:

"speed": {
"type": "Property",
"values": [
[
52,
"2026-10-24T08:00:00Z"
],
[
61,
"2026-10-24T08:05:00Z"
],
[
58,
"2026-10-24T08:10:00Z"
]
]
}

Choosing a window​

A window is a timerel with a time, and the relation decides what the time means.

  • timerel=before&timeAt=T selects instances before T, not including T.
  • timerel=after&timeAt=T selects instances at T or later.
  • timerel=between&timeAt=T1&endTimeAt=T2 selects instances at T1 or later and before T2.

The window applies to observedAt by default. timeproperty=modifiedAt applies it to when each instance was written instead, and createdAt and deletedAt work the same way. Instances written without an observedAt show up only on those other axes.

lastN=10 keeps the ten most recent instances of each attribute in the window. attrs narrows the attributes, and datasetId the instances. Date-times must be in UTC with the literal Z, as Values explains.

Querying many entities​

GET /ngsi-ld/v1/temporal/entities?type=Vehicle&attrs=speed&timerel=between&timeAt=2026-10-24T08:00:00Z&endTimeAt=2026-10-24T09:00:00Z

A temporal query needs timerel and timeAt, and something that narrows the entities, whether a type, attrs, q, a geoquery or id. It takes the same filters as an ordinary query, and returns an array of entities, each with its history. For a long query, send it as a body to POST /temporal/entityOperations/query.

A temporal query can only be ordered by id. Ordering explains why.

Long histories come in pages​

A read without lastN returns up to temporal.query.default_last_n instances per attribute, 100 by default, and pages the rest. A lastN above temporal.query.max_last_n, 1000 by default, is paged the same way.

When a read leaves instances out, it answers 206 Partial Content with a Content-Range header saying how far it got. Retrieve also sends a Link with rel="next" to the next page. For a query, you build the next request from the Content-Range. Pagination has the exact rules.

Summarising per period​

Instead of raw instances, ask for statistics per period with format=aggregatedValues. You can also give it as options=aggregatedValues, and when both are present format wins.

GET /ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Vehicle:A4567?attrs=speed&format=aggregatedValues&aggrMethods=avg,max&aggrPeriodDuration=PT1H&timerel=after&timeAt=2026-10-24T00:00:00Z

Each method then gives a list of entries, one per period that holds an instance. Each entry carries the value and the start and end of its period.

"speed": {
"type": "Property",
"avg": [
[
57,
"2026-10-24T08:00:00Z",
"2026-10-24T09:00:00Z"
]
],
"max": [
[
61,
"2026-10-24T08:00:00Z",
"2026-10-24T09:00:00Z"
]
]
}

The methods are totalCount, distinctCount, sum, avg, min, max, stddev and sumsq. aggrPeriodDuration is an ISO 8601 duration, and without it the whole window is one period. A method that doesn't fit the values, such as avg over strings, is refused. Temporal aggregation covers which methods fit which values, calendar periods, and paging.

Changing and deleting history​

Every one of these answers 204, and none of them touches the current entity:

  • PATCH /temporal/entities/{id}/attrs/{attrId}/{instanceId} changes that instance only.
  • DELETE /temporal/entities/{id}/attrs/{attrId}/{instanceId} deletes that instance only.
  • DELETE /temporal/entities/{id}/attrs/{attrId} deletes the history of the attribute, or of one instance of it with datasetId.
  • POST /temporal/entities/{id}/attrs adds the instances in the body to the history.
  • DELETE /temporal/entities/{id} deletes the whole history of the entity.

Keeping history in check​

By default, Andromeda keeps history forever. An operator can set temporal.storage.retention.mode = "enabled" and temporal.storage.retention.delete_after to a duration, and the broker then drops history partitions recorded entirely before that age. Temporal configuration has the keys.

Retention is a storage decision, not an NGSI-LD deletion. History past the horizon leaves the database without a deletion record and without a notification. Once it is gone, a query can't return it.

Retention and the conversion of older history to the columnstore run on their own schedule, whether or not requests reach the tenant. A tenant with no traffic is loaded when its oldest history passes temporal.storage.retention.delete_after or temporal.storage.columnstore.convert_after, and unloaded again once the sweeps are done.