{"templateName":"quickstart-page-template","canonicalLink":"https://www.snowflake.com/en/developers/guides/support-ops-ai-functions-dynamic-tables/","robotsTags":[],"allowedRenditionsWidth":["320","480","640","768","960","1200","1440","1920"],"cssClassNames":"page basicpage summit-page","description":"Build an automated support ticket enrichment pipeline using Cortex AI Functions, Dynamic Tables, and Streamlit in Snowflake.","language":"en","title":"AI-Powered Support Operations with Cortex AI Functions & Dynamic Tables","analyticsPageType":"quickstart-page-template","analyticsCategory":"general","analyticsSubCategory":"","excludeFromAnalytics":false,":hierarchyType":"page",":path":"/content/snowflake-site/global/en/developers/guides/support-ops-ai-functions-dynamic-tables","isPasswordProtected":false,"analyticsContentTags":["snowflake-site:taxonomy/solution-center/certification/quickstart","snowflake-site:taxonomy/snowflake-feature/cortex-llm-functions","snowflake-site:taxonomy/product/ai","snowflake-site:taxonomy/snowflake-feature/dynamic-tables"],":mappedPath":"/en/developers/guides/support-ops-ai-functions-dynamic-tables/",":type":"snowflake-site/components/structure/page","analyticsEnabled":true,"coveoConfig":{"apiKey":"xx335921a6-2a0a-40f2-a167-e390b4766c3d","organizationId":"snowflakecomputingproduction8neljofn","searchHub":"snowflake.com","pipeline":"snowflake.com"},"analyticsDebugMode":false,"analyticsData":{"excludeFromAnalytics":false,"subCategory":"","pageType":"quickstart-page-template","templateName":"quickstart-page-template","siteName":"snowflake","pageUrl":"/content/snowflake-site/global/en/developers/guides/support-ops-ai-functions-dynamic-tables","language":"en","category":"general","pageName":"AI-Powered Support Operations with Cortex AI Functions & Dynamic Tables","contentTags":["snowflake-site:taxonomy/solution-center/certification/quickstart","snowflake-site:taxonomy/snowflake-feature/cortex-llm-functions","snowflake-site:taxonomy/product/ai","snowflake-site:taxonomy/snowflake-feature/dynamic-tables"]},":items":{"root":{"columnClassNames":{"markup_editor_1950346551":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-banner":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-header":"aem-GridColumn aem-GridColumn--default--12","responsivegrid":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-footer":"aem-GridColumn aem-GridColumn--default--12","modal_container":"aem-GridColumn aem-GridColumn--default--12","markup_editor":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","columnCount":12,":items":{"experiencefragment-banner":{"id":"experiencefragment-8ef1f9163a","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/master.xfmodel.json"},"experiencefragment-header":{"id":"experiencefragment-b453a3b348","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master.xfmodel.json","languageNavPath":"/content/snowflake-site/global/en/developers/guides/support-ops-ai-functions-dynamic-tables.languagenav.json"},"markup_editor_1950346551":{"id":"markup-editor-15c2fa5dcd","title":" ","cssContent":".snowflake-markdown-table code[class*=language-],.snowflake-markdown-table code[class*=language-],.snowflake-markdown .snowflake-text code[class*=language-],.snowflake-markdown .snowflake-text pre[class*=language-]{background-color:rgba(var(--ui-12-rgb),.5);color:var(--text-01);text-shadow:none;padding:var(--spacing-00);border-radius:var(--spacing-00);font-size:smaller}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false},"responsivegrid":{"columnClassNames":{"quickstart_hero":"aem-GridColumn aem-GridColumn--default--12","flexible_column_cont":"aem-GridColumn aem-GridColumn--default--12","markup_editor":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","columnCount":12,":items":{"quickstart_hero":{"id":"quickstart-hero-f58bb2ce5e","isDeveloperGuidesPage":false,":type":"snowflake-site/components/quickstart/quickstart-hero","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/support-ops-ai-functions-dynamic-tables","quickstartHeroTitle":{"lines":["AI-Powered Support Operations with Cortex AI Functions & Dynamic Tables"],"type":"heading2",":type":"snowflake-site/components/title-v2"},"quickstartHeroAuthor":"James Cha-Earley","quickstartHeroFirstSnowflakeFeatureTag":{"tagText":"Cortex LLM","tagColor":"#29B5E8","tagPath":"/content/cq:tags/snowflake-site/taxonomy/snowflake-feature/cortex-llm-functions","tagIcon":""},"quickstartHeroForkRepoLink":{"id":"button-ef67698bc7","showOutboundIcon":false,"buttonLink":{"valid":true,"attributes":{"target":"_blank"},"url":"https://github.com/Snowflake-Labs/sfquickstarts/tree/master/site/sfguides/src/support-ops-ai-functions-dynamic-tables"},"linkTargetContentType":"GENERIC","linkType":"SNOWFLAKE_EXTERNAL",":type":"snowflake-site/components/button","text":"Fork Repo"},"quickstartHeroBreadcrumbs":[{"title":"AI-Powered Support Operations with Cortex AI Functions & Dynamic Tables","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers/guides/support-ops-ai-functions-dynamic-tables","currentPage":true},{"title":"Guides","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers/guides","currentPage":false},{"title":"Snowflake for Developers","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers","currentPage":false}],"quickstartHeroFirstCertifiedTag":{"tagText":"Quickstart","tagColor":"#29B5E8","tagPath":"/content/cq:tags/snowflake-site/taxonomy/solution-center/certification/quickstart","tagIcon":""}},"flexible_column_cont":{"id":"flexible-column-container-318b41e317","propertiesId":"quickstart-template-main-flexible-container","type":"2-column-75-25","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"none","bottomPadding":"none","spaceBetween":"small","reverseOnMobile":false,"carouselOnMobile":false,"backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-310ec06feb",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"contentfragment":{"id":"contentfragment-3a2c9cb8fb","description":"","paragraphs":["&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EOverview\u003C/h2\u003E\n","\u003Cp\u003ECustomer support teams spend significant time reading tickets to classify issues, gauge urgency, and decide on next steps. This quickstart shows how to automate that entire workflow using Snowflake Cortex AI Functions, with a Dynamic Table that keeps results fresh and a Streamlit dashboard for operational visibility.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003E\u003Cstrong\u003EHow this differs from other support quickstarts:\u003C/strong\u003E Unlike \u003Ca href=\"https://quickstarts.snowflake.com/guide/call-center-analytics-with-ai-transcribe-and-cortex-agents\"\u003Ecall-center-analytics-with-ai-transcribe-and-cortex-agents\u003C/a\u003E (which builds a full Cortex Agent for Q&amp;A) or \u003Ca href=\"https://quickstarts.snowflake.com/guide/streamlining-support-case-analysis-with-snowflake-cortex\"\u003Estreamlining-support-case-analysis\u003C/a\u003E (which uses LangChain for summarization), this guide takes a \u003Cstrong\u003Epure-SQL, zero-orchestration approach\u003C/strong\u003E. A single Dynamic Table handles transcription, classification, sentiment scoring, and recommendation generation &mdash; no agents, no external frameworks, no scheduler. New tickets are enriched automatically on refresh.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Cp\u003EYou'll build a pipeline that:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Cstrong\u003ETranscribes\u003C/strong\u003E phone call recordings into text with \u003Ccode\u003EAI_TRANSCRIBE\u003C/code\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EClassifies\u003C/strong\u003E every ticket into your taxonomy with \u003Ccode\u003EAI_CLASSIFY\u003C/code\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EScores sentiment\u003C/strong\u003E on a continuous numeric scale with \u003Ccode\u003EAI_COMPLETE\u003C/code\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EGenerates operational recommendations\u003C/strong\u003E per ticket with \u003Ccode\u003EAI_COMPLETE\u003C/code\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EAuto-refreshes\u003C/strong\u003E via a Dynamic Table &mdash; no scheduler, no Airflow\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ESurfaces results\u003C/strong\u003E in a Streamlit in Snowflake dashboard\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EArchitecture\u003C/h3\u003E\n\u003Cpre\u003E\u003Ccode\u003E  [Web/Email Forms] ──►  RAW.SUPPORT_TICKETS (text)\n  [Phone Calls]     ──►  @SUPPORT_AUDIO_STAGE (audio files)\n                              │\n                              ▼  Dynamic Table (auto-refresh)\n                    ANALYTICS.ENRICHED_TICKETS\n                      &larr; AI_TRANSCRIBE  (audio &rarr; text)\n                      &larr; AI_CLASSIFY    (issue category)\n                      &larr; AI_COMPLETE    (sentiment score)\n                      &larr; AI_COMPLETE    (recommended action)\n                              │\n                              ▼\n                    Streamlit in Snowflake (Ops Dashboard)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch3\u003EWhat You'll Need\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EA Snowflake account with ACCOUNTADMIN role in a \u003Ca href=\"https://docs.snowflake.com/user-guide/snowflake-cortex/aisql-regional-availability\"\u003Esupported region\u003C/a\u003E\u003C/li\u003E\u003Cli\u003EThe SNOWFLAKE.CORTEX_USER database role granted to your user\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EWhat You Will Learn\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EHow to use \u003Ccode\u003EAI_TRANSCRIBE\u003C/code\u003E to convert audio files to text\u003C/li\u003E\u003Cli\u003EHow to use \u003Ccode\u003EAI_CLASSIFY\u003C/code\u003E for zero-training-data ticket routing\u003C/li\u003E\u003Cli\u003EHow to use \u003Ccode\u003EAI_COMPLETE\u003C/code\u003E for numeric sentiment scoring and action generation\u003C/li\u003E\u003Cli\u003EHow to use \u003Ccode\u003ETRY_TO_DOUBLE\u003C/code\u003E for safe casting of LLM outputs\u003C/li\u003E\u003Cli\u003EHow to structure a Dynamic Table with CTEs to avoid column-alias errors\u003C/li\u003E\u003Cli\u003EHow to build a Streamlit in Snowflake operations dashboard\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EWhat You Will Build\u003C/h3\u003E\n","\u003Cp\u003EAn end-to-end support ticket enrichment pipeline that automatically classifies, scores, and recommends actions on every inbound ticket &mdash; with a live dashboard for operations managers.\u003C/p\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003ESetup\u003C/h2\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 1.\u003C/strong\u003E In Snowsight, create a SQL Worksheet and run the following to set up your environment:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Create the database and schemas\nCREATE DATABASE IF NOT EXISTS SUPPORT_OPS_AI;\nCREATE SCHEMA IF NOT EXISTS SUPPORT_OPS_AI.RAW;\nCREATE SCHEMA IF NOT EXISTS SUPPORT_OPS_AI.ANALYTICS;\n\n-- Create a warehouse for AI Function processing\nCREATE WAREHOUSE IF NOT EXISTS SUPPORT_OPS_WH\n  WAREHOUSE_SIZE = 'XSMALL'\n  AUTO_SUSPEND = 60\n  AUTO_RESUME = TRUE;\n\nUSE DATABASE SUPPORT_OPS_AI;\nUSE SCHEMA RAW;\nUSE WAREHOUSE SUPPORT_OPS_WH;\n\n-- Create the stage for phone call recordings\nCREATE STAGE IF NOT EXISTS SUPPORT_AUDIO_STAGE\n  ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE')\n  DIRECTORY = (ENABLE = TRUE);\n\n-- Create the raw tickets table\nCREATE TABLE IF NOT EXISTS SUPPORT_TICKETS (\n  ticket_id     VARCHAR DEFAULT UUID_STRING(),\n  channel       VARCHAR,\n  raw_text      VARCHAR,\n  audio_path    VARCHAR,\n  created_at    TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()\n);\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 2.\u003C/strong\u003E Insert sample support tickets:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003EINSERT INTO SUPPORT_TICKETS (channel, raw_text) VALUES\n('email', 'Our API integration has been timing out for the past 3 hours. We are losing transactions and this is affecting our revenue. This needs to be fixed immediately.'),\n('web', 'I cannot log in with SSO. Every time I click the login button it redirects me back to the login page. I have tried clearing cookies and using incognito mode. Nothing works.'),\n('email', 'I was charged the full monthly rate even though I upgraded mid-cycle. According to your pricing page, upgrades should be prorated. Please issue a credit for the difference.'),\n('web', 'Would love to see a dark mode option in the dashboard. The current white background is harsh during late-night monitoring sessions.'),\n('email', 'Hi, just wanted to confirm whether your platform supports webhooks for real-time event notifications. We are evaluating tools for our integration layer.'),\n('web', 'The API has been returning 504 errors intermittently since yesterday morning. Our batch processing jobs are failing and we have a client delivery deadline tomorrow.'),\n('email', 'I have been locked out of my account for 2 days now. The password reset email never arrives. I have checked spam. This is completely unacceptable for a paid service.'),\n('web', 'Your invoice from last month shows a charge for 50 seats but we only have 32 active users. Can someone look into this and issue a correction?'),\n('email', 'It would be really helpful if the analytics dashboard had an export-to-PDF feature. Right now I have to screenshot everything for my weekly reports.'),\n('web', 'Just wanted to say the new onboarding flow is excellent. Took me 5 minutes to get set up compared to an hour last year. Great improvement.'),\n('email', 'API latency has spiked to 8 seconds on average. Normal is under 200ms. Something is seriously wrong on your end. We need an update ASAP.'),\n('web', 'SSO login works fine on Chrome but fails completely on Firefox. The SAML assertion seems to be malformed for Firefox user agents.'),\n('email', 'Can you explain why my bill went up 40% this month? I did not change my plan or add any users. There is no explanation on the invoice.'),\n('web', 'Feature request: please add support for custom RBAC roles. The current admin/viewer split is too coarse for our team structure.'),\n('email', 'Quick question: do you offer volume discounts for organizations with 500+ seats? We are planning a company-wide rollout next quarter.');\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 3.\u003C/strong\u003E Download \u003Ca href=\"https://github.com/Snowflake-Labs/sfquickstarts/raw/master/site/sfguides/src/support-ops-ai-functions-dynamic-tables/assets/audio_files.zip\"\u003Eaudio_files.zip\u003C/a\u003E (50 sample call recordings, ~36 MB). Unzip and upload the MP3 files to the stage using Snowsight: \u003Cstrong\u003EData &rarr; Add Data &rarr; Load files into a Stage\u003C/strong\u003E &rarr; select \u003Ccode\u003ESUPPORT_OPS_AI.RAW.SUPPORT_AUDIO_STAGE\u003C/code\u003E.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 4.\u003C/strong\u003E Register the audio files as phone tickets:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Refresh directory metadata after uploading files\nALTER STAGE SUPPORT_AUDIO_STAGE REFRESH;\n\n-- Insert phone tickets referencing the uploaded audio files\nINSERT INTO SUPPORT_TICKETS (channel, audio_path)\nSELECT 'phone', RELATIVE_PATH\nFROM DIRECTORY(@SUPPORT_AUDIO_STAGE);\n\u003C/code\u003E\u003C/pre\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003ETranscribe Phone Calls\u003C/h2\u003E\n","\u003Cp\u003EFor tickets that arrive as phone recordings, \u003Ccode\u003EAI_TRANSCRIBE\u003C/code\u003E converts the audio to text. The audio never leaves Snowflake.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Preview transcription (run only if you uploaded audio files)\nSELECT\n  ticket_id,\n  AI_TRANSCRIBE(\n    TO_FILE('@SUPPORT_AUDIO_STAGE', audio_path)\n  ):text::VARCHAR AS transcript\nFROM RAW.SUPPORT_TICKETS\nWHERE channel = 'phone';\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003E\u003Ccode\u003EAI_TRANSCRIBE\u003C/code\u003E accepts a FILE type object (created with \u003Ccode\u003ETO_FILE\u003C/code\u003E) and returns a JSON object with a \u003Ccode\u003Etext\u003C/code\u003E field containing the full transcript. Language is auto-detected &mdash; no language parameter needed.\u003C/p\u003E\n","\u003Cp\u003EAfter this step, all tickets (text and audio) can be treated identically by downstream AI Functions.\u003C/p\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EClassify and Score Tickets\u003C/h2\u003E\n","\u003Ch3\u003EClassify with AI_CLASSIFY\u003C/h3\u003E\n","\u003Cp\u003E\u003Ccode\u003EAI_CLASSIFY\u003C/code\u003E routes each ticket into your predefined categories with zero training data:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003ESELECT\n  ticket_id,\n  raw_text,\n  AI_CLASSIFY(\n    raw_text,\n    ARRAY_CONSTRUCT(\n      'System Bug: API Timeout',\n      'Account Access: SSO',\n      'Billing: Prorated Upgrades',\n      'Feature Request',\n      'General Inquiry'\n    )\n  ):labels[0]::VARCHAR AS issue_category\nFROM RAW.SUPPORT_TICKETS\nWHERE channel != 'phone'\nLIMIT 5;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EUnlike rules-based classifiers, \u003Ccode\u003EAI_CLASSIFY\u003C/code\u003E handles natural language variation. A ticket saying &quot;I keep getting kicked out of the API&quot; routes to \u003Ccode\u003ESystem Bug: API Timeout\u003C/code\u003E even without those exact words.\u003C/p\u003E\n","\u003Ch3\u003EScore Sentiment Numerically\u003C/h3\u003E\n","\u003Cp\u003E\u003Ccode\u003EAI_SENTIMENT\u003C/code\u003E returns categorical labels (positive, negative, neutral). For a continuous score that lets you rank severity, use \u003Ccode\u003EAI_COMPLETE\u003C/code\u003E with a structured prompt:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003ESELECT\n  ticket_id,\n  raw_text,\n  TRY_TO_DOUBLE(\n    AI_COMPLETE(\n      'claude-sonnet-4-5',\n      CONCAT(\n        'Rate the sentiment of this support ticket on a scale from -1.0 ',\n        '(extremely frustrated) to +1.0 (satisfied/positive). ',\n        'Return ONLY a decimal number, no explanation. ',\n        'Ticket: ', raw_text\n      )\n    )::VARCHAR\n  ) AS sentiment_score\nFROM RAW.SUPPORT_TICKETS\nWHERE channel != 'phone'\nLIMIT 5;\n\u003C/code\u003E\u003C/pre\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EBuild the Dynamic Table Pipeline\u003C/h2\u003E\n","\u003Cp\u003ERather than scheduling ETL jobs, wrap the entire pipeline in a Dynamic Table. Snowflake refreshes it automatically as new tickets arrive.\u003C/p\u003E\n","\u003Cp\u003EThe key architectural decision: use \u003Cstrong\u003ECTEs\u003C/strong\u003E to avoid referencing column aliases in the same SELECT (which Snowflake does not allow):\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003EUSE SCHEMA SUPPORT_OPS_AI.ANALYTICS;\nUSE WAREHOUSE SUPPORT_OPS_WH;\n\nCREATE OR REPLACE DYNAMIC TABLE ENRICHED_TICKETS\n  TARGET_LAG = '1 hour'\n  REFRESH_MODE = FULL\n  WAREHOUSE = SUPPORT_OPS_WH\nAS\nWITH transcribed AS (\n  SELECT\n    ticket_id, channel, created_at,\n    CASE\n      WHEN channel = 'phone'\n        THEN AI_TRANSCRIBE(\n               TO_FILE('@RAW.SUPPORT_AUDIO_STAGE', audio_path)\n             ):text::VARCHAR\n      ELSE raw_text\n    END AS ticket_text\n  FROM RAW.SUPPORT_TICKETS\n),\nclassified AS (\n  SELECT *,\n    AI_CLASSIFY(\n      ticket_text,\n      ARRAY_CONSTRUCT(\n        'System Bug: API Timeout',\n        'Account Access: SSO',\n        'Billing: Prorated Upgrades',\n        'Feature Request',\n        'General Inquiry'\n      )\n    ):labels[0]::VARCHAR AS issue_category,\n    TRY_TO_DOUBLE(\n      AI_COMPLETE(\n        'claude-sonnet-4-5',\n        CONCAT('Rate sentiment -1.0 to +1.0. Return only a number. Ticket: ', ticket_text)\n      )::VARCHAR\n    ) AS sentiment_score\n  FROM transcribed\n)\nSELECT\n  ticket_id, channel, created_at, ticket_text,\n  issue_category, sentiment_score,\n  AI_COMPLETE(\n    'claude-sonnet-4-5',\n    CONCAT(\n      'Support ops manager. Category: ', issue_category,\n      '. Sentiment: ', COALESCE(ROUND(sentiment_score, 2)::VARCHAR, 'unknown'),\n      '. Recommend one operational action in 1-2 sentences.'\n    )\n  )::VARCHAR AS recommended_action\nFROM classified;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EOnce created, this table refreshes itself. Insert a new ticket into \u003Ccode\u003ERAW.SUPPORT_TICKETS\u003C/code\u003E and within an hour it appears in \u003Ccode\u003EENRICHED_TICKETS\u003C/code\u003E &mdash; classified, scored, and with a recommendation.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003E\u003Cstrong\u003ENote:\u003C/strong\u003E The first refresh may take 15&ndash;30 minutes as AI Functions transcribe and enrich all existing tickets. If the table appears empty, wait and re-query.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Ch3\u003EVerify the Pipeline\u003C/h3\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Check that the Dynamic Table populated\nSELECT * FROM ANALYTICS.ENRICHED_TICKETS LIMIT 10;\n\n-- Check refresh status\nSELECT * FROM TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_REFRESH_HISTORY(\n  NAME =&gt; 'SUPPORT_OPS_AI.ANALYTICS.ENRICHED_TICKETS'\n)) ORDER BY REFRESH_START_TIME DESC LIMIT 5;\n\u003C/code\u003E\u003C/pre\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EBuild the Streamlit Dashboard\u003C/h2\u003E\n","\u003Cp\u003ECreate a Streamlit in Snowflake app to give operations managers a live view of issue volume, sentiment trends, and AI-generated recommendations.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 1.\u003C/strong\u003E In Snowsight, open a \u003Cstrong\u003EWorkspace\u003C/strong\u003E (or go to \u003Cstrong\u003EProjects &rarr; Streamlit\u003C/strong\u003E and click \u003Cstrong\u003E+ Streamlit App\u003C/strong\u003E).\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 2.\u003C/strong\u003E Set the app database to \u003Ccode\u003ESUPPORT_OPS_AI\u003C/code\u003E, schema to \u003Ccode\u003EANALYTICS\u003C/code\u003E, and warehouse to \u003Ccode\u003ESUPPORT_OPS_WH\u003C/code\u003E.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EStep 3.\u003C/strong\u003E Paste the following code:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Eimport streamlit as st\nfrom snowflake.snowpark.context import get_active_session\nimport pandas as pd\n\nsession = get_active_session()\n\nst.title(&quot;Support Operations Intelligence&quot;)\nst.caption(&quot;Powered by Snowflake AI Functions &middot; Refreshes every hour&quot;)\n\ndf = session.sql(&quot;&quot;&quot;\n    SELECT\n        issue_category,\n        COUNT(*)                          AS volume,\n        ROUND(AVG(sentiment_score), 2)    AS avg_sentiment,\n        ANY_VALUE(recommended_action)     AS recommended_action\n    FROM analytics.enriched_tickets\n    WHERE created_at &gt;= DATEADD('day', -30, CURRENT_TIMESTAMP())\n    GROUP BY issue_category\n    ORDER BY volume DESC\n&quot;&quot;&quot;).to_pandas()\n\ndef sentiment_label(score):\n    if score &lt;= -0.7:   return &quot;Frustrated&quot;\n    elif score &lt;= -0.4: return &quot;Stressed&quot;\n    elif score &lt;= -0.1: return &quot;Annoyed&quot;\n    elif score &lt;= 0.3:  return &quot;Neutral&quot;\n    else:               return &quot;Positive&quot;\n\ndf[&quot;Sentiment Label&quot;] = df[&quot;AVG_SENTIMENT&quot;].apply(sentiment_label)\ndf[&quot;Avg. Sentiment&quot;]  = df.apply(\n    lambda r: f&quot;{r['AVG_SENTIMENT']} ({r['Sentiment Label']})&quot;, axis=1\n)\n\ncol1, col2, col3 = st.columns(3)\ncol1.metric(&quot;Total Tickets (30d)&quot;, f&quot;{df['VOLUME'].sum():,}&quot;)\ncol2.metric(&quot;Avg. Sentiment&quot;, f&quot;{(df['AVG_SENTIMENT'] * df['VOLUME']).sum() / df['VOLUME'].sum():.2f}&quot;)\ncol3.metric(&quot;Issue Categories&quot;, len(df))\n\nst.divider()\n\nst.subheader(&quot;Issue Summary &amp; Recommended Actions&quot;)\nst.dataframe(\n    df[[&quot;ISSUE_CATEGORY&quot;, &quot;Avg. Sentiment&quot;, &quot;VOLUME&quot;, &quot;RECOMMENDED_ACTION&quot;]].rename(columns={\n        &quot;ISSUE_CATEGORY&quot;:       &quot;Issue Category&quot;,\n        &quot;VOLUME&quot;:               &quot;Volume&quot;,\n        &quot;RECOMMENDED_ACTION&quot;:   &quot;Sample Recommendation&quot;\n    }),\n    use_container_width=True,\n    hide_index=True\n)\n\nst.subheader(&quot;Daily Sentiment Trend&quot;)\ntrend_df = session.sql(&quot;&quot;&quot;\n    SELECT\n        DATE_TRUNC('day', created_at) AS day,\n        issue_category,\n        ROUND(AVG(sentiment_score), 3) AS avg_sentiment\n    FROM analytics.enriched_tickets\n    WHERE created_at &gt;= DATEADD('day', -30, CURRENT_TIMESTAMP())\n    GROUP BY 1, 2\n    ORDER BY 1\n&quot;&quot;&quot;).to_pandas()\n\nst.line_chart(\n    trend_df.pivot(index=&quot;DAY&quot;, columns=&quot;ISSUE_CATEGORY&quot;, values=&quot;AVG_SENTIMENT&quot;)\n)\n\u003C/code\u003E\u003C/pre\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EBest Practices\u003C/h2\u003E\n","\u003Ch3\u003EPII Handling with AI_REDACT\u003C/h3\u003E\n","\u003Cp\u003ESupport tickets routinely contain customer PII. Use \u003Ccode\u003EAI_REDACT\u003C/code\u003E before surfacing data to broader teams:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003ESELECT\n  ticket_id,\n  AI_REDACT(ticket_text) AS redacted_text,\n  issue_category,\n  sentiment_score,\n  recommended_action\nFROM ANALYTICS.ENRICHED_TICKETS\nLIMIT 5;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EFor production, apply \u003Ca href=\"https://docs.snowflake.com/en/user-guide/security-column-ddm-intro\"\u003EDynamic Data Masking\u003C/a\u003E policies on the \u003Ccode\u003Eticket_text\u003C/code\u003E column to restrict access by role.\u003C/p\u003E\n","\u003Ch3\u003ECost Optimization\u003C/h3\u003E\n\u003Ctable\u003E\u003Cthead\u003E\u003Ctr\u003E\u003Cth colspan=\"1\" rowspan=\"1\"\u003ETask\u003C/th\u003E\u003Cth colspan=\"1\" rowspan=\"1\"\u003ERecommended Model\u003C/th\u003E\u003Cth colspan=\"1\" rowspan=\"1\"\u003EReason\u003C/th\u003E\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003EClassification\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003E\u003Ccode\u003EAI_CLASSIFY\u003C/code\u003E (managed)\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003EOptimized internally by Snowflake\u003C/td\u003E\u003C/tr\u003E\u003Ctr\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003ESentiment scoring\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003E\u003Ccode\u003Eclaude-sonnet-4-5\u003C/code\u003E\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003EGood structured output at lower cost\u003C/td\u003E\u003C/tr\u003E\u003Ctr\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003ERecommendations\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003E\u003Ccode\u003Eclaude-sonnet-4-5\u003C/code\u003E\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003EBalanced quality and cost\u003C/td\u003E\u003C/tr\u003E\u003Ctr\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003EComplex edge cases\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003E\u003Ccode\u003Eclaude-opus-4-7\u003C/code\u003E\u003C/td\u003E\u003Ctd colspan=\"1\" rowspan=\"1\"\u003EStrongest reasoning for ambiguous tickets\u003C/td\u003E\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\n","\u003Ch3\u003EHuman-in-the-Loop\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003ETreat AI recommendations as \u003Cstrong\u003Esuggestions\u003C/strong\u003E, not automated triggers\u003C/li\u003E\u003Cli\u003EAudit a 5% sample weekly against human labels\u003C/li\u003E\u003Cli\u003EUse the \u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-cortex/ai-function-studio\"\u003ECortex AI Function Studio\u003C/a\u003E to evaluate and optimize prompt quality over time\u003C/li\u003E\u003C/ul\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003ECleanup\u003C/h2\u003E\n","\u003Cp\u003ETo remove all objects created in this quickstart:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003EDROP DATABASE IF EXISTS SUPPORT_OPS_AI;\nDROP WAREHOUSE IF EXISTS SUPPORT_OPS_WH;\n\u003C/code\u003E\u003C/pre\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EConclusion And Resources\u003C/h2\u003E\n","\u003Cp\u003ECongratulations! You've built an end-to-end AI-powered support operations pipeline entirely within Snowflake &mdash; no external APIs, no separate ML infrastructure, no schedulers.\u003C/p\u003E\n","\u003Ch3\u003EWhat You Learned\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Cstrong\u003EAI_TRANSCRIBE\u003C/strong\u003E converts audio to text with auto language detection\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EAI_CLASSIFY\u003C/strong\u003E routes tickets into your taxonomy with zero training data\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EAI_COMPLETE\u003C/strong\u003E with \u003Ccode\u003ETRY_TO_DOUBLE\u003C/code\u003E provides safe numeric sentiment scoring\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EDynamic Tables with CTEs\u003C/strong\u003E create auto-refreshing pipelines without column-alias errors\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EStreamlit in Snowflake\u003C/strong\u003E gives operations teams a live dashboard with no external deployment\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003ERelated Resources\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql\"\u003ECortex AI Functions Documentation\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/dynamic-tables/overview\"\u003EDynamic Tables Guide\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/sql-reference/functions/ai_transcribe\"\u003EAI_TRANSCRIBE Reference\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/sql-reference/functions/ai_classify\"\u003EAI_CLASSIFY Reference\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-cortex/ai-function-studio\"\u003ECortex AI Function Studio\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://quickstarts.snowflake.com/guide/getting-started-with-cortex-aisql/\"\u003EGetting Started with Cortex AI Functions Quickstart\u003C/a\u003E\u003C/li\u003E\u003C/ul\u003E"],"title":"Base Quickstart CF","isDeveloperGuidesPage":false,":type":"snowflake-site/components/contentfragment",":items":{},":itemsOrder":[],"elements":{"quickstartArticleBody":{"dataType":"string","title":"Quickstart Article Body","value":"\u003C!-- ------------------------ --\u003E\n## Overview\n\nCustomer support teams spend significant time reading tickets to classify issues, gauge urgency, and decide on next steps. This quickstart shows how to automate that entire workflow using Snowflake Cortex AI Functions, with a Dynamic Table that keeps results fresh and a Streamlit dashboard for operational visibility.\n\n\u003E **How this differs from other support quickstarts:** Unlike [call-center-analytics-with-ai-transcribe-and-cortex-agents](https://quickstarts.snowflake.com/guide/call-center-analytics-with-ai-transcribe-and-cortex-agents) (which builds a full Cortex Agent for Q&A) or [streamlining-support-case-analysis](https://quickstarts.snowflake.com/guide/streamlining-support-case-analysis-with-snowflake-cortex) (which uses LangChain for summarization), this guide takes a **pure-SQL, zero-orchestration approach**. A single Dynamic Table handles transcription, classification, sentiment scoring, and recommendation generation — no agents, no external frameworks, no scheduler. New tickets are enriched automatically on refresh.\n\nYou'll build a pipeline that:\n- **Transcribes** phone call recordings into text with `AI_TRANSCRIBE`\n- **Classifies** every ticket into your taxonomy with `AI_CLASSIFY`\n- **Scores sentiment** on a continuous numeric scale with `AI_COMPLETE`\n- **Generates operational recommendations** per ticket with `AI_COMPLETE`\n- **Auto-refreshes** via a Dynamic Table — no scheduler, no Airflow\n- **Surfaces results** in a Streamlit in Snowflake dashboard\n\n### Architecture\n\n```\n  [Web/Email Forms] ──►  RAW.SUPPORT_TICKETS (text)\n  [Phone Calls]     ──►  @SUPPORT_AUDIO_STAGE (audio files)\n                              │\n                              ▼  Dynamic Table (auto-refresh)\n                    ANALYTICS.ENRICHED_TICKETS\n                      ← AI_TRANSCRIBE  (audio → text)\n                      ← AI_CLASSIFY    (issue category)\n                      ← AI_COMPLETE    (sentiment score)\n                      ← AI_COMPLETE    (recommended action)\n                              │\n                              ▼\n                    Streamlit in Snowflake (Ops Dashboard)\n```\n\n### What You'll Need\n\n- A Snowflake account with ACCOUNTADMIN role in a [supported region](https://docs.snowflake.com/user-guide/snowflake-cortex/aisql-regional-availability)\n- The SNOWFLAKE.CORTEX_USER database role granted to your user\n\n### What You Will Learn\n\n- How to use `AI_TRANSCRIBE` to convert audio files to text\n- How to use `AI_CLASSIFY` for zero-training-data ticket routing\n- How to use `AI_COMPLETE` for numeric sentiment scoring and action generation\n- How to use `TRY_TO_DOUBLE` for safe casting of LLM outputs\n- How to structure a Dynamic Table with CTEs to avoid column-alias errors\n- How to build a Streamlit in Snowflake operations dashboard\n\n### What You Will Build\n\nAn end-to-end support ticket enrichment pipeline that automatically classifies, scores, and recommends actions on every inbound ticket — with a live dashboard for operations managers.\n\n\u003C!-- ------------------------ --\u003E\n## Setup\n\n**Step 1.** In Snowsight, create a SQL Worksheet and run the following to set up your environment:\n\n```sql\n-- Create the database and schemas\nCREATE DATABASE IF NOT EXISTS SUPPORT_OPS_AI;\nCREATE SCHEMA IF NOT EXISTS SUPPORT_OPS_AI.RAW;\nCREATE SCHEMA IF NOT EXISTS SUPPORT_OPS_AI.ANALYTICS;\n\n-- Create a warehouse for AI Function processing\nCREATE WAREHOUSE IF NOT EXISTS SUPPORT_OPS_WH\n  WAREHOUSE_SIZE = 'XSMALL'\n  AUTO_SUSPEND = 60\n  AUTO_RESUME = TRUE;\n\nUSE DATABASE SUPPORT_OPS_AI;\nUSE SCHEMA RAW;\nUSE WAREHOUSE SUPPORT_OPS_WH;\n\n-- Create the stage for phone call recordings\nCREATE STAGE IF NOT EXISTS SUPPORT_AUDIO_STAGE\n  ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE')\n  DIRECTORY = (ENABLE = TRUE);\n\n-- Create the raw tickets table\nCREATE TABLE IF NOT EXISTS SUPPORT_TICKETS (\n  ticket_id     VARCHAR DEFAULT UUID_STRING(),\n  channel       VARCHAR,\n  raw_text      VARCHAR,\n  audio_path    VARCHAR,\n  created_at    TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()\n);\n```\n\n**Step 2.** Insert sample support tickets:\n\n```sql\nINSERT INTO SUPPORT_TICKETS (channel, raw_text) VALUES\n('email', 'Our API integration has been timing out for the past 3 hours. We are losing transactions and this is affecting our revenue. This needs to be fixed immediately.'),\n('web', 'I cannot log in with SSO. Every time I click the login button it redirects me back to the login page. I have tried clearing cookies and using incognito mode. Nothing works.'),\n('email', 'I was charged the full monthly rate even though I upgraded mid-cycle. According to your pricing page, upgrades should be prorated. Please issue a credit for the difference.'),\n('web', 'Would love to see a dark mode option in the dashboard. The current white background is harsh during late-night monitoring sessions.'),\n('email', 'Hi, just wanted to confirm whether your platform supports webhooks for real-time event notifications. We are evaluating tools for our integration layer.'),\n('web', 'The API has been returning 504 errors intermittently since yesterday morning. Our batch processing jobs are failing and we have a client delivery deadline tomorrow.'),\n('email', 'I have been locked out of my account for 2 days now. The password reset email never arrives. I have checked spam. This is completely unacceptable for a paid service.'),\n('web', 'Your invoice from last month shows a charge for 50 seats but we only have 32 active users. Can someone look into this and issue a correction?'),\n('email', 'It would be really helpful if the analytics dashboard had an export-to-PDF feature. Right now I have to screenshot everything for my weekly reports.'),\n('web', 'Just wanted to say the new onboarding flow is excellent. Took me 5 minutes to get set up compared to an hour last year. Great improvement.'),\n('email', 'API latency has spiked to 8 seconds on average. Normal is under 200ms. Something is seriously wrong on your end. We need an update ASAP.'),\n('web', 'SSO login works fine on Chrome but fails completely on Firefox. The SAML assertion seems to be malformed for Firefox user agents.'),\n('email', 'Can you explain why my bill went up 40% this month? I did not change my plan or add any users. There is no explanation on the invoice.'),\n('web', 'Feature request: please add support for custom RBAC roles. The current admin/viewer split is too coarse for our team structure.'),\n('email', 'Quick question: do you offer volume discounts for organizations with 500+ seats? We are planning a company-wide rollout next quarter.');\n```\n\n**Step 3.** Download [audio_files.zip](https://github.com/Snowflake-Labs/sfquickstarts/raw/master/site/sfguides/src/support-ops-ai-functions-dynamic-tables/assets/audio_files.zip) (50 sample call recordings, ~36 MB). Unzip and upload the MP3 files to the stage using Snowsight: **Data → Add Data → Load files into a Stage** → select `SUPPORT_OPS_AI.RAW.SUPPORT_AUDIO_STAGE`.\n\n**Step 4.** Register the audio files as phone tickets:\n\n```sql\n-- Refresh directory metadata after uploading files\nALTER STAGE SUPPORT_AUDIO_STAGE REFRESH;\n\n-- Insert phone tickets referencing the uploaded audio files\nINSERT INTO SUPPORT_TICKETS (channel, audio_path)\nSELECT 'phone', RELATIVE_PATH\nFROM DIRECTORY(@SUPPORT_AUDIO_STAGE);\n```\n\n\u003C!-- ------------------------ --\u003E\n## Transcribe Phone Calls\n\nFor tickets that arrive as phone recordings, `AI_TRANSCRIBE` converts the audio to text. The audio never leaves Snowflake.\n\n```sql\n-- Preview transcription (run only if you uploaded audio files)\nSELECT\n  ticket_id,\n  AI_TRANSCRIBE(\n    TO_FILE('@SUPPORT_AUDIO_STAGE', audio_path)\n  ):text::VARCHAR AS transcript\nFROM RAW.SUPPORT_TICKETS\nWHERE channel = 'phone';\n```\n\n`AI_TRANSCRIBE` accepts a FILE type object (created with `TO_FILE`) and returns a JSON object with a `text` field containing the full transcript. Language is auto-detected — no language parameter needed.\n\nAfter this step, all tickets (text and audio) can be treated identically by downstream AI Functions.\n\n\u003C!-- ------------------------ --\u003E\n## Classify and Score Tickets\n\n### Classify with AI_CLASSIFY\n\n`AI_CLASSIFY` routes each ticket into your predefined categories with zero training data:\n\n```sql\nSELECT\n  ticket_id,\n  raw_text,\n  AI_CLASSIFY(\n    raw_text,\n    ARRAY_CONSTRUCT(\n      'System Bug: API Timeout',\n      'Account Access: SSO',\n      'Billing: Prorated Upgrades',\n      'Feature Request',\n      'General Inquiry'\n    )\n  ):labels[0]::VARCHAR AS issue_category\nFROM RAW.SUPPORT_TICKETS\nWHERE channel != 'phone'\nLIMIT 5;\n```\n\nUnlike rules-based classifiers, `AI_CLASSIFY` handles natural language variation. A ticket saying \"I keep getting kicked out of the API\" routes to `System Bug: API Timeout` even without those exact words.\n\n### Score Sentiment Numerically\n\n`AI_SENTIMENT` returns categorical labels (positive, negative, neutral). For a continuous score that lets you rank severity, use `AI_COMPLETE` with a structured prompt:\n\n```sql\nSELECT\n  ticket_id,\n  raw_text,\n  TRY_TO_DOUBLE(\n    AI_COMPLETE(\n      'claude-sonnet-4-5',\n      CONCAT(\n        'Rate the sentiment of this support ticket on a scale from -1.0 ',\n        '(extremely frustrated) to +1.0 (satisfied/positive). ',\n        'Return ONLY a decimal number, no explanation. ',\n        'Ticket: ', raw_text\n      )\n    )::VARCHAR\n  ) AS sentiment_score\nFROM RAW.SUPPORT_TICKETS\nWHERE channel != 'phone'\nLIMIT 5;\n```\n\n\n\u003C!-- ------------------------ --\u003E\n## Build the Dynamic Table Pipeline\n\nRather than scheduling ETL jobs, wrap the entire pipeline in a Dynamic Table. Snowflake refreshes it automatically as new tickets arrive.\n\nThe key architectural decision: use **CTEs** to avoid referencing column aliases in the same SELECT (which Snowflake does not allow):\n\n```sql\nUSE SCHEMA SUPPORT_OPS_AI.ANALYTICS;\nUSE WAREHOUSE SUPPORT_OPS_WH;\n\nCREATE OR REPLACE DYNAMIC TABLE ENRICHED_TICKETS\n  TARGET_LAG = '1 hour'\n  REFRESH_MODE = FULL\n  WAREHOUSE = SUPPORT_OPS_WH\nAS\nWITH transcribed AS (\n  SELECT\n    ticket_id, channel, created_at,\n    CASE\n      WHEN channel = 'phone'\n        THEN AI_TRANSCRIBE(\n               TO_FILE('@RAW.SUPPORT_AUDIO_STAGE', audio_path)\n             ):text::VARCHAR\n      ELSE raw_text\n    END AS ticket_text\n  FROM RAW.SUPPORT_TICKETS\n),\nclassified AS (\n  SELECT *,\n    AI_CLASSIFY(\n      ticket_text,\n      ARRAY_CONSTRUCT(\n        'System Bug: API Timeout',\n        'Account Access: SSO',\n        'Billing: Prorated Upgrades',\n        'Feature Request',\n        'General Inquiry'\n      )\n    ):labels[0]::VARCHAR AS issue_category,\n    TRY_TO_DOUBLE(\n      AI_COMPLETE(\n        'claude-sonnet-4-5',\n        CONCAT('Rate sentiment -1.0 to +1.0. Return only a number. Ticket: ', ticket_text)\n      )::VARCHAR\n    ) AS sentiment_score\n  FROM transcribed\n)\nSELECT\n  ticket_id, channel, created_at, ticket_text,\n  issue_category, sentiment_score,\n  AI_COMPLETE(\n    'claude-sonnet-4-5',\n    CONCAT(\n      'Support ops manager. Category: ', issue_category,\n      '. Sentiment: ', COALESCE(ROUND(sentiment_score, 2)::VARCHAR, 'unknown'),\n      '. Recommend one operational action in 1-2 sentences.'\n    )\n  )::VARCHAR AS recommended_action\nFROM classified;\n```\n\nOnce created, this table refreshes itself. Insert a new ticket into `RAW.SUPPORT_TICKETS` and within an hour it appears in `ENRICHED_TICKETS` — classified, scored, and with a recommendation.\n\n\u003E **Note:** The first refresh may take 15–30 minutes as AI Functions transcribe and enrich all existing tickets. If the table appears empty, wait and re-query.\n\n### Verify the Pipeline\n\n```sql\n-- Check that the Dynamic Table populated\nSELECT * FROM ANALYTICS.ENRICHED_TICKETS LIMIT 10;\n\n-- Check refresh status\nSELECT * FROM TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_REFRESH_HISTORY(\n  NAME =\u003E 'SUPPORT_OPS_AI.ANALYTICS.ENRICHED_TICKETS'\n)) ORDER BY REFRESH_START_TIME DESC LIMIT 5;\n```\n\n\u003C!-- ------------------------ --\u003E\n## Build the Streamlit Dashboard\n\nCreate a Streamlit in Snowflake app to give operations managers a live view of issue volume, sentiment trends, and AI-generated recommendations.\n\n**Step 1.** In Snowsight, open a **Workspace** (or go to **Projects → Streamlit** and click **+ Streamlit App**).\n\n**Step 2.** Set the app database to `SUPPORT_OPS_AI`, schema to `ANALYTICS`, and warehouse to `SUPPORT_OPS_WH`.\n\n**Step 3.** Paste the following code:\n\n```python\nimport streamlit as st\nfrom snowflake.snowpark.context import get_active_session\nimport pandas as pd\n\nsession = get_active_session()\n\nst.title(\"Support Operations Intelligence\")\nst.caption(\"Powered by Snowflake AI Functions · Refreshes every hour\")\n\ndf = session.sql(\"\"\"\n    SELECT\n        issue_category,\n        COUNT(*)                          AS volume,\n        ROUND(AVG(sentiment_score), 2)    AS avg_sentiment,\n        ANY_VALUE(recommended_action)     AS recommended_action\n    FROM analytics.enriched_tickets\n    WHERE created_at \u003E= DATEADD('day', -30, CURRENT_TIMESTAMP())\n    GROUP BY issue_category\n    ORDER BY volume DESC\n\"\"\").to_pandas()\n\ndef sentiment_label(score):\n    if score \u003C= -0.7:   return \"Frustrated\"\n    elif score \u003C= -0.4: return \"Stressed\"\n    elif score \u003C= -0.1: return \"Annoyed\"\n    elif score \u003C= 0.3:  return \"Neutral\"\n    else:               return \"Positive\"\n\ndf[\"Sentiment Label\"] = df[\"AVG_SENTIMENT\"].apply(sentiment_label)\ndf[\"Avg. Sentiment\"]  = df.apply(\n    lambda r: f\"{r['AVG_SENTIMENT']} ({r['Sentiment Label']})\", axis=1\n)\n\ncol1, col2, col3 = st.columns(3)\ncol1.metric(\"Total Tickets (30d)\", f\"{df['VOLUME'].sum():,}\")\ncol2.metric(\"Avg. Sentiment\", f\"{(df['AVG_SENTIMENT'] * df['VOLUME']).sum() / df['VOLUME'].sum():.2f}\")\ncol3.metric(\"Issue Categories\", len(df))\n\nst.divider()\n\nst.subheader(\"Issue Summary & Recommended Actions\")\nst.dataframe(\n    df[[\"ISSUE_CATEGORY\", \"Avg. Sentiment\", \"VOLUME\", \"RECOMMENDED_ACTION\"]].rename(columns={\n        \"ISSUE_CATEGORY\":       \"Issue Category\",\n        \"VOLUME\":               \"Volume\",\n        \"RECOMMENDED_ACTION\":   \"Sample Recommendation\"\n    }),\n    use_container_width=True,\n    hide_index=True\n)\n\nst.subheader(\"Daily Sentiment Trend\")\ntrend_df = session.sql(\"\"\"\n    SELECT\n        DATE_TRUNC('day', created_at) AS day,\n        issue_category,\n        ROUND(AVG(sentiment_score), 3) AS avg_sentiment\n    FROM analytics.enriched_tickets\n    WHERE created_at \u003E= DATEADD('day', -30, CURRENT_TIMESTAMP())\n    GROUP BY 1, 2\n    ORDER BY 1\n\"\"\").to_pandas()\n\nst.line_chart(\n    trend_df.pivot(index=\"DAY\", columns=\"ISSUE_CATEGORY\", values=\"AVG_SENTIMENT\")\n)\n```\n\n\u003C!-- ------------------------ --\u003E\n## Best Practices\n\n### PII Handling with AI_REDACT\n\nSupport tickets routinely contain customer PII. Use `AI_REDACT` before surfacing data to broader teams:\n\n```sql\nSELECT\n  ticket_id,\n  AI_REDACT(ticket_text) AS redacted_text,\n  issue_category,\n  sentiment_score,\n  recommended_action\nFROM ANALYTICS.ENRICHED_TICKETS\nLIMIT 5;\n```\n\nFor production, apply [Dynamic Data Masking](https://docs.snowflake.com/en/user-guide/security-column-ddm-intro) policies on the `ticket_text` column to restrict access by role.\n\n### Cost Optimization\n\n| Task | Recommended Model | Reason |\n|------|-------------------|--------|\n| Classification | `AI_CLASSIFY` (managed) | Optimized internally by Snowflake |\n| Sentiment scoring | `claude-sonnet-4-5` | Good structured output at lower cost |\n| Recommendations | `claude-sonnet-4-5` | Balanced quality and cost |\n| Complex edge cases | `claude-opus-4-7` | Strongest reasoning for ambiguous tickets |\n\n### Human-in-the-Loop\n\n- Treat AI recommendations as **suggestions**, not automated triggers\n- Audit a 5% sample weekly against human labels\n- Use the [Cortex AI Function Studio](https://docs.snowflake.com/en/user-guide/snowflake-cortex/ai-function-studio) to evaluate and optimize prompt quality over time\n\n\u003C!-- ------------------------ --\u003E\n## Cleanup\n\nTo remove all objects created in this quickstart:\n\n```sql\nDROP DATABASE IF EXISTS SUPPORT_OPS_AI;\nDROP WAREHOUSE IF EXISTS SUPPORT_OPS_WH;\n```\n\n\u003C!-- ------------------------ --\u003E\n## Conclusion And Resources\n\nCongratulations! You've built an end-to-end AI-powered support operations pipeline entirely within Snowflake — no external APIs, no separate ML infrastructure, no schedulers.\n\n### What You Learned\n\n- **AI_TRANSCRIBE** converts audio to text with auto language detection\n- **AI_CLASSIFY** routes tickets into your taxonomy with zero training data\n- **AI_COMPLETE** with `TRY_TO_DOUBLE` provides safe numeric sentiment scoring\n- **Dynamic Tables with CTEs** create auto-refreshing pipelines without column-alias errors\n- **Streamlit in Snowflake** gives operations teams a live dashboard with no external deployment\n\n### Related Resources\n\n- [Cortex AI Functions Documentation](https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql)\n- [Dynamic Tables Guide](https://docs.snowflake.com/en/user-guide/dynamic-tables/overview)\n- [AI_TRANSCRIBE Reference](https://docs.snowflake.com/en/sql-reference/functions/ai_transcribe)\n- [AI_CLASSIFY Reference](https://docs.snowflake.com/en/sql-reference/functions/ai_classify)\n- [Cortex AI Function Studio](https://docs.snowflake.com/en/user-guide/snowflake-cortex/ai-function-studio)\n- [Getting Started with Cortex AI Functions Quickstart](https://quickstarts.snowflake.com/guide/getting-started-with-cortex-aisql/)\n","multiValue":false,":type":"text/x-markdown"},"quickstartArticleLogoImage":{"dataType":"string","title":"Quickstart Article Logo Image","multiValue":false,":type":"text/plain"}},"elementsOrder":["quickstartArticleBody","quickstartArticleLogoImage"],"model":"snowflake-site/models/quickstart-article"},"flexible_column_cont":{"id":"flexible-column-container-ffcdfd876c","type":"2-column-75-25","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"none","bottomPadding":"none","spaceBetween":"none","reverseOnMobile":false,"carouselOnMobile":false,"backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-436a1012f9",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"quickstart_last_modi":{"id":"quickstart-last-modified-2d590b0a53","icon":{"id":"icon","icon":"calendar",":type":"snowflake-site/components/icon","appliedCssClassNames":"snowflake-icon-blue"},"lastModifiedDatePrefix":"Updated","lastModifiedDate":"2026-08-18",":type":"snowflake-site/components/quickstart/quickstart-last-modified","appliedCssClassNames":"snowflake-responsive-component-top-padding-small"},"text":{"id":"text-3bdf019114","additionalClasses":"qs-disclaimer-text","text":"\u003Cp\u003E\u003Cspan style=\"color: #666;\"\u003EThis content is provided as is, and is not maintained on an ongoing basis. It may be out of date with current Snowflake instances\u003C/span\u003E\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/text","appliedCssClassNames":"snowflake-responsive-component-top-padding-small"}},":itemsOrder":["quickstart_last_modi","text"]},"flexible_column_content_container_2":{"layout":"SIMPLE","id":"container-ace0ed6c23",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{},":itemsOrder":[]},":type":"snowflake-site/components/flexible-column-container","isActiveTOC":false,"isBlogPage":false}},":itemsOrder":["contentfragment","flexible_column_cont"]},"flexible_column_content_container_2":{"layout":"SIMPLE","id":"container-a355b8890b",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"quickstart_table_of_":{"layout":"SIMPLE","id":"container-171ec5b48f","isDeveloperGuidesPage":false,":type":"snowflake-site/components/quickstart/quickstart-table-of-content/quickstart-table-of-content-container",":items":{"quickstart_table_of_":{"id":"quickstart-table-of-content-6d7a4f386e",":type":"snowflake-site/components/quickstart/quickstart-table-of-content","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/support-ops-ai-functions-dynamic-tables","headings":["\u003Ch2\u003EOverview\u003C/h2\u003E","\u003Ch2\u003ESetup\u003C/h2\u003E","\u003Ch2\u003ETranscribe Phone Calls\u003C/h2\u003E","\u003Ch2\u003EClassify and Score Tickets\u003C/h2\u003E","\u003Ch2\u003EBuild the Dynamic Table Pipeline\u003C/h2\u003E","\u003Ch2\u003EBuild the Streamlit Dashboard\u003C/h2\u003E","\u003Ch2\u003EBest Practices\u003C/h2\u003E","\u003Ch2\u003ECleanup\u003C/h2\u003E","\u003Ch2\u003EConclusion And Resources\u003C/h2\u003E"]},"quickstart_button":{"id":"quickstart-button-844c39dfd0",":type":"snowflake-site/components/quickstart/quickstart-button","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/support-ops-ai-functions-dynamic-tables","appliedCssClassNames":"snowflake-responsive-component-top-padding-none"}},":itemsOrder":["quickstart_table_of_","quickstart_button"]}},":itemsOrder":["quickstart_table_of_"]},":type":"snowflake-site/components/flexible-column-container","isActiveTOC":false,"isBlogPage":false},"markup_editor":{"id":"markup-editor-e8134baeb5","title":"Page CSS","cssContent":"#quickstart-template-main-flexible-container{padding:24px}#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{grid-template-columns:1fr 0}.qs-disclaimer-text p \u003E span{font-size:15px !important}@media (min-width:768px){#quickstart-template-main-flexible-container{padding:24px 32px}#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{grid-template-columns:7fr 3fr;gap:48px}}@media (max-width:767px){#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{gap:0}}@media (min-width:1024px){#quickstart-template-main-flexible-container{padding:0 92px 48px 92px}#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{gap:117px}}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false}},":itemsOrder":["quickstart_hero","flexible_column_cont","markup_editor"],":type":"wcm/foundation/components/responsivegrid"},"modal_container":{"layout":"SIMPLE","id":"container-b2faefd3b2",":type":"snowflake-site/components/modal/modal-container",":items":{},":itemsOrder":[]},"experiencefragment-footer":{"id":"experiencefragment-bf5d42efe7","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master.xfmodel.json"},"markup_editor":{"id":"markup-editor-abf5e8ae4b","title":"Quickstarts Overrides","cssContent":".snowflake-markdown blockquote{padding:24px 32px;background:#f6f9fa;border:1px solid #29b5e8;border-radius:16px}.snowflake-markdown .snowflake-image-container img{width:auto !important;max-width:100%}.snowflake-markdown .snowflake-text ol{padding-left:20px !important}.snowflake-markdown .snowflake-text li{margin:0 0 12px 0 !important}.snowflake-markdown h3.snowflake-markdown-h3{font-size:20px !important;font-family:Texta,sans-serif !important}@media (min-width:768px){.snowflake-markdown h3.snowflake-markdown-h3{font-size:28px !important}}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false}},":itemsOrder":["experiencefragment-banner","experiencefragment-header","markup_editor_1950346551","responsivegrid","modal_container","experiencefragment-footer","markup_editor"],":type":"wcm/foundation/components/responsivegrid"}},":itemsOrder":["root"],"locale":"en"}
  