# Metrics

One file per metric at metrics/name.yml, named after the metric's name field. A metric is one governed expression, used exactly, never re-derived.

As with tables, the field is authoritative and the filename must match it.

## Fields

| Field | Type | Notes |
| --- | --- | --- |
| `name` | string, required | Stable snake\_case identifier, and the filename |
| `display_name` | string, required | Human name. Enforced by import validation: the check and the import both reject a metric without it |
| `expression` | string, required | The SQL aggregate. Enforced like `display_name`. Identifiers quoted, stored case |
| `filters` | string | WHERE-clause fragment the expression assumes |
| `table_schema` | string | Schema of the base table. Note the field name: `table_schema`, not `schema_name` |
| `table_name` | string | Base table the expression runs over |
| `domain_path` | string | Domain the metric is listed under. Same rules as on tables |
| `description` | string | What the metric means in business terms |
| `notes` | string | Usage guidance for the agent: edge cases, what not to substitute |
| `precomputed_in` | string | Where the metric already exists materialized, if anywhere |
| `synonyms` | list of strings | Business vocabulary that should route to this metric |
| `unit` | string | `EUR`, `%`, `sellers` |

```yaml title="cassis/metrics/on_time_delivery_rate.yml"
description: Share of delivered orders that arrived on or before the estimated delivery
  date, as a percentage. Around 92%.
display_name: On-time delivery rate
domain_path: marketplace
expression: 100 * AVG(CASE WHEN "IS_ON_TIME" THEN 1 ELSE 0 END)
filters: '"IS_DELIVERED" = TRUE AND "ORDER_DELIVERED_CUSTOMER_DATE" IS NOT NULL'
name: on_time_delivery_rate
synonyms:
- on time rate
- punctual delivery rate
table_name: FCT_ORDERS
table_schema: STALLORA
unit: '%'
```

`expression` is the rule, not necessarily executable SQL. The agent composes the query from it, so it can carry what a bare aggregate cannot: a join requirement, a dedup key, a boundary rule. Evals gate the result against expected SQL, so prose in the expression stays checkable.

## What earns its own metric

A metric exists so a phrase resolves to one definition instead of being re-derived per question. Two signals that a phrase needs one: people disagree about how to compute it, or the computation carries a filter that is easy to forget. “On-time delivery rate” qualifies on both counts, because the delivered-only filter is exactly what a hand-written query drops.

`synonyms` is what makes that work in practice: it is the list of things people actually say. `notes` is where you tell the agent what not to substitute when the metric does not apply.

The `filters` field is part of the definition, not a convenience. It is applied whenever the metric is used, so a metric plus its filters is the whole rule. Splitting the rule between `expression` and a domain README is how two answers end up disagreeing.

## File naming and sanitization

Table and metric filenames derive from the object’s name, with every character outside ASCII letters, digits, `_`, `-`, and `.` replaced by `_`. A metric named `revenue/total` becomes `metrics/revenue_total.yml`. The `name` field keeps the original value; only the filename is sanitized.

## Precomputed metrics

`precomputed_in` names a table where the metric already exists materialized, which lets the agent read it instead of recomputing the aggregate. Removing that table clears the pointer: a stale one would send generated SQL at a table that no longer exists.
