Skip to main content
A semantic source (semantics/<connection-id>/<name>.yaml) is validated against SemanticSource at load time. name, connection, table, grain, and columns are required. Everything else defaults. See Semantics for the concepts behind these fields.

Top-level fields

Cross-field rules enforced at write time: column, measure, dimension, and join-alias names must each be unique within the file. Every grain entry and every dimensions[].column must reference a declared column. Every column a measure’s expr touches must be declared. Violations raise a located SemanticValidationError (file + YAML path), not a silent drop. name is how every other file in the project refers to this source, in joins, metric bindings, and guardrails. connection ties the source to a connection defined in your project config, and table is the physical table or view the compiler queries at runtime. grain lists the columns that make each row unique. Get this wrong and join fanout can silently double-count measures, so the compiler treats it as a required declaration, not a guess. columns, measures, dimensions, joins, and filters are where the actual modeling happens, and each gets its own section below. segments is reserved for a future feature and currently ignored by both ingestion and the compiler. finality tells the compiler how fresh this source’s data is, and meta is written by reconciliation rather than by hand.

columns[]

Each entry declares one physical column so the compiler knows its name and type without querying the warehouse at compile time. type uses canonic’s normalized type set instead of a dialect-specific one, so the same semantic source compiles correctly whether it points at Postgres, Snowflake, or BigQuery. nullable defaults to true, set it to false when you know the column can never be empty, since some compiler optimizations rely on that guarantee.

measures[]

A measure defines one aggregation the compiler can compute over the source. expr is a small SQL aggregate expression and it can only reference columns declared above, anything else fails validation. additivity tells the compiler whether a measure’s value can still be safely summed after it has already been aggregated once, for example when re-aggregating at a coarser grain or across a join fanout. Set it to semi_additive for values like account balances or inventory snapshots that sum correctly across every dimension except one (usually time), and list that dimension in semi_additive_over. Set it to non_additive for anything that can’t be derived from a partial sum, such as distinct counts, ratios, or percentiles. A measure is is_p0_compilable only if it’s additive and its expr parses to a single sum/count/min/max (not count(distinct …)). Measures outside that set are valid YAML but rejected at compile time with UNSUPPORTED_MEASURE, never at load time. In practice this means a measure can pass validation on save and still fail the first time someone actually queries it, so it’s worth checking additivity and expr together rather than assuming any aggregate expression will compile.

dimensions[]

A dimension exposes a column for grouping or filtering. column must point at a column declared in columns, canonic won’t infer or guess it. granularity only applies to time columns and controls how they get bucketed, for example day or month. Leave it null for non-time dimensions. label and description are for humans and agents reading the schema and don’t affect compilation. aliases lets a dimension resolve under more than one name, useful when different teams call the same column by different names.

joins[]

A join declares how this source relates to another semantic source, by name, not by physical table. on is the SQL join predicate, and relationship tells the compiler which side of the join can fan out rows, which in turn determines whether measures need deduplication before they’re safe to sum. Get the relationship wrong and an additive measure can silently double count across the join. name only matters when you join the same target more than once or want a specific SQL alias, otherwise it defaults to the target’s own name.

filters[]

A filter is a named SQL predicate you can reuse across measures, guardrails, and queries instead of repeating the same WHERE clause everywhere. Keeping something like “completed orders only” defined in one place means a change to the definition doesn’t require hunting down every place it was inlined.

finality

watermark tells the compiler the point before which this source’s rows are considered final and won’t change anymore. Leave it null if the source never gets late-arriving updates or corrections. In that case canonic treats it as always final. A non-null watermark is a relative expression, like business_day - 1 day, evaluated against the current query window. This combines with a contract’s FinalityRule when a metric can be served from more than one realization of the same data.

meta

System-managed, not hand-edited: reconciliation writes these fields.
These fields are written by ingestion and reconciliation, not by a human editing YAML. provenance tracks how much a fact can be trusted. board_approved and human_curated facts are locked and never silently overwritten. inferred facts can be revised automatically, always through a reviewable diff rather than a direct rewrite. source_fingerprint is a hash of the introspected schema, used to detect drift between what’s declared here and what the warehouse actually looks like. last_validated_at records when that check last ran, and frozen is a manual override you can set to stop reconciliation from touching a source at all, even one that’s only inferred.