Skip to main content

Time-series

Time-series metadata, datapoint retrieval, and datapoint ingestion (single-request or high-throughput).

A series' externalId identifies it: unique per tenant, compared without case, and stored exactly as you send it. External ids & naming →

Create a series

Timeseries series = new Timeseries()
.setExternalId("engine_temperature")
.setName("Engine temperature");
series.setUnit("celsius");

client.timeseries().create(List.of(series));

Value types

Every series has a value type that decides how its datapoints are stored. Leave it unset and the series is floating-point (float32) — right for most sensor readings, so the create above accepts decimal values as-is. Set it explicitly when you need something else:

Value typeUse it for
float32 (default)Sensor readings — 32-bit precision is plenty.
floatDouble-precision floating point.
numeric / decimal32Exact decimals — money, lab values — stored without floating-point rounding. Pass the values as strings.
bigintWhole numbers (counts, integer statuses).
textNon-numeric string values.
mixedHeterogeneous values in one series.

A float written to a bigint series is rejected, so pick the type that matches the data. For a value that must reconcile exactly, use numeric:

Timeseries price = new Timeseries()
.setExternalId("book_value_usd")
.setName("Book value (USD)");
price.setUnit("usd");
price.setValueType("numeric"); // exact decimals, no float rounding

client.timeseries().create(List.of(price));

Filter series

POST /timeseries/filter finds series by structured criteria. Everything you supply is combined with AND — a series must match every criterion to be included.

CriterionMatching
dataSetIdThe data set and every data set beneath it in the data set hierarchy, so a series attached to a child (or grandchild, …) data set matches too. Each entry is {"id": …} or {"externalId": …}.
unitPattern, case-insensitive. * and % are wildcards, _ is literal ("cel%").
unitExternalIdPattern on the unit-catalogue external id (e.g. temperature_deg_c), on the same rules.
valueTypeExact, case-insensitive, against the closed catalogue: BIGINT, FLOAT, FLOAT32, NUMERIC, DECIMAL32, TEXT, MIXED. Not a pattern.
id, externalId, name, sourceThe shared node criteria. Patterns, on the same rules as unit.
labelsSeries carrying all of these labels.
metadataEvery key/value pair given must be present. A null value matches the key alone, whatever it holds.
createdTime, lastUpdatedTime{ "min": …, "max": … } bounds.

Each field above except labels and metadata takes either a bare value or an array, and the entries of an array are combined with OR. That is why they are named in the singular: "unit": "celsius" is the common case, and "unit": ["celsius", "kelvin"] asks for either. labels and metadata require all entries to match and keep their plural names for that reason.

Results come newest first unless you ask for another order — see sorting and paging — capped by limit (default 1000, max 10000; a value <= 0 falls back to the default, and above the ceiling is a 400). Series in data sets you lack read access to are silently omitted — the result is what your token may see, not an error. For free-text lookups use POST /timeseries/search instead.

The metadataKey / metadataValue pair is gone

It existed only because metadata could not express "has this key, whatever its value". A null value in the map says that now, and {"health": "good", "tier": null} asks for both conditions at once.

import ai.intellistream.datahub.models.datafilters.TimeseriesFilter;

TimeseriesFilter criteria = new TimeseriesFilter();
criteria.setDataSetId(List.of(IdCollection.createFromId(12L))); // and every data set beneath it
criteria.setUnit(List.of("celsius"));

DataWrapper<Timeseries> series = client.timeseries().filter(criteria);

Pass a TimeseriesRetreiver instead of the bare criteria to set an explicit limit.

The hierarchy expansion is what makes "master" data sets useful: filter on the top-level data set of a site or project and you get the series of the whole family beneath it, without knowing (or maintaining a list of) the sub-data sets.

Sorting and paging

The three node filters — /timeseries/filter, /resources/filter and /datasets/filter — share this contract. (/events/filter works the same way over its own columns; see events.)

Order a page with sort, over id, externalId, name, source, description, createdTime, lastUpdatedTime or dataSetId. The default is createdTime descending — newest created first.

{ "filter": { "unit": "celsius" },
"sort": { "property": ["name"], "order": "asc" },
"limit": 100 }

Only the first property is used, and id is appended behind it: a sort column alone is not a position unless it is unique, and a page boundary inside a run of equal values repeats or drops exactly those rows. An unrecognised property falls back to the default rather than being rejected, and any order that is not exactly desc sorts ascending. Nulls sort last ascending, first descending — most of these columns are nullable, since every node type shares one table.

A page that has a successor carries a nextCursor. Echo it back as cursor to continue:

{ "filter": { "unit": "celsius" },
"sort": { "property": ["name"], "order": "asc" },
"cursor": "djE6bmFtZXxhc2N8N3x2YQ",
"limit": 100 }

The cursor is opaque — base64 of a versioned encoding carrying the sort, the boundary value and the id — so do not build or parse one. Send it with the same sort that produced it; a cursor is a position in one particular order, and continuing it under another is refused. One that does not decode restarts the walk from the first page rather than failing.

nextCursor is absent on a short page, so "keep going while it is present" is the whole loop. A full page may still be the last, so a complete walk ends with one empty request.

TimeseriesRetreiver retriever = new TimeseriesRetreiver();
retriever.getSort().setProperty(List.of("name"));
retriever.getSort().setOrder("asc");

DataWrapper<Timeseries> page = client.timeseries().filter(retriever);
while (page.getNextCursor() != null) {
retriever.setCursor(page.getNextCursor());
page = client.timeseries().filter(retriever);
}

Delete a series

Deletes the series and its datapoints. Remove any referencing subscriptions (and edges) first, or the backend responds 409.

The definition is gone when the call returns; the datapoint purge is handed off and completes shortly after. Nothing can read those datapoints in the meantime, because every read resolves the series first.

import ai.intellistream.datahub.models.IdCollection;

client.timeseries().delete(List.of(IdCollection.createFromExternalId("engine_temperature")));

Write datapoints

A datapoint is a (timestamp, value) pair grouped under a series' external id.

Timestamps are epoch milliseconds as strings:

DatapointsCollection collection = new DatapointsCollection();
collection.setExternalId("engine_temperature");
collection.setDatapoints(List.of(
new DatapointString(String.valueOf(System.currentTimeMillis()), "92.4")));

client.timeseries().insertDatapoints(List.of(collection));

High-throughput ingestion

For large or unbounded volumes the SDK chunks and sends in bulk. See the ingestion guide for the full story.

Survive outages with durable buffering

Enable durable buffering on the client and datapoint ingestion that can't reach the API spools to disk and flushes on the next call, bounded by a time and/or size window. Retries are idempotent (datapoints dedup on (series, timestamp)).

ingest chunks, parallelises and retries, returning an IngestResult tuned with IngestOptions:

IngestResult result = client.timeseries().ingest(data,
IngestOptions.builder()
.batchSize(10_000) // datapoints per request
.parallelism(16) // concurrent in-flight requests
.maxRetries(3)
.build());

System.out.printf("ingested %,d, failed %,d%n", result.succeeded(), result.failed());

Retrieve datapoints

Identify a series (external id or id) and a time window.

import java.time.ZonedDateTime;

RetrieveFilter series = new RetrieveFilter();
series.setExternalId("engine_temperature");
series.setStart(ZonedDateTime.now().minusHours(1));
series.setEnd(ZonedDateTime.now());
series.setLimit(1000);

DataRetriever<RetrieveFilter> request = new DataRetriever<>();
request.setItems(List.of(series));

DataWrapper<DatapointsCollection> points = client.timeseries().retrieve(request);
points.getItems().forEach(c ->
System.out.println(c.getExternalId() + ": " + c.getDatapoints().size() + " points"));

Delete datapoints

Clears part of a series and leaves the definition alone. To remove the series itself, see delete a series.

Each item names one series by externalId or id, and both window bounds are optional:

Bounds givenWhat is deleted
inclusiveBegin and exclusiveEndThe half-open window between them
inclusiveBegin onlyEverything from that instant onward
exclusiveEnd onlyEverything before that instant
NeitherEvery datapoint of the series, leaving its definition, edges and subscriptions

A bound is either ISO-8601 or epoch milliseconds; anything else is a 400 naming the field, as is a series that does not exist. Python, Rust and Java's Instant overload take real datetimes, so those always send the ISO form.

Like a series delete, this is handed off and completes shortly after the call returns, and it cannot be undone.

import java.time.Instant;

client.timeseries().deleteDatapoints(
"engine_temperature",
Instant.parse("2026-01-01T00:00:00Z"),
Instant.parse("2026-02-01T00:00:00Z"));

// A null bound leaves that side open, so two nulls empty the series:
client.timeseries().deleteDatapoints("engine_temperature", null, null);

For several series at once, or to name one by id, pass DeleteDatapoint items instead:

DeleteDatapoint window = new DeleteDatapoint();
window.setId(7L);
window.setInclusiveBegin("1767225600000"); // epoch millis is the other accepted form

client.timeseries().deleteDatapoints(List.of(window));

IngestOptions

The Java ingest tuning knobs (Python's insert_from_lists and Rust's insert_datapoints batch internally):

OptionDefaultMeaning
batchSize10_000Maximum items per request.
parallelism8Concurrent in-flight requests.
maxRetries3Retries for transient failures (HTTP 429/5xx, network).
failFastfalseIf true, abort on the first failed batch instead of collecting errors.

IngestOptions.defaults() returns the defaults; ingest(data) (no options) uses them.

IngestResult

long succeeded() // items ingested
long failed() // items that could not be ingested
long buffered() // items spooled to the durable buffer (0 unless buffering is on)
boolean isComplete() // true when nothing failed and nothing was buffered
List<BatchError> errors() // one entry per failed batch

BatchError is a record (int datapointCount, int statusCode, String message)statusCode is 0 when the failure was a network error rather than an HTTP status.

if (!result.isComplete()) {
result.errors().forEach(e ->
System.err.println(e.statusCode() + " on " + e.datapointCount() + " items: " + e.message()));
}

What each client covers

OperationJavaPythonRust
Createtimeseries().createtimeseries.createtime_series.create / create_one
Look up by id / external idtimeseries().byIdstimeseries.by_idstime_series.by_ids
Filtertimeseries().filtertimeseries.filtertime_series.filter
SearchHTTPtimeseries.searchtime_series.search (+ _by_name / _by_description)
ListHTTPtimeseries.listtime_series.list / list_with_limit
UpdateHTTPtimeseries.updatetime_series.update
Deletetimeseries().deletetimeseries.deletetime_series.delete
Write datapointsinsertDatapoints / ingestinsert_datapoints / insert_from_listsinsert_datapoint / insert_datapoints
Read datapointsretrieve / retrieveAggregatedretrieve_datapoints / retrieve_latest_datapointsretrieve_datapoints / retrieve_latest_datapoint
Delete datapointsdeleteDatapointstimeseries.delete_datapointstime_series.delete_datapoints

Java is the one with ingest — the chunking, parallelising, retrying path described above. It is also the one missing search, list and update, so reach for the endpoint there.