Semantic Model
The unit that binds one warehouse table to its dimensions, measures, and grain.
A semantic model binds one warehouse table (or view) to a named set of dimensions and measures. There is one model per source table. Every query is rooted in one or more models.
Minimal example
apiVersion: context-engine/v1
kind: SemanticModel
metadata:
id: 7d4c1e2f-1a3b-4c8e-9f10-2a3b4c5d6e7f
name: orders
spec:
asset_qualified_name: ANALYTICS.PUBLIC.FCT_ORDERS
description: Per-order facts. One row per order_id.
grain:
- [order_id]
default_time_dimension: order_date
dimensions:
- name: order_id
type: categorical
description: Unique order identifier (the table's grain).
- name: order_date
type: time
sql: ordered_at
time_grains: [day, week, month]
description: Date the order was placed.
- name: order_status
type: categorical
sql: status
description: Order lifecycle state.
measures:
- name: order_count
measure_type: aggregate
agg: count
sql: order_id
description: Number of orders.
- name: order_total
measure_type: aggregate
agg: sum
sql: order_amount
description: Sum of order amounts.spec fields
| Field | Required | Notes |
|---|---|---|
asset_qualified_name | Yes, unless the model is baseless | Warehouse-native three-part name (DATABASE.SCHEMA.TABLE) of the table this model sits on. Omit it only for a baseless model — one with no base table, served entirely by pre-aggregations. |
description | Yes | One paragraph describing the grain and what the table represents. Used to ground AI-generated queries. |
grain | Yes | List of column-name lists; each inner list is one valid unique-key combination (e.g. [[order_id]], [[user_id, day]]). Every column named must be a dimension on this model. See Grain. |
default_time_dimension | Yes | The time dimension used when a query doesn't specify one. Must reference a time dimension on this model. |
dimensions | Yes (≥ 1) | See Dimensions. |
measures | Optional (default empty) | A model may have only dimensions — e.g. a lookup table used purely for joins. See Measures. |
label | Optional | Display name. |
owner | Optional | Team or person. |
tags | Optional | List of strings for organizing and filtering. |
The model's name lives at metadata.name — do not also write spec.name.
Grain
grain is the column combination that is unique across every row of the table — the true unique key, not just "the columns that describe a row" (those are dimensions). Declare it accurately: too broad a key double-counts your measures, and a grain that doesn't line up across tables prevents joins that should work.
Foreign-key columns (e.g. customer_id on an orders table) are declared as categorical dimensions but are not part of the grain — they are join keys to other models, not this table's unique key.
Joins are grain-based
There are no entity types and no explicit join declarations. Two models join automatically when a dimension on one shares a name with a dimension on the other at a compatible grain — the layer aggregates each model to the shared dimension and combines the results.
When authoring:
- To make two models joinable on
customer_id, declare a dimension namedcustomer_idon both. If the names don't match, they simply won't join. - Changing a model's grain (e.g.
[[user_id]]→[[user_id, day]]) can change which joins are possible, so it may affect cross-model calculated measures that relied on the old grain.
Cross-model calculated measures (e.g. orders.revenue / inventory.cost) rely on this same shared-dimension join — see Measures → Calculated.
Still have questions?