Temporal API

The temporal API (CIM 009 clauses 5.6.11–5.6.16, 5.7.3, 5.7.4) serves the history of attribute values. Every example below runs against the quickstart broker seeded by examples/quickstart/seed.sh; $U is http://localhost:9090/ngsi-ld/v1 and $L the Link header carrying the core @context.

How history is recorded

Every write through the entity endpoints produces temporal events for the attribute instances it changed. The events are buffered for the request and drained to the temporal driver after the handler returns and before the response leaves, so a temporal read that follows a write sees it. An update that changes no value produces no event; ANTARES_TEMPORAL_RECORD narrows what enters history to observed instances or to nothing. A driver failure in the drain never changes the response already produced; it is counted as temporalDrainErrors on /q/health. The gates, the driver choice ANTARES_TEMPORAL and retention are described in Storage drivers.

Three patches of temperature with an observedAt each:

curl -X POST $U/entities/urn:ngsi-ld:TemperatureSensor:qs:1/attrs \
  -H 'Content-Type: application/json' -H "$L" \
  -d '{"temperature":{"type":"Property","value":24.1,"unitCode":"CEL","observedAt":"2026-08-26T09:00:00Z"}}'

The seed value 21.5 carried no observedAt, so it appears only under timeproperty=modifiedAt (below), never in the default observedAt history.

Querying

timerel is before, after or between around timeAt (and endTimeAt for between), applied to timeproperty (observedAt by default; createdAt, modifiedAt, deletedAt). Clause 4.11 fixes the bounds: after includes the timeAt instant, before excludes it, and between includes timeAt and excludes endTimeAt. attrs restricts the attributes.

curl -si "$U/temporal/entities/urn:ngsi-ld:TemperatureSensor:qs:1?timerel=after&timeAt=2026-08-26T08:30:00Z&attrs=temperature"
{"id": "urn:ngsi-ld:TemperatureSensor:qs:1", "type": "TemperatureSensor",
 "temperature": [
   {"type": "Property", "instanceId": "urn:ngsi-ld:Instance:6e0d857e-8845-57ee-9e5b-bbcc34d89a0b",
    "observedAt": "2026-08-26T09:00:00Z", "unitCode": "CEL", "value": 24.1},
   {"type": "Property", "instanceId": "urn:ngsi-ld:Instance:19536e56-b044-51cd-8658-2600fb344d98",
    "observedAt": "2026-08-26T10:00:00Z", "unitCode": "CEL", "value": 26.8}]}

Each instance carries an instanceId; it is the handle for the instance-level operations below. GET $U/temporal/entities?type=… queries several entities with the same parameters plus q, geoQ, scopeQ, id, idPattern and paging; POST $U/temporal/entityOperations/query takes the same query as a Query body with a temporalQ member:

curl -X POST $U/temporal/entityOperations/query -H 'Content-Type: application/json' -H "$L" -d '{
  "type": "Query", "entities": [{"type": "TemperatureSensor"}],
  "temporalQ": {"timerel": "after", "timeAt": "2026-08-25T10:00:00Z"},
  "attrs": ["temperature"], "q": "temperature>25"}'

Errors are BadRequestData 400 with the reason in detail:

{"detail":"invalid timerel \"since\"", ...}
{"detail":"timeAt must be a valid ISO 8601 DateTime (4.11)", ...}

Representation

format=temporalValues (or options=temporalValues) collapses each attribute to [value, time] pairs:

{"id": "urn:ngsi-ld:TemperatureSensor:qs:1", "type": "TemperatureSensor",
 "temperature": {"type": "Property",
   "values": [[22.4, "2026-08-26T08:00:00Z"], [24.1, "2026-08-26T09:00:00Z"], [26.8, "2026-08-26T10:00:00Z"]]}}

timeproperty=modifiedAt keys the history on the write time instead, so the seed value shows up:

"values": [[21.5, "2026-08-26T15:59:33.332Z"], [22.4, "2026-08-26T15:59:33.409Z"],
           [24.1, "2026-08-26T15:59:33.416Z"], [26.8, "2026-08-26T15:59:33.423Z"]]

lastN=2 keeps the two newest instances of each attribute, newest first:

"temperature": [
  {"type": "Property", "observedAt": "2026-08-26T10:00:00Z", "value": 26.8, ...},
  {"type": "Property", "observedAt": "2026-08-26T09:00:00Z", "value": 24.1, ...}]

lastN must be a positive integer; pick/omit and sysAttrs apply as on the entity endpoints.

Aggregation

aggrMethods (totalCount, distinctCount, sum, avg, min, max, stddev, sumsq) with aggrPeriodDuration returns one row per bucket as [value, bucketStart, bucketEnd], buckets anchored at timeAt:

curl "$U/temporal/entities/urn:ngsi-ld:TemperatureSensor:qs:1?timerel=between&timeAt=2026-08-26T08:00:00Z&endTimeAt=2026-08-26T11:00:00Z&attrs=temperature&aggrMethods=avg,max&aggrPeriodDuration=PT1H"
"temperature": {"type": "Property",
  "avg": [[22.4, "2026-08-26T08:00:00Z", "2026-08-26T09:00:00Z"],
          [24.1, "2026-08-26T09:00:00Z", "2026-08-26T10:00:00Z"],
          [26.8, "2026-08-26T10:00:00Z", "2026-08-26T11:00:00Z"]],
  "max": [[22.4, "2026-08-26T08:00:00Z", "2026-08-26T09:00:00Z"], ...]}

Without aggrPeriodDuration (or with PT0S) the whole range is one bucket, ending one second after the last instance:

"avg": [[24.433333333333334, "2026-08-26T08:00:00Z", "2026-08-26T10:00:01Z"]],
"sum": [[73.3, "2026-08-26T08:00:00Z", "2026-08-26T10:00:01Z"]],
"totalCount": [[3, "2026-08-26T08:00:00Z", "2026-08-26T10:00:01Z"]]

Which methods apply to which value type follows Tables 4.5.19.1-1 to -3; a method that does not apply to the attribute's values is a 400. A value carrying a Date, DateTime or Time datatype — written as a JSON-LD typed value, {"@type": "DateTime", "@value": "..."}, or as a string with valueType — is ordered by that datatype, so min and max apply to it and a Time also has an avg, returned as a Time. A plain JSON string is ordered lexicographically and has no average.

On postgres and timescale the aggregation runs in SQL when the query is exact there: no q, geoQ or scopeQ, the page pushed down, no omit, second-granular period, and only numeric or boolean values in the window. Any other shape, and every query on the memory and file drivers, reconstructs the instances and aggregates in the broker. The result is the same; the SQL path was measured at 5.7 s → 0.59 s for 50 entities with 150k instances.

Pagination

One response carries at most nine instances per attribute. Beyond that the broker cuts the whole entity at one instant, answers 206 Partial Content and names the window it served in Content-Range. Twelve hourly instances of a sensor:

HTTP/1.1 206 Partial Content
Content-Range: date-time 2026-08-25T00:00:00Z-2026-08-25T08:00:00Z/*
{"id": "urn:ngsi-ld:TemperatureSensor:qs:2", "type": "TemperatureSensor",
 "temperature": {"type": "Property", "values": [[20.0, "2026-08-25T00:00:00Z"], ..., [28.0, "2026-08-25T08:00:00Z"]]}}

Continue from the instant after the range end; the last page answers 200:

GET …?timerel=after&timeAt=2026-08-25T09:00:00Z&attrs=temperature&format=temporalValues
HTTP/1.1 200 OK
"values": [[29.0, "2026-08-25T09:00:00Z"], [30.0, "2026-08-25T10:00:00Z"], [31.0, "2026-08-25T11:00:00Z"]]

Every attribute is trimmed to the same boundary, so no instance of any attribute falls between two pages. Aggregated representations are computed over the whole evolution and are never cut. Entity-level paging of GET /temporal/entities uses limit/offset and Link rel="next" as the entity endpoints do.

Temporal entity maps

entityMap=true on a multi-entity query pins the matched id set for the following pages, so a client walking rel="next" links sees a stable set even while entities change:

HTTP/1.1 201 Created
Link: </ngsi-ld/v1/temporal/entities?entityMap=true&limit=1&offset=1&…>; rel="next";type="application/json"

The map lives under /temporal/entityMaps/{id} for one hour by default; a client may set expiresAt on the map, capped at 24 hours (6.4.3.2-1). It is stored in the current-state driver, so it survives a restart on file and postgres.

Writing and deleting history

operationclauserequest
create a temporal entity with its instances5.6.11POST $U/temporal/entities
add instances to an attribute5.6.12POST $U/temporal/entities/{id}/attrs with {"temperature": [instance, …]}
delete an attribute's history5.6.13DELETE $U/temporal/entities/{id}/attrs/{attr}, ?datasetId= for one instance set, ?deleteAll=true for every set
modify one instance5.6.14PATCH $U/temporal/entities/{id}/attrs/{attr}/{instanceId}
delete one instance5.6.15DELETE $U/temporal/entities/{id}/attrs/{attr}/{instanceId}
purge an entity's history5.6.16DELETE $U/temporal/entities/{id}

All answer 204; a missing instance or entity is ResourceNotFound 404:

{"detail":"instance urn:ngsi-ld:Instance:cbcc1cff-46bb-4b7f-b82e-f42a60d75542 not found","status":404,"title":"ResourceNotFound", ...}

Purging a temporal entity removes its history only; GET $U/entities/{id} still answers 200 with the current state. Deleting the current-state entity mirrors a deletion instance into history, so the entity's last state remains queryable under timeproperty=deletedAt.

Instances added through the temporal endpoints are stored as sent. Instances the entity endpoints record with an observedAt are keyed on (entity, attribute, datasetId, observedAt), so a sensor re-sending the same measurement replaces the instance instead of duplicating it.

Retention and none

ANTARES_TEMPORAL_RETENTION_DAYS starts a sweep on the postgres or timescale half that drops instances older than the horizon; unset keeps everything. With ANTARES_TEMPORAL=none every temporal endpoint answers OperationNotSupported 422 (Table 6.3.2-1) and the entity endpoints record nothing:

HTTP/1.1 422 Unprocessable Entity
{"detail":"no temporal store is configured","status":422,"title":"OperationNotSupported","type":"https://uri.etsi.org/ngsi-ld/errors/OperationNotSupported"}

/q/health names both halves: "store": "memory", "temporal": "none".