{"templateName":"quickstart-page-template","cssClassNames":"page basicpage summit-page","allowedRenditionsWidth":["320","480","640","768","960","1200","1440","1920"],"language":"en","title":"Agentic AI for Your Lakehouse: Snowflake Cortex and Gemini Enterprise on Iceberg","analyticsPageType":"homepage","analyticsCategory":"general","analyticsSubCategory":"","excludeFromAnalytics":false,":path":"/content/snowflake-site/global/en/developers/guides/quickstart-iceberg-cortex-gemini",":hierarchyType":"page",":mappedPath":"/en/developers/guides/quickstart-iceberg-cortex-gemini/",":type":"snowflake-site/components/structure/page",":items":{"root":{"columnCount":12,"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",":items":{"experiencefragment-banner":{"id":"experiencefragment-ef441fc909","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-7c24fbba67","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/quickstart-iceberg-cortex-gemini.languagenav.json"},"markup_editor_1950346551":{"id":"markup-editor-98e54aed3e","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":{"columnCount":12,"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",":items":{"quickstart_hero":{"id":"quickstart-hero-8e8de9ba61","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/quickstart-iceberg-cortex-gemini",":type":"snowflake-site/components/quickstart/quickstart-hero","quickstartHeroTitle":{"lines":["Agentic AI for Your Lakehouse: Snowflake Cortex and Gemini Enterprise on Iceberg"],"type":"heading2",":type":"snowflake-site/components/title-v2"},"quickstartHeroAuthor":"Ali Khosro (Snowflake), Bruce Sandell (Google)","quickstartHeroFirstSnowflakeFeatureTag":{"tagText":"Interoperable Storage","tagColor":"#29B5E8","tagPath":"/content/cq:tags/snowflake-site/taxonomy/snowflake-feature/interoperable-storage","tagIcon":""},"quickstartHeroForkRepoLink":{"id":"button-99d6569a68","showOutboundIcon":false,"buttonLink":{"valid":true,"attributes":{"target":"_blank"},"url":"https://github.com/Snowflake-Labs/sfquickstarts/tree/master/site/sfguides/src/quickstart-iceberg-cortex-gemini"},"linkTargetContentType":"GENERIC",":type":"snowflake-site/components/button","linkType":"SNOWFLAKE_EXTERNAL","text":"Fork Repo"},"quickstartHeroBreadcrumbs":[{"title":"Agentic AI for Your Lakehouse: Snowflake Cortex and Gemini Enterprise on Iceberg","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers/guides/quickstart-iceberg-cortex-gemini","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}],"isDeveloperGuidesPage":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-0949b98d5a","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-f7b73785cc",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"contentfragment":{"id":"contentfragment-c99f44b7bc","paragraphs":["\u003Ch2\u003EOverview\u003C/h2\u003E\n","\u003Cp\u003EWe're going to build an AI agent that answers economic questions about the wellbeing of Americans &mdash; and make it available to anyone in the organization. The agent will live in Snowflake, but employees will talk to it from Gemini Enterprise, their everyday corporate AI assistant.\u003C/p\u003E\n","\u003Cp\u003EWe start from raw public data. We land it in an \u003Ca href=\"https://iceberg.apache.org/\"\u003EApache Iceberg\u003C/a\u003E table on your own GCS bucket. We teach an AI model what the data means through a Semantic View. And we wrap it all in a Cortex Agent powered by Gemini.\u003C/p\u003E\n","\u003Cp\u003EThe key idea: define your business logic once, in the data layer, not in prompts. That way every consumer &mdash; a chat interface, a BI dashboard, an external AI assistant &mdash; gets the same correct answer from the same governed data.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003E\u003Cstrong\u003ETip:\u003C/strong\u003E This quickstart is also available as a \u003Ca href=\"https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb\"\u003ESnowflake Notebook\u003C/a\u003E that you can run directly in Snowsight Workspaces. \u003Ca href=\"https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/README.md\"\u003EMore info\u003C/a\u003E.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/quickstart-iceberg-cortex-gemini/arch-diagram.png\" alt=\"Architecture\"\u003E\u003C/p\u003E\n","\u003Ch3\u003EWhat You Will Learn\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EHow to create Snowflake-managed Iceberg tables on GCS\u003C/li\u003E\u003Cli\u003EHow to build a Semantic View that grounds AI on business logic\u003C/li\u003E\u003Cli\u003EHow to create a Cortex Agent powered by Gemini\u003C/li\u003E\u003Cli\u003EHow to expose the agent via MCP (Model Context Protocol)\u003C/li\u003E\u003Cli\u003EHow to connect Gemini Enterprise to Snowflake through MCP\u003C/li\u003E\u003Cli\u003EHow to connect Looker to the same Iceberg data for BI dashboards\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EWhat You Will Build\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EAn Apache Iceberg table on your GCS bucket with US economic indicators\u003C/li\u003E\u003Cli\u003EA Semantic View defining dimensions, facts, and metrics\u003C/li\u003E\u003Cli\u003EA Cortex Agent accessible from Snowflake CoWork and Gemini Enterprise\u003C/li\u003E\u003Cli\u003EAn MCP server with OAuth for secure cross-platform access\u003C/li\u003E\u003Cli\u003EA Looker dashboard connected to the same Iceberg data\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EPrerequisites\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EA \u003Ca href=\"https://signup.snowflake.com/\"\u003ESnowflake account\u003C/a\u003E on GCP with \u003Ccode\u003EACCOUNTADMIN\u003C/code\u003E privileges\u003C/li\u003E\u003Cli\u003EA \u003Ca href=\"https://console.cloud.google.com/\"\u003EGoogle Cloud\u003C/a\u003E project with permissions to create GCS buckets\u003C/li\u003E\u003Cli\u003EGemini Enterprise enabled in your Google Workspace (for the MCP connection step)\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch2\u003ESetup\u003C/h2\u003E\n","\u003Cp\u003EWe need three environments for this lab:\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003ETip: Use Incognito mode or create a temporary Chrome profile (top right of Chrome window &gt; Profile icon &gt; Add &gt; Stay signed out &gt; name: &quot;workshop&quot;) to manage all lab accounts. Qwiklabs and DataOps will provide URLs for your GCP and Snowflake accounts. Open all account URLs in this &quot;workshop&quot; profile or Incognito window.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Cp\u003EIf you are using official workshop that Qwiklabs provides your GCP account and DataOps provides your Snowflake account, follow the below video and instructions.\u003C/p\u003E\n","\u003Cp\u003EFollow this \u003Ca href=\"https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/assets/how-to-setup-gcp-snowflake-workshop.mov\"\u003Ehow-to setup video\u003C/a\u003E to set up your environments.\u003C/p\u003E\n","\u003Cp\u003EIn your main Chrome profile:\u003C/p\u003E\n\u003Col\u003E\u003Cli\u003E\n","\u003Cp\u003E\u003Cstrong\u003EGoogle Cloud\u003C/strong\u003E &mdash; We will create a GCS bucket for Iceberg storage and later use Gemini Enterprise to interact with our agent.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EGo to \u003Ca href=\"https://explore.qwiklabs.com\"\u003EQwiklabs\u003C/a\u003E for your GCP lab environment.\u003C/li\u003E\u003Cli\u003ELog in or sign up using the same email you used to register for the workshop.\u003C/li\u003E\u003Cli\u003EChoose the snowflake lab and click.\u003C/li\u003E\u003Cli\u003EQwiklabs will provide a URL to open the Google Cloud Console along with temporary credentials.\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003Cli\u003E\n","\u003Cp\u003E\u003Cstrong\u003ESnowflake\u003C/strong\u003E &mdash; This is where we will build everything: Iceberg tables, Semantic Views, Cortex Agents, and the MCP server.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EGo to \u003Ca href=\"https://go.dataops.live\"\u003EDataOps\u003C/a\u003E to sign in or register.\u003C/li\u003E\u003Cli\u003EDataOps will provide a URL to your Snowflake account along with a username and password.\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003Cli\u003E\n","\u003Cp\u003E\u003Cstrong\u003ELooker\u003C/strong\u003E &mdash; We will connect a BI dashboard to the same Iceberg data.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003ELogin information will be provided during the workshop.\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003EIn your Incognito window or temporary &quot;Workshop&quot; Chrome profile:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003E\n","\u003Cp\u003E\u003Cstrong\u003EGCP Environment:\u003C/strong\u003E\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EUse the Qwiklabs URL to open your provisioned GCP console, which we will use to set up GCS buckets.\u003C/li\u003E\u003Cli\u003EYou can also use \u003Ccode\u003ECloud Shell\u003C/code\u003E (located on the top right bar) as a terminal connected directly to your GCP account if scripting is needed.\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003Cli\u003E\n","\u003Cp\u003E\u003Cstrong\u003ESnowflake Environment for running the lab:\u003C/strong\u003E\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EUse the DataOps URL to open your provisioned Snowflake environment.\u003C/li\u003E\u003Cli\u003EOpen \u003Cstrong\u003EWorkspaces\u003C/strong\u003E from the left panel.\u003C/li\u003E\u003Cli\u003EOpen the shared workspace \u003Ccode\u003Egcp-snowflake-solutions\u003C/code\u003E.\u003C/li\u003E\u003Cli\u003EOpen the file \u003Ccode\u003Ehands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb\u003C/code\u003E &mdash; the notebook version of this guide &mdash; if you prefer to run the lab as a notebook, cell by cell. Otherwise follow the steps in this guide from a SQL worksheet.\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003Cli\u003E\n","\u003Cp\u003E\u003Cstrong\u003ESnowflake Environment for UI exploration:\u003C/strong\u003E\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EOpen a second Snowflake tab using the DataOps URL so you can explore the UI during the workshop.\u003C/li\u003E\u003Cli\u003EFrom the left panel, locate Cortex Agents, Analyst, Snowflake Marketplace, Database Explorer, Workspaces, dbt Projects, Streamlit, Openflow, and Dynamic Tables.\u003C/li\u003E\u003Cli\u003EFeel free to explore Snowflake before the workshop begins.\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch2\u003EWorkspace\u003C/h2\u003E\n","\u003Cp\u003ESnowflake Workspaces give you a full developer environment in the browser. It connects to a git repo so you can collaborate with a team, and runs Python and SQL files with a built-in compute engine.\u003C/p\u003E\n","\u003Cp\u003EEverything in this guide is plain SQL plus UI steps, so you can run it straight from a SQL worksheet. If you would rather run it as a notebook &mdash; mixed SQL, Python, and markdown cells sharing one session context &mdash; open the \u003Ca href=\"https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb\"\u003Enotebook version of this quickstart\u003C/a\u003E in a Snowflake Workspace:\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: Go to Projects &rarr; Workspaces.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003ECreate a public git integration and connect to this repo.\n\u003Cul\u003E\u003Cli\u003EClick \u003Cstrong\u003E+\u003C/strong\u003E on the very top left &gt; Git Workspace\u003C/li\u003E\u003Cli\u003ERepo name: \u003Ccode\u003Ehttps://github.com/sfc-gh-akhosro/gcp-snowflake-solutions\u003C/code\u003E\u003C/li\u003E\u003Cli\u003EChoose public repo connection (no auth needed).\u003C/li\u003E\u003C/ul\u003E\n\u003C/li\u003E\u003Cli\u003EOpen \u003Ccode\u003Ehands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb\u003C/code\u003E. Click &quot;Connected&quot; to start service. It takes a few minutes &mdash; start it now and read ahead while it spins up.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: Open a second browser tab at the same Snowflake instance URL for exploring components. In this tab find Marketplace, Cortex Analyst, Agents, AI Functions, dbt Projects, Database Explorer, and Workspaces.\u003C/p\u003E\n","\u003Ch2\u003ERole Based Access Control\u003C/h2\u003E\n","\u003Cp\u003EThroughout this lab we use two roles:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Cstrong\u003E\u003Ccode\u003Ehol_role\u003C/code\u003E\u003C/strong\u003E &mdash; this is us, the developer. It runs the lab and owns everything we create.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003E\u003Ccode\u003Eend_user_role\u003C/code\u003E\u003C/strong\u003E &mdash; this simulates a business user who can only ask questions through the agent but can't build or modify anything.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003ELet's create those roles and grant the needed privileges.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003EUSE ROLE ACCOUNTADMIN;\n\n-- Create a warehouse for this lab (guaranteed to exist)\nCREATE WAREHOUSE IF NOT EXISTS hol_wh\n  WAREHOUSE_SIZE = 'XSMALL' AUTO_SUSPEND = 60 INITIALLY_SUSPENDED = TRUE;\nUSE WAREHOUSE hol_wh;\n\n-- Builder role: owns all workshop objects\nCREATE ROLE IF NOT EXISTS hol_role;\n\n-- Consumer role: can only use the agent (CoWork, Gemini Enterprise)\nCREATE ROLE IF NOT EXISTS end_user_role;\n\n-- Attach both roles to the role hierarchy.\n-- hol_role OWNS everything we build, so without this an admin has no path to\n-- those objects once the workshop user goes away -- and cleanup becomes impossible.\n-- ACCOUNTADMIN inherits SYSADMIN, so it can always do whatever hol_role can do.\nGRANT ROLE hol_role      TO ROLE SYSADMIN;\nGRANT ROLE end_user_role TO ROLE SYSADMIN;\n\n-- Grant both roles to whoever is running this lab\nBEGIN\n  LET usr := CURRENT_USER();\n  EXECUTE IMMEDIATE 'GRANT ROLE hol_role TO USER ' || :usr;\n  EXECUTE IMMEDIATE 'GRANT ROLE end_user_role TO USER ' || :usr;\n  -- MCP OAuth sessions (e.g., Gemini Enterprise) fail to initialize if the connecting user's DEFAULT_WAREHOUSE is null.\n  EXECUTE IMMEDIATE 'ALTER USER ' || :usr || ' SET DEFAULT_WAREHOUSE = ''hol_wh''';\nEND;\n\n-- Builder privileges\nGRANT CREATE DATABASE        ON ACCOUNT TO ROLE hol_role;\nGRANT CREATE WAREHOUSE       ON ACCOUNT TO ROLE hol_role;\nGRANT CREATE INTEGRATION     ON ACCOUNT TO ROLE hol_role;\nGRANT CREATE EXTERNAL VOLUME ON ACCOUNT TO ROLE hol_role;\nGRANT OWNERSHIP ON WAREHOUSE hol_wh TO ROLE hol_role COPY CURRENT GRANTS;\n\n-- Cortex access for both roles\nGRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE hol_role;\nGRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE end_user_role;\n\n-- Switch to hol_role to build\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\n\nCREATE DATABASE IF NOT EXISTS hol_db;\nUSE SCHEMA hol_db.public;\n\n-- Schema-level privileges (must come after database/schema exist)\nUSE ROLE ACCOUNTADMIN;\nGRANT CREATE SEMANTIC VIEW ON SCHEMA hol_db.public TO ROLE hol_role;\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\nUSE SCHEMA hol_db.public;\n\n-- Grant consumer role usage on warehouse and database\nGRANT USAGE ON WAREHOUSE hol_wh TO ROLE end_user_role;\nGRANT USAGE ON DATABASE hol_db TO ROLE end_user_role;\nGRANT USAGE ON SCHEMA hol_db.public TO ROLE end_user_role;\n\n-- Verify context\nSELECT CURRENT_ROLE() AS role, CURRENT_WAREHOUSE() AS wh, CURRENT_DATABASE() AS db, CURRENT_SCHEMA() AS schema;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch2\u003EArchitecture\u003C/h2\u003E\n","\u003Cp\u003E\u003Ca href=\"https://iceberg.apache.org/\"\u003EApache Iceberg\u003C/a\u003E is the cornerstone of a modern data platform. It allows multiple engines to read and write directly to the same data, while that data stays in one place &mdash; your cloud storage. No copies between systems, no vendor lock-in.\u003C/p\u003E\n","\u003Cp\u003EWe'll get our data sources from Snowflake Marketplace and land them in an Iceberg table on a GCS bucket. Snowflake Horizon serves as the catalog and governance layer. We could just as easily use Google Cloud Open Lakehouse Runtime or any other IRC-compliant catalog instead.\u003C/p\u003E\n","\u003Cp\u003ENext, we use Snowflake Semantic View Autopilot to create a Semantic View that defines the business logic of our Iceberg table. We wrap this in a Cortex Agent and use Gemini as the reasoning model behind it. The biggest gain from Snowflake Cortex is the added context and logic that makes answers accurate and thorough &mdash; customers love it for reduced hallucination.\u003C/p\u003E\n","\u003Cp\u003EThen we build an MCP connection between Gemini Enterprise and our Cortex Agent, so employees can talk to their data through their corporate AI chat. Looker helps us visualize and get insights from the same data.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/quickstart-iceberg-cortex-gemini/arch-diagram.png\" alt=\"Architecture\"\u003E\u003C/p\u003E\n","\u003Cp\u003EHere's a summary of what each component does:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Cstrong\u003ESnowflake Marketplace\u003C/strong\u003E &mdash; instant access to curated, live datasets. No ETL, no ingestion pipelines.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EIceberg\u003C/strong\u003E &mdash; open table format on your GCS bucket. Multiple engines read the same files. You own the data.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ESnowflake Horizon\u003C/strong\u003E &mdash; catalog and governance layer for Iceberg tables. Access control, lineage, and discoverability.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ESemantic View\u003C/strong\u003E &mdash; business logic defined once in the data layer. Grounds the AI so it doesn't guess.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ECortex Agent\u003C/strong\u003E &mdash; natural-language interface that turns questions into governed SQL and returns correct answers.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EGemini\u003C/strong\u003E &mdash; the reasoning model powering our agent. Large context window, native to Google Cloud.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EMCP\u003C/strong\u003E &mdash; open protocol to expose the agent. Connect once, access from any MCP-compatible client.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ESnowflake CoWork\u003C/strong\u003E &mdash; chat interface inside Snowflake for business users who don't write SQL.\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003EGemini Enterprise\u003C/strong\u003E &mdash; Google Cloud's corporate AI assistant. Employees ask questions in a familiar interface and get grounded answers from Iceberg data via MCP.\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch2\u003EMarketplace\u003C/h2\u003E\n","\u003Cp\u003EWe get our source data from Snowflake Marketplace. It lets teams access curated, live datasets instantly &mdash; you click &quot;Get&quot; and the data appears in your account. No ETL pipelines, no data copying. For data providers, it's a secure channel to share or sell data to the world.\u003C/p\u003E\n","\u003Cp\u003EWe want to build an economic dataset that tracks the financial wellbeing of Americans at the state level. We need income, inflation, mortgage rates, and unemployment &mdash; all on a monthly basis. That means four source tables from the Bureau of Labor Statistics and related public data.\u003C/p\u003E\n","\u003Cp\u003ELet's go get them.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: Marketplace &rarr; Snowflake Marketplace &rarr; Data products &rarr; search &quot;Snowflake Public Data&quot; &rarr; Click on Snowflake Public Data (Free) &rarr; \u003Cstrong\u003EGet\u003C/strong\u003E.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EDatabase name: \u003Ccode\u003ESNOWFLAKE_PUBLIC_DATA_FREE\u003C/code\u003E (accept default options and don't change them).\u003C/li\u003E\u003Cli\u003EDirect link: https://app.snowflake.com/marketplace/listing/GZTSZ290BV255/snowflake-public-data-products-snowflake-public-data-free\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003EThis dataset is already available on many Snowflake accounts. The query below verifies access to all four source tables we need.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Verify marketplace data access\nSELECT 'BLS_PRICE' AS source, COUNT(*) AS row_count FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_PRICE_TIMESERIES\nUNION ALL\nSELECT 'BLS_EMPLOYMENT', COUNT(*) FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_EMPLOYMENT_TIMESERIES\nUNION ALL\nSELECT 'FREDDIE_MAC', COUNT(*) FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.FREDDIE_MAC_HOUSING_TIMESERIES\nUNION ALL\nSELECT 'IRS_INCOME', COUNT(*) FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.IRS_INDIVIDUAL_INCOME_TIMESERIES;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch2\u003EIceberg\u003C/h2\u003E\n","\u003Cp\u003ENow we need somewhere to land this data.\u003C/p\u003E\n","\u003Cp\u003EIceberg is an open table format. Parquet data files and metadata sit in your own GCS bucket &mdash; you own them. Any engine that speaks Iceberg can read them directly: Snowflake, BigQuery, Managed Spark, or any Iceberg REST Catalog&ndash;compliant runtime. No copying between systems.\u003C/p\u003E\n","\u003Cp\u003EWe use \u003Ccode\u003Ecatalog=snowflake\u003C/code\u003E, which means Snowflake manages the table through Snowflake Horizon, handling governance, access control, and discoverability. But the actual data never leaves your bucket.\u003C/p\u003E\n","\u003Cp\u003ELet's create the bucket in Google Cloud Console, give Snowflake write access, and build our economic indicators table.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-GCP\u003C/strong\u003E: Google Cloud Console &rarr; Cloud Storage &rarr; \u003Cstrong\u003ECreate Bucket\u003C/strong\u003E.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EName: \u003Ccode\u003Efirstname_lastname_hol_0729\u003C/code\u003E\u003C/li\u003E\u003Cli\u003ELocation: \u003Ccode\u003EMulti-region\u003C/code\u003E\u003C/li\u003E\u003Cli\u003ELeave everything else as default.\u003C/li\u003E\u003C/ul\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Create an external volume pointing to your GCS bucket\nCREATE OR REPLACE EXTERNAL VOLUME hol_gcs_vol\n  STORAGE_LOCATIONS = ((\n    NAME = 'hol-gcs'\n    STORAGE_PROVIDER = 'GCS'\n    STORAGE_BASE_URL = 'gcs://&lt;your-bucket-name&gt;/iceberg/'\n  ));\n\n-- Describe to get storage config\nDESCRIBE EXTERNAL VOLUME hol_gcs_vol;\nSET desc_qid = LAST_QUERY_ID();\n\n-- Extract the GCS service account to grant on the bucket\nSELECT\n  PARSE_JSON(&quot;property_value&quot;):STORAGE_GCP_SERVICE_ACCOUNT::STRING\n    AS gcs_service_account_to_grant\nFROM TABLE(RESULT_SCAN($desc_qid))\nWHERE &quot;property&quot; = 'STORAGE_LOCATION_1';\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003ECopy the service account printed above (Snowflake will use it to access your bucket).\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-GCP\u003C/strong\u003E: Google Cloud Console &rarr; your bucket &rarr; \u003Cstrong\u003EPermissions\u003C/strong\u003E tab &rarr; \u003Cstrong\u003EGrant Access\u003C/strong\u003E.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EPaste the service account you copied.\u003C/li\u003E\u003Cli\u003ERole: \u003Cstrong\u003EStorage Admin\u003C/strong\u003E &rarr; Save.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003EWe have created the bucket for Iceberg data and metadata files, and granted read/write access to Snowflake's service account.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Create Iceberg table: join four marketplace sources into one wide-format table\nCREATE OR REPLACE ICEBERG TABLE hol_db.public.economic_indicators\n  CATALOG = 'SNOWFLAKE'\n  EXTERNAL_VOLUME = 'hol_gcs_vol'\n  BASE_LOCATION = 'economic_indicators'\n  AS\nWITH cpi AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG(value) AS cpi_index\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_PRICE_TIMESERIES\n  WHERE variable_name = 'CPI: All items, Not seasonally adjusted, Monthly'\n    AND geo_id = 'country/USA'\n  GROUP BY 1\n),\nmortgage_30yr AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    ROUND(AVG(value) * 100, 2) AS mortgage_rate_30yr_pct\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.FREDDIE_MAC_HOUSING_TIMESERIES\n  WHERE variable_name = '30-Year Fixed Rate Mortgage Rate, National Average'\n    AND geo_id = 'country/USA'\n  GROUP BY 1\n),\nmortgage_15yr AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    ROUND(AVG(value) * 100, 2) AS mortgage_rate_15yr_pct\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.FREDDIE_MAC_HOUSING_TIMESERIES\n  WHERE variable_name = '15-Year Fixed Rate Mortgage Rate, National Average'\n    AND geo_id = 'country/USA'\n  GROUP BY 1\n),\nunemployment AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    geo_id,\n    AVG(value) AS unemployment_rate_pct\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_EMPLOYMENT_TIMESERIES\n  WHERE variable_name = 'Local Area Unemployment: Unemployment Rate, Not seasonally adjusted, Monthly'\n    AND LENGTH(geo_id) = 8\n  GROUP BY 1, 2\n),\nnational_unemployment AS (\n  SELECT month, ROUND(AVG(unemployment_rate_pct), 2) AS unemployment_rate_pct\n  FROM unemployment\n  GROUP BY 1\n),\nincome_raw AS (\n  SELECT\n    agi.geo_id,\n    YEAR(agi.date) AS yr,\n    ROUND(agi.value / NULLIF(ret.value, 0), 0) AS avg_income_per_return\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.IRS_INDIVIDUAL_INCOME_TIMESERIES agi\n  JOIN SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.IRS_INDIVIDUAL_INCOME_TIMESERIES ret\n    ON agi.geo_id = ret.geo_id AND agi.date = ret.date\n  WHERE agi.variable_name = 'Adjusted gross income (AGI), AGI bin: Total'\n    AND ret.variable_name = 'Number of returns, AGI bin: Total'\n    AND LENGTH(agi.geo_id) = 8\n),\nincome_indexed AS (\n  SELECT\n    geo_id,\n    yr,\n    avg_income_per_return,\n    ROUND((avg_income_per_return / FIRST_VALUE(avg_income_per_return) OVER (PARTITION BY geo_id ORDER BY yr)) * 100, 1) AS income_index\n  FROM income_raw\n),\nnational_income AS (\n  SELECT yr, ROUND(AVG(income_index), 1) AS income_index\n  FROM income_indexed\n  GROUP BY 1\n),\ngeo AS (\n  SELECT geo_id, geo_name\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.GEOGRAPHY_INDEX\n  WHERE level = 'State'\n),\nnational AS (\n  SELECT\n    c.month AS date,\n    'country/USA' AS geo_id,\n    'United States' AS geo_name,\n    ROUND(c.cpi_index, 2) AS cpi_index,\n    ROUND(((c.cpi_index - LAG(c.cpi_index, 12) OVER (ORDER BY c.month))\n      / NULLIF(LAG(c.cpi_index, 12) OVER (ORDER BY c.month), 0)) * 100, 2) AS inflation_pct,\n    m30.mortgage_rate_30yr_pct,\n    m15.mortgage_rate_15yr_pct,\n    nu.unemployment_rate_pct,\n    ni.income_index\n  FROM cpi c\n  LEFT JOIN mortgage_30yr m30 ON c.month = m30.month\n  LEFT JOIN mortgage_15yr m15 ON c.month = m15.month\n  LEFT JOIN national_unemployment nu ON c.month = nu.month\n  LEFT JOIN national_income ni ON YEAR(c.month) = ni.yr\n),\nstates AS (\n  SELECT\n    u.month AS date,\n    u.geo_id,\n    g.geo_name,\n    NULL::FLOAT AS cpi_index,\n    NULL::FLOAT AS inflation_pct,\n    NULL::FLOAT AS mortgage_rate_30yr_pct,\n    NULL::FLOAT AS mortgage_rate_15yr_pct,\n    u.unemployment_rate_pct,\n    ii.income_index\n  FROM unemployment u\n  JOIN geo g ON u.geo_id = g.geo_id\n  LEFT JOIN income_indexed ii ON u.geo_id = ii.geo_id AND YEAR(u.month) = ii.yr\n)\nSELECT * FROM national\nUNION ALL\nSELECT * FROM states\nORDER BY date, geo_id;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch3\u003EExplore Iceberg Data and Metadata\u003C/h3\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-GCP\u003C/strong\u003E: Google Cloud Console &rarr; your bucket &rarr; explore the files.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EYou'll see Parquet data files and a metadata folder with JSON files.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003ELook at the Iceberg table definition above and identify the catalog we used (\u003Ccode\u003ECATALOG = 'SNOWFLAKE'\u003C/code\u003E). All data and metadata is in your own bucket &mdash; not owned by Snowflake, BigQuery, or any other vendor. Every engine reads and writes directly while a catalog (here Snowflake Horizon) provides governance and security.\u003C/p\u003E\n","\u003Cp\u003EThis is the cornerstone of a modern data platform: one source of truth in a datalake with high data gravity. All services are drawn to this data to perform their tasks.\u003C/p\u003E\n","\u003Ch2\u003EData Profiling\u003C/h2\u003E\n","\u003Cp\u003EWe have our Iceberg table. Let's look at what's inside.\u003C/p\u003E\n","\u003Cp\u003ESnowsight gives you profiling, charting, and pivot tables right in the query results. You can understand the shape of a dataset without leaving the browser.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: After running the query, explore the results pane:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EClick \u003Cstrong\u003EChart\u003C/strong\u003E tab to visualize trends over time.\u003C/li\u003E\u003Cli\u003EClick \u003Cstrong\u003EQuery Profile\u003C/strong\u003E tab to see the execution plan.\u003C/li\u003E\u003Cli\u003EClick column headers for quick profiling stats (min, max, distribution).\u003C/li\u003E\u003C/ul\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- National economic indicators since 2015\n-- Try: Chart (line) to visualize trends, Query Profile to see execution plan\nSELECT\n  date,\n  cpi_index,\n  income_index,\n  inflation_pct,\n  mortgage_rate_30yr_pct,\n  unemployment_rate_pct\nFROM hol_db.public.economic_indicators\nWHERE geo_id = 'country/USA'\n  AND date &gt;= '2015-01-01'\n  AND inflation_pct IS NOT NULL\nORDER BY date;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch2\u003ECortex\u003C/h2\u003E\n","\u003Cp\u003EWe have a clean Iceberg table. Any analyst can query it with SQL. But that doesn't make it AI-ready.\u003C/p\u003E\n","\u003Cp\u003EHere's the gap: when an LLM sees column names like \u003Ccode\u003ECPI_INDEX\u003C/code\u003E or \u003Ccode\u003EGEO_ID\u003C/code\u003E, it guesses what they mean. It guesses wrong. We need to tell it which columns are dimensions, which are facts, how metrics are calculated, and what kinds of questions this table can answer.\u003C/p\u003E\n","\u003Cp\u003EThat's what a Semantic View does. You define your business logic once &mdash; in the data layer, not scattered across prompts &mdash; and every AI consumer inherits the same correct definitions.\u003C/p\u003E\n","\u003Ch3\u003ESemantic View\u003C/h3\u003E\n","\u003Cp\u003EThe Semantic View is the grounding layer for our agent. We define dimensions (date, geography), facts (CPI, mortgage rate, unemployment, income), and metrics (year-over-year inflation, average mortgage rate by state).\u003C/p\u003E\n","\u003Cp\u003EWe can also add verified queries &mdash; known-good question-to-SQL pairs that anchor the model's behavior for common questions. Without this layer, an LLM guesses and hallucinates. With it, a question like &quot;How has inflation compared to income growth?&quot; maps to the exact right SQL every time.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: AI &amp; ML &rarr; Cortex Analyst &rarr; \u003Cstrong\u003ECreate Semantic View\u003C/strong\u003E.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003ESelect table: \u003Ccode\u003EHOL_DB.PUBLIC.ECONOMIC_INDICATORS\u003C/code\u003E.\u003C/li\u003E\u003Cli\u003EClick \u003Cstrong\u003EAutopilot\u003C/strong\u003E to auto-generate dimensions, facts, and metrics from the table schema.\u003C/li\u003E\u003Cli\u003EReview the generated YAML &mdash; check that dimensions (DATE, GEO_ID, GEO_NAME), facts (CPI_INDEX, INFLATION_PCT, etc.), and metrics are correct.\u003C/li\u003E\u003Cli\u003EAdd or edit descriptions to clarify business meaning.\u003C/li\u003E\u003Cli\u003EOptionally add verified queries (known-good question &rarr; SQL pairs).\u003C/li\u003E\u003Cli\u003ESave as \u003Ccode\u003Eeconomic_semantic_view\u003C/code\u003E.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003EIn this lab we create it via code (YAML below), but Autopilot is the fastest way to get started.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003ECALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML(\n  'hol_db.public',\n  $$\nname: economic_semantic_view\ntables:\n  - name: economic_indicators\n    base_table:\n      database: HOL_DB\n      schema: PUBLIC\n      table: ECONOMIC_INDICATORS\n    dimensions:\n      - name: DATE\n        description: &quot;Date of the observation&quot;\n        expr: economic_indicators.DATE\n        data_type: DATE\n      - name: GEO_ID\n        description: &quot;Geographic area identifier&quot;\n        expr: economic_indicators.GEO_ID\n        data_type: TEXT\n      - name: GEO_NAME\n        description: &quot;Geographic area &mdash; United States for national, or state name (e.g. California)&quot;\n        expr: economic_indicators.GEO_NAME\n        data_type: TEXT\n    facts:\n      - name: CPI_INDEX\n        description: &quot;Consumer Price Index, base period 1982-84 = 100 (national only)&quot;\n        expr: economic_indicators.CPI_INDEX\n        data_type: NUMBER\n      - name: INFLATION_PCT\n        description: &quot;Year-over-year inflation rate as percent (national only)&quot;\n        expr: economic_indicators.INFLATION_PCT\n        data_type: NUMBER\n      - name: MORTGAGE_RATE_30YR_PCT\n        description: &quot;30-year fixed mortgage rate, national average, percent (national only)&quot;\n        expr: economic_indicators.MORTGAGE_RATE_30YR_PCT\n        data_type: NUMBER\n      - name: MORTGAGE_RATE_15YR_PCT\n        description: &quot;15-year fixed mortgage rate, national average, percent (national only)&quot;\n        expr: economic_indicators.MORTGAGE_RATE_15YR_PCT\n        data_type: NUMBER\n      - name: UNEMPLOYMENT_RATE_PCT\n        description: &quot;Unemployment rate as percent (available national and by state)&quot;\n        expr: economic_indicators.UNEMPLOYMENT_RATE_PCT\n        data_type: NUMBER\n      - name: INCOME_INDEX\n        description: &quot;Average income per tax return, indexed to earliest available year = 100. Compare to CPI_INDEX to assess purchasing power. (available national and by state, annual grain)&quot;\n        expr: economic_indicators.INCOME_INDEX\n        data_type: NUMBER\n    metrics:\n      - name: AVG_CPI_INDEX\n        description: &quot;Average Consumer Price Index&quot;\n        expr: AVG(economic_indicators.CPI_INDEX)\n      - name: AVG_INFLATION_PCT\n        description: &quot;Average year-over-year inflation rate&quot;\n        expr: AVG(economic_indicators.INFLATION_PCT)\n      - name: AVG_MORTGAGE_RATE_30YR\n        description: &quot;Average 30-year fixed mortgage rate&quot;\n        expr: AVG(economic_indicators.MORTGAGE_RATE_30YR_PCT)\n      - name: AVG_UNEMPLOYMENT_RATE\n        description: &quot;Average unemployment rate&quot;\n        expr: AVG(economic_indicators.UNEMPLOYMENT_RATE_PCT)\n      - name: AVG_INCOME_INDEX\n        description: &quot;Average income index&quot;\n        expr: AVG(economic_indicators.INCOME_INDEX)\n$$\n);\n\n-- Verify\nSHOW SEMANTIC VIEWS IN SCHEMA hol_db.public;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch3\u003ECortex Agent\u003C/h3\u003E\n","\u003Cp\u003ENow we wrap the Semantic View in a conversational interface.\u003C/p\u003E\n","\u003Cp\u003EA Cortex Agent takes a natural-language question and passes it to Cortex Analyst. Cortex Analyst uses the Semantic View to generate correct SQL, executes it, and returns a grounded answer with supporting data. We use Gemini as the reasoning model behind the agent.\u003C/p\u003E\n","\u003Cp\u003EThe whole thing is defined in a single SQL statement &mdash; reproducible and version-controlled.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: AI &amp; ML &rarr; Cortex Agents &rarr; \u003Cstrong\u003E+ Create Agent\u003C/strong\u003E.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EName: \u003Ccode\u003Ehol_economic_agent\u003C/code\u003E\u003C/li\u003E\u003Cli\u003EModel: change to \u003Cstrong\u003EGemini\u003C/strong\u003E (default may be different).\u003C/li\u003E\u003Cli\u003ETools: add Cortex Analyst tool &rarr; select \u003Ccode\u003Eeconomic_semantic_view\u003C/code\u003E.\u003C/li\u003E\u003Cli\u003EInstructions: describe what the agent does, e.g. &quot;Answer questions about US economic indicators including inflation, mortgage rates, unemployment by state, and income trends.&quot;\u003C/li\u003E\u003Cli\u003EWarehouse: \u003Ccode\u003Ehol_wh\u003C/code\u003E (for query execution).\u003C/li\u003E\u003Cli\u003ETest the agent in the preview pane before saving.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003EIn this lab we create it via SQL for reproducibility.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Create a Cortex Agent backed by the economic indicators semantic view\nCREATE OR REPLACE AGENT hol_db.public.hol_economic_agent\n  FROM SPECIFICATION $$\n  tools:\n    - tool_spec:\n        type: cortex_analyst_text_to_sql\n        name: economic_analyst\n        description: &quot;Answers questions about US economic indicators: CPI/inflation, mortgage interest rates (30-year, 15-year), unemployment rate (national and by state), and income index.&quot;\n\n  tool_resources:\n    economic_analyst:\n      semantic_view: HOL_DB.PUBLIC.ECONOMIC_SEMANTIC_VIEW\n      execution_environment:\n        type: warehouse\n        warehouse: HOL_WH\n  $$;\n\n-- Grant consumer role usage on the agent\nGRANT USAGE ON AGENT hol_db.public.hol_economic_agent TO ROLE end_user_role;\nGRANT SELECT ON SEMANTIC VIEW hol_db.public.economic_semantic_view TO ROLE end_user_role;\nGRANT SELECT ON TABLE hol_db.public.economic_indicators TO ROLE end_user_role;\n\n-- Verify\nSHOW AGENTS IN SCHEMA hol_db.public;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch3\u003ECoWork\u003C/h3\u003E\n","\u003Cp\u003ESnowflake CoWork is the chat surface for business users. No SQL knowledge needed, no SQL worksheet &mdash; just a conversation with the agent.\u003C/p\u003E\n","\u003Cp\u003ELet's switch to \u003Ccode\u003Eend_user_role\u003C/code\u003E to see what it looks like for someone who can only consume, not build.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Snowsight\u003C/strong\u003E: AI &amp; ML &rarr; Open Snowflake CoWork.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EIn CoWork, go to bottom left profile, click setting, and switch role to \u003Ccode\u003Eend_user_role\u003C/code\u003E, warehouse: \u003Ccode\u003Ehol_wh\u003C/code\u003E. Done.\u003C/li\u003E\u003Cli\u003EYou should be able to see \u003Cstrong\u003Ehol_economic_agent\u003C/strong\u003E in the agent list (control buttons of the CoWork chat).\u003C/li\u003E\u003Cli\u003EAsk: \u003Cem\u003E&quot;How has the 30-year mortgage rate changed relative to inflation since 2020?&quot;\u003C/em\u003E\u003C/li\u003E\u003Cli\u003EAsk: \u003Cem\u003E&quot;Tell the economic story of California vs Texas over the last 10 years using all available indicators.&quot;\u003C/em\u003E\u003C/li\u003E\u003Cli\u003EAsk: \u003Cem\u003E&quot;What did the COVID shock look like in data &mdash; unemployment spike, rate crash, inflation surge &mdash; and how long did each phase last?&quot;\u003C/em\u003E\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003ELook at the responses &mdash; they include the generated SQL so you can see exactly what queries were executed. Same agent, same data, different role &mdash; a chat-based surface instead of SQL.\u003C/p\u003E\n","\u003Ch2\u003EMCP\u003C/h2\u003E\n","\u003Cp\u003ESo far our agent lives inside Snowflake. But what if employees want to ask it questions from Gemini Enterprise, or from another AI tool?\u003C/p\u003E\n","\u003Cp\u003EThat's where MCP comes in. Model Context Protocol is an open standard that gives AI applications a universal way to connect to data tools. We declare our agent as an MCP tool and add OAuth for secure access. Any MCP-compatible client can then connect &mdash; no custom connector per client.\u003C/p\u003E\n","\u003Ch3\u003EMCP Server\u003C/h3\u003E\n","\u003Cp\u003ELet's create the actual MCP server. We register our Cortex Agent as a callable tool inside a Snowflake-managed MCP server, then set up an OAuth security integration so external clients can authenticate securely. The output from this step gives us the credentials we'll register in Gemini Enterprise next.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- MCP server exposing the Cortex Agent as a tool (requires schema ownership &rarr; hol_role)\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\nUSE SCHEMA hol_db.public;\n\nCREATE OR REPLACE MCP SERVER hol_db.public.hol_mcp\n  FROM SPECIFICATION $$\n  tools:\n    - name: &quot;hol-economic-agent&quot;\n      type: &quot;CORTEX_AGENT_RUN&quot;\n      identifier: &quot;HOL_DB.PUBLIC.HOL_ECONOMIC_AGENT&quot;\n      description: &quot;US economic indicators agent &mdash; answers questions about inflation (CPI), mortgage rates, unemployment, and income.&quot;\n      title: &quot;Economic Indicators Agent&quot;\n  $$;\n\n-- Grant MCP server usage to the consumer role (required for Gemini to discover tools)\n-- ALLOWED_ROLES_LIST must include the role used in the OAuth scope, otherwise\n-- Snowflake rejects it with &quot;invalid role&quot; even if the user has the role granted.\nUSE ROLE ACCOUNTADMIN;\n\nGRANT USAGE ON MCP SERVER hol_db.public.hol_mcp TO ROLE end_user_role;\n\nCREATE OR REPLACE SECURITY INTEGRATION hol_mcp_oauth\n  TYPE = OAUTH\n  OAUTH_CLIENT = CUSTOM\n  OAUTH_CLIENT_TYPE = 'CONFIDENTIAL'\n  OAUTH_REDIRECT_URI = 'https://vertexaisearch.cloud.google.com/oauth-redirect'\n  ALLOWED_ROLES_LIST = ('END_USER_ROLE')\n  ENABLED = TRUE;\n\n-- Get MCP server metadata (database, schema, name)\nDESCRIBE MCP SERVER hol_db.public.hol_mcp;\nSET mcp_qid = LAST_QUERY_ID();\n\n-- Get OAuth integration metadata (auth URL, token URL, client ID)\nDESCRIBE SECURITY INTEGRATION hol_mcp_oauth;\nSET int_qid = LAST_QUERY_ID();\n\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\nUSE SCHEMA hol_db.public;\n\n-- Retrieve all credentials for Gemini Enterprise MCP connection\n-- Values derived from DESCRIBE metadata &mdash; nothing hardcoded\nWITH mcp_meta AS (\n  SELECT &quot;database_name&quot;, &quot;schema_name&quot;, &quot;name&quot;\n  FROM TABLE(RESULT_SCAN($mcp_qid))\n),\noauth_meta AS (\n  SELECT\n    MAX(CASE WHEN &quot;property&quot; = 'OAUTH_AUTHORIZATION_ENDPOINT' THEN &quot;property_value&quot; END) AS auth_endpoint,\n    MAX(CASE WHEN &quot;property&quot; = 'OAUTH_TOKEN_ENDPOINT' THEN &quot;property_value&quot; END) AS token_endpoint,\n    MAX(CASE WHEN &quot;property&quot; = 'OAUTH_CLIENT_ID' THEN &quot;property_value&quot; END) AS client_id\n  FROM TABLE(RESULT_SCAN($int_qid))\n),\nsecrets AS (\n  SELECT PARSE_JSON(SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('HOL_MCP_OAUTH')) AS s\n),\naccount_base AS (\n  -- Org-account URL format (required by Gemini Enterprise)\n  SELECT 'https://' || CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME()\n         || '.snowflakecomputing.com' AS url\n)\nSELECT field_name, value\nFROM (\n  SELECT 1 AS ord, 'MCP Server URL' AS field_name,\n    ab.url || '/api/v2/databases/' || m.&quot;database_name&quot; || '/schemas/' || m.&quot;schema_name&quot; || '/mcp-servers/' || m.&quot;name&quot; AS value\n    FROM account_base ab, mcp_meta m\n  UNION ALL\n  SELECT 2, 'Auth URL', o.auth_endpoint FROM oauth_meta o\n  UNION ALL\n  SELECT 3, 'Auth URL Params', '' FROM oauth_meta o\n  UNION ALL\n  SELECT 4, 'Token URL', o.token_endpoint FROM oauth_meta o\n  UNION ALL\n  SELECT 5, 'Client ID', o.client_id FROM oauth_meta o\n  UNION ALL\n  SELECT 6, 'Client Secret', s.s:OAUTH_CLIENT_SECRET::STRING FROM secrets s\n  UNION ALL\n  SELECT 7, 'Scopes', 'session:role:end_user_role' FROM secrets s\n  UNION ALL\n  SELECT 8, 'MCP Server Description', 'Snowflake Cortex Agent for US economic indicators (CPI, mortgage rates, unemployment, income)' FROM secrets s\n  UNION ALL\n  SELECT 9, 'Agent Instructions', 'Use the hol-economic-agent tool to answer questions about US economic data including inflation, mortgage rates, unemployment by state, and income trends.' FROM secrets s\n  UNION ALL\n  SELECT 10, 'Data Connector Name', 'hol_cortex_gemini_economic_agent' FROM secrets s\n)\nORDER BY ord;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch2\u003EGemini Enterprise\u003C/h2\u003E\n","\u003Cp\u003EGemini Enterprise is Google Cloud's corporate AI assistant &mdash; the chat interface employees across the organization already use daily.\u003C/p\u003E\n","\u003Cp\u003EBy registering our Snowflake MCP server as a data connector, the Cortex Agent becomes a tool that Gemini calls when it needs economic data. Employees ask questions in Gemini and get grounded answers from governed Iceberg data. They don't need to know anything about Snowflake or SQL underneath.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-GCP\u003C/strong\u003E: Google Cloud Console &rarr; search &quot;Gemini Enterprise&quot; &rarr; Data stores &rarr; +Create data store &rarr; Add MCP Server.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EFill in the fields using values from the MCP output above (server URL, client ID, client secret, scopes, etc.).\u003C/li\u003E\u003Cli\u003EComplete the OAuth authorization flow when prompted.\u003C/li\u003E\u003Cli\u003EClick \u003Cstrong\u003EActions\u003C/strong\u003E and then &quot;Reload Custom Actions&quot; and log in with your account.\u003C/li\u003E\u003Cli\u003ESelect the tool &quot;hol-economic-agent&quot; and then &quot;Enable Actions&quot; and confirm.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003ENow open Gemini Enterprise chat and ask the same question:\u003C/p\u003E\n","\u003Cp\u003E\u003Cem\u003E&quot;How has the 30-year mortgage rate changed relative to inflation since 2020?&quot;\u003C/em\u003E\u003C/p\u003E\n","\u003Cp\u003ESame question we asked in Snowflake CoWork, same correct answer &mdash; just a different surface.\u003C/p\u003E\n","\u003Ch3\u003ETroubleshooting\u003C/h3\u003E\n","\u003Cp\u003E\u003Cstrong\u003ENetwork Policy\u003C/strong\u003E &mdash; If Gemini can't reach Snowflake (OAuth errors, timeouts), a network policy may be blocking external IPs. Run the statement below to temporarily allow all connections.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003E-- Temporarily disable account network policy to allow Gemini Enterprise OAuth\nUSE ROLE ACCOUNTADMIN;\nALTER ACCOUNT UNSET NETWORK_POLICY;\n\n-- To re-enable later:\n-- ALTER ACCOUNT SET NETWORK_POLICY = &lt;your_policy_name&gt;;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003E\u003Cstrong\u003EGoogle Cloud Org Policy\u003C/strong\u003E &mdash; If you see \u003Ccode\u003Econstraints/discoveryengine.managed.disableCustomMcpServerConnector\u003C/code\u003E:\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-GCP\u003C/strong\u003E: IAM &amp; Admin &rarr; Organization Policies &rarr; search \u003Ccode\u003EdisableCustomMcpServerConnector\u003C/code\u003E &rarr; Enforcement: \u003Cstrong\u003EOff\u003C/strong\u003E &rarr; Save. Retry connector setup.\u003C/p\u003E\n","\u003Ch2\u003ELooker\u003C/h2\u003E\n","\u003Cp\u003EThe same Iceberg data that powers the AI agent also feeds traditional BI. Looker connects directly to the Snowflake table &mdash; no additional copies, no separate pipeline. One data product serves both governed dashboards and AI chat.\u003C/p\u003E\n","\u003Cp\u003E\u003Cstrong\u003EUI-Looker\u003C/strong\u003E: Admin &rarr; Database &rarr; Connections &rarr; add Snowflake connection.\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EUse lab credentials, point to \u003Ccode\u003EHOL_DB.PUBLIC\u003C/code\u003E.\u003C/li\u003E\u003Cli\u003ECreate LookML project on \u003Ccode\u003EECONOMIC_INDICATORS\u003C/code\u003E.\u003C/li\u003E\u003Cli\u003EBuild Explore + Dashboard.\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003EPlease follow \u003Ca href=\"https://docs.google.com/document/d/14DwWTrCz4YLreXNiYfJ3cI86MUNT44lj_yXlIq__pwg/edit?usp=sharing&amp;resourcekey=0-s31XT4gARcUOk4CX6ZYWvw\"\u003ELooker instructions\u003C/a\u003E\u003C/p\u003E\n","\u003Cp\u003EWe would like to:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003ELog in to Looker (given account, username, password)\u003C/li\u003E\u003Cli\u003ECreate a secure connection to your Snowflake account\u003C/li\u003E\u003Cli\u003ECreate a project and database and explore Looker\u003C/li\u003E\u003Cli\u003EGet familiar with LookML (which defines the semantic model of your data)\u003C/li\u003E\u003Cli\u003ETalk to your Snowflake data\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch2\u003EConclusion And Resources\u003C/h2\u003E\n","\u003Cp\u003ELet's step back and look at what we built.\u003C/p\u003E\n","\u003Cp\u003EOne copy of data on open Iceberg in your GCS bucket. A Semantic View that teaches AI what the data means. A Cortex Agent powered by Gemini that turns questions into governed SQL. And we consume it from Snowflake CoWork, Gemini Enterprise, Looker, and any MCP client &mdash; all pointing at the same source of truth.\u003C/p\u003E\n","\u003Cp\u003ENo data copies between systems. No custom integrations for each surface. No hallucination from ungrounded prompts. Build it once, consume it everywhere.\u003C/p\u003E\n","\u003Ch3\u003EWhat You Learned\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EHow to create Snowflake-managed Iceberg tables on your own GCS bucket\u003C/li\u003E\u003Cli\u003EHow to use Snowflake Marketplace for instant data access\u003C/li\u003E\u003Cli\u003EHow to define business logic in a Semantic View\u003C/li\u003E\u003Cli\u003EHow to build a Cortex Agent with Gemini as the reasoning model\u003C/li\u003E\u003Cli\u003EHow to expose an agent via MCP with OAuth security\u003C/li\u003E\u003Cli\u003EHow to connect Gemini Enterprise to Snowflake through MCP\u003C/li\u003E\u003Cli\u003EHow to connect Looker to the same Iceberg data for BI dashboards\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/cortex-agents\"\u003ESnowflake Cortex Agents Documentation\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/tables-iceberg\"\u003EApache Iceberg on Snowflake\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst/semantic-view\"\u003ESemantic Views\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-cortex/mcp-server\"\u003EMCP Servers in Snowflake\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb\"\u003ESource Notebook on GitHub\u003C/a\u003E\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003ECleanup\u003C/h3\u003E\n","\u003Cp\u003ERun only when you're done with the lab.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003EUSE ROLE ACCOUNTADMIN;\n\n-- Drop every object the workshop roles own BEFORE dropping the roles themselves.\n-- hol_role owns hol_gcs_vol, so dropping the roles first leaves the external\n-- volume orphaned and the cleanup does not run clean on a copy-paste.\n\n-- Drop database first (cascades all objects inside: tables, views, agents, MCP servers)\nDROP DATABASE IF EXISTS hol_db;\n\n-- The external volume can only be dropped after the Iceberg tables that reference it.\n-- Dropping it means redoing the GCS bucket IAM binding if you run the lab again.\nDROP EXTERNAL VOLUME IF EXISTS hol_gcs_vol;\n\nDROP WAREHOUSE IF EXISTS hol_wh;\nDROP INTEGRATION IF EXISTS hol_mcp_oauth;\n\n-- Roles last, once nothing they own is left\nDROP ROLE IF EXISTS hol_role;\nDROP ROLE IF EXISTS end_user_role;\n\n-- Re-enable network policy if it was disabled\n-- ALTER ACCOUNT SET NETWORK_POLICY = ACCOUNT_VPN_POLICY_SE;\n\n-- The Iceberg data/metadata files still sit in your GCS bucket.\n-- Delete them in Google Cloud Console (Cloud Storage -&gt; your bucket), or:\n--   gcloud storage rm --recursive gs://&lt;your-bucket&gt;/**\n\nSHOW ROLES LIKE '%HOL%';\n\u003C/code\u003E\u003C/pre\u003E"],"description":"","title":"Base Quickstart CF",":type":"snowflake-site/components/contentfragment",":items":{},":itemsOrder":[],"elements":{"quickstartArticleBody":{"dataType":"string","title":"Quickstart Article Body","value":"## Overview\n\nWe're going to build an AI agent that answers economic questions about the wellbeing of Americans — and make it available to anyone in the organization. The agent will live in Snowflake, but employees will talk to it from Gemini Enterprise, their everyday corporate AI assistant.\n\nWe start from raw public data. We land it in an [Apache Iceberg](https://iceberg.apache.org/) table on your own GCS bucket. We teach an AI model what the data means through a Semantic View. And we wrap it all in a Cortex Agent powered by Gemini.\n\nThe key idea: define your business logic once, in the data layer, not in prompts. That way every consumer — a chat interface, a BI dashboard, an external AI assistant — gets the same correct answer from the same governed data.\n\n\u003E **Tip:** This quickstart is also available as a [Snowflake Notebook](https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb) that you can run directly in Snowsight Workspaces. [More info](https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/README.md).\n\n![Architecture](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/quickstart-iceberg-cortex-gemini/arch-diagram.png)\n\n### What You Will Learn \n- How to create Snowflake-managed Iceberg tables on GCS\n- How to build a Semantic View that grounds AI on business logic\n- How to create a Cortex Agent powered by Gemini\n- How to expose the agent via MCP (Model Context Protocol)\n- How to connect Gemini Enterprise to Snowflake through MCP\n- How to connect Looker to the same Iceberg data for BI dashboards\n\n### What You Will Build\n- An Apache Iceberg table on your GCS bucket with US economic indicators\n- A Semantic View defining dimensions, facts, and metrics\n- A Cortex Agent accessible from Snowflake CoWork and Gemini Enterprise\n- An MCP server with OAuth for secure cross-platform access\n- A Looker dashboard connected to the same Iceberg data\n\n### Prerequisites\n- A [Snowflake account](https://signup.snowflake.com/) on GCP with `ACCOUNTADMIN` privileges\n- A [Google Cloud](https://console.cloud.google.com/) project with permissions to create GCS buckets\n- Gemini Enterprise enabled in your Google Workspace (for the MCP connection step)\n\n## Setup\n\nWe need three environments for this lab:\n\n\u003E Tip: Use Incognito mode or create a temporary Chrome profile (top right of Chrome window \u003E Profile icon \u003E Add \u003E Stay signed out \u003E name: \"workshop\") to manage all lab accounts. Qwiklabs and DataOps will provide URLs for your GCP and Snowflake accounts. Open all account URLs in this \"workshop\" profile or Incognito window.\n\nIf you are using official workshop that Qwiklabs provides your GCP account and DataOps provides your Snowflake account, follow the below video and instructions.\n\nFollow this [how-to setup video](https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/assets/how-to-setup-gcp-snowflake-workshop.mov) to set up your environments.\n\nIn your main Chrome profile:\n\n1. **Google Cloud** — We will create a GCS bucket for Iceberg storage and later use Gemini Enterprise to interact with our agent.\n    * Go to [Qwiklabs](https://explore.qwiklabs.com) for your GCP lab environment.\n    * Log in or sign up using the same email you used to register for the workshop.\n    * Choose the snowflake lab and click.\n    * Qwiklabs will provide a URL to open the Google Cloud Console along with temporary credentials.\n\n2. **Snowflake** — This is where we will build everything: Iceberg tables, Semantic Views, Cortex Agents, and the MCP server.\n    * Go to [DataOps](https://go.dataops.live) to sign in or register.\n    * DataOps will provide a URL to your Snowflake account along with a username and password.\n\n3. **Looker** — We will connect a BI dashboard to the same Iceberg data.\n    * Login information will be provided during the workshop.\n\nIn your Incognito window or temporary \"Workshop\" Chrome profile:\n\n* **GCP Environment:**\n    * Use the Qwiklabs URL to open your provisioned GCP console, which we will use to set up GCS buckets.\n    * You can also use `Cloud Shell` (located on the top right bar) as a terminal connected directly to your GCP account if scripting is needed.\n\n* **Snowflake Environment for running the lab:**\n    * Use the DataOps URL to open your provisioned Snowflake environment.\n    * Open **Workspaces** from the left panel.\n    * Open the shared workspace `gcp-snowflake-solutions`.\n    * Open the file `hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb` — the notebook version of this guide — if you prefer to run the lab as a notebook, cell by cell. Otherwise follow the steps in this guide from a SQL worksheet.\n\n* **Snowflake Environment for UI exploration:**\n    * Open a second Snowflake tab using the DataOps URL so you can explore the UI during the workshop.\n    * From the left panel, locate Cortex Agents, Analyst, Snowflake Marketplace, Database Explorer, Workspaces, dbt Projects, Streamlit, Openflow, and Dynamic Tables.\n    * Feel free to explore Snowflake before the workshop begins.\n\n## Workspace\n\nSnowflake Workspaces give you a full developer environment in the browser. It connects to a git repo so you can collaborate with a team, and runs Python and SQL files with a built-in compute engine.\n\nEverything in this guide is plain SQL plus UI steps, so you can run it straight from a SQL worksheet. If you would rather run it as a notebook — mixed SQL, Python, and markdown cells sharing one session context — open the [notebook version of this quickstart](https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb) in a Snowflake Workspace:\n\n**UI-Snowsight**: Go to Projects → Workspaces.\n- Create a public git integration and connect to this repo.\n    - Click **+** on the very top left \u003E Git Workspace\n    - Repo name: `https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions`\n    - Choose public repo connection (no auth needed).\n- Open `hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb`. Click \"Connected\" to start service. It takes a few minutes — start it now and read ahead while it spins up.\n\n**UI-Snowsight**: Open a second browser tab at the same Snowflake instance URL for exploring components. In this tab find Marketplace, Cortex Analyst, Agents, AI Functions, dbt Projects, Database Explorer, and Workspaces.\n\n## Role Based Access Control\n\nThroughout this lab we use two roles:\n\n- **`hol_role`** — this is us, the developer. It runs the lab and owns everything we create.\n- **`end_user_role`** — this simulates a business user who can only ask questions through the agent but can't build or modify anything.\n\nLet's create those roles and grant the needed privileges.\n\n```sql\nUSE ROLE ACCOUNTADMIN;\n\n-- Create a warehouse for this lab (guaranteed to exist)\nCREATE WAREHOUSE IF NOT EXISTS hol_wh\n  WAREHOUSE_SIZE = 'XSMALL' AUTO_SUSPEND = 60 INITIALLY_SUSPENDED = TRUE;\nUSE WAREHOUSE hol_wh;\n\n-- Builder role: owns all workshop objects\nCREATE ROLE IF NOT EXISTS hol_role;\n\n-- Consumer role: can only use the agent (CoWork, Gemini Enterprise)\nCREATE ROLE IF NOT EXISTS end_user_role;\n\n-- Attach both roles to the role hierarchy.\n-- hol_role OWNS everything we build, so without this an admin has no path to\n-- those objects once the workshop user goes away -- and cleanup becomes impossible.\n-- ACCOUNTADMIN inherits SYSADMIN, so it can always do whatever hol_role can do.\nGRANT ROLE hol_role      TO ROLE SYSADMIN;\nGRANT ROLE end_user_role TO ROLE SYSADMIN;\n\n-- Grant both roles to whoever is running this lab\nBEGIN\n  LET usr := CURRENT_USER();\n  EXECUTE IMMEDIATE 'GRANT ROLE hol_role TO USER ' || :usr;\n  EXECUTE IMMEDIATE 'GRANT ROLE end_user_role TO USER ' || :usr;\n  -- MCP OAuth sessions (e.g., Gemini Enterprise) fail to initialize if the connecting user's DEFAULT_WAREHOUSE is null.\n  EXECUTE IMMEDIATE 'ALTER USER ' || :usr || ' SET DEFAULT_WAREHOUSE = ''hol_wh''';\nEND;\n\n-- Builder privileges\nGRANT CREATE DATABASE        ON ACCOUNT TO ROLE hol_role;\nGRANT CREATE WAREHOUSE       ON ACCOUNT TO ROLE hol_role;\nGRANT CREATE INTEGRATION     ON ACCOUNT TO ROLE hol_role;\nGRANT CREATE EXTERNAL VOLUME ON ACCOUNT TO ROLE hol_role;\nGRANT OWNERSHIP ON WAREHOUSE hol_wh TO ROLE hol_role COPY CURRENT GRANTS;\n\n-- Cortex access for both roles\nGRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE hol_role;\nGRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE end_user_role;\n\n-- Switch to hol_role to build\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\n\nCREATE DATABASE IF NOT EXISTS hol_db;\nUSE SCHEMA hol_db.public;\n\n-- Schema-level privileges (must come after database/schema exist)\nUSE ROLE ACCOUNTADMIN;\nGRANT CREATE SEMANTIC VIEW ON SCHEMA hol_db.public TO ROLE hol_role;\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\nUSE SCHEMA hol_db.public;\n\n-- Grant consumer role usage on warehouse and database\nGRANT USAGE ON WAREHOUSE hol_wh TO ROLE end_user_role;\nGRANT USAGE ON DATABASE hol_db TO ROLE end_user_role;\nGRANT USAGE ON SCHEMA hol_db.public TO ROLE end_user_role;\n\n-- Verify context\nSELECT CURRENT_ROLE() AS role, CURRENT_WAREHOUSE() AS wh, CURRENT_DATABASE() AS db, CURRENT_SCHEMA() AS schema;\n```\n\n## Architecture\n\n[Apache Iceberg](https://iceberg.apache.org/) is the cornerstone of a modern data platform. It allows multiple engines to read and write directly to the same data, while that data stays in one place — your cloud storage. No copies between systems, no vendor lock-in.\n\nWe'll get our data sources from Snowflake Marketplace and land them in an Iceberg table on a GCS bucket. Snowflake Horizon serves as the catalog and governance layer. We could just as easily use Google Cloud Open Lakehouse Runtime or any other IRC-compliant catalog instead.\n\nNext, we use Snowflake Semantic View Autopilot to create a Semantic View that defines the business logic of our Iceberg table. We wrap this in a Cortex Agent and use Gemini as the reasoning model behind it. The biggest gain from Snowflake Cortex is the added context and logic that makes answers accurate and thorough — customers love it for reduced hallucination.\n\nThen we build an MCP connection between Gemini Enterprise and our Cortex Agent, so employees can talk to their data through their corporate AI chat. Looker helps us visualize and get insights from the same data.\n\n![Architecture](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/quickstart-iceberg-cortex-gemini/arch-diagram.png)\n\nHere's a summary of what each component does:\n\n- **Snowflake Marketplace** — instant access to curated, live datasets. No ETL, no ingestion pipelines.\n- **Iceberg** — open table format on your GCS bucket. Multiple engines read the same files. You own the data.\n- **Snowflake Horizon** — catalog and governance layer for Iceberg tables. Access control, lineage, and discoverability.\n- **Semantic View** — business logic defined once in the data layer. Grounds the AI so it doesn't guess.\n- **Cortex Agent** — natural-language interface that turns questions into governed SQL and returns correct answers.\n- **Gemini** — the reasoning model powering our agent. Large context window, native to Google Cloud.\n- **MCP** — open protocol to expose the agent. Connect once, access from any MCP-compatible client.\n- **Snowflake CoWork** — chat interface inside Snowflake for business users who don't write SQL.\n- **Gemini Enterprise** — Google Cloud's corporate AI assistant. Employees ask questions in a familiar interface and get grounded answers from Iceberg data via MCP.\n\n## Marketplace\n\nWe get our source data from Snowflake Marketplace. It lets teams access curated, live datasets instantly — you click \"Get\" and the data appears in your account. No ETL pipelines, no data copying. For data providers, it's a secure channel to share or sell data to the world.\n\nWe want to build an economic dataset that tracks the financial wellbeing of Americans at the state level. We need income, inflation, mortgage rates, and unemployment — all on a monthly basis. That means four source tables from the Bureau of Labor Statistics and related public data.\n\nLet's go get them.\n\n**UI-Snowsight**: Marketplace → Snowflake Marketplace → Data products → search \"Snowflake Public Data\" → Click on Snowflake Public Data (Free) → **Get**.\n- Database name: `SNOWFLAKE_PUBLIC_DATA_FREE` (accept default options and don't change them).\n- Direct link: https://app.snowflake.com/marketplace/listing/GZTSZ290BV255/snowflake-public-data-products-snowflake-public-data-free\n\nThis dataset is already available on many Snowflake accounts. The query below verifies access to all four source tables we need.\n\n```sql\n-- Verify marketplace data access\nSELECT 'BLS_PRICE' AS source, COUNT(*) AS row_count FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_PRICE_TIMESERIES\nUNION ALL\nSELECT 'BLS_EMPLOYMENT', COUNT(*) FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_EMPLOYMENT_TIMESERIES\nUNION ALL\nSELECT 'FREDDIE_MAC', COUNT(*) FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.FREDDIE_MAC_HOUSING_TIMESERIES\nUNION ALL\nSELECT 'IRS_INCOME', COUNT(*) FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.IRS_INDIVIDUAL_INCOME_TIMESERIES;\n```\n\n## Iceberg\n\nNow we need somewhere to land this data.\n\nIceberg is an open table format. Parquet data files and metadata sit in your own GCS bucket — you own them. Any engine that speaks Iceberg can read them directly: Snowflake, BigQuery, Managed Spark, or any Iceberg REST Catalog–compliant runtime. No copying between systems.\n\nWe use `catalog=snowflake`, which means Snowflake manages the table through Snowflake Horizon, handling governance, access control, and discoverability. But the actual data never leaves your bucket.\n\nLet's create the bucket in Google Cloud Console, give Snowflake write access, and build our economic indicators table.\n\n**UI-GCP**: Google Cloud Console → Cloud Storage → **Create Bucket**.\n- Name: `firstname_lastname_hol_0729`\n- Location: `Multi-region`\n- Leave everything else as default.\n\n```sql\n-- Create an external volume pointing to your GCS bucket\nCREATE OR REPLACE EXTERNAL VOLUME hol_gcs_vol\n  STORAGE_LOCATIONS = ((\n    NAME = 'hol-gcs'\n    STORAGE_PROVIDER = 'GCS'\n    STORAGE_BASE_URL = 'gcs://\u003Cyour-bucket-name\u003E/iceberg/'\n  ));\n\n-- Describe to get storage config\nDESCRIBE EXTERNAL VOLUME hol_gcs_vol;\nSET desc_qid = LAST_QUERY_ID();\n\n-- Extract the GCS service account to grant on the bucket\nSELECT\n  PARSE_JSON(\"property_value\"):STORAGE_GCP_SERVICE_ACCOUNT::STRING\n    AS gcs_service_account_to_grant\nFROM TABLE(RESULT_SCAN($desc_qid))\nWHERE \"property\" = 'STORAGE_LOCATION_1';\n```\n\nCopy the service account printed above (Snowflake will use it to access your bucket).\n\n**UI-GCP**: Google Cloud Console → your bucket → **Permissions** tab → **Grant Access**.\n- Paste the service account you copied.\n- Role: **Storage Admin** → Save.\n\nWe have created the bucket for Iceberg data and metadata files, and granted read/write access to Snowflake's service account.\n\n```sql\n-- Create Iceberg table: join four marketplace sources into one wide-format table\nCREATE OR REPLACE ICEBERG TABLE hol_db.public.economic_indicators\n  CATALOG = 'SNOWFLAKE'\n  EXTERNAL_VOLUME = 'hol_gcs_vol'\n  BASE_LOCATION = 'economic_indicators'\n  AS\nWITH cpi AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG(value) AS cpi_index\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_PRICE_TIMESERIES\n  WHERE variable_name = 'CPI: All items, Not seasonally adjusted, Monthly'\n    AND geo_id = 'country/USA'\n  GROUP BY 1\n),\nmortgage_30yr AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    ROUND(AVG(value) * 100, 2) AS mortgage_rate_30yr_pct\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.FREDDIE_MAC_HOUSING_TIMESERIES\n  WHERE variable_name = '30-Year Fixed Rate Mortgage Rate, National Average'\n    AND geo_id = 'country/USA'\n  GROUP BY 1\n),\nmortgage_15yr AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    ROUND(AVG(value) * 100, 2) AS mortgage_rate_15yr_pct\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.FREDDIE_MAC_HOUSING_TIMESERIES\n  WHERE variable_name = '15-Year Fixed Rate Mortgage Rate, National Average'\n    AND geo_id = 'country/USA'\n  GROUP BY 1\n),\nunemployment AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    geo_id,\n    AVG(value) AS unemployment_rate_pct\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.BUREAU_OF_LABOR_STATISTICS_EMPLOYMENT_TIMESERIES\n  WHERE variable_name = 'Local Area Unemployment: Unemployment Rate, Not seasonally adjusted, Monthly'\n    AND LENGTH(geo_id) = 8\n  GROUP BY 1, 2\n),\nnational_unemployment AS (\n  SELECT month, ROUND(AVG(unemployment_rate_pct), 2) AS unemployment_rate_pct\n  FROM unemployment\n  GROUP BY 1\n),\nincome_raw AS (\n  SELECT\n    agi.geo_id,\n    YEAR(agi.date) AS yr,\n    ROUND(agi.value / NULLIF(ret.value, 0), 0) AS avg_income_per_return\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.IRS_INDIVIDUAL_INCOME_TIMESERIES agi\n  JOIN SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.IRS_INDIVIDUAL_INCOME_TIMESERIES ret\n    ON agi.geo_id = ret.geo_id AND agi.date = ret.date\n  WHERE agi.variable_name = 'Adjusted gross income (AGI), AGI bin: Total'\n    AND ret.variable_name = 'Number of returns, AGI bin: Total'\n    AND LENGTH(agi.geo_id) = 8\n),\nincome_indexed AS (\n  SELECT\n    geo_id,\n    yr,\n    avg_income_per_return,\n    ROUND((avg_income_per_return / FIRST_VALUE(avg_income_per_return) OVER (PARTITION BY geo_id ORDER BY yr)) * 100, 1) AS income_index\n  FROM income_raw\n),\nnational_income AS (\n  SELECT yr, ROUND(AVG(income_index), 1) AS income_index\n  FROM income_indexed\n  GROUP BY 1\n),\ngeo AS (\n  SELECT geo_id, geo_name\n  FROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.GEOGRAPHY_INDEX\n  WHERE level = 'State'\n),\nnational AS (\n  SELECT\n    c.month AS date,\n    'country/USA' AS geo_id,\n    'United States' AS geo_name,\n    ROUND(c.cpi_index, 2) AS cpi_index,\n    ROUND(((c.cpi_index - LAG(c.cpi_index, 12) OVER (ORDER BY c.month))\n      / NULLIF(LAG(c.cpi_index, 12) OVER (ORDER BY c.month), 0)) * 100, 2) AS inflation_pct,\n    m30.mortgage_rate_30yr_pct,\n    m15.mortgage_rate_15yr_pct,\n    nu.unemployment_rate_pct,\n    ni.income_index\n  FROM cpi c\n  LEFT JOIN mortgage_30yr m30 ON c.month = m30.month\n  LEFT JOIN mortgage_15yr m15 ON c.month = m15.month\n  LEFT JOIN national_unemployment nu ON c.month = nu.month\n  LEFT JOIN national_income ni ON YEAR(c.month) = ni.yr\n),\nstates AS (\n  SELECT\n    u.month AS date,\n    u.geo_id,\n    g.geo_name,\n    NULL::FLOAT AS cpi_index,\n    NULL::FLOAT AS inflation_pct,\n    NULL::FLOAT AS mortgage_rate_30yr_pct,\n    NULL::FLOAT AS mortgage_rate_15yr_pct,\n    u.unemployment_rate_pct,\n    ii.income_index\n  FROM unemployment u\n  JOIN geo g ON u.geo_id = g.geo_id\n  LEFT JOIN income_indexed ii ON u.geo_id = ii.geo_id AND YEAR(u.month) = ii.yr\n)\nSELECT * FROM national\nUNION ALL\nSELECT * FROM states\nORDER BY date, geo_id;\n```\n\n### Explore Iceberg Data and Metadata\n\n**UI-GCP**: Google Cloud Console → your bucket → explore the files.\n- You'll see Parquet data files and a metadata folder with JSON files.\n\nLook at the Iceberg table definition above and identify the catalog we used (`CATALOG = 'SNOWFLAKE'`). All data and metadata is in your own bucket — not owned by Snowflake, BigQuery, or any other vendor. Every engine reads and writes directly while a catalog (here Snowflake Horizon) provides governance and security.\n\nThis is the cornerstone of a modern data platform: one source of truth in a datalake with high data gravity. All services are drawn to this data to perform their tasks.\n\n## Data Profiling\n\nWe have our Iceberg table. Let's look at what's inside.\n\nSnowsight gives you profiling, charting, and pivot tables right in the query results. You can understand the shape of a dataset without leaving the browser.\n\n**UI-Snowsight**: After running the query, explore the results pane:\n- Click **Chart** tab to visualize trends over time.\n- Click **Query Profile** tab to see the execution plan.\n- Click column headers for quick profiling stats (min, max, distribution).\n\n```sql\n-- National economic indicators since 2015\n-- Try: Chart (line) to visualize trends, Query Profile to see execution plan\nSELECT\n  date,\n  cpi_index,\n  income_index,\n  inflation_pct,\n  mortgage_rate_30yr_pct,\n  unemployment_rate_pct\nFROM hol_db.public.economic_indicators\nWHERE geo_id = 'country/USA'\n  AND date \u003E= '2015-01-01'\n  AND inflation_pct IS NOT NULL\nORDER BY date;\n```\n\n## Cortex\n\nWe have a clean Iceberg table. Any analyst can query it with SQL. But that doesn't make it AI-ready.\n\nHere's the gap: when an LLM sees column names like `CPI_INDEX` or `GEO_ID`, it guesses what they mean. It guesses wrong. We need to tell it which columns are dimensions, which are facts, how metrics are calculated, and what kinds of questions this table can answer.\n\nThat's what a Semantic View does. You define your business logic once — in the data layer, not scattered across prompts — and every AI consumer inherits the same correct definitions.\n\n### Semantic View\n\nThe Semantic View is the grounding layer for our agent. We define dimensions (date, geography), facts (CPI, mortgage rate, unemployment, income), and metrics (year-over-year inflation, average mortgage rate by state).\n\nWe can also add verified queries — known-good question-to-SQL pairs that anchor the model's behavior for common questions. Without this layer, an LLM guesses and hallucinates. With it, a question like \"How has inflation compared to income growth?\" maps to the exact right SQL every time.\n\n**UI-Snowsight**: AI & ML → Cortex Analyst → **Create Semantic View**.\n- Select table: `HOL_DB.PUBLIC.ECONOMIC_INDICATORS`.\n- Click **Autopilot** to auto-generate dimensions, facts, and metrics from the table schema.\n- Review the generated YAML — check that dimensions (DATE, GEO_ID, GEO_NAME), facts (CPI_INDEX, INFLATION_PCT, etc.), and metrics are correct.\n- Add or edit descriptions to clarify business meaning.\n- Optionally add verified queries (known-good question → SQL pairs).\n- Save as `economic_semantic_view`.\n\nIn this lab we create it via code (YAML below), but Autopilot is the fastest way to get started.\n\n```sql\nCALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML(\n  'hol_db.public',\n  $$\nname: economic_semantic_view\ntables:\n  - name: economic_indicators\n    base_table:\n      database: HOL_DB\n      schema: PUBLIC\n      table: ECONOMIC_INDICATORS\n    dimensions:\n      - name: DATE\n        description: \"Date of the observation\"\n        expr: economic_indicators.DATE\n        data_type: DATE\n      - name: GEO_ID\n        description: \"Geographic area identifier\"\n        expr: economic_indicators.GEO_ID\n        data_type: TEXT\n      - name: GEO_NAME\n        description: \"Geographic area — United States for national, or state name (e.g. California)\"\n        expr: economic_indicators.GEO_NAME\n        data_type: TEXT\n    facts:\n      - name: CPI_INDEX\n        description: \"Consumer Price Index, base period 1982-84 = 100 (national only)\"\n        expr: economic_indicators.CPI_INDEX\n        data_type: NUMBER\n      - name: INFLATION_PCT\n        description: \"Year-over-year inflation rate as percent (national only)\"\n        expr: economic_indicators.INFLATION_PCT\n        data_type: NUMBER\n      - name: MORTGAGE_RATE_30YR_PCT\n        description: \"30-year fixed mortgage rate, national average, percent (national only)\"\n        expr: economic_indicators.MORTGAGE_RATE_30YR_PCT\n        data_type: NUMBER\n      - name: MORTGAGE_RATE_15YR_PCT\n        description: \"15-year fixed mortgage rate, national average, percent (national only)\"\n        expr: economic_indicators.MORTGAGE_RATE_15YR_PCT\n        data_type: NUMBER\n      - name: UNEMPLOYMENT_RATE_PCT\n        description: \"Unemployment rate as percent (available national and by state)\"\n        expr: economic_indicators.UNEMPLOYMENT_RATE_PCT\n        data_type: NUMBER\n      - name: INCOME_INDEX\n        description: \"Average income per tax return, indexed to earliest available year = 100. Compare to CPI_INDEX to assess purchasing power. (available national and by state, annual grain)\"\n        expr: economic_indicators.INCOME_INDEX\n        data_type: NUMBER\n    metrics:\n      - name: AVG_CPI_INDEX\n        description: \"Average Consumer Price Index\"\n        expr: AVG(economic_indicators.CPI_INDEX)\n      - name: AVG_INFLATION_PCT\n        description: \"Average year-over-year inflation rate\"\n        expr: AVG(economic_indicators.INFLATION_PCT)\n      - name: AVG_MORTGAGE_RATE_30YR\n        description: \"Average 30-year fixed mortgage rate\"\n        expr: AVG(economic_indicators.MORTGAGE_RATE_30YR_PCT)\n      - name: AVG_UNEMPLOYMENT_RATE\n        description: \"Average unemployment rate\"\n        expr: AVG(economic_indicators.UNEMPLOYMENT_RATE_PCT)\n      - name: AVG_INCOME_INDEX\n        description: \"Average income index\"\n        expr: AVG(economic_indicators.INCOME_INDEX)\n$$\n);\n\n-- Verify\nSHOW SEMANTIC VIEWS IN SCHEMA hol_db.public;\n```\n\n### Cortex Agent\n\nNow we wrap the Semantic View in a conversational interface.\n\nA Cortex Agent takes a natural-language question and passes it to Cortex Analyst. Cortex Analyst uses the Semantic View to generate correct SQL, executes it, and returns a grounded answer with supporting data. We use Gemini as the reasoning model behind the agent.\n\nThe whole thing is defined in a single SQL statement — reproducible and version-controlled.\n\n**UI-Snowsight**: AI & ML → Cortex Agents → **+ Create Agent**.\n- Name: `hol_economic_agent`\n- Model: change to **Gemini** (default may be different).\n- Tools: add Cortex Analyst tool → select `economic_semantic_view`.\n- Instructions: describe what the agent does, e.g. \"Answer questions about US economic indicators including inflation, mortgage rates, unemployment by state, and income trends.\"\n- Warehouse: `hol_wh` (for query execution).\n- Test the agent in the preview pane before saving.\n\nIn this lab we create it via SQL for reproducibility.\n\n```sql\n-- Create a Cortex Agent backed by the economic indicators semantic view\nCREATE OR REPLACE AGENT hol_db.public.hol_economic_agent\n  FROM SPECIFICATION $$\n  tools:\n    - tool_spec:\n        type: cortex_analyst_text_to_sql\n        name: economic_analyst\n        description: \"Answers questions about US economic indicators: CPI/inflation, mortgage interest rates (30-year, 15-year), unemployment rate (national and by state), and income index.\"\n\n  tool_resources:\n    economic_analyst:\n      semantic_view: HOL_DB.PUBLIC.ECONOMIC_SEMANTIC_VIEW\n      execution_environment:\n        type: warehouse\n        warehouse: HOL_WH\n  $$;\n\n-- Grant consumer role usage on the agent\nGRANT USAGE ON AGENT hol_db.public.hol_economic_agent TO ROLE end_user_role;\nGRANT SELECT ON SEMANTIC VIEW hol_db.public.economic_semantic_view TO ROLE end_user_role;\nGRANT SELECT ON TABLE hol_db.public.economic_indicators TO ROLE end_user_role;\n\n-- Verify\nSHOW AGENTS IN SCHEMA hol_db.public;\n```\n\n### CoWork\n\nSnowflake CoWork is the chat surface for business users. No SQL knowledge needed, no SQL worksheet — just a conversation with the agent.\n\nLet's switch to `end_user_role` to see what it looks like for someone who can only consume, not build.\n\n**UI-Snowsight**: AI & ML → Open Snowflake CoWork.\n- In CoWork, go to bottom left profile, click setting, and switch role to `end_user_role`, warehouse: `hol_wh`. Done.\n- You should be able to see **hol_economic_agent** in the agent list (control buttons of the CoWork chat).\n- Ask: *\"How has the 30-year mortgage rate changed relative to inflation since 2020?\"*\n- Ask: *\"Tell the economic story of California vs Texas over the last 10 years using all available indicators.\"*\n- Ask: *\"What did the COVID shock look like in data — unemployment spike, rate crash, inflation surge — and how long did each phase last?\"*\n\nLook at the responses — they include the generated SQL so you can see exactly what queries were executed. Same agent, same data, different role — a chat-based surface instead of SQL.\n\n## MCP\n\nSo far our agent lives inside Snowflake. But what if employees want to ask it questions from Gemini Enterprise, or from another AI tool?\n\nThat's where MCP comes in. Model Context Protocol is an open standard that gives AI applications a universal way to connect to data tools. We declare our agent as an MCP tool and add OAuth for secure access. Any MCP-compatible client can then connect — no custom connector per client.\n\n### MCP Server\n\nLet's create the actual MCP server. We register our Cortex Agent as a callable tool inside a Snowflake-managed MCP server, then set up an OAuth security integration so external clients can authenticate securely. The output from this step gives us the credentials we'll register in Gemini Enterprise next.\n\n```sql\n-- MCP server exposing the Cortex Agent as a tool (requires schema ownership → hol_role)\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\nUSE SCHEMA hol_db.public;\n\nCREATE OR REPLACE MCP SERVER hol_db.public.hol_mcp\n  FROM SPECIFICATION $$\n  tools:\n    - name: \"hol-economic-agent\"\n      type: \"CORTEX_AGENT_RUN\"\n      identifier: \"HOL_DB.PUBLIC.HOL_ECONOMIC_AGENT\"\n      description: \"US economic indicators agent — answers questions about inflation (CPI), mortgage rates, unemployment, and income.\"\n      title: \"Economic Indicators Agent\"\n  $$;\n\n-- Grant MCP server usage to the consumer role (required for Gemini to discover tools)\n-- ALLOWED_ROLES_LIST must include the role used in the OAuth scope, otherwise\n-- Snowflake rejects it with \"invalid role\" even if the user has the role granted.\nUSE ROLE ACCOUNTADMIN;\n\nGRANT USAGE ON MCP SERVER hol_db.public.hol_mcp TO ROLE end_user_role;\n\nCREATE OR REPLACE SECURITY INTEGRATION hol_mcp_oauth\n  TYPE = OAUTH\n  OAUTH_CLIENT = CUSTOM\n  OAUTH_CLIENT_TYPE = 'CONFIDENTIAL'\n  OAUTH_REDIRECT_URI = 'https://vertexaisearch.cloud.google.com/oauth-redirect'\n  ALLOWED_ROLES_LIST = ('END_USER_ROLE')\n  ENABLED = TRUE;\n\n-- Get MCP server metadata (database, schema, name)\nDESCRIBE MCP SERVER hol_db.public.hol_mcp;\nSET mcp_qid = LAST_QUERY_ID();\n\n-- Get OAuth integration metadata (auth URL, token URL, client ID)\nDESCRIBE SECURITY INTEGRATION hol_mcp_oauth;\nSET int_qid = LAST_QUERY_ID();\n\nUSE ROLE hol_role;\nUSE WAREHOUSE hol_wh;\nUSE SCHEMA hol_db.public;\n\n-- Retrieve all credentials for Gemini Enterprise MCP connection\n-- Values derived from DESCRIBE metadata — nothing hardcoded\nWITH mcp_meta AS (\n  SELECT \"database_name\", \"schema_name\", \"name\"\n  FROM TABLE(RESULT_SCAN($mcp_qid))\n),\noauth_meta AS (\n  SELECT\n    MAX(CASE WHEN \"property\" = 'OAUTH_AUTHORIZATION_ENDPOINT' THEN \"property_value\" END) AS auth_endpoint,\n    MAX(CASE WHEN \"property\" = 'OAUTH_TOKEN_ENDPOINT' THEN \"property_value\" END) AS token_endpoint,\n    MAX(CASE WHEN \"property\" = 'OAUTH_CLIENT_ID' THEN \"property_value\" END) AS client_id\n  FROM TABLE(RESULT_SCAN($int_qid))\n),\nsecrets AS (\n  SELECT PARSE_JSON(SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('HOL_MCP_OAUTH')) AS s\n),\naccount_base AS (\n  -- Org-account URL format (required by Gemini Enterprise)\n  SELECT 'https://' || CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME()\n         || '.snowflakecomputing.com' AS url\n)\nSELECT field_name, value\nFROM (\n  SELECT 1 AS ord, 'MCP Server URL' AS field_name,\n    ab.url || '/api/v2/databases/' || m.\"database_name\" || '/schemas/' || m.\"schema_name\" || '/mcp-servers/' || m.\"name\" AS value\n    FROM account_base ab, mcp_meta m\n  UNION ALL\n  SELECT 2, 'Auth URL', o.auth_endpoint FROM oauth_meta o\n  UNION ALL\n  SELECT 3, 'Auth URL Params', '' FROM oauth_meta o\n  UNION ALL\n  SELECT 4, 'Token URL', o.token_endpoint FROM oauth_meta o\n  UNION ALL\n  SELECT 5, 'Client ID', o.client_id FROM oauth_meta o\n  UNION ALL\n  SELECT 6, 'Client Secret', s.s:OAUTH_CLIENT_SECRET::STRING FROM secrets s\n  UNION ALL\n  SELECT 7, 'Scopes', 'session:role:end_user_role' FROM secrets s\n  UNION ALL\n  SELECT 8, 'MCP Server Description', 'Snowflake Cortex Agent for US economic indicators (CPI, mortgage rates, unemployment, income)' FROM secrets s\n  UNION ALL\n  SELECT 9, 'Agent Instructions', 'Use the hol-economic-agent tool to answer questions about US economic data including inflation, mortgage rates, unemployment by state, and income trends.' FROM secrets s\n  UNION ALL\n  SELECT 10, 'Data Connector Name', 'hol_cortex_gemini_economic_agent' FROM secrets s\n)\nORDER BY ord;\n```\n\n## Gemini Enterprise\n\nGemini Enterprise is Google Cloud's corporate AI assistant — the chat interface employees across the organization already use daily.\n\nBy registering our Snowflake MCP server as a data connector, the Cortex Agent becomes a tool that Gemini calls when it needs economic data. Employees ask questions in Gemini and get grounded answers from governed Iceberg data. They don't need to know anything about Snowflake or SQL underneath.\n\n**UI-GCP**: Google Cloud Console → search \"Gemini Enterprise\" → Data stores → +Create data store → Add MCP Server.\n- Fill in the fields using values from the MCP output above (server URL, client ID, client secret, scopes, etc.).\n- Complete the OAuth authorization flow when prompted.\n- Click **Actions** and then \"Reload Custom Actions\" and log in with your account.\n- Select the tool \"hol-economic-agent\" and then \"Enable Actions\" and confirm.\n\nNow open Gemini Enterprise chat and ask the same question:\n\n*\"How has the 30-year mortgage rate changed relative to inflation since 2020?\"*\n\nSame question we asked in Snowflake CoWork, same correct answer — just a different surface.\n\n### Troubleshooting\n\n**Network Policy** — If Gemini can't reach Snowflake (OAuth errors, timeouts), a network policy may be blocking external IPs. Run the statement below to temporarily allow all connections.\n\n```sql\n-- Temporarily disable account network policy to allow Gemini Enterprise OAuth\nUSE ROLE ACCOUNTADMIN;\nALTER ACCOUNT UNSET NETWORK_POLICY;\n\n-- To re-enable later:\n-- ALTER ACCOUNT SET NETWORK_POLICY = \u003Cyour_policy_name\u003E;\n```\n\n**Google Cloud Org Policy** — If you see `constraints/discoveryengine.managed.disableCustomMcpServerConnector`:\n\n**UI-GCP**: IAM & Admin → Organization Policies → search `disableCustomMcpServerConnector` → Enforcement: **Off** → Save. Retry connector setup.\n\n## Looker\n\nThe same Iceberg data that powers the AI agent also feeds traditional BI. Looker connects directly to the Snowflake table — no additional copies, no separate pipeline. One data product serves both governed dashboards and AI chat.\n\n**UI-Looker**: Admin → Database → Connections → add Snowflake connection.\n- Use lab credentials, point to `HOL_DB.PUBLIC`.\n- Create LookML project on `ECONOMIC_INDICATORS`.\n- Build Explore + Dashboard.\n\nPlease follow [Looker instructions](https://docs.google.com/document/d/14DwWTrCz4YLreXNiYfJ3cI86MUNT44lj_yXlIq__pwg/edit?usp=sharing&resourcekey=0-s31XT4gARcUOk4CX6ZYWvw)\n\nWe would like to:\n- Log in to Looker (given account, username, password)\n- Create a secure connection to your Snowflake account\n- Create a project and database and explore Looker\n- Get familiar with LookML (which defines the semantic model of your data)\n- Talk to your Snowflake data\n\n## Conclusion And Resources\n\nLet's step back and look at what we built.\n\nOne copy of data on open Iceberg in your GCS bucket. A Semantic View that teaches AI what the data means. A Cortex Agent powered by Gemini that turns questions into governed SQL. And we consume it from Snowflake CoWork, Gemini Enterprise, Looker, and any MCP client — all pointing at the same source of truth.\n\nNo data copies between systems. No custom integrations for each surface. No hallucination from ungrounded prompts. Build it once, consume it everywhere.\n\n### What You Learned\n- How to create Snowflake-managed Iceberg tables on your own GCS bucket\n- How to use Snowflake Marketplace for instant data access\n- How to define business logic in a Semantic View\n- How to build a Cortex Agent with Gemini as the reasoning model\n- How to expose an agent via MCP with OAuth security\n- How to connect Gemini Enterprise to Snowflake through MCP\n- How to connect Looker to the same Iceberg data for BI dashboards\n\n### Related Resources\n- [Snowflake Cortex Agents Documentation](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents)\n- [Apache Iceberg on Snowflake](https://docs.snowflake.com/en/user-guide/tables-iceberg)\n- [Semantic Views](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst/semantic-view)\n- [MCP Servers in Snowflake](https://docs.snowflake.com/en/user-guide/snowflake-cortex/mcp-server)\n- [Source Notebook on GitHub](https://github.com/sfc-gh-akhosro/gcp-snowflake-solutions/blob/main/hands-on-lab-cortex-gemini/hol-cortex-gemini.ipynb)\n\n### Cleanup\n\nRun only when you're done with the lab.\n\n```sql\nUSE ROLE ACCOUNTADMIN;\n\n-- Drop every object the workshop roles own BEFORE dropping the roles themselves.\n-- hol_role owns hol_gcs_vol, so dropping the roles first leaves the external\n-- volume orphaned and the cleanup does not run clean on a copy-paste.\n\n-- Drop database first (cascades all objects inside: tables, views, agents, MCP servers)\nDROP DATABASE IF EXISTS hol_db;\n\n-- The external volume can only be dropped after the Iceberg tables that reference it.\n-- Dropping it means redoing the GCS bucket IAM binding if you run the lab again.\nDROP EXTERNAL VOLUME IF EXISTS hol_gcs_vol;\n\nDROP WAREHOUSE IF EXISTS hol_wh;\nDROP INTEGRATION IF EXISTS hol_mcp_oauth;\n\n-- Roles last, once nothing they own is left\nDROP ROLE IF EXISTS hol_role;\nDROP ROLE IF EXISTS end_user_role;\n\n-- Re-enable network policy if it was disabled\n-- ALTER ACCOUNT SET NETWORK_POLICY = ACCOUNT_VPN_POLICY_SE;\n\n-- The Iceberg data/metadata files still sit in your GCS bucket.\n-- Delete them in Google Cloud Console (Cloud Storage -\u003E your bucket), or:\n--   gcloud storage rm --recursive gs://\u003Cyour-bucket\u003E/**\n\nSHOW ROLES LIKE '%HOL%';\n```","multiValue":false,":type":"text/x-markdown"},"quickstartArticleLogoImage":{"dataType":"string","title":"Quickstart Article Logo Image","multiValue":false,":type":"text/plain"}},"elementsOrder":["quickstartArticleBody","quickstartArticleLogoImage"],"isDeveloperGuidesPage":false,"model":"snowflake-site/models/quickstart-article"},"flexible_column_cont":{"id":"flexible-column-container-271349f231","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-4d92e52eba",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"quickstart_last_modi":{"id":"quickstart-last-modified-80d419a78b","icon":{"id":"icon","icon":"calendar",":type":"snowflake-site/components/icon","appliedCssClassNames":"snowflake-icon-blue"},"lastModifiedDatePrefix":"Updated","lastModifiedDate":"2026-07-28",":type":"snowflake-site/components/quickstart/quickstart-last-modified","appliedCssClassNames":"snowflake-responsive-component-top-padding-small"},"text":{"id":"text-fd07d9863c","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-6418997b83",":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-492efcf28d",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"quickstart_table_of_":{"layout":"SIMPLE","id":"container-c3062fe534","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-09f99b2b3f","headings":["\u003Ch2\u003EOverview\u003C/h2\u003E","\u003Ch2\u003ESetup\u003C/h2\u003E","\u003Ch2\u003EWorkspace\u003C/h2\u003E","\u003Ch2\u003ERole Based Access Control\u003C/h2\u003E","\u003Ch2\u003EArchitecture\u003C/h2\u003E","\u003Ch2\u003EMarketplace\u003C/h2\u003E","\u003Ch2\u003EIceberg\u003C/h2\u003E","\u003Ch2\u003EData Profiling\u003C/h2\u003E","\u003Ch2\u003ECortex\u003C/h2\u003E","\u003Ch2\u003EMCP\u003C/h2\u003E","\u003Ch2\u003EGemini Enterprise\u003C/h2\u003E","\u003Ch2\u003ELooker\u003C/h2\u003E","\u003Ch2\u003EConclusion And Resources\u003C/h2\u003E"],"fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/quickstart-iceberg-cortex-gemini",":type":"snowflake-site/components/quickstart/quickstart-table-of-content"},"quickstart_button":{"id":"quickstart-button-652472cda8","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/quickstart-iceberg-cortex-gemini",":type":"snowflake-site/components/quickstart/quickstart-button","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-8ce483ad8c","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-c7a660da15",":type":"snowflake-site/components/modal/modal-container",":items":{},":itemsOrder":[]},"experiencefragment-footer":{"id":"experiencefragment-3511051f7c","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-381ace21e5","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"],"isPasswordProtected":false,"analyticsContentTags":["snowflake-site:taxonomy/solution-center/certification/quickstart"],"analyticsEnabled":true,"coveoConfig":{"searchHub":"snowflake.com","organizationId":"snowflakecomputingproduction8neljofn","apiKey":"xx335921a6-2a0a-40f2-a167-e390b4766c3d","pipeline":"snowflake.com"},"analyticsDebugMode":false,"analyticsData":{"excludeFromAnalytics":false,"subCategory":"","pageType":"homepage","templateName":"quickstart-page-template","siteName":"snowflake","pageUrl":"/content/snowflake-site/global/en/developers/guides/quickstart-iceberg-cortex-gemini","language":"en","category":"general","pageName":"Agentic AI for Your Lakehouse: Snowflake Cortex and Gemini Enterprise on Iceberg","contentTags":["snowflake-site:taxonomy/solution-center/certification/quickstart"]},"locale":"en"}
  