Declaring a view
A view is one flat, analysable table over one entity your App owns. You declare the columns and where each comes from; the platform builds the view.
{
"name": "locations",
"source": "locations",
"key": "id",
"title": "Stock locations",
"description": "One row per location, with its type and market.",
"depends_on": ["zones"],
"roles": {
"time": "created_at",
"entity": "location_id",
"measures": ["capacity"],
"dimensions": ["type", "zone"]
},
"columns": [
{ "name": "location_id", "from": "column:id", "type": "uuid", "optional": false },
{ "name": "code", "from": "column:code", "type": "text", "optional": false },
{ "name": "capacity", "from": "column:capacity", "type": "numeric" },
{ "name": "zone", "from": "relation:zone_id→zones.code", "type": "text" }
]
}
The view fields
| Field | Required | What it does |
|---|---|---|
name | ✓ | Logical view name, lowercase with underscores. Decides the emitted dataset name (below). |
source | ✓ | The entity from your schema.json this view is built from. |
columns | ✓ | The flat columns of the view. At least one. |
key | Primary key column on the source entity. Defaults to id. | |
default_locale | Locale used to collapse attribute:locale.* columns into a single scalar. Defaults to en. Analytics is one row per key, not one row per locale — add explicit columns if you need per-locale breakouts. | |
depends_on | Other source entities this view reads from through relations. Documents the dependency. | |
title, description | How the dataset is labelled in the catalog. Without a title the name is humanised. | |
roles | Semantic roles. See Semantic roles. |
name, source and key are lowercase identifiers: a letter first, then letters, digits and underscores. default_locale is a language code, optionally with a region — en, de, en-GB.
Nothing else is allowed inside a view. A stray field is a validation error, not an ignored hint, which is why pointing your editor at the published schema is worth the one line.
Columns
Each column has a name and a from locator:
| Field | Required | What it does |
|---|---|---|
name | ✓ | The column name in the flat view. Lowercase identifier. |
from | ✓ | Where the value comes from. See Column locators. |
type | The type the column is cast to. Inferred when you leave it out. | |
optional | Whether the column may be null. Defaults to true. |
Set "optional": false on the columns that can never be null. It is the one piece of information the studio cannot guess, and it is what stops a chart quietly dropping rows.
Declaring type is worth it for anything you will aggregate or scope by time. The inference is good — text for strings and attributes, the source column's type for column:, a text array for array relations — but an explicit numeric or timestamptz documents your intent and protects against a schema change underneath you.
What flattening means
Your entity is not flat. It has scalar columns, but also attributes held in structured buckets, relations to other entities, and references to code lists. A view is flat: one row per key, one scalar per column.
So flattening is the work the locators do for you:
- A column is taken as it is.
- An attribute is lifted out of its bucket into a column of its own — and a localised one is resolved at
default_locale, because a row cannot hold every language at once. - A relation to one row is followed, and one column is taken off the far side.
relation:zone_id→zones.codegives you the zone's code as a plain text column, so an operator groups by a readable value instead of a foreign key. - A relation across a join is aggregated into an array column, so a row keeps its cardinality instead of fanning out.
That last point is the grain rule restated: a view never multiplies rows. If you need one row per line item rather than per order, that is a second view over the line-item entity, not a relation on the first one. See Modelling for why mixing grains produces plausible wrong numbers.
The emitted dataset name
The name follows a fixed pattern from your manifest's vendor and app name plus the view name:
analytics.<vendor>__<app>__<view>
So a view named locations in the acme/warehouse App is registered as analytics.acme__warehouse__locations.
Two consequences worth planning for:
- The view name is part of a public identifier. Renaming a view renames the dataset, and anything an operator built on the old name is looking at a dataset that no longer exists. Name it for what it is, not for how it is currently implemented.
- Names have to be unique within your app, and they are namespaced by vendor and app, so you cannot collide with the platform's own datasets or another vendor's.
How many views
One view per grain, and per audience question. A warehouse app might reasonably ship three: locations, movements, and stock levels. What does not work is one wide view that tries to be all three — the roles cannot be right for all of them, and neither can the grain.
Where to go next
- Column locators — the
fromvocabulary in full. - Semantic roles — making the view usable without configuration.
- Example — a complete file, walked line by line.