Refine a semantic view from agent logs: triage, SME review, and verified queries¶
Mine Cortex Agent traces for guessed, exploratory, and no-data answers, get the right answer from a subject matter expert, ship it into the semantic view, and re-test.
| Stage | Evaluate, Operate |
| Topics | Semantic Views, AI Observability, Cortex Agents |
| Status | GA |
| Time to complete | 30min |
| Edition | Any edition with semantic views and Cortex Agents |
| Regions | Regions where Cortex Agents and Cortex Analyst are available |
| Privileges | ACCOUNTADMIN for the account statements in 01, the READ UNREDACTED AI OBSERVABILITY EVENTS TABLE grant in 02, and teardown; everything else runs as COOKBOOK_REFINEMENT_LOOP_FROM_AGENT_LOGS_ROLE |
| Related skills | semantic-view-patterns (Snowflake-Labs/coco-skills (https://github.com/Snowflake-Labs/coco-skills)), agent-studio (bundled with Cortex Code) |
| Last verified | 2026-10-04 |
| Authors | Bala Krishnamoorthy |
| Code | View all files or download refinement-loop-from-agent-logs.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.
An agent’s first week of real questions is the best specification its semantic view will ever get. This recipe turns that traffic into changes to the view on a fixed cadence: read every turn from the agent’s traces, sort the turns into review buckets, send the uncertain ones to a subject matter expert as plain question-and-answer pairs, turn each confirmed answer into a description, dimension, metric, custom instruction, or verified query, and re-ask the same questions to confirm the change. The runnable example does one full cycle on a small fictional support-ticket dataset: before the change, the agent guesses what “urgent” means, answers “no escalated tickets” when there are five, and averages resolution time over the wrong population; after, each of those questions is answered correctly.
When to use this¶
- An agent (in Snowflake CoWork, an MCP client, or your own app over the Agent REST API) is live on a semantic view and you want its answers to improve week over week instead of staying at first-draft quality.
- Your users say things the data doesn’t: business words (“urgent”, “escalated”, “waiting on the customer”) over coded values (P1, OPEN, PENDING_CUSTOMER).
- You want a review process where subject matter experts judge answers, not SQL or semantic view diffs.
When not to use it:
- There is no traffic yet. Build the first evaluation set from expected questions instead; see agent-feedback-to-evals for turning traffic into an evaluation dataset once it exists.
- You are debugging one slow or failing request. diagnose-agent-requests breaks a single trace down span by span.
- The wrong numbers come from the data model (mixed grains, fan traps), not from missing definitions. mixed-grain-and-verified-queries covers the mixed-grain case; the
sv_diagnosticssnippet under Related skills covers joins.
How it works¶
- The trace is the log. Every agent turn writes one trace to
SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS, read per agent withSNOWFLAKE.LOCAL.GET_AI_OBSERVABILITY_EVENTS(db, schema, agent, 'CORTEX AGENT'). A trace holds the question, the answer, each SQL statement the agent ran, and whether a verified query was used. Direct Cortex Analyst traffic (not through an agent) is logged separately and read withSNOWFLAKE.LOCAL.CORTEX_ANALYST_REQUESTS('SEMANTIC_VIEW', '<view>'); Analyst calls made by an agent are in the agent trace, not there.CORTEX_AGENT_USAGE_HISTORYis for cost and adoption and has no question text, so it can’t drive this loop. - Triage sorts work; people judge answers.
REFRESH_TRIAGElabels each turnNO_DATA(said the data isn’t there),NO_TOOL(answered without SQL),GUESSED(said it interpreted or assumed),EXPLORED(ran more than one query, usually to discover coded values),VERIFIED(used a verified query), orANSWERED. These are text and count heuristics. A confident wrong answer looks exactly like a right one, so a random sample ofANSWEREDturns goes to review too. In this example, that sample is what catches the Billing average reported over 42 tickets when only 20 were resolved. - The expert sees the question and the answer, never the SQL. Their reply (“urgent means P1”, “escalated means P1 or P2 and still open”) is the raw material. The engineer translates it into the view.
- Fix the view, not the agent, and not the client. Each confirmed answer becomes a durable artifact in the semantic view, so every surface inherits it. The trace only records what reached the agent: instructions kept in an MCP client’s system prompt or skills never reach it, aren’t reviewable here, and don’t help any other client.
- Version every change.
CHANGE_LOGrecords each shipped change, its view version, and whose answer it came from. Triage labels every turn with the version that answered it, which gives a before and after on the same questions, and the log is the close-out note you send back to the experts.
Prerequisites¶
| Requirement | Notes |
|---|---|
| Cortex Agents and Cortex Analyst | The recipe role gets SNOWFLAKE.CORTEX_USER. |
| ACCOUNTADMIN | Only for the account statements in 01_example_setup.sql, the grant in 02_grant_unredacted.sql, and 99_teardown.sql. |
READ UNREDACTED AI OBSERVABILITY EVENTS TABLE | Account-level privilege. Without it the table functions return span names, tools, latency, and tokens, but the question and answer text is redacted and there is nothing to triage. A role that holds it can read every conversation with every agent it can monitor: grant it only to the small role that curates the semantic layer, after privacy review. |
MONITOR (or OWNERSHIP) on the agent | The recipe role owns the agent. For your own agents, GRANT MONITOR ON AGENT or GRANT MONITOR ON FUTURE AGENTS IN SCHEMA. |
| Default role | The agent’s Cortex Analyst tool runs as the caller. Make sure your default role inherits COOKBOOK_REFINEMENT_LOOP_FROM_AGENT_LOGS_ROLE (setup grants it to you and to SYSADMIN). |
Quickstart¶
Run in order:
- sql/01_example_setup.sql: role, warehouse, database, 120 fictional tickets, the thin first-draft semantic view, the agent, and
CHANGE_LOG. - sql/02_grant_unredacted.sql: the account-level privilege that makes conversation text readable. Read the prerequisite above first.
- sql/03_generate_traffic.sql: five questions through
DATA_AGENT_RUN, one statement at a time. Wait about a minute. - sql/04_triage.sql: creates
REFRESH_TRIAGE, buildsTRIAGE, and prints the bucket counts, the review queue, and the list for the expert. - sql/05_apply_refinements.sql: the expert’s answers as a
CREATE OR ALTER SEMANTIC VIEW, theCHANGE_LOGentries, and a YAML round-trip check. - sql/06_retest.sql: the same five questions against the refined view, then the before and after comparison. Wait about a minute before its
CALL. - sql/99_teardown.sql.
Walkthrough¶
1. Read one row per turn¶
REFRESH_TRIAGE is a caller’s-rights procedure, so it only reads traces the calling role may monitor. Every name it uses arrives as a parameter: the agent goes to the table function as bind variables, and the change log is read with IDENTIFIER(:CHANGE_LOG_TABLE). It writes TRIAGE to the caller’s current schema. To point it at another agent, call it by its full name from a schema where your role can create a table, and pass that agent’s change log, or NULL if its semantic view has none:
With NULL, VIEW_VERSION is empty and the window is the lookback alone. The core of the procedure groups spans by trace:
The documentation defines the table function, the event layout, and the CORTEX_AGENT_FEEDBACK record name for explicit feedback. The span and attribute names above are what the events hold today, not a documented contract, so 04_triage.sql shows how to list them if a column comes back empty. Explicit thumbs up or down is a separate signal: agent-feedback-to-evals links feedback events to turns.
2. Triage the first-draft view¶
Against the thin view (every column present, descriptions like 'Priority.'), three runs of the same five questions triaged the same way each time:
| Question | Bucket | What happened |
|---|---|---|
| How many urgent tickets are still open? | GUESSED | Queried for priority ILIKE 'urgent', got nothing, listed the values, then said it “interpreted urgent as the highest priority (P1)”. The number (6) was right; the definition was a guess. |
| How many escalated tickets do we have for the Mobile App? | EXPLORED | Searched for an “Escalated” status, found none, and answered “no escalated tickets” or said the data has no escalated status. The support team’s answer is 5. |
| How many tickets are waiting on the customer? | ANSWERED or EXPLORED | Found PENDING_CUSTOMER, sometimes after listing the status values. Right (30). |
| What is the average time to resolve Billing tickets? | ANSWERED | 19.25 hours, which is right, “based on 42 Billing tickets”. Only 20 are resolved; the other 22 have no resolution time. |
| Which support engineer closed the most tickets? | NO_DATA | Said the data doesn’t track engineers. Correct: a real scope gap. |
The ANSWERED Billing turn is the reason for the spot-check sample: no heuristic flags it, and a support lead reading “based on 42 tickets” would.
3. Route each finding to its fix¶
The trace tells you which part of the view to change. Each cause has a different fix:
| Symptom in the trace | Cause | Fix in the semantic view |
|---|---|---|
Filter on a literal that doesn’t exist (ILIKE 'urgent'), then a query to list values | The words users say aren’t mapped to the codes | Describe the codes in the column COMMENT; add SAMPLE_VALUES, and IS_ENUM when the list is closed |
| “No such status” for a business term | A business definition that lives in people’s heads | A dimension or metric that encodes it (is_escalated), with the definition in its comment |
| Right SQL, misleading answer | The metric is correct but its population isn’t stated | A companion metric (resolved_ticket_count) and an AI_SQL_GENERATION instruction to report it |
| The same question answered ad hoc every week | No verified query | Add it to AI_VERIFIED_QUERIES, with ONBOARDING_QUESTION TRUE for the ones new users should see |
| A question the view can’t answer | Scope | AI_QUESTION_CATEGORIZATION to say so, or a different agent or view |
| Wrong view or tool chosen | Overlapping tool descriptions in the agent | Rewrite the agent’s tool descriptions to be mutually exclusive |
| Wrong totals, not wrong words | The data model (grain, joins) | See mixed-grain-and-verified-queries and the sv_diagnostics snippet |
For writing the descriptions themselves, description-quality has a rubric and a linter. For pattern-level fixes (derived metrics, semi-additive metrics, time intelligence, range joins), use the semantic-view-patterns skill in apply mode rather than hand-writing them.
4. Ship the expert’s answers¶
05_apply_refinements.sql applies all five answers in one CREATE OR ALTER SEMANTIC VIEW, which keeps existing grants and unsets anything omitted, so always send the full definition. The parts that came from the expert:
VERIFIED_BY names a team contact, not a person. The same script then records the change in CHANGE_LOG as version v1, one row per answer, and prints the close-out list for the experts: which of their answers shipped, and as what. That message is what keeps them answering the next batch.
5. Keep the view in version control¶
Semantic views export as YAML (SYSTEM$READ_YAML_FROM_SEMANTIC_VIEW) or DDL (GET_DDL('SEMANTIC_VIEW', ...)). Pick one format, commit it, and validate every change in CI with the four-argument procedure:
Without the fourth argument, the procedure replaces an existing view (CREATE OR REPLACE ... COPY GRANTS). 05_apply_refinements.sql exports the refined view and validates the export with TRUE, TRUE. The exported YAML keeps the custom instructions under module_custom_instructions (sql_generation, question_categorization) and the verified queries under verified_queries with verified_by, verified_at, and use_as_onboarding_question.
6. Re-ask and compare¶
06_retest.sql sends the same five questions and rebuilds TRIAGE. In all three runs:
| Question | Before | After |
|---|---|---|
| Urgent tickets still open | GUESSED, 2 to 3 queries | VERIFIED, 1 query: “6 urgent (P1) tickets” |
| Escalated tickets for the Mobile App | EXPLORED, “no escalated tickets” | VERIFIED, 1 query: “5 escalated tickets (P1 or P2 and still OPEN)” |
| Waiting on the customer | ANSWERED or EXPLORED | ANSWERED, 1 query: 30 |
| Average time to resolve Billing | “based on 42 Billing tickets” | “based on 20 resolved tickets” |
| Support engineer with most closures | NO_DATA | NO_DATA |
The escalated question matched vq_escalated_by_product even though it was phrased differently and filtered to one product. The urgent question matched its verified query word for word, which is the expected outcome for a recurring question; in production, judge the loop on next week’s new questions, not only on replays.
Who does what¶
- Platform or analytics engineering owns the semantic views, agents, and the triage cadence, and holds the unredacted privilege.
- Subject matter experts and analysts supply correct answers and definitions and are the contacts on verified queries. They don’t review DDL.
- Consumers reach the agent through CoWork, an MCP client, or an app, and need no role beyond what the agent path requires.
Run the cycle weekly at first and monthly once the review queue shrinks. The output of each cycle is a change to the view, not a report.
Verify¶
After 04_triage.sql, the bucket counts cover exactly the five turns from 03, all on v0:
Agent answers vary, so the split between buckets varies too. In one run the urgent question needed two queries and landed in EXPLORED rather than GUESSED, and the escalated question said the data has no escalated status and landed in NO_DATA.
The CALL REFRESH_TRIAGE result reads 5 turns since <time> UTC, 5 with question text. If the second number is 0, the role lacks the unredacted privilege.
After 05, the YAML check returns YAML file is valid for creating a semantic view. No object has been created yet. and CHANGE_LOG has six rows (one v0, five v1); rerunning 05 keeps it at six.
After 06, the comparison shows the urgent and escalated questions moving to VERIFIED with SQL_RUNS_AFTER = 1, and the answers match the ground-truth row that the script prints: URGENT_OPEN 6, ESCALATED_MOBILE_APP 5, WAITING_ON_CUSTOMER 30, AVG_HOURS_BILLING 19.25, RESOLVED_BILLING 20. Agent answers vary between runs, so treat one run as a sample; the before and after pattern above held in every run while this recipe was built.
Gotchas¶
| Symptom | Cause | Fix |
|---|---|---|
QUESTION and ANSWER are NULL, but turns are counted | The role lacks READ UNREDACTED AI OBSERVABILITY EVENTS TABLE | Run 02_grant_unredacted.sql for the curating role only |
| Triage shows turns from an earlier run | Events outlive a dropped agent; a recreated agent with the same name sees them again | REFRESH_TRIAGE never reads earlier than the first row of the change log you pass; keep that row. With NULL only the lookback limits the window |
| A time window is off by several hours | The event TIMESTAMP is UTC TIMESTAMP_NTZ | Compare against SYSDATE(), not CURRENT_TIMESTAMP() |
| Triage is empty right after the questions | Traces arrive within about a minute | Wait, then CALL REFRESH_TRIAGE again |
SQL_RUNS is 0 and AGENT_SQL is empty on every turn | Span or attribute names changed | List them with SELECT DISTINCT RECORD:name FROM TABLE(SNOWFLAKE.LOCAL.GET_AI_OBSERVABILITY_EVENTS(...)) and update the procedure |
| Wrong answers never reach review | They land in ANSWERED; heuristics can’t see confident errors | Keep the random spot-check sample (SPOT_CHECK) |
| A description or instruction disappeared after a change | CREATE OR ALTER unsets anything omitted | Always apply the full definition from version control |
| A YAML deploy recreated the view | SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML without the fourth argument replaces the view | Pass create_or_alter = TRUE |
| Agent calls fail with privilege errors | The Cortex Analyst tool runs as the caller | The caller’s role needs SELECT on the semantic view and its base tables |
| Business logic keeps drifting between clients | It lives in MCP client prompts or skills, which never reach the agent | Move it into the semantic view or the agent’s instructions |
Teardown¶
The agent’s events stay in SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS: ingested rows can’t be modified, and only a role with the SNOWFLAKE.AI_OBSERVABILITY_ADMIN application role can delete them for retention management.
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, 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¶
semantic-view-patterns(coco-skills): apply mode adapts a modeling pattern to your own view. Theai_metadatasnippet teachesAI_SQL_GENERATION,AI_QUESTION_CATEGORIZATION, andAI_VERIFIED_QUERIES;sv_diagnosticscovers joins that produce wrong numbers. Use it for the pattern-level fixes the loop identifies.agent-studio(bundled): creating, auditing, and optimizing semantic views, verified queries, and agents, including an agent observability report over the same event table.
References¶
- Monitor Cortex Agent requests
- AI_OBSERVABILITY_EVENTS table
- GET_AI_OBSERVABILITY_EVENTS (SNOWFLAKE.LOCAL)
- Account privilege READ UNREDACTED AI OBSERVABILITY EVENTS TABLE
- Cortex Analyst administrator monitoring
- CREATE SEMANTIC VIEW
- Using SQL commands to create and manage semantic views
- SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML
- Custom instructions in Cortex Analyst
- Cortex Analyst Verified Query Repository
- Suggestions for semantic models and views
- Optimize an existing semantic view or model with verified queries (Preview)
- Best practices for modeling semantic views