04 · Cortex AI Functions¶
Support tickets, reviews, call notes, contracts: most companies have more text than anyone reads. Cortex AI functions run
large language models from SQL, inside Snowflake's service perimeter, so classifying a million tickets is a SELECT rather than
a pipeline to an external API. This lesson covers the functions, who may call them, where the data goes, and the part that
surprises teams on their first bill: how tokens add up.
Not run for this course
Cortex functions need a Snowflake account in a supported region, so none were called here and no model outputs are shown. Syntax, privileges, billing rules and limits come from Snowflake's documentation as of October 2026. This area changes monthly: functions move from preview to GA, and models are added and retired. Check the function reference and the regional availability page before relying on any detail.
The functions¶
Per the documentation, the task-specific functions include:
| Function | Does | Returns |
|---|---|---|
AI_CLASSIFY |
assigns text (or an image or document) to one or more of your categories | object with a labels array |
AI_FILTER |
evaluates a natural-language condition | BOOLEAN, usable in WHERE and JOIN … ON |
AI_SENTIMENT |
overall sentiment, optionally per aspect (up to 10) | object with categories: positive, negative, neutral, mixed or unknown |
AI_EXTRACT |
pulls named fields out of text or documents | object |
AI_SUMMARIZE_AGG, AI_AGG |
summarise or answer a prompt across many rows | string |
AI_TRANSLATE, AI_REDACT |
translate, remove PII | string |
AI_EMBED, AI_SIMILARITY |
vectors and similarity for search and clustering | vector, number |
AI_COMPLETE |
general prompt to a chosen model, optionally with a response schema | string or object |
Some are preview features. The documentation marks each one, and only GA functions are meant for production.
Examples on support tickets¶
-- route tickets: one label each, with descriptions to sharpen the boundaries
SELECT ticket_id,
AI_CLASSIFY(body,
[ {'label': 'billing', 'description': 'charges, refunds, invoices, payment failures'},
{'label': 'bug', 'description': 'a feature that worked before or should work does not work'},
{'label': 'how-to', 'description': 'questions about how to use an existing feature'},
{'label': 'account', 'description': 'users, admins, access, sign-in and workspace settings'} ],
{'task_description': 'Route customer support tickets for a SaaS product'}
):labels[0]::VARCHAR AS queue
FROM support.raw.tickets;
-- a semantic WHERE clause
SELECT ticket_id, body
FROM support.raw.tickets
WHERE created_at >= DATEADD(day, -7, CURRENT_TIMESTAMP())
AND AI_FILTER(PROMPT('The customer says they are considering cancelling: {0}', body));
-- aspect-based sentiment
SELECT ticket_id, AI_SENTIMENT(body, ['pricing', 'support', 'reliability']) AS sentiment
FROM support.raw.tickets;
-- structured extraction with a typed response format
SELECT ticket_id,
AI_COMPLETE(
model => 'llama3.3-70b',
prompt => 'Extract the order number and the product mentioned in this ticket: ' || body,
response_format => TYPE OBJECT(order_number STRING, product STRING),
model_parameters => {'temperature': 0}
) AS extracted
FROM support.raw.tickets;
-- one answer across many rows (not limited by a single context window, per the documentation)
SELECT AI_AGG(body, 'List the three most common reasons for refund requests, with one example each')
FROM support.raw.tickets
WHERE created_at >= DATEADD(month, -1, CURRENT_TIMESTAMP());
Two behaviours to plan for. Errors return NULL by default, row by row, and the query still succeeds. Pass
return_error_details => TRUE (the last argument) to get an object with the value or the error, and count the failures.
And outputs are probabilistic. temperature defaults to 0 for AI_COMPLETE, but a model or function version change can
still change results, so store the outputs you depend on rather than recomputing them.
Who may call them¶
From the documentation on privileges:
- A role needs both the account privilege
USE AI FUNCTIONSand one of the database rolesSNOWFLAKE.CORTEX_USERorSNOWFLAKE.AI_FUNCTIONS_USER. - By default,
USE AI FUNCTIONSandCORTEX_USERare granted toPUBLIC, so every user can call them.AI_FUNCTIONS_USERisn't. - Which models a role can use is controlled by model RBAC: application roles such as
SNOWFLAKE."CORTEX-MODEL-ROLE-LLAMA3.3-70B". The older account parameterCORTEX_MODELS_ALLOWLISTis being deprecated (since August 2026 it can only be set to'None'). ACCOUNTADMINalways has access to all models, including as a secondary role. Test restrictions with another role andUSE SECONDARY ROLES NONE.
USE ROLE ACCOUNTADMIN;
REVOKE USE AI FUNCTIONS ON ACCOUNT FROM ROLE PUBLIC;
REVOKE DATABASE ROLE SNOWFLAKE.CORTEX_USER FROM ROLE PUBLIC;
CREATE ROLE IF NOT EXISTS ai_user;
GRANT USE AI FUNCTIONS ON ACCOUNT TO ROLE ai_user;
GRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE ai_user;
GRANT APPLICATION ROLE SNOWFLAKE."CORTEX-MODEL-ROLE-LLAMA3.3-70B" TO ROLE ai_user;
GRANT ROLE ai_user TO ROLE support_analytics;
SHOW APPLICATION ROLES LIKE 'CORTEX-MODEL%' IN APPLICATION SNOWFLAKE;
Whether to lock it down is a governance decision, not a technical one. Leaving it open means anyone who can read a table can send it to a model, and spend credits doing so. Masking and row access policies (Level 3) still apply to what a role can read, and so to what it can send.
Where the data goes¶
The models run inside Snowflake's service perimeter, but not necessarily in your region. If a model isn't available
locally, cross-region inference (account parameter CORTEX_ENABLED_CROSS_REGION) can route the request elsewhere. Per
the documentation, data at rest stays in your account's region. The prompt and response travel encrypted to the processing
region and aren't persisted there, and billing happens in your region. The default depends on the account: new
organizations created after March 9, 2026 default to ANY_REGION. If your obligations require in-region processing, check the
parameter and set it explicitly:
SHOW PARAMETERS LIKE 'CORTEX_ENABLED_CROSS_REGION' IN ACCOUNT;
ALTER ACCOUNT SET CORTEX_ENABLED_CROSS_REGION = 'DISABLED'; -- or a list of allowed regions
What it costs¶
Cortex AI functions are billed per token (credits per million tokens, by function and model, in Snowflake's Service Consumption Table), plus the warehouse that runs the query. The documented rules that matter most:
- For generating functions (
AI_COMPLETE,AI_CLASSIFY,AI_FILTER,AI_AGG,AI_SUMMARIZE,AI_TRANSLATE), input and output tokens are both billed. For embeddings and similarity, input only. - Task-specific functions add their own prompt to your text, so billed tokens exceed your text.
- For
AI_CLASSIFY, the labels, descriptions and examples are billed as input for every row, not once per query. - Snowflake recommends a warehouse no larger than MEDIUM. A larger one doesn't make these functions faster.
The third rule is easy to overlook, because the labels look like a constant in the query. A lower-bound model of input tokens for the routing query above over one million tickets, using the documented four-characters-per-token convention:
# Lower-bound input-token model for AI_CLASSIFY over 1,000,000 tickets, using the documented ~4 characters per token
# convention and the documented rule that labels, descriptions and examples are billed as input FOR EVERY ROW.
# The function's own hidden prompt (also billed, per the docs) is unknown and left out, so real numbers are higher.
import json
tok = lambda s: len(s) / 4
tickets = ["My invoice for September shows a double charge for the Pro plan, please refund one of them.",
"The export to CSV button does nothing since yesterday's update.",
"How do I add a second admin to our workspace?",
"Card declined again although the bank says everything is fine. Order 88231."]
avg_ticket = sum(map(tok, tickets)) / len(tickets)
labels = ["billing", "bug", "how-to", "account"]
described = [{"label": "billing", "description": "charges, refunds, invoices, payment failures"},
{"label": "bug", "description": "a feature that worked before or should work does not work"},
{"label": "how-to", "description": "questions about how to use an existing feature"},
{"label": "account", "description": "users, admins, access, sign-in and workspace settings"}]
examples = [{"input": t, "labels": [l], "explanation": "matches the category description"}
for t, l in zip(tickets[:3], ["billing", "bug", "how-to"])]
ROWS = 1_000_000
variants = {"labels only": tok(json.dumps(labels)),
"+ descriptions": tok(json.dumps(described)),
"+ descriptions + 3 examples": tok(json.dumps(described)) + tok(json.dumps(examples))}
print(f"average ticket: {avg_ticket:.0f} tokens")
for name, extra in variants.items():
per_row = avg_ticket + extra
print(f"{name:<28} {per_row:6.0f} tokens/row {per_row * ROWS / 1e6:7.1f} M input tokens ({per_row / avg_ticket:.1f}x the tickets alone)")
average ticket: 17 tokens
labels only 27 tokens/row 26.9 M input tokens (1.6x the tickets alone)
+ descriptions 107 tokens/row 106.9 M input tokens (6.2x the tickets alone)
+ descriptions + 3 examples 222 tokens/row 222.1 M input tokens (13.0x the tickets alone)
With short tickets, the category definitions dominate: descriptions multiply input tokens about six times, and three few-shot examples about thirteen times, before the function's own prompt is counted. Descriptions and examples often improve accuracy, so the answer isn't to drop them. Measure accuracy with and without them on a labelled sample, and pay for the version that earns it. Then cut the volume:
- Classify each ticket once. Run the function from a stream-driven task (Level 2 · 06–07) and store the result, so dashboards read a column instead of calling a model.
- Filter with SQL first. A
WHEREon date, language or product beforeAI_FILTERsends fewer rows to the model. - Measure tokens before scaling.
AI_COUNT_TOKENSreturns the token count for a function and input, at warehouse cost only.
Track actual spend in ACCOUNT_USAGE (with its usual latency, Level 3 · 09):
SELECT function_name, model_name, SUM(tokens) AS tokens, SUM(token_credits) AS credits
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_FUNCTIONS_QUERY_USAGE_HISTORY
GROUP BY function_name, model_name
ORDER BY credits DESC;
How It Actually Works¶
An AI function call is part of an ordinary query. The warehouse reads and filters the rows, and for each input that reaches the function, Snowflake builds a request (your text plus the function's prompt, labels or schema) and sends it to the model-serving service, which Snowflake optimizes for throughput. Results come back as values in the query's output. That's why the documentation recommends batch use over interactive calls, why a larger warehouse doesn't help (the model service is the bottleneck), and why billing has two parts: warehouse time for the query, and tokens for the model. Access checks happen at both ends. Privileges decide whether the role may call the function and use the model, and the usual table privileges and policies decide which rows and values can be read and sent at all.
Common mistakes¶
- Leaving the default PUBLIC grants without deciding to. Every user can send any table they can read to a model.
- Recomputing model outputs in dashboards. Classify once, store the result.
- Ignoring
NULLresults. Errors are silent by default. Count them. - Long label descriptions and many examples on large tables without measuring their value. They're billed per row.
- Large warehouses for AI queries. Medium or smaller, per the documentation.
- Assuming in-region processing. Check
CORTEX_ENABLED_CROSS_REGION. - Treating outputs as deterministic facts. Evaluate against a labelled sample, and re-evaluate when a model changes.
Exercise¶
- In a trial account, check who can call AI functions (
SHOW GRANTS OF DATABASE ROLE SNOWFLAKE.CORTEX_USER) and whatCORTEX_ENABLED_CROSS_REGIONis set to. Write down whether either should change for your organization. - Label 50 synthetic tickets by hand. Run
AI_CLASSIFYwith labels only, with descriptions, and with examples, and record accuracy andTOKENSfrom the usage view for each. Which version would you pay for? - Build the "classify once" pipeline: a stream on
tickets, a task that insertsAI_CLASSIFYresults for new rows intoticket_labels, and a check that re-running the task classifies nothing. - Use
AI_COUNT_TOKENSon your ticket sample and compare it withclassify_tokens.py's four-characters estimate.