Skip to content
metrics
Metrics

Metrics — the semantic layer

A metric is one canonical, documented definition of a number — “MRR”, “active users” — as a named, parameterizable SQL template. Query it with dimension and filter overrides so every dashboard reads the same source of truth.

Read
5 min
Sections
4
Limits
Free 5 · Pro 50 · Business ∞

Why a semantic layer

Without one, “what is MRR?” has three answers in three dashboards. A metric pins the definition in one place: the SQL, the allowed dimensions, the valid filters, the display format. Everything that consumes it — dashboards, the API, a report — resolves the same way.

Define a metric

The sqlTemplate is rendered by Scriban in its Liquid dialect, so control flow is {% if … %} / {% endif %} — not Scriban's own {{ if … }}. Only two variables exist: {{ filters }} and {{ dims }}. Nothing else is injected, so a template cannot reach server state.

http
POST /api/v1/metrics
Authorization: Bearer <token>
Content-Type: application/json

{
  "slug": "monthly_active_users",
  "displayName": "Monthly Active Users",
  "description": "Distinct users with an event in the past 30 days",
  "connectionId": "conn-uuid",
  "sqlTemplate": "SELECT COUNT(DISTINCT user_id) AS value FROM events WHERE created_at >= NOW() - INTERVAL '30 days' AND {{ filters }}",
  "dimensionsJson": "[\"country\",\"platform\"]",
  "filtersJson": "[{\"column\":\"country\"},{\"column\":\"platform\"}]",
  "format": "number",
  "tags": ["product","growth"]
}

{{ filters }} renders 1=1 when no filters are active, so you can write AND {{ filters }} unconditionally — no {% if %} wrapper needed. {{ dims }} renders a comma-separated, quoted column list, or empty when nothing is selected.

Entries in filtersJson are keyed on column (with optional operator and value). A filter whose column is not in that list is dropped silently rather than erroring, so a typo in the key name shows up as “my filter did nothing”.

format (number, currency, percent, duration) is informational — it drives how the frontend renders the result. Slugs are lowercase [a-z0-9_]+, unique per workspace.

Query a metric

http
POST /api/v1/metrics/{id}/query
Authorization: Bearer <token>
Content-Type: application/json

{
  "dims": ["country"],
  "filters": [
    { "column": "platform", "operator": "eq", "value": "web" }
  ]
}

The metric runs against its connection and returns a typed result:

json
{
  "columns": [{ "name": "value", "dataType": "bigint" }],
  "rows": [[12453]],
  "totalRows": 1,
  "durationMs": 42
}
How the whitelist protects you: a requested dimension must appear in dimensionsJson and a filter column in filtersJson, or it is dropped. Operators are normalised against a fixed list (=, !=, >, >=, <, <=, LIKE, ILIKE, IS NULL, IS NOT NULL) — anything else is dropped. Column names are rendered as quoted identifiers and values as escaped SQL literals. Note that these are escaped literals, not bound parameters: the whitelist is what makes it safe, so keep filtersJson tight.

On dashboards

Add a metric widget to any dashboard. Because the definition is shared, changing the metric once updates every dashboard that references it. Results are cached briefly per (metric, params) to keep boards snappy.