Calculated Metrics
Create reusable KPIs as dataset metrics or composed metrics with nested references
Calculated metrics combine existing fields into a single aggregated number: a KPI. They produce values like profit margin, average order value, or conversion rate. The result appears in the VALUES area of a card.
In a domain, every metric lives in one top-level metrics list, in one of two forms:
- A dataset metric aggregates one dataset's rows. It names that dataset with
datasetand uses its field names directly:SUM(amount) / NULLIF(COUNT(DISTINCT order_id), 0). - A composed metric combines named
inputswith{token}syntax. Use it when a calculation spans datasets, or builds on other composed metrics.
Metrics vs Fact Expressions
A metric aggregates. For a row-level transform you want aggregated later (e.g., amount_cents / 100), use a fact with an expression instead. See Inline Expressions.
Creating in the Explorer
The Explorer provides a guided builder for creating calculated metrics without writing code.
Steps
- Open the Explorer and click + Add Calculated Field
- Choose the Aggregate type
- Enter a name (e.g., "Profit Margin")
- Choose an operation (e.g., Ratio)
- Select fields for Input A and Input B
- Choose aggregation functions for each input
- Click Create Metric
Supported Operations
For standard operations, you select two input fields (A and B) and an aggregation function for each. Semaphor builds the expression for you.
| Operation | Description | Example |
|---|---|---|
| Sum | Add two values | Revenue + Shipping |
| Difference | Subtract one value from another | Revenue - Cost |
| Ratio | Divide one value by another | Revenue / Orders |
| Percent of Total | Calculate percentage contribution | Region Sales / Total Sales |
| Custom | Write your own SQL expression | SUM({Amount} - {Discount}) / NULLIF(COUNT(DISTINCT {Order ID}), 0) |
Aggregation Functions
For standard operations, choose how each input field is aggregated before the calculation:
SUM, AVG, COUNT, COUNT DISTINCT, MIN, MAX
Example: Average Order Value
- Operation: Ratio
- Input A:
AmountwithSUMaggregation - Input B:
Order IDwithCOUNT DISTINCTaggregation - Result:
SUM(Amount) / COUNT(DISTINCT Order ID)
Custom Expressions
When none of the standard operations fit your calculation, select Custom to write a SQL expression directly. The UI switches from the two-input builder to an expression editor.
How it works
- Set the operation to Custom
- The Input A / Input B selectors are replaced by an expression editor
- Write your SQL expression using
{FieldName}tokens to reference fields - Click fields in the left panel to insert tokens at your cursor position
- Click Validate Expression to check for errors
- Click Create Metric
Token matching is case-insensitive. {Amount}, {amount}, and {AMOUNT} all resolve to the same field.
Example: Discounted Revenue per Unit
You want SUM(amount - discount) / SUM(quantity) -- a ratio where the numerator does row-level arithmetic before aggregating. This can't be expressed with the standard Ratio operation because Ratio aggregates each input independently.
Expression:
SUM({Amount} - {Discount Amount}) / NULLIF(SUM({Quantity}), 0)Result: Semaphor generates:
SUM(t0.amount - t0.discount_amount) / NULLIF(SUM(t0.quantity), 0)Example: Weighted Average Price
You want a quantity-weighted average price -- SUM(price * quantity) / SUM(quantity).
Expression:
SUM({Unit Price} * {Quantity}) / NULLIF(SUM({Quantity}), 0)When to use Custom vs standard operations
| Use standard operations when... | Use Custom when... |
|---|---|
| The calculation is a simple ratio, sum, or difference of two fields | The expression involves row-level arithmetic before aggregation |
| Each input needs a single aggregation function | You need multiple aggregate functions in one expression |
| You don't need conditional logic | You need CASE WHEN, COALESCE, or other SQL constructs |
You can also use Generate with AI to describe your calculation in plain English and have Semaphor generate the expression for you.
Not sure if you need a calculated metric or a calculated dimension? See Choosing the Right Type.
Defining in a Template
For version-controlled definitions, bulk configuration, or CI/CD workflows, define metrics in the metrics list of your domain template.
Dataset Metrics
A dataset metric is an aggregate over one dataset's rows. dataset names that dataset, and the expression uses its field names (and its other dataset metrics) directly:
metrics:
- name: average_order_value
dataset: orders
label: "Average Order Value"
expression: "SUM(amount) / NULLIF(COUNT(DISTINCT order_id), 0)"
dataType: number
format:
type: currency
decimals: 2
currency: USDThe expression must aggregate. A row-level expression is refused (row_expression_in_dataset_metric): add it as a fact instead. A reference to another dataset's field is refused (dataset_metric_references_other_dataset): use a composed metric.
You can write field names plain (amount) or in braces ({amount}). Two datasets can each have a metric with the same name; a metric is identified by its dataset and name.
Composed Metrics
A composed metric uses {input_name} tokens as placeholders. Each token maps to an input in inputs, which names where its value comes from:
metrics:
- name: net_revenue
label: "Net Revenue"
expression: "{total_sales} - COALESCE({total_returns}, 0)"
inputs:
total_sales:
dataset: orders
field: amount
aggregate: SUM
total_returns:
dataset: returns
field: amount
aggregate: SUM
format:
type: currency
decimals: 2
currency: USDThree Input Types
Field reference points to a field (identifier, fact, or dimension) of a dataset:
inputs:
total_sales:
dataset: orders # Dataset name
field: amount # Field name
aggregate: SUM # Aggregation functionDataset metric reference points to a metric of a dataset:
inputs:
revenue:
metric: revenue # Name of the dataset metric
dataset: orders # Its datasetComposed metric reference points to another composed metric (enables nesting):
inputs:
net_revenue:
metric: net_revenue # Name of another composed metric (no dataset)Aggregation Rules
A composed metric's aggregation comes from one of these places:
1. Expression wraps the token in an aggregate function:
# The SUM is in the expression; no aggregate needed on the input
- name: gross_sales
expression: "SUM({amount})"
inputs:
amount: { dataset: orders, field: amount }2. Expression uses bare tokens:
# The tokens are bare; each field input MUST specify an aggregate
- name: sales_per_return
expression: "{total_sales} / NULLIF({return_count}, 0)"
inputs:
total_sales: { dataset: orders, field: amount, aggregate: SUM }
return_count: { dataset: returns, field: return_id, aggregate: COUNT }3. Metric inputs bring their own aggregation:
- name: net_revenue
expression: "{revenue} - COALESCE({returned}, 0)"
inputs:
revenue: { metric: revenue, dataset: orders }
returned: { dataset: returns, field: amount, aggregate: SUM }If a field token has no aggregate in either place, validation fails with error E010. A composed metric that works row by row needs defaultAggregate (row_composed_metric_without_default_aggregate).
Supported aggregates: SUM, COUNT, AVG, MIN, MAX, MEDIAN, DISTINCT
Example: Building a Custom Metric
Suppose you have an orders dataset with amount, quantity, and discount_amount facts.
Step 1: Define a dataset metric with a custom expression.
Revenue after discounts subtracts discount_amount from amount before summing. Place the arithmetic inside SUM() so it runs row by row:
metrics:
- name: discounted_revenue
dataset: orders
label: "Discounted Revenue"
description: "Total revenue after applying line-item discounts"
expression: "SUM(amount - discount_amount)"
dataType: number
format: currencySemaphor generates:
SUM(t0.amount - t0.discount_amount) AS discounted_revenueStep 2: Build a ratio metric on top.
Average revenue per unit references the metric from Step 1 by name:
- name: avg_revenue_per_unit
dataset: orders
label: "Avg Revenue per Unit"
description: "Discounted revenue divided by total units sold"
expression: "discounted_revenue / NULLIF(SUM(quantity), 0)"
dataType: number
format:
type: currency
decimals: 2Semaphor expands the reference and generates:
(SUM(t0.amount - t0.discount_amount) / NULLIF(SUM(t0.quantity), 0)) AS avg_revenue_per_unitNesting Metrics
Metrics can build on other metrics. Within one dataset, dataset metrics reference each other by name:
metrics:
# Layer 1: Base metrics
- name: revenue
dataset: orders
label: "Revenue"
expression: "SUM(amount)"
dataType: number
- name: total_cost
dataset: orders
label: "Cost"
expression: "SUM(cost)"
dataType: number
# Layer 2: Derived from base metrics
- name: profit
dataset: orders
label: "Profit"
expression: "revenue - total_cost"
dataType: number
# Layer 3: Derived from derived metrics
- name: profit_margin
dataset: orders
label: "Profit Margin %"
expression: "profit / NULLIF(revenue, 0) * 100"
dataType: numberAt query time, Semaphor recursively expands nested references. The profit_margin metric resolves to:
((SUM(t0.amount) - SUM(t0.cost)) / NULLIF(SUM(t0.amount), 0) * 100)Composed metrics nest the same way through metric inputs: { metric: profit, dataset: orders } for a dataset metric, or { metric: net_revenue } for another composed metric.
Maximum nesting depth is 10 levels. Circular references (A references B which references A) are detected and rejected with error E001.
Cross-Dataset Metrics
When a composed metric's inputs come from different datasets, Semaphor generates separate CTEs to aggregate each dataset independently, then joins them. This prevents metric inflation from fan-out joins.
metrics:
- name: net_revenue
label: "Net Revenue"
description: "Revenue minus returns"
expression: "{total_sales} - COALESCE({total_returns}, 0)"
inputs:
total_sales: { dataset: orders, field: amount, aggregate: SUM }
total_returns: { dataset: returns, field: amount, aggregate: SUM }When grouped by order_month, Semaphor generates:
WITH orders_agg AS (
SELECT DATE_TRUNC('month', t0.order_date) AS order_month,
SUM(t0.amount) AS net_revenue__field_0
FROM orders AS t0
GROUP BY DATE_TRUNC('month', t0.order_date)
),
returns_agg AS (
SELECT DATE_TRUNC('month', t0.return_date) AS order_month,
SUM(t0.amount) AS net_revenue__field_1
FROM returns AS t0
GROUP BY DATE_TRUNC('month', t0.return_date)
)
SELECT COALESCE(orders_agg.order_month, returns_agg.order_month) AS order_month,
orders_agg.net_revenue__field_0
- COALESCE(returns_agg.net_revenue__field_1, 0) AS net_revenue
FROM orders_agg
FULL OUTER JOIN returns_agg
ON orders_agg.order_month = returns_agg.order_month
ORDER BY order_monthCross-dataset metrics require a grain mapping when grouped by a composed dimension, so Semaphor knows which column in each dataset maps to the same time grain.
Field Reference
Both forms:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique within its dataset (dataset metrics) or among composed metrics |
label | string | No | Display name |
description | string | No | Help text |
aiContext | string | object | No | Context for agents: instructions, other names, example questions |
expression | string | Yes | The calculation |
format | object | No | Display formatting (see Format Spec below) |
comparisonPolarity | enum | No | higher_is_better, lower_is_better, neutral |
hidden | boolean | No | Hide from end users |
tags | string[] | No | Categorization tags |
Dataset metric only:
| Field | Type | Required | Description |
|---|---|---|---|
dataset | string | Yes | The dataset whose rows the metric aggregates |
dataType | enum | Yes | Logical type, usually number |
nativeDataType | string | No | The database's exact type |
unit | string | No | Unit label |
isDefault | boolean | No | Mark as the dataset's default metric |
Composed metric only:
| Field | Type | Required | Description |
|---|---|---|---|
inputs | object | Yes | Map of token name to a field, dataset metric, or composed metric reference |
defaultAggregate | enum | Conditional | Required when the expression works row by row |
metricType | enum | No | base, derived, calculated. Defaults to derived |
aggregationStrategy | enum | No | default, symmetric_aggregate, aggregate_then_join, weighted |
filters | array | No | Filters applied to this metric, each with dataset, field, operator, value |
A metric with both dataset and inputs, or neither, is refused.
Format Spec:
format:
type: currency # number, currency, percentage, date, string
decimals: 2 # 0-10
currency: USD # For currency type
prefix: "$" # Optional prefix
suffix: "" # Optional suffix
thousandsSeparator: trueFor a full reference of format properties, see Inline Expressions: Format Object Reference.