
Snowflake tag-based policies now support aggregation, row access, projection, and join policies (Public Preview), so I tried all four types together
This page has been translated by machine translation. View original
This is Kawabata.
In data governance operations, it is not uncommon for the task of assigning policies to each table to become burdensome as the number of tables requiring protection increases. While masking policies have long supported tag-based application, row access policies, aggregation policies, and others required direct assignment to the target objects.
On July 21, 2026, tag-based application of Aggregation Policy, Row Access Policy, Projection Policy, and Join Policy became available in Public Preview. By setting a policy on a tag, any object to which that tag is applied will automatically be protected.
In this article, I will verify these four types of tag-based policies using a trial account (Enterprise Edition). I will cover everything from setting policies on tags, applying tags to objects, and confirming enforcement for restricted roles, describing the behavior of all four types as well as the detailed specifications confirmed during testing (such as policy replacement using the FORCE option and the constraint of one policy per tag).
Note: This article reflects verification results from the Public Preview stage as of August 4, 2026. Since this is a preview stage, behavior may change in the future.
It should also be noted that during testing conducted on July 24, 2026, shortly after the preview launch, there was an issue where tag-based control for types applied to tables/views (aggregation, row access, join) could not be confirmed in this verification environment. In the re-verification on August 4, 2026, this issue no longer reproduced in the same environment. The cause has not been identified, but it is possible that the timing of the feature rollout to the environment had an impact, given that it was immediately after the launch.
【Addendum】
I was selected as a finalist in the "RISING COMMUNITY LEADER OF THE YEAR" category, APJ slot, of the Snowflake Community Awards.
Please see the link below for details.
Overview of Tag-Based Policies
Tag-based policies are a mechanism that combines the object tag feature with data protection policies. By setting a policy on a tag with ALTER TAG ... SET <POLICY_TYPE> POLICY, the policy is automatically applied to all objects to which that tag is assigned.
With this release, the policy types that can be set on a tag are now the following five:
| Policy Type | Control Target | Tag-Based Application Availability |
|---|---|---|
| Masking Policy | Concealing column values | GA (available previously) |
| Aggregation Policy | Enforcing aggregation with minimum group size or more | Public Preview (added this time) |
| Row Access Policy | Excluding rows that do not meet conditions | Public Preview (added this time) |
| Projection Policy | Controlling whether columns can be included in query results | Public Preview (added this time) |
| Join Policy | Requiring joins for table queries and optionally restricting allowed join keys | Public Preview (added this time) |
The levels at which tags can be applied differ by policy type.
| Policy Type | Database | Schema | Table/View | Column |
|---|---|---|---|---|
| Aggregation Policy | ○ | ○ | ○ | - |
| Row Access Policy | ○ | ○ | ○ | - |
| Projection Policy | ○ | ○ | ○ | ○ |
| Join Policy | ○ | ○ | ○ | - |
When a tag is applied to a database or schema, the tables and views beneath it inherit the tag and are protected collectively.
Prerequisites
- Tag-based aggregation, row access, projection, and join policies are available with Enterprise Edition or higher
- The
CREATE TAGprivilege at the schema level is required to create tags - The
CREATE <POLICY_TYPE>privilege at the schema level is required to create policies - The
APPLY <POLICY_TYPE>privilege at the account level is required to set policies on tags - The
APPLY TAGprivilege (or object-specific APPLY privilege) is required to apply tags to objects
Verification was performed against a trial account (Enterprise Edition) via Snowflake CLI 3.23.0.
Preparation
Create a SALES schema for data and a separate GOVERNANCE schema for tag and policy management.
Preparation
USE ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS TAG_POLICY_DEMO;
CREATE SCHEMA IF NOT EXISTS TAG_POLICY_DEMO.SALES;
CREATE SCHEMA IF NOT EXISTS TAG_POLICY_DEMO.GOVERNANCE;
USE SCHEMA TAG_POLICY_DEMO.SALES;
CREATE OR REPLACE TABLE customers (
customer_id INT,
customer_name VARCHAR,
email VARCHAR,
region VARCHAR
);
INSERT INTO customers VALUES
(1, 'Sato', 'sato@example.com', 'EAST'),
(2, 'Suzuki', 'suzuki@example.com', 'EAST'),
(3, 'Takahashi', 'takahashi@example.com', 'WEST'),
(4, 'Tanaka', 'tanaka@example.com', 'WEST'),
(5, 'Ito', 'ito@example.com', 'NORTH'),
(6, 'Watanabe', 'watanabe@example.com', 'EAST');
CREATE OR REPLACE TABLE orders (
order_id INT,
customer_id INT,
order_date DATE,
amount INT,
region VARCHAR
);
INSERT INTO orders VALUES
(101, 1, '2026-07-01', 1000, 'EAST'),
(102, 2, '2026-07-02', 1500, 'EAST'),
(103, 6, '2026-07-03', 2000, 'EAST'),
(104, 1, '2026-07-04', 1200, 'EAST'),
(105, 2, '2026-07-05', 1800, 'EAST'),
(106, 3, '2026-07-06', 3000, 'WEST'),
(107, 4, '2026-07-07', 2500, 'WEST'),
(108, 3, '2026-07-08', 1100, 'WEST'),
(109, 4, '2026-07-09', 900, 'WEST'),
(110, 5, '2026-07-10', 4000, 'NORTH'),
(111, 5, '2026-07-11', 600, 'NORTH');
The row count by region in orders is 5 for EAST, 4 for WEST, and 2 for NORTH. Since the aggregation policy's minimum group size is set to 3, this design allows us to verify the case where NORTH is consolidated into the remainder group.
Create the TAG_POLICY_ANALYST role as the role subject to policy restrictions. Within the policy definition, SYSADMIN is excluded from restrictions.
Role
USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS TAG_POLICY_ANALYST;
GRANT USAGE ON DATABASE TAG_POLICY_DEMO TO ROLE TAG_POLICY_ANALYST;
GRANT USAGE ON ALL SCHEMAS IN DATABASE TAG_POLICY_DEMO TO ROLE TAG_POLICY_ANALYST;
GRANT SELECT ON ALL TABLES IN DATABASE TAG_POLICY_DEMO TO ROLE TAG_POLICY_ANALYST;
GRANT SELECT ON FUTURE TABLES IN DATABASE TAG_POLICY_DEMO TO ROLE TAG_POLICY_ANALYST;
GRANT USAGE ON WAREHOUSE compute_wh TO ROLE TAG_POLICY_ANALYST;
GRANT ROLE TAG_POLICY_ANALYST TO USER <verification username>;
Setting policies on tags requires the account-level APPLY <POLICY_TYPE> privilege, and applying tags to objects requires the APPLY TAG privilege. In this article, to simplify verification, these are granted to SYSADMIN.
USE ROLE SECURITYADMIN;
GRANT APPLY TAG ON ACCOUNT TO ROLE SYSADMIN;
GRANT APPLY AGGREGATION POLICY ON ACCOUNT TO ROLE SYSADMIN;
GRANT APPLY ROW ACCESS POLICY ON ACCOUNT TO ROLE SYSADMIN;
GRANT APPLY PROJECTION POLICY ON ACCOUNT TO ROLE SYSADMIN;
GRANT APPLY JOIN POLICY ON ACCOUNT TO ROLE SYSADMIN;
Note: The above grants are for simplifying verification. In production operations, it is recommended to manage these using dedicated roles with separation of duties, such as separating the tag administrator role from the policy administrator role.
Let's Try It Out
Tag-Based Aggregation Policy
First, create a tag and an aggregation policy, then set the policy on the tag using ALTER TAG ... SET AGGREGATION POLICY. This is a policy that imposes no restrictions on SYSADMIN, while requiring a minimum group size of 3 for all other roles.
USE ROLE SYSADMIN;
USE SCHEMA TAG_POLICY_DEMO.GOVERNANCE;
CREATE OR REPLACE TAG agg_protection;
CREATE OR REPLACE AGGREGATION POLICY min3_agg_policy
AS () RETURNS AGGREGATION_CONSTRAINT ->
CASE
WHEN CURRENT_ROLE() = 'SYSADMIN' THEN NO_AGGREGATION_CONSTRAINT()
ELSE AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 3)
END;
ALTER TAG agg_protection SET AGGREGATION POLICY min3_agg_policy;
ALTER TABLE TAG_POLICY_DEMO.SALES.orders SET TAG agg_protection = 'protected';
When a query without aggregation is executed under the TAG_POLICY_ANALYST role, the aggregation policy is applied via the tag, resulting in an error.
USE ROLE TAG_POLICY_ANALYST;
SELECT * FROM TAG_POLICY_DEMO.SALES.orders;

An aggregation query that satisfies the minimum group size constraint of 3 succeeds. Since the row count by region is 5 for EAST, 4 for WEST, and 2 for NORTH, NORTH, which has fewer than 3 rows, should be consolidated into the remainder group and returned as such.
SELECT region, COUNT(*) AS cnt
FROM TAG_POLICY_DEMO.SALES.orders
GROUP BY region;

Verifying Tag-Based Associations
Tag-based policy associations can be confirmed with the POLICY_REFERENCES table function. The TAG_NAME column shows which tag is being used to apply the policy.
SELECT policy_name, policy_kind, ref_entity_name, tag_name, policy_status
FROM TABLE(TAG_POLICY_DEMO.INFORMATION_SCHEMA.POLICY_REFERENCES(
REF_ENTITY_DOMAIN => 'TABLE',
REF_ENTITY_NAME => 'TAG_POLICY_DEMO.SALES.ORDERS'
));

Note that TAG_NAME and POLICY_STATUS in POLICY_REFERENCES can be used to confirm tag-based policy associations. However, the official definition of POLICY_STATUS = 'ACTIVE' is written with the assumption of column-level association. Especially when introducing preview features, it is recommended to not only check metadata but also execute actual queries under the restricted role to confirm that control is working as expected.
Tag-Based Row Access Policy
The row access policy follows the same flow. It is applied tag-based to orders, which has a column (region) with the same name and type as the policy argument.
CREATE OR REPLACE TAG row_protection;
CREATE OR REPLACE ROW ACCESS POLICY region_rap
AS (region VARCHAR) RETURNS BOOLEAN ->
CURRENT_ROLE() = 'SYSADMIN'
OR region = 'EAST';
ALTER TAG row_protection SET ROW ACCESS POLICY region_rap;
ALTER TABLE TAG_POLICY_DEMO.SALES.orders SET TAG row_protection = 'protected';
When querying with the TAG_POLICY_ANALYST role, the results are limited to only EAST rows.
USE ROLE TAG_POLICY_ANALYST;
SELECT region, COUNT(*) AS cnt
FROM TAG_POLICY_DEMO.SALES.orders
GROUP BY region;

For tables with different column names, you can specify a signature alias with the ON clause when setting the policy on the tag. Alias tuples are matched in the order specified, and the first tuple that can be resolved to the table's column names and data types is used. If multiple tuples can be resolved to the same table, an error occurs; if no tuple can be resolved, the policy is not applied to that table, as stated in the official documentation. Let's test this with a table that has a sales_area column.
CREATE OR REPLACE TABLE TAG_POLICY_DEMO.SALES.orders_by_area (
order_id INT,
sales_area VARCHAR,
amount INT
);
INSERT INTO TAG_POLICY_DEMO.SALES.orders_by_area VALUES
(201, 'EAST', 1000),
(202, 'WEST', 2000),
(203, 'EAST', 1500),
(204, 'NORTH', 3000);
CREATE OR REPLACE TAG row_protection_alias;
ALTER TAG row_protection_alias SET ROW ACCESS POLICY region_rap
ON (region VARCHAR), (sales_area VARCHAR);
ALTER TABLE TAG_POLICY_DEMO.SALES.orders_by_area
SET TAG row_protection_alias = 'protected';
Since the sales_area column resolves to the second tuple, only the 2 EAST rows are returned from orders_by_area as well.

Tag-Based Join Policy
A join policy is a policy that prohibits standalone queries on a table and requires a join. When setting it on a tag, you can also restrict the allowed join keys with ALLOWED JOIN KEYS.
CREATE OR REPLACE TAG join_protection;
CREATE OR REPLACE JOIN POLICY customer_join_policy
AS () RETURNS JOIN_CONSTRAINT ->
CASE
WHEN CURRENT_ROLE() = 'SYSADMIN' THEN JOIN_CONSTRAINT(JOIN_REQUIRED => FALSE)
ELSE JOIN_CONSTRAINT(JOIN_REQUIRED => TRUE)
END;
ALTER TAG join_protection SET JOIN POLICY customer_join_policy
ALLOWED JOIN KEYS (customer_id);
ALTER TABLE TAG_POLICY_DEMO.SALES.customers SET TAG join_protection = 'protected';
When executing a standalone SELECT under the TAG_POLICY_ANALYST role, the join policy is applied via the tag, resulting in an error.
USE ROLE TAG_POLICY_ANALYST;
SELECT customer_id, region FROM TAG_POLICY_DEMO.SALES.customers;

A query that joins with orders using the allowed join key (customer_id) succeeds.
SELECT c.region, COUNT(*) AS cnt
FROM TAG_POLICY_DEMO.SALES.customers c
JOIN TAG_POLICY_DEMO.SALES.orders o
ON c.customer_id = o.customer_id
GROUP BY c.region;

Once confirmed, since the subsequent verification (projection and masking) uses standalone queries against customers, let's remove the join policy tag from customers. If left on, the subsequent standalone SELECT statements will result in a join-required error.
USE ROLE SYSADMIN;
ALTER TABLE TAG_POLICY_DEMO.SALES.customers
UNSET TAG TAG_POLICY_DEMO.GOVERNANCE.join_protection;
Tag-Based Projection Policy
The projection policy is unique among the four types in that tags can also be applied at the column level. This allows the control of "not showing the value itself, but allowing it to be used for filtering" to be rolled out at the column level.
USE ROLE SYSADMIN;
USE SCHEMA TAG_POLICY_DEMO.GOVERNANCE;
CREATE OR REPLACE TAG pii_column;
CREATE OR REPLACE PROJECTION POLICY pii_proj_policy
AS () RETURNS PROJECTION_CONSTRAINT ->
CASE
WHEN CURRENT_ROLE() = 'SYSADMIN' THEN PROJECTION_CONSTRAINT(ALLOW => TRUE)
ELSE PROJECTION_CONSTRAINT(ALLOW => FALSE)
END;
ALTER TAG pii_column SET PROJECTION POLICY pii_proj_policy;
ALTER TABLE TAG_POLICY_DEMO.SALES.customers
ALTER COLUMN email SET TAG pii_column = 'email';
When the TAG_POLICY_ANALYST role attempts to project the email column, an error occurs immediately after the tag is applied.
USE ROLE TAG_POLICY_ANALYST;
SELECT customer_name, email FROM TAG_POLICY_DEMO.SALES.customers;

A query that does not include the email column, and a query that uses it only as a condition in a WHERE clause without projecting it, both succeed. The original behavior of the projection policy—"not showing the value itself, but allowing it to be used for filtering"—works via tags as well.
-- Succeeds
SELECT customer_name, region FROM TAG_POLICY_DEMO.SALES.customers;
-- Using in WHERE clause without projecting is allowed
SELECT customer_name FROM TAG_POLICY_DEMO.SALES.customers
WHERE email LIKE '%example.com';


Testing with a table-level tag also applied the policy similarly, making all columns in the table non-projectable.
Reference: Tag-Based Masking Policy (GA)
For reference, I also verified the tag-based masking policy, which has been available in GA for some time, in the same environment.
USE ROLE SYSADMIN;
USE SCHEMA TAG_POLICY_DEMO.GOVERNANCE;
CREATE OR REPLACE TAG mask_test;
CREATE OR REPLACE MASKING POLICY mask_string AS (val STRING) RETURNS STRING ->
CASE WHEN CURRENT_ROLE() = 'SYSADMIN' THEN val ELSE '***MASKED***' END;
ALTER TAG mask_test SET MASKING POLICY mask_string;
ALTER TABLE TAG_POLICY_DEMO.SALES.customers
MODIFY COLUMN customer_name SET TAG mask_test = 'on';
Immediately after the tag was applied, customer_name was masked to ***MASKED***.

Schema-Level Tag Inheritance and Priority of Direct Assignment
When a tag is applied to a schema, the tables and views beneath it inherit the tag and are protected collectively. Tags should also be automatically applied to tables created after the tag is applied. I will verify this using a dedicated SALES_PROTECTED schema (applying it directly to the SALES schema would impose the aggregation policy on customers and others, interfering with other tests).
USE ROLE SYSADMIN;
CREATE SCHEMA IF NOT EXISTS TAG_POLICY_DEMO.SALES_PROTECTED;
CREATE OR REPLACE TABLE TAG_POLICY_DEMO.SALES_PROTECTED.orders_copy AS
SELECT * FROM TAG_POLICY_DEMO.SALES.orders;
-- Apply tag to schema → tables beneath inherit and are protected
ALTER SCHEMA TAG_POLICY_DEMO.SALES_PROTECTED
SET TAG TAG_POLICY_DEMO.GOVERNANCE.agg_protection = 'protected';
-- Table newly created after the tag was applied
CREATE OR REPLACE TABLE TAG_POLICY_DEMO.SALES_PROTECTED.new_table AS
SELECT 1 AS id UNION ALL SELECT 2;

New Table

This schema inheritance is particularly important when combined with dbt. In dbt-snowflake's general table materialization, models are recreated with something equivalent to CREATE OR REPLACE on every dbt run, so tags and policies applied directly to tables are not retained after recreation (the actual DDL issued depends on the dbt version, materialization, and project customizations). With schema inheritance, however, protection should be automatically applied to recreated models as well. Let's verify this by simulating dbt's recreation with CREATE OR REPLACE.
-- Simulating model recreation by dbt run
CREATE OR REPLACE TABLE TAG_POLICY_DEMO.SALES_PROTECTED.orders_copy AS
SELECT * FROM TAG_POLICY_DEMO.SALES.orders;

Also, when a direct-assignment policy and a tag-based policy conflict, the direct assignment takes priority (for aggregation policies, this is only when the entity keys match; if they do not match, both may be applied). Let's verify this by directly assigning a separate policy with a minimum group size of 5 to orders, which already has the tag-based (min3) policy applied.
USE ROLE SYSADMIN;
-- Remove the row access policy tag first, as leaving it would limit results to EAST only, making the difference hard to see
ALTER TABLE TAG_POLICY_DEMO.SALES.orders UNSET TAG TAG_POLICY_DEMO.GOVERNANCE.row_protection;
CREATE OR REPLACE AGGREGATION POLICY TAG_POLICY_DEMO.GOVERNANCE.min5_agg_policy
AS () RETURNS AGGREGATION_CONSTRAINT ->
CASE
WHEN CURRENT_ROLE() = 'SYSADMIN' THEN NO_AGGREGATION_CONSTRAINT()
ELSE AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)
END;
ALTER TABLE TAG_POLICY_DEMO.SALES.orders
SET AGGREGATION POLICY TAG_POLICY_DEMO.GOVERNANCE.min5_agg_policy;
If the tag-based min3 were in effect, both EAST (5 rows) and WEST (4 rows) would be displayed, but the following confirms that the directly assigned min5 takes priority.

Verifying Limitations on Actual Hardware
I confirmed what errors occur for the limitations described in the official documentation.
Tags with policies set cannot be dropped.
DROP TAG TAG_POLICY_DEMO.GOVERNANCE.agg_protection;

Policies set on tags also cannot be dropped.
DROP AGGREGATION POLICY TAG_POLICY_DEMO.GOVERNANCE.min3_agg_policy;

It is not possible to set multiple policies of the same type on a single tag. The error message suggests using the FORCE option for replacement.
ALTER TAG agg_protection SET AGGREGATION POLICY min5_agg_policy;

Actually executing with FORCE succeeded, and upon checking with POLICY_REFERENCES, the policy associated with the tag had been replaced with MIN5_AGG_POLICY. The official documentation also describes the procedure of using FORCE for single-statement replacement, noting that splitting UNSET and SET into two statements may create a window during which the tag-based policy protection is absent. This is an option to use proactively when replacing policies.
ALTER TAG agg_protection SET AGGREGATION POLICY min5_agg_policy FORCE;

Also, coexistence of different policy types on the same tag is not possible. Attempting to set a row access policy on a tag that already has an aggregation policy results in the following error. If you want to use multiple types together, the design must use separate tags per type. Note that masking policies are an exception; as stated in the official documentation: "A tag can have only one masking policy per data type," meaning one can be set per data type on a single tag.
ALTER TAG agg_protection SET ROW ACCESS POLICY region_rap;

Schemas containing tags with policies also cannot be dropped. The error message reveals that the reason for the drop being blocked is that the tag is assigned to (in use by) objects in other schemas.
DROP SCHEMA TAG_POLICY_DEMO.GOVERNANCE;

To drop it, you must first disassociate the tag and policy using ALTER TAG ... UNSET <POLICY_TYPE> POLICY.
Limitations and Notes
Here is a summary of the key points from the verification and official documentation.
Limitations and Notes
- Enterprise Edition or higher is required (currently in Public Preview)
- During testing conducted shortly after the preview launch (July 24, 2026), there was an issue where tag-based control for types applied to tables/views (aggregation, row access, join) could not be confirmed (this did not reproduce in re-verification on August 4, 2026; the cause has not been identified, but the timing of the feature rollout to the environment may have had an impact). When introducing preview features, it is recommended to confirm application not just by checking metadata, but by running actual queries under the restricted role
- The 4 types added this time allow only one policy per type per tag, and coexistence of different policy types is also not permitted (confirmed by testing). Masking policies are an exception and can be set one per data type
- Multiple tag-based policies of the same type cannot be associated with the same object (multiple tag-based row access policies or join policies on a single table/view, and multiple tag-based projection policies on a single column, are not allowed). If a configuration has multiple tag-based aggregation policies reaching the same table, please verify the application behavior in advance
- When changing the value of a tag with a policy, in configurations where the tag owner and the policy administrator differ, there may be cases where the schema owner and others cannot change the tag value. In such cases, it may be necessary to first disassociate the policy from the tag, change the value, and then reassociate
- Policy replacement can be performed without
UNSETusing theFORCEoption (confirmed by testing; also described in the official documentation) - When a direct-assignment policy and a tag-based policy conflict, the direct-assignment policy takes priority (for aggregation policies, this is only when entity keys match; if they do not match, both may be applied)
- Tags with policies set, policies set on tags, and the databases/schemas containing them cannot be dropped (the association must be removed first)
- Policies cannot be set on system tags
Closing
With the support for 4 types of tag-based policies, it becomes possible to consolidate the management of data protection policies from "per object" to "per tag." In the verification, I confirmed that for all four types—aggregation, row access, projection, and join—protection is applied simply by setting a policy on a tag and applying the tag to an object. Combined with schema-level tag inheritance, this enables a governance operation where "attaching a tag to a schema automatically protects tables created afterward."
My impression is that what was previously only possible with masking policies has now been extended to four policies, greatly broadening the scope of application.
I look forward to the GA release.
I hope this article is helpful to someone!
