Types: how to create and manage them

In Relata, a type is what other databases call a table — think of it as a tab in a spreadsheet. Unlike a traditional database you never run CREATE TABLE: you register types at runtime through the API or an SDK, change them online while the server is running, and the same types are instantly usable from every door (HTTP, psql, S3, MongoDB, your AI agent…).

This page walks through the whole lifecycle with copy-paste examples. No database administration background needed.

"I want to create my first type"

With the Python SDK (see Python SDK):

client.register_type(
    "Customer",
    description="People who buy from us",
    owner="growth-team",
    properties={
        "id":    {"type": "text"},
        "name":  {"type": "text"},
        "email": {"type": "text"},
    },
)
# {"created": True, "name": "Customer"}

The same call over plain HTTP (what the SDK does under the hood):

curl -X POST http://localhost:9090/types \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
  -d '{
        "name": "Customer",
        "description": "People who buy from us",
        "properties": {
          "id":    {"type": "text"},
          "name":  {"type": "text"},
          "email": {"type": "text"}
        }
      }'
Tip
Naming rules: start with a letter or _, continue with letters, digits, or _, up to 128 characters. Customer, sanctions_hit, and Order2026 are valid; 2026Orders and my-type are not (you'll get a clear 400 error).
Warning
Registering and deleting types is an admin operation — it needs the admin token (RELATA_ADMIN_TOKEN on server/cluster profiles), not the regular bearer token. Reading and writing rows uses the normal token.

"How do I see what I've registered?"

client.list_types()
# {"types": [{"name": "Customer", "rows": 4}, {"name": "Order", "rows": 12}, ...]}
 
client.type_detail("Customer")   # full schema + row stats for one type

Or: GET /types and GET /types/Customer over HTTP. The detail view includes the properties, owner, and row count — handy sanity check after ingest.

"I need another column — do I have to take the server down?"

No. Schema changes are online; existing rows keep working:

# Add a column (existing rows get it as empty)
client.schema_alter("Customer", "add_column", "tier", col_type="text")
 
# Rename a column (values move with it)
client.schema_alter("Customer", "rename_column", "tier", new_column="plan_tier")
 
# Retype a column
client.schema_alter("Customer", "change_type", "plan_tier", col_type="float")
 
# Drop a column
client.schema_alter("Customer", "remove_column", "plan_tier")
Note
The action names are the server's four exact verbs: add_column, remove_column, rename_column, change_type. (Some older examples elsewhere show shortened names like "add" — those are rejected with a 400; use the full names.)
Tip
Add first, backfill second. Because rows are bi-temporal, the add is instant and history stays intact — you can then backfill values at your own pace and still query "what did this row look like before the backfill?"

"How do I connect two types?" (relationships)

Register a typed edge — a named, directional link between types that the graph operators (MATCH, path queries, PageRank…) understand:

client.register_edge_type("Customer", "Order", "PLACED")
# {"from_type": "Customer", "to_type": "Order", "label": "PLACED"}
 
client.list_edge_types()   # inspect what exists

"My new type returns 403 when I query it"

Custom types aren't readable until you grant them. One line in your environment fixes it:

RELATA_ACL_GRANT="Customer:read+write" relata serve   # restart required

Full walkthrough (including read-only sharing and column hiding): Access Control & Permissions.

"I want to remove a type"

client.deregister_type("ExperimentalType")
# {"deleted": True, "name": "ExperimentalType"}
Danger
Deleting a type drops every row of that type immediately and permanently — this is not "hide it", it is gone (only the audit chain still proves it existed). Export first if you might need the data: client.export_data("ExperimentalType", format="json").
Tip
Not sure a type is safe to delete? client.type_detail(name) shows its row count first — anything non-zero deserves an export.

The long tail (optional extras)

Computed columns, state machines, search tuning, auto-detection

POST /types accepts more than a name and properties:

  • Computed columns — values derived from other columns (concat of fields, or a static), maintained automatically. Cyclic definitions (A needs B, B needs A) are rejected up front with a 400.
  • State machines — constrain a status column to legal transitions (e.g. new → reviewed → closed), so an illegal update fails at write time.
  • BM25 tuning — per-type bm25_params (k1, b) to shape keyword-search ranking for that type's text.
  • Redefinitions — changing a computed-column formula or state machine on a type that already has rows returns a 409 unless you pass "force": true (existing rows are never silently recomputed — you opt in).

Two companion endpoints hang off the same surface:

  • GET/POST /types/{name}/detect-config — per-type SmartIngest detector packs, so Customer documents auto-detect your internal account-number format on ingest.
  • GET/POST /types/{name}/embed-config — per-type embedding configuration for vector search.

And when you want to land a whole ontology (types + links + constraints) in one governed, SHACL-validated call:

client.ontology_migrate({
    "types": [
        {"name": "Customer", "properties": {"email": {"type": "text"}}},
        {"name": "Order",    "properties": {"total": {"type": "float"}}},
    ],
    "links": [
        {"from": "Customer", "to": "Order", "label": "PLACED"},
    ],
})

Next: Ingest data — put rows into your new type, or Schema cookbook for the condensed SDK cheat-sheet.