Snowflake gave everyone 102 models and didn't tell me

200 users, zero curation, and a bootstrap grant I didn’t ask for.

Share
Snowflake gave everyone 102 models and didn't tell me

TL/DR: ❄️ Snowflake is moving away from CORTEX_MODELS_ALLOWLIST toward model RBAC. When I checked if that is of any relevance to my account, the bootstrap already happened in the background: all 102 models currently in SNOWFLAKE.MODELS were reachable by my ~200 active users, with no heads-up when model 103 arrives. So I built a monitor first and the lockdown second.


BCR-2378 was the trigger: Snowflake is deprecating the allowlist parameter and switching to role-based access control for models. Starting 2026-09-08 with full retirement on 2026-11-18.

CORTEX_MODELS_ALLOWLIST deprecation and embedding model RBAC enforcement (Pending) | Snowflake Documentation

Nothing breaks... which might be a problem

First check: what is my account's allowlist currently set to? CORTEX_MODELS_ALLOWLIST = 'All', the default, never explicitly modified by me. Second check: SHOW GRANTS TO APPLICATION ROLE SNOWFLAKE.PUBLIC. That's where it got interesting: Snowflake had already applied its own compatibility bootstrap, granting SNOWFLAKE."CORTEX-MODEL-ROLE-ALL" to SNOWFLAKE.PUBLIC. Nothing breaks on enforcement day. My account is already RBAC-equivalent to its previous allowlist behavior 👍

Privileges and model access for Cortex AI Functions | Snowflake Documentation

But "nothing breaks" isn't the same as "nothing to do". That bootstrap grant reminded me of something I pushed ahead for weeks: every model Snowflake has shipped, the full Claude/GPT/Grok/Gemini families included, is callable by any of my ~200 active users today. And that could be rather expensive really quickly 👎

Curating that grant down to a named model list is the goal. But first, I need a monitor as prerequisite: I can't write a curated list without knowing what exists, and Snowflake (currently) doesn't really have a channel to tell me when models become available to me. So how do I ever update this curated list? 🤔

Existing is not the same as callable

Two facts made a one-time cleanup insufficient. SHOW MODELS IN SNOWFLAKE.MODELS refreshes roughly daily on its own... but a model appearing there doesn't really say much about whether my account can actually use it. Since I only want to use models available in the EU, cross-region inference is enabled for exactly two region groups:

alter account set cortex_enabled_cross_region = 'AWS_EU,AZURE_EU';

Therefore, a model existing globally says nothing about whether it's callable from my region under that setting. And this is why Snowflake currently doesn't really have a useful channel to tell me when new models show up - I may read on LinkedIn when a new frontier model arrives in US regions, but nobody informs me when it arrives in the EU 😅 The only way to know, short of reading Snowflake's internal region-rollout schedule (which I don't have) is to actually try to call a model.

I started setting up my monitor with a snapshot table carrying two facts: what model showed up when and whether it answered from my region (under my account-level settings):

create or alter table meta.cortex.t_model
(
      model_name        varchar         not null primary key
    , model_type        varchar
    , created_on        timestamp_ntz
    , first_seen        timestamp_ntz   default current_timestamp()
    , last_seen         timestamp_ntz
    , eu_available      boolean         comment 'result of a 1-token AI_COMPLETE probe under CORTEX_ENABLED_CROSS_REGION; null = not a chat model, not probed'
    , eu_probe_error    varchar         comment 'AI_COMPLETE error text if the probe failed, or "skipped: ..." if the model type is not probable'
    , notified          timestamp_ntz   comment 'when this row was last included in a cortex_models_new notification'
)
comment = 'Snapshot of SNOWFLAKE.MODELS, used to detect and alert on newly available Cortex models'
;

notified does two things here: it marks a row as already reported and it gets cleared back to null whenever a previously failing model flips to available (we'll get to that in a bit).

The procedure

Then I (well, not me personally, of course! 😅) frequently refresh the list of current models and probe those that need to be probed:

create or replace procedure meta.cortex.p_refresh_model_inventory()
copy grants
returns varchar
language sql
comment = 'Snapshot SHOW MODELS IN SNOWFLAKE.MODELS into T_MODEL and probe newly-seen chat models for EU availability'
as
$$
declare
    model_count       integer default 0;
    probe_result      varchar;
    v_model_name      varchar;
    c_unprobed cursor for
        select model_name, model_type
        from meta.cortex.t_model
        where (eu_available is null and eu_probe_error is null)                       -- never probed
            or (eu_available = false and dayofweek(current_date()) = 1)               -- previously failed, re-check Mondays only
    ;
begin
    show models in snowflake.models;

    create or replace temporary table meta.cortex.t_current_models as
        select
              "name"          as model_name
            , "model_type"    as model_type
            , "created_on"::timestamp_ntz as created_on
        from table(result_scan(last_query_id(-1)))
    ;

    model_count := (select count(*) from meta.cortex.t_current_models);

    merge into meta.cortex.t_model tgt
    using meta.cortex.t_current_models src
        on tgt.model_name = src.model_name
    when matched then
        update set tgt.last_seen = current_timestamp()
    when not matched then
        insert (model_name, model_type, created_on, first_seen, last_seen)
        values (src.model_name, src.model_type, src.created_on, current_timestamp(), current_timestamp())
    ;

    -- probe eu availability for models not yet probed, one AI_COMPLETE call per model (1 token)
    for rec in c_unprobed do
        v_model_name := rec.model_name;

        if (rec.model_type != 'CORTEX_BASE'
            or v_model_name ilike any ('%EMBED%', '%EXTRACT%', '%PARSE%', '%TRANSCRIBE%', '%SENTIMENT%', '%TRANSLATE%', '%GUARD%', '%TEXT2SQL%')
        ) then
            update meta.cortex.t_model
            set eu_probe_error = 'skipped: not an AI_COMPLETE-callable chat model'
            where model_name = :v_model_name
            ;
            continue;
        end if;

        begin
            select ai_complete(:v_model_name, 'ping', {'max_tokens': 1}) into :probe_result;
            update meta.cortex.t_model
            set eu_available = true
                , eu_probe_error = null
                , notified = null   -- un-notify: a model that just became EU-available is worth reporting again
            where model_name = :v_model_name
            ;
        exception
            when other then
                update meta.cortex.t_model
                set eu_available = false
                    , eu_probe_error = left(:sqlerrm, 500)
                where model_name = :v_model_name
                ;
        end;
    end for;

    return 'refreshed ' || model_count || ' models';
end;
$$
;

The SHOW ... → temp table via RESULT_SCAN(LAST_QUERY_ID(-1)) → MERGE shape is necessary here as there is no other way to get the current list of models programmatically than SHOW MODELS, but this can't sit inside a CTE. And the type/name filter before probing skips embeddings, extraction, translation, guard, and text2sql models... none of them take an AI_COMPLETE chat-style call, so probing them would just burn credits on a guaranteed error.

Probing 67 models every day?

The first working version of the re-probe (after the initial probe of all models) cursor was:

where (eu_available is null and eu_probe_error is null)
    or eu_available = false

67 previously failed models (usually account-blocked models) got re-probed cleanly. Unfortunately, that would also mean re-probing all non-EU models every day. Its cost is fairly small per call (1-token AI_COMPLETE ping), but daily... trying to raise region rollout of new models to my attention, that doesn't happen daily.

I fixed it by only doing the full re-probe once a week: or (eu_available = false and dayofweek(current_date()) = 1). Running the procedure on a Friday (dayofweek = 5) results in a run duration of 9.5 seconds, so zero probes fired... vs 3-4 minutes when the cursor included the 67 failing rows.

Wired into what already exists

The view feeding the notification (to Teams in my case) only ever returns rows not yet reported:

create or replace view consume.monitor.v_cortex_models_new
comment = 'Cortex models newly seen in SNOWFLAKE.MODELS, not yet reported to Teams'
as
select
      m.model_name
    , m.first_seen
    , m.eu_available
from meta.cortex.t_model m
where m.notified is null
order by m.first_seen desc
;

A daily task calls the refresh procedure and a generic Teams-alert procedure I already had. This procedure posts an Adaptive Card if the view's row count is not zero:

create or alter task meta.cortex.ta_model_monitor
user_task_managed_initial_warehouse_size = 'XSMALL'
serverless_task_max_statement_size = 'XSMALL'
schedule = 'USING CRON 15 7 * * 1-5 Europe/Zurich'
target_completion_interval = '30 MINUTES'
user_task_timeout_ms = 1800000 -- 30 minutes
allow_overlapping_execution = false
suspend_task_after_num_failures = 10
task_auto_retry_attempts = 1
comment = 'Detect new Cortex models, alert to Teams'
query_tag = '{"task":{"database":"META","schema":"CORTEX","task_name":"TA_MODEL_MONITOR"}}'
as
begin
    call meta.cortex.p_refresh_model_inventory();
    call meta.load.p_monitor('cortex_models_new'); -- this sends the notification to Teams, but that could also be any other channel
    update meta.cortex.t_model set notified = current_timestamp() where notified is null;
end;
;
alter task meta.cortex.ta_model_monitor resume;

Now the curated RBAC grant itself, the actual point of this whole effort, is easy enough: the monitor tells me what models I can grant users access to and I'll be sooo ready as soon as an EU-hosted version of DeepSeek V4 Flash becomes available 😎