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
| Feature | Fact Expression | Dataset Metric | Composed Metric |
|---|---|---|---|
| Where defined | On a dataset fact (datasets[].fields.facts[]) | In metrics, with dataset | In metrics, with inputs |
| Computed | Row by row, then aggregated | As an aggregate | From its inputs |
| Scope | One dataset | One dataset | Can span datasets |
| References | Fields by name | Fields and metrics by name | Named inputs with explicit dataset and field |
| Aggregation control | defaultAggregate, or the aggregate a card chooses | Written in the expression | In the expression or per input |
| Use case | Unit conversion, rounding, conditional logic | KPIs 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: SUMSupported SQL Constructs
- Arithmetic:
+,-,*,/ CASE WHEN:CASE WHEN status = 'active' THEN amount ELSE 0 ENDCAST: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: currencyPer-Row Rate
facts:
- name: conversion_rate
label: "Conversion Rate"
dataType: number
sourceField: conversions
expression: "conversions / NULLIF(visits, 0) * 100"
defaultAggregate: AVG
format: percentConditional 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: currencyRounding
facts:
- name: amount_rounded
label: "Amount (Rounded)"
dataType: number
sourceField: amount
expression: "ROUND(amount, 2)"
defaultAggregate: SUM
format: numberAggregate 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: currencyFormat 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: currencySupported string values:
| Value | Display Effect | Example Output |
|---|---|---|
currency | Currency symbol (USD), 2 decimal places | $1,234.56 |
percent | Percent suffix, values treated as fractions | 27.5% |
percentage | Alias for percent | 27.5% |
number | Plain number, 2 decimal places | 1,234.56 |
scientific | Scientific notation | 1.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: truemetrics:
- name: margin_pct
dataset: orders
expression: "(SUM(amount) - SUM(cost)) / NULLIF(SUM(amount), 0)"
dataType: number
format:
type: percent
decimals: 1
percentValueMode: fractionFormat Object Reference
| Property | Type | Applies To | Description |
|---|---|---|---|
type | string | All | number, currency, percent, percentage, scientific, date, string |
decimals | number | Numeric | Fixed decimal places (0-10) |
currency | string | currency | ISO currency code (e.g., USD, EUR, GBP). Defaults to USD |
locale | string | All | BCP 47 locale tag (e.g., en-US, de-DE). Defaults to en-US |
prefix | string | All | Text prepended to the value |
suffix | string | All | Text appended to the value |
thousandsSeparator | boolean | Numeric | Enable grouping separators. Defaults to true |
negativeStyle | string | Numeric | minus (default) or parentheses |
compact | boolean | Numeric | Use compact notation (e.g., 1.2K) |
scale | number | Numeric | Multiply value before display |
percentValueMode | string | percent | fraction (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:
- User-set formatting (highest priority): any formatting the user explicitly configures on the card
- Semantic format: the
formatvalue from the field or metric definition - 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: AVGAggregate 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:
| Code | Meaning | Common Cause | Fix |
|---|---|---|---|
aggregate_in_row_field | A fact expression aggregates rows | SUM(...) or COUNT(...) in a fact | Move it to metrics as a dataset metric |
row_expression_in_dataset_metric | A dataset metric works row by row | A metric without an aggregate, like amount_cents / 100 | Add it as a fact (to aggregate) or a dimension (to group by) |
dataset_metric_references_other_dataset | A dataset metric uses another dataset's fields | A qualified reference like returns.amount | Use a composed metric with named inputs |
| E012 | Unknown name in an expression | The expression references a name that is not a field or metric of the dataset | Check spelling; the name must be a field of the same dataset, a metric of it, or the field's own sourceField |
| E013 | Invalid expression or fact filter | Circular dependency, unsupported AS aliasing, invalid filter, or other syntax issue | Review the expression for cycles (A references B which references A), remove AS aliases, check the filter, and verify SQL syntax |
| E014 | Unknown field in a fact filter | A filter references a field name not in the dataset | Check filter field names against the dataset's fields |
| E015 | Unsupported filter path | A filter references a field across a one-to-many or many-to-many relationship | Use same-dataset fields or fields reachable via many-to-one joins |
| E016 | Unknown field reference in dimension expression | The expression references a name that isn't an identifier or dimension of the dataset | Check spelling, or set the dimension's sourceField to the column it reads |
| E017 | Invalid dimension expression | AS aliases or syntax errors | Remove AS aliases and fix the SQL |
For a complete list of validation errors, see Validation & Examples.
Restrictions
- AS aliasing:
amount_cents AS usdis not supported. Use the field'snameinstead. - 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:
filtersapply to column facts only, not to facts with anexpression. - Braces in string literals: String values containing
{or}are not supported inside expressions.
Best Practices
- Prefer
sourceFieldfor simple facts: If a fact maps directly to a database column with no transformation, usesourceFieldwithoutexpression. Expressions are for when you need to transform the value. - Use
NULLIFto prevent division by zero: Writeamount / NULLIF(orders, 0)instead ofamount / orders. - Add a
formathint 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
labelanddescription: 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
| Feature | Dimension Expression | Composed Dimension |
|---|---|---|
| Where defined | On a dataset dimension (datasets[].fields.dimensions[]) | In the composedDimensions list |
| Scope | One dataset | Named inputs from any dataset |
| References | Identifiers and dimensions by name, plus its own sourceField column | Named inputs with explicit dataset and field |
| Cross-dataset | No | No (but grain mappings enable cross-dataset alignment) |
| Use case | New groupings | Existing 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
| Code | Meaning | Common Cause | Fix |
|---|---|---|---|
aggregate_in_row_field | The expression aggregates rows | SUM(...) or COUNT(...) in a dimension | Move it to metrics |
| E016 | Unknown field reference | Expression references a name that isn't an identifier or dimension of the dataset | Check spelling, or set sourceField to the column the expression reads |
| E017 | Invalid dimension expression | AS aliases or syntax errors | Remove 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
namefield 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.