Snowflake gave every user access to Cortex AI by default.
The release notes on 13 May said granularity arrives. The
GRANTS_TO_ROLES view, opened ten days later in the same
account, said something different: granularity arrives after the
default already says yes. Both lines are true. Only one of them
gets read — and the bill keeps running while the wrong one is
on the screen.
Six already are. Nobody ratified them.
n_per_function_grants: 0.n_can_today: 12 · n_did_30d: 6 · the new GA privilege, still idle.
- Platform
- Snowflake on AWS, eu-west-1, Enterprise edition
- Audit window
- 2026-04-25 → 2026-05-25 (30-day rolling, inspected 2026-05-25)
- Surfaces
SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES·SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY·SHOW GRANTS ON DATABASE SNOWFLAKE- Sources
- Snowflake Cortex AI access control docs · the May 2026
USE AI FUNCTIONGA release note - Cost basis
- Not a billing audit; this is access-control surface area — who can spend, not who is
The release note and the grants view tell different stories.
Open the 13 May release note and you are reading about a new
lever. Open SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES
in the same browser tab and you are reading about a door that
has been ajar since the account was provisioned. Two paragraphs,
five tabs apart. Both true. We ran the query first, then went
back to the release note — and only then noticed the
difference between what was being announced and what was already
the case.
The lever is the lever the docs lead with: per-function
USE AI FUNCTION <name> privileges, GA on
2026-05-13, scoped to each entry in the Cortex catalog.
AI_COMPLETE (legacy
SNOWFLAKE.CORTEX.COMPLETE),
AI_SUMMARIZE (legacy
SNOWFLAKE.CORTEX.SUMMARIZE),
AI_CLASSIFY, AI_TRANSLATE,
AI_EMBED — each grantable to a role on its
own. On paper this is the privilege a security review has been
asking for: a summarisation pipeline can be trusted with
AI_SUMMARIZE without inheriting arbitrary
AI_COMPLETE against a foundation model. The lock is
well-designed.
The door is the SNOWFLAKE.CORTEX_USER database
role, granted to PUBLIC at account creation. Every
user the account has ever provisioned, and every user it ever
will, inherits the right to call any Cortex function on first
connection. The new privilege refines access along a path the
default has already opened. Granting the new one without
revoking the old changes nothing about who can spend.
- Cortex AISQL functions consume Snowflake credits on a per-token basis against the published per-model rate card, billed against the active warehouse. There is no per-user cap by default.
- A single curious user with
SELECT AI_COMPLETE(…)(or the legacySNOWFLAKE.CORTEX.COMPLETE) in a worksheet can move a non-trivial number of credits before anyone notices. - The new
USE AI FUNCTIONprivilege is the surface to fix this. Until you revoke the default, granting the new one changes nothing.
Entitled, exercised, and actually governed.
The shape of the gap is three integers. We pulled them in the order below because each one reframes the previous: the first tells you the surface area, the second tells you how much of that surface is actually used, and the third tells you how much is governed by anything other than the default. Read in sequence, they are the “here is your Cortex exposure” report most security reviews would have asked for if they knew the views existed.
| Number | Source | Meaning | Value |
|---|---|---|---|
n_can_today |
GRANTS_TO_ROLES → USERS |
Users entitled via PUBLIC → CORTEX_USER |
12 |
n_did_30d |
CORTEX_AI_FUNCTIONS_USAGE_HISTORY |
Distinct users who actually called any Cortex function in the last 30 days | 6 |
n_per_function_grants |
SHOW GRANTS ON DATABASE SNOWFLAKE |
Distinct USE AI FUNCTION grants currently in place (the new privilege) |
0 |
| Gap that matters | n_can_today − n_did_30d — entitled, never observed |
6 |
The triplet our worksheet returned matches the shape we expect
every production account to return on first audit:
n_can_today is essentially the active-user count
(12 of 14 here), n_did_30d is a small fraction of
that (6), and n_per_function_grants is exactly
0 because the new privilege has existed for under
two weeks and nobody has touched it. The default did
the granting. No human ratified the list.
-- Returns the row that says "yes, every user can call Cortex." -- If the result is non-empty, n_can_today is the count of active users. SELECT grantee_name, privilege, granted_on, name, created_on FROM snowflake.account_usage.grants_to_roles WHERE grantee_name = 'PUBLIC' AND name ILIKE '%CORTEX_USER%' AND deleted_on IS NULL ORDER BY created_on DESC;
-- n_did_30d is the COUNT(DISTINCT user_name) of this result. -- The per-function breakdown is the second cut: what is the entitled -- population actually using? Pull query_id too so any one call can be -- traced back through QUERY_HISTORY. SELECT user_name, function_name, COUNT(*) AS call_count, SUM(tokens) AS tokens, MAX(query_id) AS last_query_id FROM snowflake.account_usage.cortex_ai_functions_usage_history WHERE start_time >= DATEADD('day', -30, CURRENT_TIMESTAMP()) GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 20;
-- The GA privilege is granted on the SNOWFLAKE database object. -- Filter the result of SHOW GRANTS for "USE AI FUNCTION" rows; -- n_per_function_grants is the count of those rows. SHOW GRANTS ON DATABASE snowflake; SELECT "privilege", "granted_on", "name", "grantee_name", "granted_by" FROM TABLE(RESULT_SCAN(LAST_QUERY_ID())) WHERE "privilege" ILIKE 'USE AI FUNCTION%';
Three queries, one account, fifteen minutes. The triplet is the “here is your Cortex exposure” report most security reviews would have asked for if they knew the view existed. — Field report, §2
One privilege, one function, one role.
USE AI FUNCTION is granted on the
SNOWFLAKE database, scoped to a named Cortex
function. The grammar is regular SQL and the semantics are
ordinary RBAC. Two grants below: one narrow, one as broad as the
old database role:
CREATE OR REPLACE ROLE cortex_summariser; -- Either form works; the AI_* form aligns with current docs. GRANT USE AI FUNCTION snowflake.cortex.summarize ON DATABASE snowflake TO ROLE cortex_summariser; -- or, equivalently: -- GRANT USE AI FUNCTION snowflake.ai.ai_summarize -- ON DATABASE snowflake -- TO ROLE cortex_summariser; -- The role can call AI_SUMMARIZE (legacy SNOWFLAKE.CORTEX.SUMMARIZE), -- and only that. AI_COMPLETE, AI_CLASSIFY, AI_TRANSLATE, AI_EMBED -- (and their legacy SNOWFLAKE.CORTEX.* aliases) all return -- "insufficient privileges" on this role. GRANT ROLE cortex_summariser TO USER svc_summary_pipeline;
GRANT USE AI FUNCTION ON ALL ai_functions IN DATABASE snowflake TO ROLE cortex_power_user; -- Equivalent surface to the old SNOWFLAKE.CORTEX_USER database role, -- but now an explicit, named, auditable act — not a default.
The model surface is correct. Each privilege is a row in
SHOW GRANTS; each call traces to the function and
the granting role in CORTEX_AI_FUNCTIONS_USAGE_HISTORY;
an Auditor role can review the lot without elevated access.
Snowflake’s docs land this clearly. The piece they understate
is what stays in place while you adopt the new privilege:
the PUBLIC → CORTEX_USER grant is
not revoked by adopting USE AI FUNCTION. You revoke
it yourself, or it persists.
- Function-scoped, not database-scoped. Grant the function the role needs. Resist
ALL ai_functionsexcept for explicit power-user roles. - Granted to a named role, never to
PUBLIC. A grant toPUBLICdefeats the entire reason the privilege exists. - Paired with the corresponding revoke. Granting
SUMMARIZEto a service role only matters if theCORTEX_USERdefault is gone for that role’s users. - Linked to a service account, not a human. Cortex roles for pipelines belong on service principals; for humans, on named “cortex-approved” roles only.
- Recorded by the audit, not just the change ticket. The grant itself is the audit;
SHOW GRANTSis the source of truth.
Revoke. Inventory. Grant. In that order.
Read the docs top-down and you reach the new privilege first, the default-grant footnote later, the actual remediation last. That order leaves the default in place the whole time. The order that closes the door is the reverse: revoke the default, enumerate who actually used Cortex, re-grant per function against the named list. Revoke. Inventory. Grant. The only sequence that ends with a governed account.
Revoke. Remove the default CORTEX_USER
grant from PUBLIC. This is a one-line statement.
It is also the only step that is irreversible without a
re-grant, and the only one that produces an immediate behaviour
change for the population in n_can_today − n_did_30d.
The expected outcome is “nothing breaks”, because by
construction those users have not called Cortex in the audit
window.
REVOKE DATABASE ROLE snowflake.cortex_user FROM ROLE "PUBLIC";
Inventory. Run query 2 from §2 with a
wider window (90 days, not 30) to surface every user, function,
and pipeline that has actually used Cortex. Group by
warehouse_name as well as user_name to
catch pipelines that use a service warehouse but a human
principal. That list is the candidate set for the next step.
Grant. For each name on the inventory, decide:
is this a human worksheet user (probably gets a single
cortex_summariser-like role granting one function),
a service principal (gets a tightly-scoped role with only the
functions its pipeline calls), or an exploratory data-science
role (gets USE AI FUNCTION ON ALL ai_functions,
but only that role, only those users). Re-grant on the basis of
the inventory, not on the basis of who used to have it.
The docs lead with the new privilege because that is what is new. The audit leads with the revoke, because that is what is unsafe. The order matters because in between the two reads, the bill keeps running. — Field report, §4
One worksheet, three queries, one decision.
The audit runs on any Snowflake account a
SECURITYADMIN-equivalent role can read. Open a worksheet, run
the three queries from §2, write down the triplet. If
n_can_today − n_did_30d is large, you have a
decision in front of you: revoke and re-grant, or document why
the default was acceptable. The point is not the answer; it is
to make the decision an act, rather than a non-act.
The Crosshire version of the audit adds warehouse spend
attribution per Cortex function over the same 30-day window,
plus a join against SNOWFLAKE.ACCOUNT_USAGE.USERS
so the entitled set names its members rather than just counting
them. The discipline is the same either way.
Every number above traces to a row in ACCOUNT_USAGE.
A model never produces the numbers; a human ratifies every
revoke. The audit is honest because the queries are.
- Cortex spend attribution per function. Joining
CORTEX_AI_FUNCTIONS_USAGE_HISTORYtoWAREHOUSE_METERING_HISTORYto turn token counts into credits per function per role. The shape, the gotchas, the rate-card edge cases. - The Cortex Search and Cortex Agents privilege surfaces. Separate from
USE AI FUNCTION; same default-grant pattern; same revoke-first discipline. A follow-up note. - The
ANALYSTpersona andUSE AI FUNCTIONtogether. The first role most accounts will want: read-mostly, with two or three explicit Cortex functions. A reference implementation.
ACCOUNT_USAGE, with the
revoke-then-grant plan written against your actual user
population — named, scoped, and reviewed by a human before
any privilege is changed. The workbook stays with you. The
triplet is the headline; the plan is the artefact.
- Snowflake docs · Cortex AISQL functions — catalogue of
AI_COMPLETE,AI_SUMMARIZE,AI_CLASSIFY,AI_TRANSLATE,AI_EMBED(legacySNOWFLAKE.CORTEX.*names still resolve) - Snowflake docs · Cortex access control —
CORTEX_USERdatabase role, default grant toPUBLIC, the newUSE AI FUNCTIONprivilege - Snowflake docs ·
ACCOUNT_USAGE.GRANTS_TO_ROLES— the view query 1 reads - Snowflake docs ·
CORTEX_AI_FUNCTIONS_USAGE_HISTORY— the view query 2 reads - Snowflake release notes ·
USE AI FUNCTIONGA (2026-05-13) — the GA announcement this note is built around
Numbers in this note come from a single controlled-trial Snowflake account — illustrative of the shape every first audit returns, not a universal benchmark. The queries above produce the same triplet on any account they are run against. The discipline is the artefact — query, count, decide, revoke, re-grant. The same triplet ships in every Crosshire access-control audit. — Crosshire
Every Snowflake user can call Cortex AI by default.
We opened a worksheet, ran one query against
GRANTS_TO_ROLES, and the result came back with a
sting: twelve of fourteen active users were already entitled to
spend Cortex credits. None had asked. None had been ratified. The
per-function privilege Snowflake shipped on 13 May is real RBAC.
The door it locks was opened the day the account was created.
Six already are. Nobody ratified them. The new lever, idle.
n_can_today: 12 · n_did_30d: 6 · n_per_function_grants: 0
The audit is one worksheet, three queries, twenty minutes. The decision afterwards is the work; the queries are only the lights. — Crosshire audit, trial workspace
1The release note for 2026-05-13 read like good news: per-function USE AI FUNCTION privileges had reached GA, with SUMMARIZE, COMPLETE, CLASSIFY, TRANSLATE and EMBED each grantable on their own. We opened a worksheet expecting to draft a role design.
2Before drafting anything, we asked the simpler question: who can already call Cortex today? One query against GRANTS_TO_ROLES returned the answer in seconds. SNOWFLAKE.CORTEX_USER was granted to PUBLIC. The new privilege was refining an access path that had been open since the account was created.
3Two more queries finished the picture. CORTEX_AI_FUNCTIONS_USAGE_HISTORY said six of the twelve had actually called Cortex in the last thirty days. SHOW GRANTS ON DATABASE SNOWFLAKE said zero USE AI FUNCTION grants were in place. Entitled, exercised, governed — twelve, six, zero. The triplet was the report.
Twelve users entitled, six already spending, none of them ratified.
We were drafting a role design for the new
USE AI FUNCTION privilege when we paused to check a
hunch. One SELECT against
SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES —
filter grantee_name = 'PUBLIC', look for
CORTEX_USER, sort by created_on. The
row came back. It had been there since the account was
provisioned. Every user on the account had been entitled to
spend Cortex credits from the first day they logged in.
The default did the granting. Nobody had to ask. The new per-function privilege is well-designed RBAC — the lock Snowflake shipped is the lock you would have asked for — but it refines an access path the default has already opened. Until you revoke the default, granting the new privilege narrows nothing.
The release note announces the lever. The footnote owns the door.
Read the release notes top-down and you reach the new privilege
first, the catalog of grantable functions second, the worked
examples third. The line that says CORTEX_USER is
granted to PUBLIC by default lives two clicks away
on the access-control page, in the kind of paragraph a reader
scans for context rather than for instruction. It is not
hidden; it is just not where the action of the post is.
That is the trap. A reader who follows the docs in their natural order will adopt the new privilege without revoking the old default. Both stay in place. The default wins because the default is the union, not the intersection. The lock you just fitted does not actually lock until the door it is fitted to has been closed first.
Revoke first. Then inventory. Only then grant.
The instinct is to start with the new privilege — grant
USE AI FUNCTION to a couple of named roles and
feel productive. That order leaves the default in place the
whole time. The order that closes the door is the reverse: one
REVOKE against PUBLIC, then an
inventory pass over the real Cortex population from
CORTEX_AI_FUNCTIONS_USAGE_HISTORY across a 90-day
window, then per-function grants against that named list.
REVOKE DATABASE ROLE snowflake.cortex_user FROM ROLE "PUBLIC";
The revoke is the irreversible step and the only one that
produces a behaviour change in the gap population
(n_can_today − n_did_30d). The expected
outcome is “nothing breaks”, because by construction
those users had not been calling Cortex in the audit window
anyway. The inventory query tells you who you do need to
re-grant, with what scope.
CREATE OR REPLACE ROLE cortex_summariser; GRANT USE AI FUNCTION snowflake.cortex.summarize ON DATABASE snowflake TO ROLE cortex_summariser; GRANT ROLE cortex_summariser TO USER svc_summary_pipeline;
- The exact
SELECTwe ran againstGRANTS_TO_ROLES. Plus the per-function usage query againstCORTEX_AI_FUNCTIONS_USAGE_HISTORYand theSHOW GRANTSrecipe that surfacesUSE AI FUNCTIONrows. Copy-paste runnable on your own account. - The reasoning behind revoke before grant. Why the order matters, why the inventory query has to span 90 days not 30, and the role-design pattern that catches service-principal pipelines hiding behind human warehouses.
- Five questions to interrogate every Cortex grant. A checklist for whether a proposed
USE AI FUNCTIONgrant is scoped correctly, paired with its revoke, and bound to the right principal — the same list we use on a Crosshire audit.