Semantic roles

The four roles — time, entity, measures and dimensions — what each one unlocks in the studio, the type-heuristic fallback when you omit them, and the rules they must obey.

roles is what turns a flat table into something the studio can drive. Every role is optional — when one is absent the studio falls back to guessing from column types — but declaring them is the difference between a dataset that works out of the box and one an operator has to configure by hand.

analytics.json
{
  "roles": {
    "time": "created_at",
    "entity": "location_id",
    "measures": ["capacity"],
    "dimensions": ["type", "zone"]
  }
}

The four roles

time

The primary time column. Enables date-range scoping and time-series trends — which is to say, almost everything an operator does first. A dataset without a time role is a table they can look at; a dataset with one is a report.

Declare it as timestamptz or date. Pick the timestamp that answers "when did this happen", not "when did we last touch the row": a created_at makes a meaningful trend, an updated_at makes a trend of your own write pattern.

entity

The key to group by for cohort analysis — a customer id, a product id, a location id. This is what a cohort needs, alongside time: without both, a cohort cannot be built over your view at all. See Cohorts.

It is the identity of the thing being followed over time, which is not always the view's key. In a movements view keyed on a movement id, the entity is more usefully the product or the location.

measures

The numeric columns that are meaningful to sum or average. These become the metrics an operator can put on a chart.

Be selective. An id that happens to be an integer is not a measure, and a year is not a measure. If summing a column would produce a number nobody wants, leave it out — declaring it invites somebody to chart it.

dimensions

The columns that are meaningful to group or segment by — a type, a status, a zone, a category.

Prefer columns with modest cardinality and readable values. This is why relation and reference locators matter: zone resolved to a code is a usable dimension, and zone_id as a UUID is a list of things nobody recognises.

The rule

Every name in roles must refer to a column you declared above it. A role naming a column that is not in columns is a validation error, and there is no way to reference something that exists in your entity but not in the view.

time and entity take one column name each; measures and dimensions take arrays. Nothing else is allowed inside roles.

The fallback, and why not to rely on it

When a role is absent, the studio guesses from column types — a timestamp column looks like a time axis, a numeric column looks like a measure, a low-cardinality text column looks like a dimension.

The guess is reasonable and it is not good enough:

  • It cannot tell which of three timestamps is the meaningful one.
  • It cannot tell an id from a measure when the id is numeric.
  • It has no concept of your domain, so it will offer to average things that should be summed and to sum things that should be neither.
  • It cannot produce an entity, so cohort analysis over your view stays unavailable.

Declaring roles costs four lines and is the difference between a dataset an operator uses and one they open once.

Choosing roles for a view

A short checklist that produces a usable dataset:

  1. One time — the timestamp that answers "when did this happen".
  2. One entity — the thing somebody would follow over time.
  3. Measures: only what should genuinely be summed or averaged, cast as a numeric type.
  4. Dimensions: only readable, modest-cardinality columns — codes and labels, not ids and not free text.
  5. Read the list back as a sentence: "count/sum of MEASURES by DIMENSIONS over TIME, per ENTITY." If that sentence is not something a person would ask for, the roles are wrong.

Where to go next

Was this page helpful?