Datasets & Fields
Define data sources and their field structure in semantic domain templates
Datasets
Datasets represent your data sources. A dataset's source says where its rows come from: a table or view (kind: table) or a SQL query (kind: sql).
Table Datasets
A table dataset reads a database table or view:
datasets:
- name: orders
label: "Orders"
description: "Customer order transactions"
analyticsRole: fact
source:
kind: table
connectionId: "conn_abc123"
connectionType: PostgreSQL
schema: public
table: orders
primaryKey: [order_id]
fields:
identifiers: []
facts: []
dimensions: []SQL Datasets
A SQL dataset (also called a virtual dataset) reads the rows a query returns:
You can author SQL datasets entirely from the UI via Add Dataset → SQL Dataset, with no hand-editing of YAML. See Virtual Datasets for the walkthrough.
datasets:
- name: monthly_revenue
label: "Monthly Revenue"
analyticsRole: snapshot
source:
kind: sql
connectionId: "conn_abc123"
connectionType: PostgreSQL
sql: |
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS revenue,
COUNT(*) AS order_count
FROM public.orders
GROUP BY DATE_TRUNC('month', order_date)
primaryKey: [month]
fields:
dimensions:
- name: month
label: "Month"
dataType: date
sourceField: month
facts:
- name: revenue
label: "Revenue"
dataType: number
sourceField: revenue
defaultAggregate: SUM
- name: order_count
label: "Order Count"
dataType: number
sourceField: order_count
defaultAggregate: SUMUse SQL datasets for pre-aggregated data, complex transformations, or combining tables with custom logic.
Dataset Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier (snake_case) |
label | string | Yes | User-friendly display name |
description | string | No | Help text for users and agents |
aiContext | string | object | No | Context for agents and natural-language questions |
analyticsRole | enum | No | Semantic role used by query planning and relationship diagnostics |
source | object | Yes | Where rows come from (see below) |
primaryKey | string[] | No | Field names that identify one row. Defaults to []. See Keys |
uniqueKeys | string[][] | No | Other field-name lists that identify one row |
primaryDateField | string | No | The dataset's default date dimension |
fields | object | Yes | Contains identifiers, facts, and dimensions lists |
source properties:
| Field | Type | Required | Description |
|---|---|---|---|
kind | table | sql | Yes | Table-backed or query-backed |
connectionId | string | Yes | ID of the data connection |
connectionType | enum | Yes | Database type |
database | string | No | Database, catalog, or project (BigQuery, Snowflake) |
schema | string | No | Database schema name |
table | string | No | Table or view name (kind: table). Defaults to the dataset name |
sql | string | Yes for kind: sql | The query that returns the rows |
Supported connectionType values: PostgreSQL, MySQL, MSSQL, BigQuery, Redshift, Snowflake, clickhouse, S3, GoogleSheets, FileUpload, API
Dataset Analytics Role
analyticsRole tells Semaphor what kind of dataset this is. It is not a permission setting and it does not create joins by itself. It helps the semantic query planner, MCP tools, and relationship diagnostics choose safer defaults.
| Value | Use for | Example |
|---|---|---|
fact | Transaction or line-level tables with additive business measures | orders, invoice_lines, scale_tickets |
dimension | Descriptive entity tables used to group or filter facts | customers, suppliers, products |
event | Timestamped activity or log tables | page_events, machine_events |
snapshot | Point-in-time state tables | daily_inventory_snapshot, account_balances |
lookup | Small reference or mapping tables | status_codes, stage_lookup |
bridge | Associative tables connecting entities | user_groups, product_categories |
unknown | Insufficient information | temporary or newly imported tables |
Use fact for the dataset that owns the metric grain, and dimension or lookup for datasets joined to enrich that fact. For example, a supplier concentration analysis should usually start from fact_purchase_line and join dim_supplier, not start from every supplier and join purchases.
analyticsRole is especially useful for MCP and agent-driven analysis. It lets Semaphor expose relationship-aware fields, prefer fact-rooted queries for metrics, and warn or refuse unsafe multi-fact joins that could inflate numbers.
Keys
A key is a list of field names whose values identify one row. Semaphor uses keys to plan joins that don't double count.
primaryKeyis the dataset's main key, for example[order_id], or[order_id, line_number]for a composite key.uniqueKeyslists any other keys, for example a natural key like[order_number].
datasets:
- name: order_lines
label: "Order Lines"
primaryKey: [order_id, line_number]
uniqueKeys:
- [line_uuid]In the editor, marking an identifier as Primary key adds it to primaryKey. In a template, primaryKey is the declaration that counts, so set it on every dataset that has a key.
Relationships should join on a key
A many_to_one relationship's to fields should equal a key of the to dataset. When they don't, the save still succeeds, but you get a relationship_keys_unmatched warning: joins through that relationship may repeat rows and inflate totals. Fix it by joining on the key, or by declaring those fields in uniqueKeys. See Relationships.
Primary Date Field
Each dataset can declare one date dimension as its primary date field. This is the dataset's canonical date: the one dashboard-level features use when they need a "default" date to filter on.
Today it powers the Global Date Filter: a single date range set once on a dashboard that applies to every eligible card, without per-card setup.
Declare it with primaryDateField on the dataset, referencing a dimension by name:
datasets:
- name: orders
label: "Orders"
primaryDateField: order_date
fields:
dimensions:
- name: order_date
label: "Order Date"
dataType: date
sourceField: order_date
- name: ship_date
label: "Ship Date"
dataType: datetime
sourceField: ship_dateIn the semantic domain editor UI, open a dataset's dimensions and toggle Set as primary date field on the column you want. The toggle only appears on date or datetime dimensions.
Rules
A few things to keep in mind when setting a primary date field:
- The field you reference must exist on the dataset.
- It must be a
dateordatetimetype. Other data types aren't eligible, and the toggle won't appear on them. - Renames are safe. If you rename the field later, the
primaryDateFieldreference updates automatically, so no dashboard re-wiring is needed.
Each dataset has one primary date field at a time. To switch to a different field, toggle Set as primary date field on the new date dimension (the previous one clears automatically), or update primaryDateField directly in YAML.
See Global Date Filter for the dashboard-side feature this unlocks.
Fields
Every dataset has three field lists: identifiers, facts, and dimensions. All three work row by row. Aggregates live in the domain's metrics list (see Metrics below).
| List | Holds | Example |
|---|---|---|
identifiers | Key columns, primary or foreign, used for joins | order_id, customer_id |
facts | Row-level quantities you aggregate | amount, quantity, amount_cents / 100 |
dimensions | Row-level attributes you group and filter by | region, order_date, DATE_TRUNC('month', order_date) |
A field name is unique within its dataset, across all three lists and the dataset's metrics.
Data Types
Every field has a logical dataType. You can also record the database's exact type in nativeDataType.
| Property | Values | Used for |
|---|---|---|
dataType | string, number, boolean, date, datetime, json, geo, unknown | How Semaphor treats the field: formatting, filters, date grains |
nativeDataType | Any database type, such as numeric(12,2) or DateTime64(3, 'UTC') | Exact storage details, such as time zones and precision |
facts:
- name: amount
dataType: number
nativeDataType: "numeric(12,2)"
sourceField: amountdataType takes one of the logical types above; a 2.1 template with a database type there (for example timestamp with time zone) is refused when you save. Templates in the 2.0 shape still accept one: Semaphor stores the logical type in dataType and moves the original value to nativeDataType. Sync data types on a dataset reads exact types from the database again.
Identifiers
Identifiers are primary and foreign key fields used for joins:
identifiers:
- name: order_id
label: "Order ID"
dataType: number
sourceField: order_id
type: primary
- name: customer_id
label: "Customer ID"
dataType: number
sourceField: customer_id
type: foreign| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Field name, used in relationships and expressions |
label | string | No | Display name |
description | string | No | Help text |
aiContext | string | object | No | Context for agents |
dataType | enum | Yes | Logical type (see Data Types) |
nativeDataType | string | No | The database's exact type |
type | primary | foreign | Yes | primary for a column of the dataset's key, foreign for a column that points at another dataset |
sourceField | string | No | Physical column name |
hidden | boolean | No | Hide from end users |
tags | string[] | No | Categorization tags |
Declare the key with primaryKey on the dataset (see Keys). Semaphor's fan-out prevention reads it to avoid metric inflation when joining across relationships.
Dimensions
Dimensions are attributes you group and filter by. They appear in GROUP BY clauses:
dimensions:
- name: order_date
label: "Order Date"
dataType: date
sourceField: order_date
granularity: month
format:
type: temporal_bucket
presentation:
mode: pattern
pattern: MMM YYYY
- name: status
label: "Status"
dataType: string
sourceField: status
- name: region
label: "Region"
dataType: string
sourceField: region
isDisplayField: true
- name: order_month
label: "Order Month"
dataType: date
expression: "DATE_TRUNC('month', order_date)"| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Field name |
label | string | No | Display name |
description | string | No | Help text |
aiContext | string | object | No | Context for agents |
dataType | enum | Yes | Logical type (see Data Types) |
nativeDataType | string | No | The database's exact type |
granularity | enum | No | Analytical grain: second, minute, hour, day, week, month, quarter, or year. Valid only for date/datetime; date-only fields reject sub-day grains. |
format | object | No | Structured grouped presentation. Requires a compatible granularity. Omit it, or use presentation.mode: auto, to delegate to viewer preferences. |
isDisplayField | boolean | No | Mark as the default display label for this dataset |
hidden | boolean | No | Hide from end users |
tags | string[] | No | Categorization tags |
sourceField | string | Conditional | Physical column name. Required when there is no expression |
expression | string | Conditional | Row-level SQL expression. Required when there is no sourceField. See Inline Expressions |
A dimension expression is evaluated row by row. Aggregate functions (SUM, COUNT, etc.) are refused with aggregate_in_row_field; an aggregate belongs in metrics.
Grouped temporal formats
Temporal presentation is structured and separate from the canonical grouped value. Use one of these shapes:
# Viewer preference for this grain
format:
type: temporal_bucket
presentation:
mode: auto
# Grain-specific preset
format:
type: temporal_bucket
presentation:
mode: preset
preset: quarter_shortFor a bounded custom pattern:
format:
type: temporal_bucket
presentation:
mode: pattern
pattern: MMM YYYYPreset values are week_of_short, week_of_long, quarter_short,
quarter_long, fiscal_quarter_short, and fiscal_year. Pattern tokens are
YYYY, YY, MMMM, MMM, MM, DD, HH, mm, and ss; Semaphor also
validates that the token combination matches the declared grain. A format
without granularity, a sub-day grain on a date, or a temporal format on a
non-temporal dimension is rejected.
Card-level presentation wins over semantic presentation. When neither is explicit, Semaphor applies the viewer's Display formats preferences.
Facts
A fact is a row-level quantity you aggregate. It reads a column (sourceField) or computes a row-level expression, and defaultAggregate says how a card rolls it up when nothing else is chosen:
facts:
- name: amount
label: "Revenue"
dataType: number
sourceField: amount
defaultAggregate: SUM
- name: quantity
label: "Quantity"
dataType: number
sourceField: quantity
defaultAggregate: SUM
- name: unit_price
label: "Unit Price"
dataType: number
sourceField: unit_price
defaultAggregate: AVG
- name: discounted_total
label: "Discounted Total"
dataType: number
expression: "amount * (1 - discount_rate)"
defaultAggregate: SUM| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Field name |
label | string | No | Display name |
description | string | No | Help text |
aiContext | string | object | No | Context for agents |
dataType | enum | Yes | Usually number |
nativeDataType | string | No | The database's exact type |
sourceField | string | Conditional | Physical column name. Required when there is no expression |
expression | string | Conditional | Row-level SQL expression. Required when there is no sourceField |
defaultAggregate | enum | Recommended | SUM, COUNT, AVG, MIN, MAX, MEDIAN, or DISTINCT |
format | string | object | No | Display format specification |
unit | string | No | Unit label (e.g., "USD", "kg") |
isDefault | boolean | No | Mark as the default measure for this dataset |
hidden | boolean | No | Hide from end users |
filters | array | No | Row conditions applied whenever the fact is aggregated. Column facts only |
tags | string[] | No | Categorization tags |
Every fact needs sourceField or expression. A fact expression works row by row: one that aggregates (SUM(amount)) is refused with aggregate_in_row_field and belongs in metrics.
defaultAggregate is the semantic default
defaultAggregate defines how Semaphor rolls up the fact when a dashboard, MCP call, or agent asks for it without choosing a different aggregate. A fact without one is treated as SUM for compatibility, but new domains should set it explicitly.
Choose the aggregate that matches the business meaning:
| Fact shape | Recommended aggregate | Example |
|---|---|---|
| Additive amount, value, cost, revenue, margin, quantity, duration, or weight | SUM | total revenue, net tons, gross margin value |
| Count or numeric indicator that represents occurrences | COUNT | order count, ticket count |
| Rate, ratio, score, price, or per-unit measurement | AVG | conversion rate, reliability score, unit cost per ton |
| Latest/highest/lowest observed value | MAX or MIN | latest snapshot version, lowest inventory level |
| Median-sensitive distribution | MEDIAN | median handle time |
| Unique-value count | DISTINCT | distinct customers |
Dashboards and MCP can still request a different aggregate for a specific analysis. For example, a fact modeled with defaultAggregate: SUM can be analyzed with an explicit AVG when the user asks for average margin instead of total margin. That choice is query-specific; it does not change the semantic model.
Facts and dimensions both support row-level expressions. See Inline Expressions for syntax and examples.
Metrics
Metrics aggregate rows. They live in the domain's top-level metrics list, not inside a dataset. A metric that aggregates one dataset names it with dataset:
metrics:
- name: revenue
dataset: orders
label: "Revenue"
expression: "SUM(amount)"
dataType: number
- name: average_order_value
dataset: orders
label: "Average Order Value"
expression: "SUM(amount) / NULLIF(COUNT(DISTINCT order_id), 0)"
dataType: numberA metric that combines inputs from several datasets uses inputs instead. See Calculated Metrics for both forms, and Choosing the Right Type for where a new calculation goes.
Context
Context tells agents and natural-language questions how to read an object: what it means, what people call it, and what it answers. You can add it to the domain, a dataset, a field, a metric, a composed dimension, or a relationship. In the editor it's the Context section; in a template it's aiContext.
datasets:
- name: orders
label: "Orders"
aiContext:
instructions: "One row per order. Cancelled orders have status = 'cancelled'; exclude them from revenue questions."
synonyms: [purchases, sales orders]
examples:
- "How many orders did we ship last week?"
- "Which region had the most cancelled orders?"A plain string is shorthand for instructions:
facts:
- name: amount
dataType: number
sourceField: amount
aiContext: "Order amount in USD after discounts."| Property | Editor label | Limit |
|---|---|---|
instructions | Instructions | 2,000 characters |
synonyms | Other names | 50 names, 100 characters each |
examples | Example questions | 10 questions, 300 characters each |
A value over a limit is refused when you save (ai_context_too_long, ai_context_too_many). Empty entries are dropped, and repeated synonyms are kept once. Context is shown to the same audiences as the object's description.
Complete Fields Example
Putting it all together for an orders dataset:
datasets:
- name: orders
label: "Orders"
analyticsRole: fact
source:
kind: table
connectionId: "conn_abc123"
connectionType: PostgreSQL
schema: public
table: orders
primaryKey: [order_id]
primaryDateField: order_date
fields:
identifiers:
- name: order_id
label: "Order ID"
dataType: number
sourceField: order_id
type: primary
- name: customer_id
label: "Customer ID"
dataType: number
sourceField: customer_id
type: foreign
dimensions:
- name: order_date
label: "Order Date"
dataType: date
sourceField: order_date
granularity: day
- name: status
label: "Status"
dataType: string
sourceField: status
- name: channel
label: "Sales Channel"
dataType: string
sourceField: channel
- name: order_quarter
label: "Order Quarter"
dataType: date
expression: "DATE_TRUNC('quarter', order_date)"
facts:
- name: amount
label: "Revenue"
dataType: number
nativeDataType: "numeric(12,2)"
sourceField: amount
defaultAggregate: SUM
- name: quantity
label: "Quantity"
dataType: number
sourceField: quantity
defaultAggregate: SUM