Entity historic data


Entity historic records are time-stamped snapshots of an Entity's data. You can assign a set of your attributes to be tied to one of your date-time attributes, and the system will automatically create a new historic record each time the date-time attribute changes.

You can have multiple date-time attributes that create different historic records for different sets of attributes.

We call these date-times "Historic keys", and attributes that change "Historic attributes".


You can flag an attribute as tied to a historic key by setting the HistoricKey property on the attribute in the Entity Type's Data Schema.

See below a preview of the Data Schema definition that you can find more information about in the Entity Type Data Schema documentation.

export interface IAttribute {
    ...

    // The historic key (if any) this attribute is associated with.
    // This is a date-time attribute that drives the change for this attribute value.
    // Attribute path segments are separated by a forward slash. Eg: 'address/city'.
    HistoricKey?: string;
}

Once set, updating Entities in that Entity Type will check for valid date-time values in that key and create/update historic records based on the changes.


If you update an Entity with no valid date-time, then the default attribute values are updated within the 'normal' Entity record.

These default values are returned when querying an Entity without a timestamp, or when no historic records are found for the timestamp provided.

Note that there is a default limit of 1,000 historic records per Entity ID unless an upgraded limit is discussed.

To request for historic data, simply use the "HistoricPoint=an_iso_8601_date_time" query parameter during Entity or DataLab requests.

The latest historic record to that point in time will be returned. Records in the future will not be returned.

Note that historic records are applied to found Entity records after the filtering and sorting is done.

Returned records will have additional metadata on what historic records were overlaid on the Entity record.

Below is a preview of how an Entity's data structure is extended to include historic metadata.

interface IEntity {
    // Bruce is a code-word for the Nextspace API.
    // These are internal fields.
    Bruce: {
        // Outline on what parts of the Entity were loaded from where.
        // This is a preview on what properties are available (focused on Historic here).
        "Outline"?: {
            // Indicates that this is the baseline source of Entity data.
            // If true, then any attributes not specified by another source are assumed to be from this one.
            "Baseline"?: boolean;
            // Human readable name of the source.
            "Source.Name": string;
            // Foreign-key, for historic data this is the date-time attribute path.
            "ForeignKey"?: string;
            // DateTime representing the time of the record.
            "DateTime"?: string;
            // Indicates that this source is editable.
            // When false, inputs for the related attribute are disabled.
            "Editable": boolean;
            // Array of attribute paths that were loaded from this source.
            // If Baseline=true, then all attributes that weren't specified are loaded from this source.
            "Attribute"?: string[];
        }[];
    }
}

Work with records directly

Overlaying a point in time answers what an Entity looked like then. To work with the records themselves, reading a series or writing one, use the endpoints below.

A record is shaped like the Entity it belongs to. Your attribute values sit at the root of its Entity object and platform managed values sit under that object's Bruce key, exactly as they do on a live Entity. The same shape is returned by a read, accepted by a write, and accepted by a delete, so a record can be moved between them without translation.

interface IHistoricRecord {
    // ID of the Entity this record belongs to.
    "Entity.ID": string;
    // Quoted path of the attribute history is tracked against.
    // Eg: '"reading_taken"', or '"readings"/"taken"' when nested in a Structure.
    "AttrKey": string;
    // ISO 8601 date-time for when these values applied.
    "DateTime": string;
    // The Entity as it was at that date-time.
    "Entity": {
        // Platform managed values as at that date-time.
        "Bruce"?: {
            "Location"?: any;
            "Transform"?: any;
            "Boundaries"?: any;
            "VectorGeometry"?: any;
        };
        // Your attribute values sit alongside it, at the root.
        [attribute: string]: any;
    };
    // Scenario this record belongs to, if any.
    "Scenario"?: string | number;

    // Read-only.
    "Created"?: string;
    "Updated"?: string;
    "Entity.InternalID"?: number;
}

Together, Entity.ID, AttrKey and DateTime are what identify a record. That is all a delete needs, and it is why a record read back can be handed straight to one.

Get records

There are three ways to read, differing only in how the Entities are named. Use the body form when the list of Entity IDs is long enough to be awkward in a url.

Get historic records for one Entity

Response
interface IResponse {
    // Records for this page. See IHistoricRecord above.
    "Items": IHistoricRecord[];
    "PageIndex": number;
    "PageSize": number;
}
Javascript example

Get historic records for many Entities

Response
interface IResponse {
    "Items": IHistoricRecord[];
    "PageIndex": number;
    "PageSize": number;
}
Javascript example

Get historic records with a request body

Request body
interface IRequest {
    // IDs of the Entities to read records for.
    // A single ID may be sent as a string.
    "Entity.ID": string[];

    // Quoted paths of the attributes history is tracked against.
    // A single path may be sent as a string.
    "AttrKey"?: string[];
    // ISO 8601 date-time for the inclusive start of the range to read.
    "DateTimeFrom"?: string;
    // ISO 8601 date-time for the inclusive end of the range to read.
    "DateTimeTo"?: string;

    // ISO 8601 date-time to read only records written after.
    "CreatedAfter"?: string;

    // Scenario to read records for. Omit for real world data.
    "Scenario"?: string | number;

    "OrderBy"?: "ID" | "DateTime" | "AttrKey" | "Created";
    "OrderDir"?: "ASC" | "DESC";
    "PageIndex"?: number;
    // Capped at 1,000.
    "PageSize"?: number;

    // Returns counts across the matched range instead of the records themselves.
    // Use it to find where history exists before paging through it.
    "Analysis"?: boolean;
}
Response
interface IResponse {
    "Items": IHistoricRecord[];
    "PageIndex": number;
    "PageSize": number;
}

Update records

Writing a record directly is how you add history the platform did not capture itself, such as a backfill from another system.

The date-time attribute named by AttrKey should also appear in the recorded values, since that attribute is what the record is keyed on.

A record that cannot be written does not fail the request. It is named in Warning and the remaining records still land, so check that rather than assuming every record was accepted.

Note that there is a default limit of 1,000 historic records per Entity ID unless an upgraded limit is discussed. Past that, the oldest records are dropped.

Update historic records for one Entity

Request body
interface IRequest {
    // Records to create or update. See IHistoricRecord above.
    "Items": IHistoricRecord[];
    // Scenario to write every record against, instead of naming one per record.
    "Scenario"?: string | number;
}
Response
interface IResponse {
    // The records that were written.
    "Items": IHistoricRecord[];
    // One entry per record that was skipped, naming its position and why.
    "Warning"?: string[];
}

Update historic records for many Entities

Request body
interface IRequest {
    // Records to create or update. Each names its Entity with "Entity.ID".
    "Items": IHistoricRecord[];
    // Scenario to write every record against, instead of naming one per record.
    "Scenario"?: string | number;
}
Response
interface IResponse {
    "Items": IHistoricRecord[];
    "Warning"?: string[];
}
Javascript example

Delete records

A delete either names the records to remove or describes them with a filter. Both endpoints take both forms, and Items wins when a request carries both.

Naming the records takes them in the shape a read returns, so records you just listed can be handed straight back. Only the identifying fields are read, and anything else on the object is ignored.

Filtering uses the same fields a read accepts. Outside a Scenario, AttrKey and both ends of the range are all required. That is deliberate: it is what stops an unbounded delete. Matching no records is not an error.

Delete historic records for one Entity

Request body
// Optional. Without a body, the query params above are the filter.
interface IRequest {
    // The records to remove, as a read returns them.
    // Only "AttrKey" and "DateTime" are needed on each, since the route names the Entity.
    "Items"?: IHistoricRecord[];
    // Scenario every named record belongs to, instead of naming one per record.
    "Scenario"?: string | number;
}
Javascript example

Delete historic records for many Entities

Request body
interface IRequest {
    // Either name the records to remove, as a read returns them.
    // Each needs "Entity.ID" plus "AttrKey" and "DateTime", or "Entity.ID" and "Scenario".
    "Items"?: IHistoricRecord[];

    // Or describe them with a filter.
    // IDs of the Entities to delete records for.
    // A single ID may be sent as a string.
    "Entity.ID"?: string[];
    // Quoted path of the attribute to delete records for.
    // Required unless a Scenario is supplied.
    "AttrKey"?: string;
    // ISO 8601 date-time for the inclusive start of the range to delete.
    // Required unless a Scenario is supplied.
    "DateTimeFrom"?: string;
    // ISO 8601 date-time for the inclusive end of the range to delete.
    // Required unless a Scenario is supplied.
    "DateTimeTo"?: string;

    // Scenario to delete records from.
    // Supplying one relaxes the attribute and range requirements.
    "Scenario"?: string | number;
}
Javascript example

Doing this in Python

The Python library wraps all of the above behind one namespace, including paging, so there is no need to pick between these endpoints yourself. See the Python Entity requests documentation.

import bruce_models as BM

records = list(BM.EntityHistoricData.iterate(
    bruce,
    ["entity-id"],
    attr_key=["reading_taken"],
    date_time_from="2026-01-01T00:00:00Z",
    date_time_to="2026-01-31T23:59:59Z"
))

# Records read can be handed straight back to remove exactly those.
BM.EntityHistoricData.delete_records(bruce, records)