Agentic schema mapping: messy CSVs to canonical tables with a Cortex Agent¶
A Cortex Agent profiles messy CSV files, proposes column mappings with AI_COMPLETE, resolves free-text values, and loads approved mappings through allowlisted SQL.
| Stage | Build |
| Topics | Cortex AI Functions, Cortex Agents |
| Status | GA |
| Time to complete | 45min |
| Edition | Any edition with Cortex AI Functions and Cortex Agents |
| Regions | Regions where the chosen AI_COMPLETE model and Cortex Agents are available, or cross-region inference enabled |
| Privileges | ACCOUNTADMIN for the account statements in 01_setup.sql and for teardown; everything else runs as COOKBOOK_AGENTIC_SCHEMA_MAPPING_ROLE |
| Related skills | cortex-ai-function-studio (bundled with Cortex Code), agent-optimization (bundled with Cortex Code), check-agent-access (Snowflake-Labs/coco-skills), semantic-view-patterns (Snowflake-Labs/coco-skills) |
| Last verified | 2026-10-04 |
| Authors | Parker Erickson |
| Code | View all files or download agentic-schema-mapping.zip, which includes this page as README.md. Teardown: sql/99_teardown.sql |
This recipe is a reference implementation: review the code and adapt it to your environment before production use.
Finance teams receive the same kinds of data (expenses, vendor invoices, mileage claims) in endless shapes: terse
headers like Amt and Dt, amounts written $1,200 or ~38, dates in four formats, and vendors spelled five ways.
This recipe builds a Cortex Agent that maps each file onto one canonical table. The model proposes the mapping and
matches free-text values to canonical lists, a person approves, and deterministic SQL does the load. You end with
three messy sample files loaded cleanly, a cache that makes repeat file shapes free, and a semantic view the agent can
answer questions from.
When to use this¶
- Files arrive in shapes you do not control, and writing a pipeline per shape does not scale.
- The target schema is fixed and known, and a human can review a mapping before it runs.
- Free-text values (vendor, department, payment method) must be normalized to a canonical list.
When not to use it:
- The file layout is stable. Write the
COPY INTOonce; no model is needed. - Loads must run unattended. This design requires an approval step before every new mapping executes.
- Files are wider than 20 columns, or huge. The landing step reads up to 20 columns and refreshes every staged file
on each
list_filescall.
How it works¶
- The model proposes; SQL decides.
AI_COMPLETEwithresponse_formatreturns a mapping whoseoperationandref_domainare constrained byenum. Nothing the model returns is executed as SQL. The loader builds theINSERTfrom fixed templates (TEXT,AMOUNT,DATE,RESOLVE) and identifiers that passed an allowlist. - Allowlists, not string cleaning. File names must exist in
RAW_ROWS, target tables inTARGET_REGISTRY, target columns inTARGET_COLUMNS(built fromINFORMATION_SCHEMA), and reference domains inREF_VALUES. ThePROPOSAL_LINESview reports the reason any line fails. File values are never concatenated into SQL: the file name and run id are bind variables, and resolved values are joined from a table. - One fixed landing statement. Every CSV, whatever its header, lands in
RAW_ROWSas an array of strings per line, so profiling and resolution are plain SQL with no dynamic statements. - Pay the model once per shape and per value. Approved mappings are cached by a hash of the header. Resolutions are stored per value, so only values never seen before go to the model, and any canonical value the model invents is discarded.
- Caller’s rights throughout. Every procedure runs
EXECUTE AS CALLER, so the agent can only read and write what the calling user’s role can.
Prerequisites¶
| Requirement | Notes |
|---|---|
| Cortex AI Functions and Cortex Agents | The recipe role gets SNOWFLAKE.CORTEX_USER. |
| Model access | Default claude-sonnet-4-6 (in MODEL_CONFIG). Enable cross-region inference if it is not in your region, or change MODEL in 01_setup.sql. The agent’s orchestration model is auto. |
| ACCOUNTADMIN | Only for the account statements at the top of 01_setup.sql and for 99_teardown.sql. |
| Default role | Cortex Agents in Snowflake CoWork use the calling user’s default role, not the session role. Give users a default role that has been granted COOKBOOK_AGENTIC_SCHEMA_MAPPING_ROLE, and a default warehouse. SNOWFLAKE.CORTEX.DATA_AGENT_RUN called from SQL ran the tools with the session role in testing, so 07 and the SQL example below work without it. |
| Fixed object names | The procedure bodies, the semantic view YAML, and the agent spec name COOKBOOK_AGENTIC_SCHEMA_MAPPING literally, so tool calls work regardless of the caller’s current database. Keep the names as written; change only the warehouse size and MODEL. |
Quickstart¶
Run in order, in one session (Snowsight worksheet, or snow sql -f <file> for each):
- sql/01_setup.sql: role, warehouse, database, stage, target tables, allowlists, operational tables.
- sql/02_seed_reference_data.sql: canonical values (fictional vendors).
- sql/03_stage_sample_files.sql: writes three messy CSV files to the stage.
- sql/04_deploy_tools.sql: parsers, views, model UDFs, and the six tool procedures.
- sql/05_semantic_view.sql: the
EXPENSE_DATAsemantic view. - sql/06_agent.sql: the
SCHEMA_MAPPER_AGENT. - sql/07_run_and_test.sql: runs the whole pipeline without the agent and prints PASS/FAIL checks.
- Talk to the agent in Snowflake CoWork: “Map invoices_chaotic.csv”.
- sql/99_teardown.sql when done.
To map your own file, upload it to @COOKBOOK_AGENTIC_SCHEMA_MAPPING.INGESTION.UPLOAD_STAGE (Snowsight, or
PUT file:///path/file.csv @... AUTO_COMPRESS = FALSE) and ask the agent to map it.
Walkthrough¶
1. Land and profile¶
LIST_FILES() runs one COPY INTO RAW_ROWS with a fixed column list (ARRAY_CONSTRUCT($1, ..., $20)) over every
.csv on the stage. The header row is row 1. PROFILE_FILE(file_name) is plain SQL over RAW_ROWS:
An unknown file name returns Unknown file: call list_files ..., so a path like ../../etc/passwd never reaches a
stage reference.
2. Propose a mapping¶
PROPOSE_MAPPING(file_name) first looks up MAPPING_CONFIGS by a SHA-2 hash of the sorted, lowercased header. On a
miss it calls AI_PROPOSE_MAPPING, a SQL UDF over AI_COMPLETE:
The proposal is stored as PENDING and returned with a per-line check from PROPOSAL_LINES:
3. Resolve free text¶
PREVIEW_RESOLUTION(proposal_id) collects the distinct values of each RESOLVE column, skips values already in
VALUE_RESOLUTION, and asks AI_RESOLVE_VALUES to match the rest to the domain’s canonical list. An answer is kept
only if it exactly matches a value in REF_VALUES; anything else is stored as no match. On the sample expenses file,
“gas station (Northwind)” resolves to Northwind Fuel and “Sales & BD” to Sales, while the bare “Amex”, “visa”, and
“petty cash” come back as NO MATCH. That is the point of the preview: the user settles them with
SET_RESOLUTION('PAYMENT_METHOD', 'Amex', 'Corporate Amex'), which rejects any value that is not canonical. User
corrections take precedence over model answers in VALUE_RESOLUTION_CURRENT.
4. Execute an approved mapping¶
EXECUTE_MAPPING(proposal_id, approved) refuses unless approved is true, the proposal is still PENDING, and every
line passes validation. It then builds the load from templates:
It runs with EXECUTE IMMEDIATE :load_sql USING (v_file, run_id) inside a transaction that first deletes the file’s
earlier rows, so reloading a file replaces it. The generated SQL is kept in RUN_HISTORY.LOAD_SQL for audit, and the
approved mapping is saved to MAPPING_CONFIGS.
5. The agent¶
06_agent.sql creates SCHEMA_MAPPER_AGENT with CREATE AGENT IF NOT EXISTS, then applies the spec with
ALTER AGENT ... MODIFY LIVE VERSION SET SPECIFICATION, so reruns update it in place. The spec is assembled in a
Snowflake Scripting block so $WH (checked to be a plain identifier) becomes each tool’s execution_environment
warehouse. The orchestration instructions encode the workflow and forbid calling execute_mapping before an explicit
yes. A cortex_analyst_text_to_sql tool over EXPENSE_DATA answers questions about loaded data.
Run it from SQL:
The response calls list_files, profile_file, propose_mapping, and preview_resolution in order, and stops
without loading.
Verify¶
07_run_and_test.sql ends with a PASS/FAIL table. Expected, all PASS:
| Test | Expected detail |
|---|---|
| all three files loaded in full | expenses_messy.csv=10/10, invoices_chaotic.csv=6/6, mileage_messy.csv=5/5 |
| every file went to a different canonical table | PURCHASE_EXPENSES, VENDOR_INVOICES, MILEAGE_CLAIMS |
| expense amounts and dates parsed | total 9812.7100 |
| expense departments and vendors are canonical | canonical department names only |
| invoice dates parsed / mileage distance units stripped | no NULLs; total distance 338.8000 |
| repeat header served from cache | cache |
| unapproved execute refused / executed proposal cannot be replayed | Not executed... / Proposal is EXECUTED... |
| injected identifiers refused | Not executed. Fix these lines first: ... |
| non-canonical resolution refused / unknown file refused | Rejected... / Unknown file... |
Then ask the agent “How much did each department spend on purchases that are already loaded?”. It answers through
query_loaded_data, for example Engineering 7,150.00 USD across 2 purchases.
Gotchas¶
| Symptom | Cause | Fix |
|---|---|---|
| Agent tool fails with “does not exist or not authorized” | Agents run as the user’s default role, not the session role | Grant the recipe role to the user’s default role and set a default warehouse |
| “Argument number 0 … needs to be a string literal” | The AI_COMPLETE model argument came from a column | Pass the model as a bind variable or literal, as the procedures do |
| Proposal line shows “target column does not exist on the target table” | The model guessed a column name | Nothing runs; call propose_mapping again or adjust the prompt |
A RESOLVE value loads as NULL | It was NO MATCH and was not corrected | Call set_resolution before execute_mapping |
01/02/2025 loads as January 2 | PARSE_DATE_LOOSE reads slash dates as month first | Add a day-first format for your locale, or map that column with your own parser |
| Columns beyond the 20th are missing | LIST_FILES lands $1 to $20 | Extend ARRAY_CONSTRUCT in LIST_FILES |
| File contents try to steer the mapping | The profile, including sample values, is part of the prompt | The prompt labels it as data, outputs are allowlisted, and a person approves; keep the approval step |
Tools fail after $DB was changed | Procedure bodies, YAML, and spec use the literal name | Keep the fixed names. Rerun 01_setup.sql to 06_agent.sql as written |
Teardown¶
The recipe makes no account-level changes beyond the role, warehouse, and database.
Adapt this recipe¶
Keep these, so a customized version keeps the recipe’s security model:
- Least privilege:
ACCOUNTADMINonly for the scripts listed under Privileges; everything else runs as the recipe’s own role. - No grants to
PUBLIC. - Caller’s rights: procedures run as
EXECUTE AS CALLER, so each user’s own grants apply. - Credentials: create keys, tokens, and secrets yourself, in a separate terminal, and read them from environment variables. Never put them in a script or give them to an agent.
- Dynamic SQL, if you add any, uses bind variables or an allowlist of identifiers, never text concatenated from user or model input.
Change these to fit your use case:
- Object names. Rename them in every script, including teardown: the recipe repeats them as literals, including inside
$$bodies, so change them all at once, then check that teardown still removes everything. - Roles, personas, and users: map them to your own, each granted no more than the recipe grants.
- Data: point the scripts at your tables instead of the sample data, then run Verify again.
- Models and warehouse sizes.
To have a coding agent adapt it:
Run it with a coding agent¶
Related skills¶
cortex-ai-function-studio(bundled): designing and costingAI_COMPLETEcalls with structured output.agent-optimization(bundled): evaluating and tuning the agent’s instructions and tools.check-agent-access(coco-skills): auditing who can run the agent and each custom tool.semantic-view-patterns(coco-skills): extendingEXPENSE_DATA(for example with verified queries from theai_metadatasnippet) once you load real data.