Calculated Dimensions
Build custom groupings with row-level SQL expressions and grain mappings
Calculated dimensions are row-level computed fields that produce grouping values. Unlike metrics (which aggregate numbers into KPIs), dimensions transform each row before any grouping happens. The result appears in the ROWS or COLUMNS area of a card.
Use them to bucket orders by size, truncate dates to months or quarters, combine text fields, or apply any row-level logic that your raw data doesn't already have.
Dimension Expressions vs Composed Dimensions
In a domain, add a new calculated dimension as a dimension with an expression on its dataset (e.g., DATE_TRUNC('month', order_date)). See Inline Expressions.
This page also covers composed dimensions: domain-level row expressions in the template's composedDimensions list, with named inputs and grain mappings for cross-dataset alignment. Existing composed dimensions keep working and can be edited.
Creating in the Explorer
The Explorer lets you create calculated dimensions by writing a SQL expression with field references.
Steps
- Open the Explorer and click + Add Calculated Field
- Choose the Per row type
- Enter a name (e.g., "Order Size Bucket")
- Write your SQL expression using
{FieldName}tokens to reference fields - Click Validate Expression to check for errors
- Select the output type (String, Number, or Date)
- Click Create Dimension
Expression Syntax
Use {FieldName} tokens as placeholders for field references. The tokens are replaced with fully-qualified column names at query time.
CASE WHEN bucketing:
CASE
WHEN {Amount} > 1000 THEN 'Large'
WHEN {Amount} > 100 THEN 'Medium'
ELSE 'Small'
ENDDate truncation:
DATE_TRUNC('month', {Order Date})Combining text fields:
CONCAT({First Name}, ' ', {Last Name})Handling nulls:
COALESCE({Region}, 'Unknown')Example: Order Size Bucket
- Expression:
CASE WHEN {Amount} > 1000 THEN 'Large' WHEN {Amount} > 100 THEN 'Medium' ELSE 'Small' END - Output Type: String
- Result: Each row is categorized as Large, Medium, or Small based on its amount
Expression syntax varies by database. Semaphor validates expressions against your connection's SQL dialect (PostgreSQL, MySQL, BigQuery, etc.).
Not sure if you need a dimension or a metric? See Choosing the Right Type.
Defining in a Template
For version-controlled definitions, bulk configuration, or CI/CD workflows, define calculated dimensions in your domain template.
On a Dataset
Most calculated dimensions belong on their dataset, as a dimension with an expression:
datasets:
- name: orders
fields:
dimensions:
- name: order_month
label: "Order Month"
dataType: date
expression: "DATE_TRUNC('month', order_date)"
granularity: month
format:
type: temporal_bucket
presentation:
mode: pattern
pattern: MMM YYYYThe expression uses the dataset's identifiers and dimensions by name, plus the column in the dimension's own sourceField. See Inline Expressions.
Composed Dimensions
A composed dimension lives in the top-level composedDimensions list. Its expression uses {input_name} tokens, and each input names its dataset:
composedDimensions:
- name: order_month
label: "Order Month"
expression: "DATE_TRUNC('month', {order_date})"
inputs:
order_date: { dataset: orders, field: order_date }
dataType: date
granularity: month
format:
type: temporal_bucket
presentation:
mode: pattern
pattern: MMM YYYYTwo Input Types
Field reference points to a field of a dataset:
inputs:
order_date:
dataset: orders # Dataset name
field: order_date # Field nameDimension reference points to another composed dimension (enables nesting):
inputs:
order_month:
dimension: order_month # Name of another composed dimensionComposed dimensions cannot reference metrics. If you try, validation fails with error E007.
Common Patterns
These patterns work as dataset dimension expressions. Each example shows the dimension entry under fields.dimensions.
Date truncation:
- name: order_quarter
label: "Order Quarter"
dataType: date
expression: "DATE_TRUNC('quarter', order_date)"
granularity: quarter
format:
type: temporal_bucket
presentation:
mode: preset
preset: quarter_shortCASE WHEN bucketing (sourceField lets the expression read the amount column):
- name: order_size
label: "Order Size"
dataType: string
sourceField: amount
expression: |
CASE
WHEN amount > 1000 THEN 'Large'
WHEN amount > 100 THEN 'Medium'
ELSE 'Small'
ENDCombining text fields:
- name: full_name
label: "Full Name"
dataType: string
expression: "CONCAT(first_name, ' ', last_name)"Handling nulls:
- name: region_clean
label: "Region"
dataType: string
expression: "COALESCE(region, 'Unknown')"Grain Mappings
Grain mappings tell Semaphor how a composed dimension maps to columns in other datasets. They are needed when a composed metric uses inputs from multiple datasets and you group by a composed dimension.
The problem: If you define order_month as DATE_TRUNC('month', orders.order_date), Semaphor needs to know what column in the returns dataset represents the same time grain.
grainMappings:
- sourceDimension: order_month # The composed dimension
targetDataset: returns # The other dataset
targetColumn: return_date # The column to DATE_TRUNC in that datasetWhen are grain mappings needed?
Only when all three conditions are true:
- You have a composed metric that spans multiple datasets (cross-dataset metric)
- You're grouping by a composed dimension
- The dimension's time grain needs to be applied to columns in other datasets
Example: Net Revenue (orders.amount - returns.amount) grouped by Order Month requires a grain mapping so Semaphor knows to use DATE_TRUNC('month', returns.return_date) for the returns dataset CTE.
Without the grain mapping, Semaphor wouldn't know how to align the returns data to the same monthly grain as orders.
Field Reference
Composed dimension properties:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique among composed dimensions |
label | string | Yes | Display name |
description | string | No | Help text |
aiContext | string | object | No | Context for agents |
expression | string | Yes | SQL expression with {input} tokens |
inputs | object | Yes | Map of input name to field or dimension reference |
dataType | enum | No | string, number, boolean, date, datetime |
granularity | enum | No | second, minute, hour, day, week, month, quarter, or year; valid only for date/datetime results, and date-only results reject sub-day grains |
format | object | No | Structured temporal_bucket presentation. Requires a compatible granularity; omit it or use automatic presentation to inherit viewer preferences. |
grainMapping | object | No | Maps dimension to columns in other datasets (see Grain Mappings) |
For dataset dimension properties, see Datasets & Fields.
Temporal validation
granularity describes the analytical bucket; format controls only its
display. Semaphor rejects these configurations:
# Invalid: a format cannot exist without a grain
dataType: date
format:
type: temporal_bucket
presentation: { mode: pattern, pattern: MMM YYYY }
# Invalid: date-only fields cannot use sub-day grains
dataType: date
granularity: hour
# Invalid: temporal presentation cannot be attached to a string result
dataType: string
granularity: month
format:
type: temporal_bucket
presentation: { mode: auto }Presentation never changes grouping identity. Query results retain the canonical bucket value and metadata; card or semantic presentation controls the label, and the viewer's Display formats preferences apply when neither layer supplies an explicit format.