Semaphor
Semantic DomainsCalculated Fields

Inline Expressions

Define row-level SQL expressions and display formatting on dataset facts and dimensions

Dataset facts and dataset dimensions can both include an expression field containing a row-level SQL expression. For facts, the expression transforms source data before it's aggregated: useful for unit conversions, conditional logic, and rounding. For dimensions, the expression transforms grouping values: useful for date truncation, bucketing, and string manipulation.

In both cases, inline expressions let you define calculations directly on a field, next to the data they read. An expression that aggregates rows (SUM(...), COUNT(...)) is a metric instead.

The optional format field (facts and metrics) controls display formatting, either as a simple string shorthand (currency, percent, number) or a full object with properties like decimals, currency, and locale. When a user drags the field onto a card, the format seeds the card's default display settings.


When to Use Fact Expressions vs Metrics

FeatureFact ExpressionDataset MetricComposed Metric
Where definedOn a dataset fact (datasets[].fields.facts[])In metrics, with datasetIn metrics, with inputs
ComputedRow by row, then aggregatedAs an aggregateFrom its inputs
ScopeOne datasetOne datasetCan span datasets
ReferencesFields by nameFields and metrics by nameNamed inputs with explicit dataset and field
Aggregation controldefaultAggregate, or the aggregate a card choosesWritten in the expressionIn the expression or per input
Use caseUnit conversion, rounding, conditional logicKPIs over one dataset (ratios, averages)KPIs across datasets, multi-level nesting

Expression Syntax

Two authoring styles are supported.

Plain field references:

expression: "amount_cents / 100"

Brace notation (useful when field names contain special characters, or for clarity):

expression: "{amount_cents} / 100"

Both produce the same result. When you save, field names are resolved against the dataset's fields. An expression can also use the column named in its own sourceField.

Referencing Other Facts

A fact expression can reference other fields in the same dataset, including other facts. If you have a fact amount_usd with expression amount_cents / 100, another fact can build on it:

facts:
  - name: amount_usd
    expression: "amount_cents / 100"
    sourceField: amount_cents
    dataType: number
    defaultAggregate: SUM
  - name: amount_with_tax
    expression: "amount_usd * 1.08"
    dataType: number
    defaultAggregate: SUM

Supported SQL Constructs

  • Arithmetic: +, -, *, /
  • CASE WHEN: CASE WHEN status = 'active' THEN amount ELSE 0 END
  • CAST: CAST(amount_cents AS DECIMAL) / 100
  • Functions: ROUND(...), COALESCE(...), CONCAT(...), ABS(...), NULLIF(...)
  • EXTRACT: EXTRACT(YEAR FROM order_date)

Aggregate functions (SUM(...), COUNT(...), AVG(...)) make an expression an aggregate. Put those in a dataset metric; in a fact or dimension they are refused with aggregate_in_row_field.

Expression syntax follows your database's SQL dialect (PostgreSQL, MySQL, BigQuery, etc.). Use functions supported by your connection type.


Examples

Currency Conversion

facts:
  - name: amount_usd
    label: "Amount (USD)"
    dataType: number
    sourceField: amount_cents
    expression: "amount_cents / 100"
    defaultAggregate: SUM
    format: currency

Per-Row Rate

facts:
  - name: conversion_rate
    label: "Conversion Rate"
    dataType: number
    sourceField: conversions
    expression: "conversions / NULLIF(visits, 0) * 100"
    defaultAggregate: AVG
    format: percent

Conditional Logic

facts:
  - name: active_revenue
    label: "Active Revenue"
    dataType: number
    sourceField: amount
    expression: "CASE WHEN status = 'active' THEN amount ELSE 0 END"
    defaultAggregate: SUM
    format: currency

Rounding

facts:
  - name: amount_rounded
    label: "Amount (Rounded)"
    dataType: number
    sourceField: amount
    expression: "ROUND(amount, 2)"
    defaultAggregate: SUM
    format: number

Aggregate Expression (a Metric)

When the expression aggregates rows itself, it's a dataset metric, not a fact. It goes in the top-level metrics list and names its dataset:

metrics:
  - name: avg_order_value
    dataset: orders
    label: "Average Order Value"
    dataType: number
    expression: "SUM(amount) / NULLIF(COUNT(DISTINCT order_id), 0)"
    format: currency

Format Specification

The format field controls how a fact's or metric's values are displayed. It supports two forms: a string shorthand for common cases, and a full object for fine-grained control.

String Shorthand

For quick setup, pass a format type as a string:

facts:
  - name: amount_usd
    expression: "amount_cents / 100"
    sourceField: amount_cents
    dataType: number
    format: currency

Supported string values:

ValueDisplay EffectExample Output
currencyCurrency symbol (USD), 2 decimal places$1,234.56
percentPercent suffix, values treated as fractions27.5%
percentageAlias for percent27.5%
numberPlain number, 2 decimal places1,234.56
scientificScientific notation1.23E+3

Object Form

For more control, pass a format object with specific properties:

facts:
  - name: revenue_eur
    expression: "amount_cents / 100"
    sourceField: amount_cents
    dataType: number
    format:
      type: currency
      currency: EUR
      decimals: 0
      locale: de-DE
      thousandsSeparator: true
metrics:
  - name: margin_pct
    dataset: orders
    expression: "(SUM(amount) - SUM(cost)) / NULLIF(SUM(amount), 0)"
    dataType: number
    format:
      type: percent
      decimals: 1
      percentValueMode: fraction

Format Object Reference

PropertyTypeApplies ToDescription
typestringAllnumber, currency, percent, percentage, scientific, date, string
decimalsnumberNumericFixed decimal places (0-10)
currencystringcurrencyISO currency code (e.g., USD, EUR, GBP). Defaults to USD
localestringAllBCP 47 locale tag (e.g., en-US, de-DE). Defaults to en-US
prefixstringAllText prepended to the value
suffixstringAllText appended to the value
thousandsSeparatorbooleanNumericEnable grouping separators. Defaults to true
negativeStylestringNumericminus (default) or parentheses
compactbooleanNumericUse compact notation (e.g., 1.2K)
scalenumberNumericMultiply value before display
percentValueModestringpercentfraction (default, multiply by 100) or whole (display as-is)

When using percentValueMode: fraction, a raw value of 0.275 displays as 27.5%. When using whole, the value 27.5 displays as 27.5%.

Format Precedence

When a user drags a fact or metric with a format onto a card, the format seeds the card's default display settings. The precedence order is:

  1. User-set formatting (highest priority): any formatting the user explicitly configures on the card
  2. Semantic format: the format value from the field or metric definition
  3. Visual defaults: Semaphor's built-in defaults

Formats are non-destructive

Format values only apply when a field is first added to a card and only if the card has no existing format configured. They never overwrite user customizations.


Aggregation Behavior

Fact Expressions

A fact expression is row-level. When a card uses the fact, Semaphor wraps the expression in the fact's defaultAggregate, or in the aggregate the card chooses.

# This fact:
expression: "amount_cents / 100"
defaultAggregate: SUM

# Becomes this in the generated SQL:
# SUM(amount_cents / 100)

Use AVG, MIN, MAX, MEDIAN, or another supported aggregate when that matches the fact's meaning:

expression: "quality_score"
defaultAggregate: AVG

Aggregate Expressions

An expression that already contains aggregate functions is used as written, and it belongs in a dataset metric:

# This dataset metric's expression:
expression: "SUM(amount) / NULLIF(COUNT(DISTINCT order_id), 0)"

# Is used directly in the generated SQL:
# SUM(amount) / NULLIF(COUNT(DISTINCT order_id), 0)

Non-Numeric Expressions

Facts should be numeric. If you need to count non-numeric values, model a fact with defaultAggregate: COUNT, or a dataset metric such as COUNT(DISTINCT customer_id).

Keep row-level work in facts and set defaultAggregate. Write aggregate functions only in metrics, where the expression is meant to aggregate, such as SUM(amount) / COUNT(DISTINCT order_id).


Validation Errors

When you save a domain template with expressions or fact filters, Semaphor validates them. These codes apply to fact expressions and dataset metrics:

CodeMeaningCommon CauseFix
aggregate_in_row_fieldA fact expression aggregates rowsSUM(...) or COUNT(...) in a factMove it to metrics as a dataset metric
row_expression_in_dataset_metricA dataset metric works row by rowA metric without an aggregate, like amount_cents / 100Add it as a fact (to aggregate) or a dimension (to group by)
dataset_metric_references_other_datasetA dataset metric uses another dataset's fieldsA qualified reference like returns.amountUse a composed metric with named inputs
E012Unknown name in an expressionThe expression references a name that is not a field or metric of the datasetCheck spelling; the name must be a field of the same dataset, a metric of it, or the field's own sourceField
E013Invalid expression or fact filterCircular dependency, unsupported AS aliasing, invalid filter, or other syntax issueReview the expression for cycles (A references B which references A), remove AS aliases, check the filter, and verify SQL syntax
E014Unknown field in a fact filterA filter references a field name not in the datasetCheck filter field names against the dataset's fields
E015Unsupported filter pathA filter references a field across a one-to-many or many-to-many relationshipUse same-dataset fields or fields reachable via many-to-one joins
E016Unknown field reference in dimension expressionThe expression references a name that isn't an identifier or dimension of the datasetCheck spelling, or set the dimension's sourceField to the column it reads
E017Invalid dimension expressionAS aliases or syntax errorsRemove AS aliases and fix the SQL

For a complete list of validation errors, see Validation & Examples.


Restrictions

  • AS aliasing: amount_cents AS usd is not supported. Use the field's name instead.
  • No aggregates in facts or dimensions: Facts and dimensions are row-level. Aggregate functions (SUM, COUNT, etc.) belong in metrics.
  • Cross-dataset references: Inline expressions can only reference fields within the same dataset. For cross-dataset calculations, use composed metrics.
  • Fact filters: filters apply to column facts only, not to facts with an expression.
  • Braces in string literals: String values containing { or } are not supported inside expressions.

Best Practices

  • Prefer sourceField for simple facts: If a fact maps directly to a database column with no transformation, use sourceField without expression. Expressions are for when you need to transform the value.
  • Use NULLIF to prevent division by zero: Write amount / NULLIF(orders, 0) instead of amount / orders.
  • Add a format hint for user convenience: Format hints save dashboard users from manually configuring number formatting every time they use the field.
  • Keep expressions simple: For calculations across datasets, use composed metrics instead.
  • Use descriptive label and description: Help dashboard users understand what the transformed value represents.

Inline Dimension Expressions

Dataset dimensions can also include an expression field for row-level transforms such as date truncation, CASE-WHEN bucketing, and string manipulation. This is the recommended way to add a new calculated dimension to a domain.

Dimension Expressions vs Composed Dimensions

FeatureDimension ExpressionComposed Dimension
Where definedOn a dataset dimension (datasets[].fields.dimensions[])In the composedDimensions list
ScopeOne datasetNamed inputs from any dataset
ReferencesIdentifiers and dimensions by name, plus its own sourceField columnNamed inputs with explicit dataset and field
Cross-datasetNoNo (but grain mappings enable cross-dataset alignment)
Use caseNew groupingsExisting domain-level groupings, cross-dataset grain alignment

Expression Syntax

Dimension expressions use the same syntax as fact expressions: plain field references and brace notation. See the Expression Syntax section above for details. A dimension expression can use the dataset's identifiers and dimensions; to read another column, such as a fact's column, set the dimension's sourceField to it.

Examples

Date Truncation

dimensions:
  - name: order_month
    label: "Order Month"
    dataType: date
    expression: "DATE_TRUNC('month', order_date)"

Bucketing with CASE WHEN

dimensions:
  - name: status_band
    label: "Status Band"
    dataType: string
    expression: "CASE WHEN status = 'active' THEN 'Active' WHEN status = 'trial' THEN 'Trial' ELSE 'Inactive' END"

String Transform

dimensions:
  - name: upper_region
    label: "Region (Uppercase)"
    dataType: string
    expression: "UPPER(region)"

Concatenation

dimensions:
  - name: full_location
    label: "Full Location"
    dataType: string
    expression: "CONCAT(city, ', ', state)"

Aggregation Behavior

Dimension expressions are row-level: they appear in both SELECT and GROUP BY in the generated SQL. They are not wrapped in any aggregate function.

# This dimension expression:
expression: "DATE_TRUNC('month', order_date)"

# Appears in the generated SQL as:
# SELECT DATE_TRUNC('month', order_date) ... GROUP BY DATE_TRUNC('month', order_date)

Do not use aggregate functions (SUM, COUNT, AVG, etc.) in dimension expressions. Semaphor refuses the save with aggregate_in_row_field.

Validation Errors

CodeMeaningCommon CauseFix
aggregate_in_row_fieldThe expression aggregates rowsSUM(...) or COUNT(...) in a dimensionMove it to metrics
E016Unknown field referenceExpression references a name that isn't an identifier or dimension of the datasetCheck spelling, or set sourceField to the column the expression reads
E017Invalid dimension expressionAS aliases or syntax errorsRemove AS aliases and fix the SQL

Restrictions

  • No aggregate functions: Dimension expressions are row-level. Use SUM, COUNT, etc. only in metrics.
  • AS aliasing not supported: Use the dimension's name field instead.
  • Single-dataset only: For cross-dataset groupings, use composed dimensions with grain mappings.
  • Braces in string literals: String values containing { or } are not supported inside expressions.

On this page