Horizon Context is the governed store that Cortex Sense reads from, the library to Cortex Sense's librarian. It governs the definition, the lineage, and even labels which metrics are non-additive. Governing a definition, and labeling it, is still not the same as verifying the calculation an agent runs against it.
1. What Horizon Context is, in one paragraph
Horizon Context is Snowflake's governed context layer. Snowflake announced it at Snowflake Summit on June 2, 2026, and built it into the Horizon Catalog. It holds your business definitions, your metrics, the relationships between your tables, and the lineage of how each metric was built. Every BI tool, AI agent, and application can then read from the same governed source, instead of a metric copied into a dashboard or hardcoded into a prompt.
Snowflake organizes it as three steps:
- In Collect, Snowflake pulls metadata from across your systems, including Snowflake objects, query logs, BI dashboard definitions, and external systems through connectors and an OpenLineage feed.
- In Enrich, Snowflake builds that metadata into column-level lineage and adds descriptions and popularity signals.
- In Activate, an agent can search the store and query the right semantic view when it answers.
If Cortex Sense is the librarian that fetches the right definition when a question arrives, Horizon Context is the library it reads from. Most of this is in preview today, not generally available.
The old pattern was copying the same CASE WHEN and SUM logic across many dashboards. One governed definition is better than that. The catch is that a governed definition can still produce a wrong number, and the catalog cannot tell you that it is wrong.
2. How a Snowflake semantic view actually works
In Snowflake, a governed definition is stored in an object called a semantic view. You need to know how it computes, because the failures later in this post are hard to see otherwise.
2.1 CREATE SEMANTIC VIEW: tables, relationships, facts, dimensions, metrics
A semantic view is a database object you create with CREATE SEMANTIC VIEW. It has five parts, and the order is fixed:
- You list the source tables in TABLES, each with a primary key.
- You declare how they join in RELATIONSHIPS.
- You put the row-level quantitative columns in FACTS.
- You put the columns and expressions you slice by in DIMENSIONS.
- You define the business numbers, as aggregations, in METRICS.
Only TABLES is required, and you need at least one of DIMENSIONS or METRICS. The order matters. Snowflake's own docs say you must specify FACTS before DIMENSIONS. Here is a small example.
sqlCREATE OR REPLACE SEMANTIC VIEW orders_sv TABLES ( o AS orders PRIMARY KEY (order_id) ) DIMENSIONS ( o.order_month AS DATE_TRUNC('month', order_date) ) METRICS ( o.revenue AS SUM(amount), o.distinct_customers AS COUNT(DISTINCT customer_id) );
A metric is not a stored number. It is a named aggregation that Snowflake computes when you ask for it.
2.2 Why it computes correctly at query time
This is the part Snowflake gets right. A semantic view does not store a frozen total. Snowflake recomputes the metric at the grain of your query. Two things follow, and most semantic layers get them wrong:
- Distinct counts are safe across joins.
COUNT(DISTINCT customer_id)is a distinct aggregate, so it does not double-count when a join fans out rows. Snowflake's general rule is to aggregate at the right grain before the join, and additive aggregates likeSUMand plainCOUNTare held to the lowest grain. Distinct counts can be applied above it without inflating, so a distinct count over base data is correct at any grain you query. - Ratios are computed the right way. Snowflake supports derived metrics, which are scalar expressions over other metrics, for example
profit_margin AS DIV0(total_revenue, total_cost). The numerator and denominator are each aggregated first, then divided. That avoids the average of averages mistake, where you average a column of per-row ratios and get a number that means nothing.
Keep this in mind, because it is what makes the failures later in this post hard to see. On base data, defined carefully, a Snowflake semantic view returns the right number.
3. How Horizon Context relates to dbt
3.1 The semantic view sits on a table, and dbt usually built that table
A semantic view does not transform data. It sits on top of tables that something else produced, and usually that something is dbt. The semantic view governs what the metric is called and how it is aggregated. The shape of the data underneath it, whether each row is one raw event or one summary row for a whole day, was decided upstream in the transformation. Snowflake makes the pairing official. There is a dbt package, dbt_semantic_view, that lets you manage semantic views as a dbt materialization, version-controlled and tested alongside the rest of your models. That is a real convenience. It also means that whether a metric is safe to add across time is decided in the dbt code, one layer below the semantic view, where the catalog does not look.
3.2 Where OSI fits
Snowflake is leading the Open Semantic Interchange, or OSI. It is a vendor-neutral, open-source spec for describing semantic models, with their metrics, dimensions, and relationships, in a YAML and JSON format that any tool can read and write. It is backed by Salesforce, dbt Labs, and a couple dozen other data and BI vendors. OSI helps with portability. A governed definition can travel to your BI tools and agents without being re-keyed in each one. OSI carries the declared meaning of a metric, including its SQL expression, and it ships a validator that checks a definition against the schema. It does not check whether a given calculation an agent runs against that metric is valid. OSI makes a definition portable everywhere. It does not make it verified anywhere.
4. The part Snowflake gets right: NON ADDITIVE BY and correct aggregation
Give Snowflake full credit first. It goes further than most semantic layers.
First, Snowflake recomputes the metric at query time, as shown above. A layer that recomputes from base data is better than one that stores frozen totals.
Second, and this is rare, Snowflake models semi-additive metrics. Some numbers can be added across some dimensions but not others. An account balance can be summed across accounts but not across days, because adding Monday's balance to Tuesday's balance counts the same money twice. In March 2026 Snowflake shipped a clause for exactly this, called NON ADDITIVE BY. You write it inside a metric, and it changes the math. Snowflake sorts the rows by the dimensions you name and takes the latest snapshot instead of summing.
sqlMETRICS ( accounts.balance NON ADDITIVE BY (year, month, day) AS SUM(balance) )
This clause changes the math. Snowflake encodes the semi-additive rule into the governed definition and runs it at query time. Most semantic layers cannot express this. So Snowflake can represent additivity, and it computes the metric correctly when you tell it to. The rest of this post is about what that does not prevent.
5. Where governance is not verification: three places the math still breaks
Each of the next three cases is the system working exactly as documented. None is a bug. Each one shows a limit of the governed definition.
5.1 NON ADDITIVE BY is a clause a human fills in, not a fact the system derives
NON ADDITIVE BY works, but someone has to remember to write it, and to name the right dimensions in the right order. Nothing in Snowflake derives that a metric is non-additive, or checks that a declared non-additive dimension actually is one. It is a trusted assertion. dbt's MetricFlow has the same shape with its non_additive_dimension config, and it is just as manual. So the metric that most needs the flag, a distinct count of users or servers, is safe only if a human classified it correctly by hand. A governed store full of carefully named metrics is still a store of declarations. A declaration is something a person hopes got filled in. It is not a property derived from the code.
5.2 A semantic view over a pre-aggregated source cannot see the grain collapsed upstream
This is the case you can reproduce exactly, in the next section. Suppose a dbt model already rolled events up to a daily count of distinct active servers. The distinctness is now gone. Each day is a single number. Build a monthly metric as the SUM of those daily counts and it inflates, because a server active on twenty days is counted twenty times. The semantic view summed the column it was given, and that is correct. The rollup dropped the detail that would have caught this, which server was active. That information is not in the table the view reads. NON ADDITIVE BY does not save you here either. It would take the last daily snapshot, which is a different number from the true monthly distinct count.
5.3 Horizon Context has column-level lineage, but it serves context, it does not run a check
Here is the strongest version of the objection, and the most important to answer honestly. Horizon Context does build lineage. For data inside Snowflake, the lineage goes down to the column level, stitched from query logs and other feeds through the Collect, Enrich, and Activate pipeline. You might expect it to already see that the daily count fed the monthly sum.
The lineage records that one column feeds another. It does not record the rule that a distinct count cannot be added across time. The lineage Horizon Context holds is a record of which column feeds which, observed from query logs and served to agents as context. It is not a derivation of which calculations are valid, run before an answer goes out. Snowflake does have a product that checks answers, Cortex Agent Evaluations, but it is a separate, opt-in evaluation loop that grades answers after the fact, using a language model and, optionally, a set of correct answers to compare against. It does not run a fixed check on the live answer path, and it is not part of Horizon Context. Lineage that is observed and served as context is not the same as lineage that is compiled from code and checked before an answer.
6. See it yourself: a semantic view that inflates
You can reproduce the whole problem in about twenty lines on any Snowflake account. Build the same metric, monthly active servers, two ways. Build it once over a daily rollup, and once over the raw event log. The rollup inflates the number. The raw definition is correct. Both are governed semantic views.
Start with a raw event log. One row is one server heartbeat, so a server that is active all month appears many times.
sqlCREATE OR REPLACE TABLE server_events (event_id NUMBER, event_date DATE, server_id STRING); INSERT INTO server_events VALUES (1, '2026-08-01', 's1'), (2, '2026-08-01', 's1'), -- same server, second heartbeat the same day (3, '2026-08-01', 's2'), (4, '2026-08-02', 's1'), (5, '2026-08-02', 's3');
Now build the daily rollup, the kind of model a dbt run produces upstream. The daily numbers are correct. Each day gets a clean COUNT(DISTINCT).
sqlCREATE OR REPLACE TABLE daily_active_servers AS SELECT event_date AS activity_date, COUNT(DISTINCT server_id) AS active_servers FROM server_events GROUP BY event_date; -- 2026-08-01 -> 2 (s1, s2) -- 2026-08-02 -> 2 (s1, s3)
Here is the trap. This is a governed semantic view over the rollup, with a monthly metric that sums the daily counts.
sqlCREATE OR REPLACE SEMANTIC VIEW monthly_active_wrong TABLES ( das AS daily_active_servers PRIMARY KEY (activity_date) ) DIMENSIONS ( das.activity_month AS DATE_TRUNC('month', activity_date) ) METRICS ( das.monthly_active_servers AS SUM(active_servers) ); SELECT * FROM SEMANTIC_VIEW( monthly_active_wrong DIMENSIONS das.activity_month METRICS das.monthly_active_servers ) ORDER BY activity_month; -- 2026-08-01 | 4
The wrong view returns four. There were three servers all month. s1 was active on both days, so it got counted twice. The rollup dropped the detail that would have caught this, which server was active. The view only has the daily counts, so summing them is all it can do.
Now define the same metric over the raw event log.
sqlCREATE OR REPLACE SEMANTIC VIEW monthly_active_right TABLES ( ev AS server_events PRIMARY KEY (event_id) ) DIMENSIONS ( ev.activity_month AS DATE_TRUNC('month', event_date) ) METRICS ( ev.monthly_active_servers AS COUNT(DISTINCT server_id) ); SELECT * FROM SEMANTIC_VIEW( monthly_active_right DIMENSIONS ev.activity_month METRICS ev.monthly_active_servers ) ORDER BY activity_month; -- 2026-08-01 | 3
The right view returns three. Nothing else changed between the two runs. The only difference is whether the metric was defined over the grain that makes a distinct count valid.
The numbers here are small, so you can check them by hand. The same error happens at production scale. On a governed semantic view, Cortex Analyst reported 477 active servers, and growing every month, when the true number was 48 and flat. A server active on most days of the month is counted once for each active day, so the monthly sum is far too high. The wrong number looks fine in a demo, and it comes with a governed definition to cite.
The obvious fixes do not close the gap:
NON ADDITIVE BYdoes not rescue the wrong view. It makes a metric semi-additive by sorting on the named dimension and taking the last snapshot, so over August it would return one day's count, not the month's distinct total. That is a different calculation from recounting the distinct servers.- Snowflake's own fix for distinct counts across grains is the array and HLL pattern described in "Using Arrays to Compute Distinct Values for Hierarchical Aggregations." HLL is a method for estimating distinct counts. It works, and it makes the point. Getting the number right takes a derived calculation, not a label on the metric.
A governed definition told you the metric's name and where it is stored. It did not tell you that summing it across August was invalid. Whether that sum is valid depends on how the metric was built, which is in the code. Only a step that reads the code and checks the math before the answer ships can catch it.
7. Declared vs derived: what actually makes additivity safe
Put the three cases together and the pattern is clear. A catalog can hold the metric's name, where it is stored, what it is declared to do, and what feeds it. All of that is either declared by a person, or observed from usage and served as context. What a catalog cannot do is derive, from the transformation code, whether a given way of combining the metric is valid, and then check that before the number goes out.
You can catch many cases just by reading the metric expression. If a metric is written as SUM(revenue) / COUNT(DISTINCT customer_id), the non-additivity is visible in the expression, and a tool can flag it without looking anywhere else. Typedef ships that today. The harder case is the buried one above, where the non-additivity is hidden in an upstream model and the metric expression looks fine on its own. Catching that requires following the lineage back into the transformation code and deriving the metric's properties from how it was built. That part is a prototype today, not shipped, and it would be dishonest to imply otherwise. Additivity is made safe by deriving it from the code. A label that someone remembered to attach does not make it safe.
8. A compiler in the loop for data agents
This problem is worse now than a year ago, because the consumer changed. A human analyst who sees 477 servers growing every month might pause, because they remember it was 48 last quarter. An AI agent will not pause. It will return the inflated number with full confidence and a governed definition to cite, because the governed definition is what told it the metric was trustworthy. Defining a metric once makes it reusable everywhere. When the metric is wrong, the wrong number is reused everywhere too, which is worse than a single mistake.
The fix is not more context. The fix is a compiler in the loop. It reads the transformation code. It derives what each metric is and how it may be combined. Then it checks the answer against those rules before the answer ships. That is the category Typedef is building, a compiler for data agents. Horizon Context is a good library. The missing piece is the check between the library and the answer.
9. FAQ
Is Horizon Context GA or preview?
Mostly preview as of June 2026. Snowflake announced Horizon Context at Summit on June 2, 2026. The metadata connectors and Semantic Studio are in private preview, the OpenLineage API is in public preview, and the underlying pieces it builds on, semantic views and the NON ADDITIVE BY clause, are generally available. Treat the broader Horizon Context experience as preview, and check the current docs before you depend on a specific connector or capability.
How is Horizon Context different from Cortex Sense? Horizon Context is the store. Cortex Sense is the retriever. Horizon Context holds the governed definitions, lineage, and glossary. Cortex Sense reads that store at the moment a question arrives and serves the fitting definitions to Snowflake's agents. This is the library and librarian split. Neither one checks whether the answer the agent then computes is valid. See What Is Cortex Sense? for the runtime half of the story.
How is Horizon Context different from Unity Catalog metric views? They are the same idea on different platforms. Both are a governed definition store. Snowflake's is the semantic view inside Horizon Context. Databricks' is the metric view inside Unity Catalog. Both recompute at query time, both let you declare semi-additivity by hand, and both leave the same gap between a governed definition and a verified calculation. The Databricks version is covered in What Are Metrics in Unity Catalog?.
Do Snowflake semantic views double-count distinct measures?
Not on base data. Defined over raw rows, a semantic view computes COUNT(DISTINCT) correctly at any grain you query. The double-count appears only when the distinctness was already collapsed upstream and the metric sums those pre-aggregated counts across time, the case shown above. The view is doing correct arithmetic on the data it was given. The error came from one layer up.
What is time-additivity? Time-additivity is whether a metric can be validly added across time. Revenue is time-additive, because January plus February is a sensible year-to-date total. A distinct count of users or servers is not, because adding each day's distinct count counts the same user once per active day. The trouble is that both look like a number in a column, so a tool that only reads the definition cannot tell them apart.
Sources
- Snowflake, "Snowflake Horizon Context: The Governed Context Layer for AI, BI and Apps" - https://www.snowflake.com/en/blog/horizon-context-governed-context/
- Snowflake docs, CREATE SEMANTIC VIEW - https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view
- Snowflake docs, semi-additive metrics (NON ADDITIVE BY), released 2026-03-05 - https://docs.snowflake.com/en/release-notes/2026/other/2026-03-05-semantic-views-semi-additive-metrics
- Snowflake docs, querying a semantic view (SEMANTIC_VIEW) - https://docs.snowflake.com/en/sql-reference/constructs/semantic_view
- Snowflake engineering, "Why Do We Need Semantic Views?" - https://www.snowflake.com/en/blog/engineering/why-we-need-semantic-views/
- Snowflake docs, "Using Arrays to Compute Distinct Values for Hierarchical Aggregations" - https://docs.snowflake.com/en/user-guide/querying-arrays-for-distinct-counts
- Snowflake, Open Semantic Interchange - https://www.snowflake.com/en/blog/open-semantic-interchange-ai-standard/
