Custom object types
Extend the catalog beyond sites and assets with your own record types and attribute schemas.
sites and assets are the built-in object types. A workspace adds its own: metering points, grid zones, plants, contracts, whatever your data is about. A custom type gets the same generic routes as the built-ins, its records can hold or reference other records, and they carry timeseries exactly like sites do.
Define a type
r = requests.post(f"{BASE}/objects", headers=HEADERS, json={
"name": "meters",
"singular": "Meter",
"plural": "Meters",
"icon": "🔌",
"description": "Grid metering points",
"attributes": [
{"slug": "ean", "title": "EAN", "type": "text", "required": True},
{"slug": "voltage", "title": "Voltage level", "type": "select",
"options": ["LV", "MV", "HV"]},
{"slug": "zone", "title": "Price zone", "type": "object", "targets": ["zones"]},
],
})
r.raise_for_status()nameis the URL slug (/objects/meters) and is immutable once created. Built-in slugs and reserved words are rejected. Prefer kebab-case for multi-word slugs.singularandpluralare display labels.- Each attribute has a
slug(the key undervalues), atitle, atype, an optionaldescription, andrequired(defaultfalse). Any slug is legal, becausevaluesnever shares a namespace with the envelope.
Attribute types
| Type | Value under values |
|---|---|
text | String. |
number | Number. |
boolean | true or false. |
select | One of the strings in options. |
date | ISO 8601 date, for example 2024-06-01. |
timestamp | ISO 8601 timestamp with a UTC offset. |
geometry | A GeoJSON geometry in WGS84 (lon, lat): Point, LineString, Polygon, or their Multi forms. Validated on write and stored verbatim. |
object | One other record. Written as its id, read back as {"id", "name", "object"}. |
objects | A list of other records. |
between | Exactly the two records each record of this type sits between. One per type. |
The last three hold records rather than values. object and objects take targets (the allowed types, or null for any) and a relation: part or reference. Linking records covers them and the between attribute in full. required attributes are enforced on record create.
A geometry attribute is what makes a record mappable and what the GeoJSON export builds features from:
requests.post(f"{BASE}/objects/zones", headers=HEADERS, json={
"name": "SE3",
"values": {
"country": "SE",
"area": {"type": "MultiPolygon", "coordinates": [[[[15.1, 58.2], [18.9, 58.2], [18.9, 61.0], [15.1, 61.0], [15.1, 58.2]]]]},
},
}).raise_for_status()Records of a custom type
Records use the generic routes with the same name plus values shape as every other record. The type's attributes are keys under values.
r = requests.post(f"{BASE}/objects/meters", headers=HEADERS, json={
"name": "Meter 7071",
"values": {
"ean": "735999100000000001",
"voltage": "MV",
"zone": se3_id,
},
})
r.raise_for_status()
meter = r.json()
print(meter["values"]["zone"])
# {"id": "...", "name": "SE3", "object": "zones"}Unknown keys under values are rejected. An object attribute is written as the target's id; the target must exist (404 otherwise) and be of a declared type (422). List, get, patch, and delete work as described in Objects and records, and on custom types ?filter[<slug>]= narrows the list server-side:
r = requests.get(f"{BASE}/objects/meters", headers=HEADERS,
params={"filter[voltage]": "MV", "filter[zone]": se3_id})Timeseries attach to custom records exactly like to sites:
requests.post(f"{BASE}/objects/meters/{meter['id']}/timeseries", headers=HEADERS, json={
"type": "consumption",
"shape": "singleindex",
"unit": "kWh",
"points": [{"valid_time": "2024-06-01T00:00:00Z", "value": 412.0}],
}).raise_for_status()Evolve a type
PATCH /objects/{name} updates the display fields (singular, plural, description, icon) and the attribute list. The slug cannot change.
requests.patch(f"{BASE}/objects/meters", headers=HEADERS, json={
"attributes": [
{"slug": "ean", "title": "EAN", "type": "text", "required": True},
{"slug": "voltage", "title": "Voltage level", "type": "select",
"options": ["LV", "MV", "HV"]},
{"slug": "zone", "title": "Price zone", "type": "object", "targets": ["zones"]},
{"slug": "grid-area", "title": "Grid area", "type": "text"},
],
}).raise_for_status()attributes replaces the whole list
When attributes is present in a PATCH it replaces the type's attribute list entirely, so include every attribute you want to keep, not just the new one.
Records are never rewritten on a type edit. Values of a removed attribute are kept but no longer validated or returned, and re-adding the slug brings them back. Adding a required attribute does not retro-validate existing records. The one attribute that cannot change after create is between, because it decides how the type's records are stored; only its title and description may be edited.
Remove a type
DELETE /objects/{name} removes a custom type. While the type still has records, or while another type's attribute targets it, the request is refused with 409. Pass force=true to delete the records and their attached series first and leave the other types' declarations dangling:
requests.delete(f"{BASE}/objects/meters", headers=HEADERS,
params={"force": "true"}).raise_for_status()Built-in types cannot be modified or deleted; attempts return 403.