<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Google Cloud - Community - Medium]]></title>
        <description><![CDATA[A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don&#39;t necessarily reflect those of Google. - Medium]]></description>
        <link>https://medium.com/google-cloud?source=rss----e52cf94d98af---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Google Cloud - Community - Medium</title>
            <link>https://medium.com/google-cloud?source=rss----e52cf94d98af---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 13 Oct 2026 14:56:30 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/google-cloud" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Step-by-Step Guide: Configuring SAP BDC Connect for Google BigQuery]]></title>
            <link>https://medium.com/google-cloud/from-setup-to-first-query-configuring-sap-bdc-connect-for-google-bigquery-9ca4c5018de6?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/9ca4c5018de6</guid>
            <category><![CDATA[sap-bdc]]></category>
            <category><![CDATA[google-big-query]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[sap-business-data-cloud]]></category>
            <dc:creator><![CDATA[Parishudh Reddy Marupurolu]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 14:20:42 GMT</pubDate>
            <atom:updated>2026-10-13T14:20:40.869Z</atom:updated>
            <content:encoded><![CDATA[<p>From Setup to First Query: <em>Set up zero-copy access from SAP Business Data Cloud to BigQuery in seven steps, with step-by-step screenshots.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FV76BidXd3S782RbLUex-w.png" /></figure><h3>Unifying Enterprise Context: The Power of SAP and Google Cloud Synergy</h3><p>SAP systems run the core of the enterprise, including finance, supply chain, HR, and customer experience, through end-to-end processes like Order-to-Cash, Procure-to-Pay, Plan-to-Produce, Record-to-Report, and Hire-to-Retire. To make fast, data-driven decisions and put AI to work, organizations want to combine native SAP business semantics with Google Cloud data and AI capabilities.</p><p>Historically, connecting these platforms required data replication pipelines, which added complexity, latency, and challenges in maintaining native SAP business context and hierarchies.</p><p>To simplify this, Google Cloud and SAP co-engineered SAP Business Data Cloud (BDC) Connect for Google BigQuery — now generally available.</p><p>Using a zero-copy architecture powered by open-source Delta Sharing with built-in access controls, this integration lets you query live SAP BDC data products directly from BigQuery — and vice versa — without building replication pipelines. This helps bring the scale of BigQuery to your SAP data while keeping governance in place and data fresh.</p><h3>Official Resources &amp; Key Documentation</h3><p>Before diving into the configuration steps, bookmark these official resources to reference during your rollout:</p><p><strong><em>Google Cloud Blog: </em></strong><a href="https://cloud.google.com/blog/products/sap-google-cloud/sap-and-google-cloud-launch-bdc-connect-for-bigquery"><em>SAP and Google Cloud launch BDC Connect for BigQuery</em></a><em><br></em><strong><em>Google Documentation: </em></strong><a href="https://docs.cloud.google.com/lakehouse/docs/sap-bdc-overview"><em>About SAP Business Data Cloud integration | Lakehouse Docs</em></a><em><br></em><strong><em>SAP Community: </em></strong><a href="https://community.sap.com/t5/technology-blog-posts-by-sap/announcing-general-availability-of-sap-business-data-cloud-connect-for/ba-p/14441637"><em>Announcing GA of SAP BDC Connect for Google BigQuery</em></a><em><br></em><strong><em>Google Cloud docs:</em></strong><em> </em><a href="https://docs.cloud.google.com/lakehouse/docs/set-up-borderless-lakehouse-sap-bdc"><em>Set up cross-cloud connection for SAP BDC</em></a><em><br></em><strong><em>SAP Help:</em></strong><em> </em><a href="https://help.sap.com/docs/business-data-cloud/sap-business-data-cloud-connect/sharing-data-products-from-sap-business-data-cloud-trough-sap-bdc-connect?locale=en-US&amp;version=LATEST"><em>Obtaining Access to Data Products through SAP Business Data Cloud Connect</em></a></p><p>Keep these open in a tab. The rest of this post walks through each configuration step.</p><h3>Roles &amp; Responsibilities: Team Alignment Matrix</h3><p>You’ll need three people (or one person wearing three hats):</p><ul><li><strong>BigQuery Administrator: </strong>Handles BigQuery Enterprise Plus capacity setup, sets up the Lakehouse catalog, and executes queries on shared tables through the Google Cloud Console.</li><li><strong>SAP Administrator: </strong>Deploys the BDC Connect instance, links the Google Cloud Service Account, and generates the activation link (connector endpoint and invitation code) in SAP for Me.</li><li><strong>SAP BDC User:</strong> Finds data products in the SAP BDC catalog, requests access, and installs them to the BDC Connect instance for BigQuery (Step 7).</li></ul><h3>Prerequisites &amp; System Readiness</h3><p>Before you begin the step-by-step walkthrough, verify that you have completed these preparation steps on both platforms.</p><ul><li><strong>Tenant deployment: </strong>Ensure your SAP BDC environment is active on a supported cloud platform (such as Google Cloud or AWS).</li><li>Ensure your SAP administrator has access to your SAP BDC environment to configure the BDC Connect application</li><li>You must have appropriate IAM permissions in Google Cloud to create and manage Lakehouse Catalogs: BigLake Admin (roles/biglake.admin) or BigQuery Admin (roles/bigquery.admin).</li><li>The target Google Cloud project must use BigQuery Enterprise Plus Edition.</li></ul><h3>Step-by-Step Screenshot-Guided Walkthrough</h3><ul><li><strong>Phase 1: Google Cloud Environment Preparation (Steps 1–3)</strong> — Configure BigQuery Enterprise Plus capacity, assign your project to the reservation, and initiate the Lakehouse Delta Sharing catalog.</li><li><strong>Phase 2: SAP BDC Provisioning &amp; Mutual Handshake (Steps 4–6)</strong> — Provision the connector in SAP for Me, exchange credentials to enroll the catalog, and link the connection into the SAP landscape formation.</li><li><strong>Phase 3: Data Federation &amp; Live BigQuery Analytics (Step 7)</strong> — Share SAP data products from the SAP BDC Catalog &amp; Marketplace and execute zero-copy SQL queries directly within BigQuery Studio.</li></ul><h3>Step 1: [BigQuery Admin | Google Cloud] Enable BigQuery Enterprise Plus Capacity</h3><p>1. Access the BigQuery interface via the <a href="https://console.cloud.google.com/">Google Cloud Console</a>.</p><p>2. Navigate through the sidebar to <strong>Administration &gt; Workload management &gt; Capacity management.</strong></p><p>3. Click Create Reservation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bucbcoBf9QfM5R9ov4mJxQ.png" /><figcaption><em>Administration → Workload management → Capacity Management. Click Create Reservation to start.</em></figcaption></figure><p>4. <strong>Reservation Name: </strong>Assign a unique identifier, such as gcp-sap-bdc-connect-bq (the name used in this walkthrough).</p><p>5. <strong>Location: </strong>Choose the specific region hosting your datasets; this must align with your Lakehouse catalog setting.</p><p>6. <strong>Edition: </strong>Pick Enterprise Plus from the available options.</p><ul><li><strong>Why Enterprise Plus? </strong>BDC Connect queries require BigQuery Enterprise Plus edition. With baseline slots set to 0 and autoscaling on (below), you pay for autoscaled slots while queries run instead of paying for idle baseline capacity.</li></ul><p>7. <strong>Slots:</strong></p><ul><li>Configure Baseline slots to 0: This avoids paying for idle baseline capacity.</li><li>Turn on Auto Scaling so BigQuery adds slots only while queries are running, up to the maximum you set. For this demo we used Extra Small (50 slots) as the maximum, which is enough for a few test queries. For production, size the maximum to your own workloads as part of your sizing exercise. The <a href="https://cloud.google.com/bigquery/docs/slot-estimator">BigQuery slot estimator</a> can help.</li></ul><p>8. Finalize the configuration by clicking Save.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y18nkpHkPBOF2TrUgOtszQ.png" /></figure><p>Your new reservation now appears under Capacity management:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZglJ9Vo46UWofTgFU_bQzQ.png" /><figcaption><em>The new Enterprise Plus reservation, ready for an assignment.</em></figcaption></figure><p>Step 2: [BigQuery Admin | Google Cloud] Assign Your Project to the Reservation</p><ol><li>Navigate to the Capacity management page.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CqUc7jCOr5DI4b1VMnJPIQ.png" /><figcaption><em>From the reservation’s Actions menu, choose Create assignment.</em></figcaption></figure><p>2. Select your newly created reservation ID, then click the More actions menu (three dots) under the Actions column.</p><p>3. Click + Create Assignment.</p><p>4. For Rule type, select Job Routing (Assignment).</p><p>5. Select a resource: Choose the Project, Folder, or Organization that will be running the BigQuery jobs.</p><p>6. Job type: Select Query (this routes query jobs to the Enterprise Plus reservation). You can also add assignments for Pipeline or Background jobs if required.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qYg_UOYUyN7VD-uGfond-Q.png" /><figcaption><em>A Job Routing rule for your project with job type QUERY. The Rule Summary shows where the jobs go.</em></figcaption></figure><p>7. Click Continue.</p><p>8. Reservation: Select the Enterprise Plus reservation created in the previous step (e.g., “gcp-sap-bdc-connect-bq”).</p><p>9. Click Continue. Your project will appear under your reservation (in this walkthrough, the reservation name and project ID share the name gcp-sap-bdc-connect-bq).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZglJ9Vo46UWofTgFU_bQzQ.png" /><figcaption><em>Your project now sits under the reservation, so its queries use Enterprise Plus capacity.</em></figcaption></figure><p>Verification: Once assigned, query jobs run within that project use Enterprise Plus capacity. You can verify this by checking the Job Information in the BigQuery Job History; it should list the reservation name and edition.</p><h3>Step 3: [BigQuery Admin | Google Cloud] Create the Lakehouse Delta Sharing Catalog</h3><p>A Delta Sharing catalog in Lakehouse serves as your gateway to federating with the remote SAP BDC catalog.</p><p>1. Enable the <strong>BigLake API.</strong></p><p>2. Review the roles required to enable APIs, then select Enable the API.</p><p>3. In the Google Cloud console, open the Lakehouse page: <a href="https://console.cloud.google.com/biglake">Go to Lakehouse</a></p><p>4. In the navigation menu, click <strong>Runtime catalog</strong>.</p><p>5. Click <strong>Create catalog</strong> and select <strong>Federated catalog</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZcefeF6ceeO8_WMYbP5gBQ.png" /><figcaption><em>Lakehouse → Runtime catalog → Create catalog → Federated catalog.</em></figcaption></figure><p>6. In the <strong>Catalog configuration</strong> section, enter the following:</p><p>a. For <strong>Federated catalog source,</strong> select SAP (Business Data Cloud).</p><p>b. For <strong>Catalog name (in Lakehouse)</strong>, enter a name for your catalog.</p><p>c. For <strong>Data location</strong>, select the same Lakehouse region as reservation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1005/1*I12I_RsE2wp0ybNfrzONkw.png" /><figcaption><em>Choose SAP (Business Data Cloud) as the source, name the catalog, and pick its region.</em></figcaption></figure><p>Click Continue. The next screen, Connection details, asks for a Connector endpoint and an Invitation code. You don’t have these yet. Your SAP admin generates them in Step 4, and you’ll come back here to enter them in Step.5 For now, continue past this screen.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/772/1*1hqlJY0slTRZRpf_Rrng6Q.png" /><figcaption><em>Copy this service account. Your SAP administrator needs it in Step 4.</em></figcaption></figure><p>7. Click <strong>Continue</strong>.</p><p>8. Retrieve the Google Cloud Service Account ID displayed on the page and provide this ID to your SAP administrator.</p><p><strong>Why this matters:</strong> This service account is how SAP BDC recognizes and trusts BigQuery. When your SAP administrator registers this ID during BDC Connect provisioning, SAP BDC authorizes BigQuery to query Delta Sharing endpoints directly without needing static credentials, passwords, or shared secret keys.</p><h3>Step 4: [SAP Admin | SAP for Me] Configure the BDC Connect Instance</h3><p>Your SAP administrator must sign in to <em>SAP for Me</em> and provision a new instance of the BDC Connect application:</p><ol><li>Go to <strong>Portfolio &amp; Products</strong> and select <strong>Business Data Cloud.</strong></li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/903/1*SkeqcawaBCvZPMN6OF_A6Q.png" /><figcaption><em>In SAP for Me, open Portfolio &amp; Products and select your Business Data Cloud package.</em></figcaption></figure><p>2. Under Business Data Cloud, click Application.</p><p>3. Locate SAP Business Data Cloud Connect and click Start Provisioning.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/622/1*WDNjUZRR6e70KTBKS5tyHw.png" /><figcaption><em>On the Applications tab, find SAP BDC Connect and click Start Provisioning.</em></figcaption></figure><p>4. Enter the <strong>Display Name</strong>.</p><p>5. Select or create a <strong>Resource Group</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g_Kyvz5vCipWvv3aqlf2_A.png" /><figcaption><em>Give the solution a name and choose where it lives, then click Next Step.</em></figcaption></figure><p>6. Allocate <strong>BDC Capacity Units (CUs)</strong>. How many you need depends entirely on your SAP BDC sizing exercise. Estimate your expected workloads with SAP’s Capacity Unit Cost Estimator, then assign quota to match. We used 10 CUs for this demo, which may not a production recommendation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/1*aMXiN9yy3UApDyIlXEHaTw.png" /><figcaption><em>Assign Capacity Units to SAP BDC Connect. We used 10 for this demo.</em></figcaption></figure><p>7. In the <strong>External System Instance Identifier</strong> field, provide the Google Cloud service account ID you copied in Step 3.</p><p>8. Choose a <strong>Region</strong>. Ensure this matches the region where your SAP BDC tenant is located (the BDC Core region).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XmH9VbK1ZjfUUp8dfRZkAw.png" /><figcaption><em>Paste the Google Cloud service account and choose the region of your SAP BDC tenant.</em></figcaption></figure><p>9. Click Review, verify the details, and then click <strong>Finish</strong>.</p><p>10. Upon selecting Finish, a notification prompt entitled <strong>Creating solution</strong> will appear:</p><pre>Your provisioning request for the &#39;GCP BDC Demo&#39; solution has started and its resources are being set up in the selected resource group. The provisioning process can take a few moments to complete.<br>The new Solution resource will achieve the Ready status when all tenants are successfully provisioned, and integrations are fully established.</pre><p>11. Navigate to the <strong>Resources</strong> view to monitor deployment progress for provisioned components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qXiOdPUXQB9e_4-YOm7usw.png" /><figcaption><em>While provisioning runs, the status shows Processing. View Notifications shows the progress.</em></figcaption></figure><p>After completion, go to the tenant details view. Hold the pointer over the <strong>View Tenant Notifications</strong> button to get the <strong>Activation Link</strong>, which contains the <strong>SAP Connector Endpoint</strong> and the <strong>SAP Invitation Code</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/913/1*F-tca43yqqYwyhbJ6D5OzA.png" /><figcaption><em>The tenant notification holds the activation URL, with both the endpoint and the invitation code.</em></figcaption></figure><p>Provide the following activation parameters to your BigQuery administrator to complete the connection handshake:</p><h4>Deconstructing the Handshake URL</h4><p>When enrolling the catalog in the Google Cloud Console, split the activation URL into its two core parameters:</p><ul><li><strong>Connector Endpoint: </strong>The base URL preceding <em>?code=</em> (e.g., <a href="https://d1xxxx-xxx-xxxxx-xxx-xxxxxx.sharing.xx.xxxx-xxxx.hanaxxxx.ondemand.com).">https://d1xxxx-xxx-xxxxx-xxx-xxxxxx.sharing.xx.xxxx-xxxx.hanaxxxx.ondemand.com).</a></li><li><strong>Invitation Code: </strong>The alphanumeric token following <em>?code=</em> (e.g., 11exxx-xxxx-xxxx-xxxxx-xxxxxx6).</li></ul><p>Separating these parameters accurately helps prevent handshake authentication errors during catalog enrollment. The most common setup mistake is pasting the whole URL into the endpoint field.</p><p>The status of this connection in SAP BDC remains “Processing” until the enrollment steps in BigQuery are complete.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bO5NrO1yr_wmhAA0G5gRKw.png" /><figcaption><em>Both the solution and the connector stay in Processing until you finish enrollment in BigQuery.</em></figcaption></figure><h3>Step 5: [BigQuery Admin | Google Cloud] Complete the Handshake &amp; Enroll Catalog</h3><p>To finalize the authentication handshake between SAP BDC and the Google Cloud Console, enroll the catalog using the values from your SAP admin.</p><p>1. Navigate to the <strong>Lakehouse</strong> page in Google Cloud Console. If the session timed out or was closed, re-open it via the main menu by selecting <strong>Runtime catalog</strong>.</p><p>2. Open the <strong>Catalog details</strong> page by selecting your targeted SAP Delta Sharing catalog.</p><p>3. Locate the top banner and click <strong>Enroll catalog</strong>.</p><p>4. Fill in the required information under <strong>Connection details</strong>:</p><ul><li><strong>Connector endpoint: </strong>Paste the URL supplied by your SAP administrator.</li><li><strong>Invitation code: </strong>Enter the authentication code provided by your SAP administrator.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/804/1*e_slXsCt5t4RGINRW6_ucg.png" /><figcaption><em>Paste the endpoint and invitation code into Connection details, then click Save.</em></figcaption></figure><p>5. Click <strong>Save.</strong></p><p>This completes the authentication handshake between SAP BDC and Google Cloud. The two platforms now trust each other.</p><p>Before sharing SAP BDC data products with Google BigQuery using the SAP BDC connection, complete one final setup and configuration step within SAP BDC to finalize system integration and permit data access.</p><h3>Step 6: [SAP Admin | SAP BDC Landscape] Link BDC Connection to System Formation</h3><p>Once enrollment finishes, the connection status in SAP BDC changes from “Processing” to “Ready.”</p><p>Why Formation Linking is Essential: In SAP BDC, components operate inside isolated security boundaries. Deploying a connector instance establishes the runtime, but it cannot access your enterprise SAP tenant data products until it is formally bound into your Customer Landscape System Formation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bO5NrO1yr_wmhAA0G5gRKw.png" /><figcaption><em>After enrollment, both the solution and the connector show Ready.</em></figcaption></figure><p>1. Add the newly created instance (<em>bdc-connect-854cc5e7</em>) directly into your designated system formation.</p><p>2. Navigate to the <strong>Customer Landscape</strong> overview tab.</p><p>3. Locate your specific target BDC tenant and select <strong>Include System</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kvaKfiEobR_sue44Ykqlvg.png" /><figcaption><em>On Customer Landscape, open your BDC formation and click Include Systems.</em></figcaption></figure><p>4. Choose your established BDC connection from the list, click <strong>Next</strong> to review the system assignment details, and confirm by selecting <strong>Include</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DZB1He-AzFBknDNqPYR31g.png" /><figcaption><em>Select your BDC Connect instance to include it in the formation.</em></figcaption></figure><p>5. The system initiates automated background synchronization across systems; once this workflow finishes, the fully configured integration appears within your assigned formation.</p><p>6. Following successful setup, the instance <em>bdc-connect-854cc5e7</em> becomes fully visible and active in SAP BDC, enabling direct live data product sharing with Google BigQuery. If it doesn’t appear, sync the BDC formation and check again.</p><h3>Step 7: [SAP BDC User &amp; BigQuery Analyst] Share a Data Product and Query It in BigQuery</h3><p>This is where everything comes together. The connection is in place, so now you decide which SAP data BigQuery can see. We’ll share one data product, <strong>Maintenance Order</strong>, and then query it from BigQuery Studio. In our demo, the SAP side took only a few minutes.</p><h4>7a. Confirm the connector is in your landscape</h4><p>In SAP BDC, open <strong>System Landscape</strong>. You should see your connector (bdc-connect-854cc5e7 in our case) listed in your formation with the type SAP BDC Connect for Google BigQuery. If it’s there, Step 6 worked and you’re ready to share.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JIcArnI_C96BFPVtYBY9nQ.png" /><figcaption><em>The BDC Connect instance is listed in the formation. That’s your green light.</em></figcaption></figure><h4>7b. Find the data product</h4><p>Open <strong>Catalog &amp; Marketplace → Search</strong> and switch to the <strong>Data Products</strong> tab.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vJ-JDo5i5ZkAJhE3EH0ttA.png" /><figcaption><em>Catalog &amp; Marketplace → Search, on the Data Products tab.</em></figcaption></figure><p>Search for Maintenance Order. You’ll probably see more than one result, because the same data product can come from different source systems. Check the system name on each card and pick the one from the source system connected to your formation.</p><h4>7c. Choose BigQuery as the target</h4><p>Click the <strong>cart icon</strong> at the top right to open the <strong>Request Cart</strong>. Under <strong>Target System</strong>, click <strong>Select System</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bi0Xjbhv2HSZsfUfNLkdVQ.png" /><figcaption><em>Open the Request Cart and pick a target system before adding anything.</em></figcaption></figure><p>Search for your connector and select it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b7w86e8WMZ5BKoftwa1HJA.png" /><figcaption><em>Typing the last few characters of the instance name helps you find it quickly.</em></figcaption></figure><p><strong>Tip:</strong> If you see “Some systems are unavailable for selection,” don’t worry. It refers to other systems in the formation. Your connector just needs to appear in the list.</p><h4>7d. Add the data product and send the request</h4><p>With the target set, click <strong>Supported Data Products</strong> so you only see products this connector can receive. Then click the <strong>cart icon on the Maintenance Order card</strong>, check that it appears in the Request Cart, and click <strong>Proceed</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3MYmQcnMPX-jxO60ovb4hQ.png" /><figcaption><em>Target system set and Maintenance Order in the cart. Next, click Proceed.</em></figcaption></figure><p>In the <strong>Request Access</strong> dialog, add a short comment (it’s required, so something like “SAP demo share” is fine), tick the terms-and-conditions box, and click <strong>Send</strong>. The end date is optional.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mEhgZnQa280Q8S9GNd0kKg.png" /><figcaption><em>Add a comment, accept the terms, and click Send.</em></figcaption></figure><h4>7e. Confirm the share is installed</h4><p>A notification should appear shortly saying the data product is installed to your connector and ready to use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qqnBLVs17272SRis69kxuQ.png" /><figcaption><em>Access Request Approved” means the share is live.</em></figcaption></figure><p>In our demo tenant, approval came through in seconds. In your landscape, the request may wait for the data product owner to approve it, so check with your SAP administrator if nothing arrives.</p><p>To double-check, click on the Notification or open <strong>Catalog &amp; Marketplace → Data Product Access</strong> and select the <strong>Agreements</strong> tab. Maintenance Order should show <strong>Installed</strong> with status <strong>Current</strong>. If it hasn’t been installed yet, open the agreement and click <strong>Install</strong>. The data product does not appear in BigQuery until this is done.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cYBagTTMlJf8xvXhHFvaVQ.png" /><figcaption><em>The agreement details confirm the data product is installed on the BigQuery connector.</em></figcaption></figure><p>That’s everything on the SAP side. Over to BigQuery.</p><h4>7f. Find the data in BigQuery Studio</h4><p>In the Google Cloud console, open <strong>BigQuery → Studio</strong>. In the Explorer, expand your project and click <strong>Datasets</strong>. The shared data product appears as a new dataset with its source shown as <strong>SAP</strong>. You don’t need to import or load anything. If it isn’t there yet, give it a minute and click <strong>Refresh</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Tw9EuTIQ13jlf4QPCGhuQg.png" /><figcaption><em>The SAP data product appears as a dataset, with Source shown as SAP.</em></figcaption></figure><p>The dataset name may look long. It’s built from your catalog, the data product (share), and its schema: catalog.share.schema. Open it and you’ll see that one data product contains a whole set of related tables, such as the order header, operations, partners, and status texts, organized just as they are in SAP.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sz0p9kAKOqKKikFRjX3rTg.png" /><figcaption><em>One data product, many tables, all queryable in place.</em></figcaption></figure><p>On the <strong>Details</strong> tab, the catalog info shows <strong>Source: SAP</strong> and <strong>Type: Delta Sharing</strong>. That confirms BigQuery is reading the data from SAP through Delta Sharing, not from a replicated copy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*omHh_c6Gi-rGrWR5w3jt-Q.png" /><figcaption><em>Type: Delta Sharing confirms the data is read in place, with no replication.</em></figcaption></figure><p>Click a table, such as maintenanceorder, to see its schema. The field names are the SAP business names your teams already know.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/688/1*IxQQVxQSbs5NvMpdBOFf9Q.png" /><figcaption><em>SAP field names and data types, preserved as-is.</em></figcaption></figure><h4>7g. Run your first query</h4><p>Click <strong>Query</strong> (or open a new query tab). Start with a simple SELECT to see the data, then try a more focused question, such as the 50 most recent preventive maintenance orders (PM01). Replace &lt;source_system&gt; with the dataset name you see in your Explorer:</p><pre>SELECT *<br>FROM `gcp-sap-bdc-connect-bq.bdc-con-bq-catalog.maintenanceorder_v1_&lt;source_system&gt;.maintenanceorder.maintenanceorder`<br>LIMIT 1000;<br> - Preventive maintenance orders (PM01), most recent first<br>SELECT<br>MaintenanceOrder,<br>MaintenanceOrderType,<br>MaintenanceOrderDesc,<br>MaintOrdBasicStartDate,<br>MaintOrdBasicEndDate<br>FROM `gcp-sap-bdc-connect-bq.bdc-con-bq-catalog.maintenanceorder_v1_&lt;source_system&gt;.maintenanceorder.maintenanceorder`<br>WHERE MaintenanceOrderType = &#39;PM01&#39;<br>ORDER BY MaintOrdBasicStartDate DESC<br>LIMIT 50;</pre><p>Both queries read live data straight from SAP BDC, so you don’t need to schedule copies or keep pipelines running. Here’s the first query running in BigQuery Studio:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/778/1*VAUmPZSukdCQPsBK2lRvaw.png" /><figcaption><em>Live SAP maintenance orders, queried from BigQuery. Some values are masked in this screenshot.</em></figcaption></figure><h3>Summary</h3><p>In seven steps you connected SAP Business Data Cloud to BigQuery, without building a single pipeline:</p><ul><li><strong>On Google Cloud</strong>, you set up Enterprise Plus capacity and created a Lakehouse Delta Sharing catalog.</li><li><strong>In SAP</strong> for Me, you provisioned BDC Connect and generated the activation link. Back in Google Cloud, you enrolled the catalog to complete the handshake, and then added the connector to your SAP BDC formation.</li><li><strong>Then</strong> your SAP BDC user installed the Maintenance Order data product, and it appeared in BigQuery as a dataset you could query right away, with native SAP field names intact.</li></ul><p>The result: your analysts can work with up-to-date SAP data where it lives, governed by SAP at the source and by IAM on the Google Cloud side, with no copies to schedule or maintain.</p><h3>Good to know before production</h3><p>Before you move to production, review the current limitations in the Google Cloud documentation (<a href="https://docs.cloud.google.com/lakehouse/docs/set-up-borderless-lakehouse-sap-bdc#limitations-and-considerations">Limitations and considerations</a>) and the <a href="https://help.sap.com/docs/business-data-cloud/sap-business-data-cloud-connect/sharing-data-products-from-sap-business-data-cloud-trough-sap-bdc-connect?locale=en-US&amp;version=LATEST">SAP BDC Connect documentation</a> on SAP Help. They cover access control, supported data product names, metadata refresh and caching, and data residency, and they change as the product evolves.</p><h3>Appendix: Key Concepts and Terms</h3><ul><li><a href="https://docs.cloud.google.com/lakehouse/docs/introduction"><strong>Lakehouse</strong></a><strong>: </strong>The Google Cloud service that manages catalogs and metadata for open table formats such as Apache Iceberg, so engines like BigQuery can query data where it lives, including across clouds.</li><li><a href="https://docs.cloud.google.com/lakehouse/docs/set-up-borderless-lakehouse-sap-bdc#create-lakehouse-catalog"><strong>Federated Delta Lake Catalog</strong></a><strong>: </strong>A Lakehouse catalog type that connects to SAP BDC using the open <a href="https://delta.io/sharing/">Delta Sharing protocol</a>. This is the catalog you create in Step 3.</li><li><a href="https://docs.cloud.google.com/lakehouse/docs/sap-bdc-overview#core-concepts"><strong>Federated Catalog</strong></a><strong>: </strong>How BigQuery keeps SAP BDC metadata current. It asks the remote SAP BDC catalog for its shares, schemas, and tables, so you don’t copy or maintain that metadata by hand.</li><li><a href="https://docs.cloud.google.com/lakehouse/docs/set-up-borderless-lakehouse-sap-bdc#update-catalog"><strong>Connection</strong></a><strong>: </strong>The part of the Lakehouse catalog that links and authenticates BigQuery and SAP BDC, using the connector endpoint and invitation code from Steps 4 and 5.</li></ul><p>SAP BDC data appears in BigQuery in a <a href="https://docs.cloud.google.com/lakehouse/docs/sap-bdc-overview#data-hierarchy">five-level hierarchy</a> that follows the Delta Sharing protocol:</p><pre>PROJECT_ID.CATALOG_ID.SHARE_NAME.SCHEMA_NAME.TABLE_NAME</pre><p>The queries in Step 7g use exactly this path.</p><ul><li><strong>Catalog ID: </strong>The Lakehouse catalog resource in your Google Cloud project, and the top-level container for everything shared from one SAP BDC Connector (bdc-con-bq-catalog in this walkthrough).</li><li><strong>Share (SAP Data Product): </strong>A logical group of tables shared for consumption, such as Maintenance Order. Each data product maps to a Delta Sharing share, the first level under the catalog.</li><li><strong>Schema (Delta Schema): </strong>The second level, inside a share. It groups related tables.</li><li><strong>Table (SAP Table): </strong>The data object inside a schema, such as maintenanceorder. This is what you query in BigQuery.</li><li><a href="https://docs.cloud.google.com/lakehouse/docs/set-up-borderless-lakehouse-sap-bdc#configure-sap-connector"><strong>SAP BDC Connector</strong></a><strong>: </strong>The SAP BDC configuration that authorizes access from a specific Google Cloud project, through the service account you registered in Step 4.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ca4c5018de6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/from-setup-to-first-query-configuring-sap-bdc-connect-for-google-bigquery-9ca4c5018de6">Step-by-Step Guide: Configuring SAP BDC Connect for Google BigQuery</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Serving Polish Large Language Model (PLLuM) on TPUs]]></title>
            <link>https://medium.com/google-cloud/serving-polish-large-language-model-pllum-on-tpus-877478f77dda?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/877478f77dda</guid>
            <category><![CDATA[tpu]]></category>
            <category><![CDATA[vllm]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <dc:creator><![CDATA[Maciej Strzelczyk]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:45:51 GMT</pubDate>
            <atom:updated>2026-10-13T11:45:50.347Z</atom:updated>
            <content:encoded><![CDATA[<p>There are many really good and popular LLMs that support multiple languages. Yet sometimes, you might find yourself in need of a real specialist when it comes to a specific language or culture. One of such specialists is <a href="https://pllum.org.pl/#about">PLLuM (Polish Large Language Model)</a>, a model created specifically for handling interactions in the Polish language. Trained on vast sets of native source materials, models like this can handle communication in a given language better than bigger, more universal models.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8T2g-A3CzUmjCew6sP5yTg.png" /></figure><p>Deploying a model like PLLuM on GPU architecture is as simple as with any other model. However, when you consider running it on a TPU, you may discover that inference engines like vLLM don’t list it as a <a href="https://docs.vllm.ai/projects/tpu/en/latest/recommended_models/">supported model</a>. Does it mean you can’t run it on TPUs? No! In this article, I’ll show you how to set up a PLLuM inference service on Trillium TPU nodes in GKE.</p><h3>Setting up the GKE cluster</h3><p>We will start with setting up a simple GKE Autopilot cluster. This will allow us to focus on deploying the model, without manual node management.</p><p>Let’s begin with some environment variables:</p><pre>export PROJECT_ID=&quot;YOUR_PROJECT_ID&quot;<br>export CLUSTER_NAME=&quot;YOUR_CLUSTER_NAME&quot;<br>export REGION=&quot;CLUSTER_REGION&quot; <br>export ZONE=&quot;CLUSTER_ZONE&quot;<br>export RESERVATION=&quot;RESERVATION_NAME&quot;<br>export HF_TOKEN=&quot;your-hugging-face-token&quot;</pre><p>In my example, I will be using a TPU reservation I made using <a href="https://docs.cloud.google.com/kubernetes-engine/docs/how-to/tpu-calendar-mode">DWS Calendar Mode</a>. This way I won’t have to worry about hardware availability. Other deployment options are also available, but don’t guarantee hardware availability.</p><p>Create the GKE cluster, set up kubectl to work on this cluster and create a Kubernetes secret that will hold your Hugging Face read token:</p><pre>gcloud container clusters create-auto $CLUSTER_NAME \<br>    --project=$PROJECT_ID \<br>    --region=$REGION<br><br>gcloud container clusters get-credentials $CLUSTER_NAME --location=$REGION --project=$PROJECT_ID<br>    <br>kubectl create secret generic hf-secret \<br>    --from-literal=hf_api_token=${HF_TOKEN} \<br>    --dry-run=client -o yaml | kubectl apply -f -</pre><h3>Deploying the model on a single TPU v6e chip</h3><p>We can start with a simple deployment of <a href="https://huggingface.co/CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512">CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512</a> — this is a small model (16 GB) that fits just fine in the memory of a single TPU v6e chip (32 GB). That makes our deployment simple and easy, though limited in performance. It is the best option for development and testing. Use the following YAML to start your single-chip deployment (save it as pllum-1.yaml):</p><pre>apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>  name: vllm-tpu<br>spec:<br>  replicas: 1<br>  selector:<br>    matchLabels:<br>      app: vllm-tpu<br>  template:<br>    metadata:<br>      labels:<br>        app: vllm-tpu<br>    spec:<br>      nodeSelector:<br>        cloud.google.com/gke-tpu-topology: 1x1<br>        # Following values can be used to run v6e TPUs with a reservation.<br>        cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice<br>        cloud.google.com/reservation-affinity: &quot;specific&quot;<br>        cloud.google.com/reservation-name: RESERVATION_NAME<br>      containers:<br>      - name: vllm-tpu<br>        image: docker.io/vllm/vllm-tpu:latest<br>        command: [&quot;python3&quot;, &quot;-m&quot;, &quot;vllm.entrypoints.openai.api_server&quot;]<br>        args:<br>        - --host=0.0.0.0<br>        - --port=8000<br>        - --tensor-parallel-size=1<br>        - --max-model-len=4096<br>        - --model=CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512<br>        - --max-num-batched-tokens=512<br>        - --max-num-seqs=128<br>        env:<br>        - name: HUGGING_FACE_HUB_TOKEN<br>          valueFrom:<br>            secretKeyRef:<br>              name: hf-secret<br>              key: hf_api_token<br>        ports:<br>        - containerPort: 8000<br>        resources:<br>          limits:<br>            google.com/tpu: 1<br>        readinessProbe:<br>          tcpSocket:<br>            port: 8000<br>          initialDelaySeconds: 1800<br>          periodSeconds: 60<br>          failureThreshold: 10<br>          timeoutSeconds: 5<br>        volumeMounts:<br>        - name: dshm<br>          mountPath: /dev/shm<br>      volumes:<br>      - name: dshm<br>        emptyDir:<br>          medium: Memory<br>---<br>apiVersion: v1<br>kind: Service<br>metadata:<br>  name: vllm-service<br>spec:<br>  selector:<br>    app: vllm-tpu<br>  type: LoadBalancer<br>  ports:<br>    - name: http<br>      protocol: TCP<br>      port: 8000<br>      targetPort: 8000</pre><p>Remember to replace <em>RESERVATION_NAME</em> with a proper value. Then apply the file with kubectl apply -f pllum-1.yaml.</p><p>The deployment will take a while. You can watch how first the TPU node is created (kubectl get nodes) and how the vLLM pod starts (kubectl logs -l app=vllm-tpu).</p><p>Once the pod is ready to work, you can check the external IP of the created service with: kubectl get svc— just check what’s in the External IP column.</p><p>Now, when we know that external IP, we can send a request to our model:</p><pre>curl http://${EXTERNAL_IP}:8000/v1/completions \<br>    -H &quot;Content-Type: application/json&quot; \<br>    -d &#39;{<br>        &quot;model&quot;: &quot;CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512&quot;,<br>        &quot;prompt&quot;: &quot;Jak to jest być skrybą?&quot;,<br>        &quot;max_tokens&quot;: 150,<br>        &quot;temperature&quot;: 0.7<br>    }&#39;</pre><p>The answer you get is a JSON containing various information, including the completion generated by the model!</p><h3>Scaling to 8 TPUs</h3><p>To handle more requests concurrently and increase the efficiency of your service, you may want to scale the vLLM service to work with more than one TPU chip. If you try to simply switch — tensor-parallel-size to 8, you will get an error message similar to:</p><blockquote>(EngineCore pid=572) jax._src.sharding.IndivisibleError: One of device_put args was given the sharding NamedSharding(mesh=Mesh(‘data’: 1, ‘model’: 8, axis_types=(Auto, Auto)), spec=P((‘data’, ‘model’), None), memory_kind=device) implies that array axis 0 is partitioned 8 times, but the dimension size is 128259 (full shape: (128259, 4096), per-dimension tiling factors: (8, 1) should evenly divide the shape). This error occurs at source: /workspace/tpu_inference/tpu_inference/layers/common/utils.py:149:23 (general_device_put.&lt;locals&gt;._put)</blockquote><p>The model we chose (Llama-PLLuM-8B-instruct) has a vocabulary size of 128259 — that’s not divisible by 8! The way vLLM spreads the work across multiple chips requires the model to have a vocabulary divisible by the number of chips that will be used. In this case, we will need to pad the model’s vocabulary to 128264 with “fake tokens”.</p><p>Here’s a script that will do just that. It will download the model locally, modify it, and store it in a new directory:</p><pre># pip install torch --index-url https://download.pytorch.org/whl/cpu<br># pip install huggingface_hub safetensors<br><br>import json<br>import os<br>import torch<br>from huggingface_hub import snapshot_download<br>from safetensors.torch import load_file, save_file<br><br>MODEL_ID = &quot;CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512&quot;<br>OUTPUT_DIR = &quot;./Llama-PLLuM-8B-instruct-2512-padded&quot;<br>TARGET_VOCAB_SIZE = 128264  # 16033 * 8<br><br># 1. Download model snapshot<br>model_path = snapshot_download(repo_id=MODEL_ID)<br>os.makedirs(OUTPUT_DIR, exist_ok=True)<br><br># 2. Copy and update config.json<br>with open(os.path.join(model_path, &quot;config.json&quot;), &quot;r&quot;) as f:<br>    config = json.load(f)<br><br>old_vocab_size = config[&quot;vocab_size&quot;]<br>pad_amount = TARGET_VOCAB_SIZE - old_vocab_size<br>config[&quot;vocab_size&quot;] = TARGET_VOCAB_SIZE<br><br>with open(os.path.join(OUTPUT_DIR, &quot;config.json&quot;), &quot;w&quot;) as f:<br>    json.dump(config, f, indent=2)<br><br># 3. Copy other metadata files (tokenizers, generation configs, etc.)<br>for file_name in os.listdir(model_path):<br>    if not file_name.endswith(&quot;.safetensors&quot;) and file_name != &quot;config.json&quot;:<br>        src = os.path.join(model_path, file_name)<br>        dst = os.path.join(OUTPUT_DIR, file_name)<br>        if os.path.isfile(src):<br>            os.system(f&quot;cp {src} {dst}&quot;)<br><br># 4. Pad embedding and lm_head tensors in safetensors<br>for file_name in os.listdir(model_path):<br>    if file_name.endswith(&quot;.safetensors&quot;):<br>        tensors = load_file(os.path.join(model_path, file_name))<br>        modified = False<br><br>        for key in [&quot;model.embed_tokens.weight&quot;, &quot;lm_head.weight&quot;]:<br>            if key in tensors:<br>                tensor = tensors[key]<br>                if tensor.shape[0] == old_vocab_size:<br>                    padding = torch.zeros(<br>                        (pad_amount, *tensor.shape[1:]), dtype=tensor.dtype<br>                    )<br>                    tensors[key] = torch.cat([tensor, padding], dim=0)<br>                    modified = True<br><br>        save_file(tensors, os.path.join(OUTPUT_DIR, file_name))<br><br>print(f&quot;✅ Successfully padded model saved to: {OUTPUT_DIR}&quot;)</pre><p>To make use of the new model, we need to upload it to a Cloud Storage Bucket and allow the vLLM pod access to it, using the Workload Identity bindings.</p><pre># We start with creating a new bucket<br>export BUCKET_NAME=&quot;padded-pllum-model&quot;<br><br>gcloud storage buckets create gs://${BUCKET_NAME} \<br>    --project=${PROJECT_ID} \<br>    --location=${REGION}<br><br># Upload the modified model to the bucket<br>gcloud storage cp -r ./Llama-PLLuM-8B-instruct-2512-padded gs://${BUCKET_NAME}/<br><br># Create GCP Service Account and bind with GKE Service Account via Workload Identity for GCS Fuse<br>export GSA_NAME=&quot;vllm-pllum-gcs-sa&quot;<br>export KSA_NAME=&quot;default&quot;<br>export NAMESPACE=&quot;default&quot;<br><br>gcloud iam service-accounts create ${GSA_NAME} \<br>    --project=${PROJECT_ID}<br><br>gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \<br>    --member=&quot;serviceAccount:${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com&quot; \<br>    --role=&quot;roles/storage.objectViewer&quot;<br><br>gcloud iam service-accounts add-iam-policy-binding ${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \<br>    --project=${PROJECT_ID} \<br>    --role=&quot;roles/iam.workloadIdentityUser&quot; \<br>    --member=&quot;serviceAccount:${PROJECT_ID}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]&quot;<br><br>kubectl annotate serviceaccount ${KSA_NAME} \<br>    --namespace=${NAMESPACE} \<br>    iam.gke.io/gcp-service-account=${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \<br>    --overwrite</pre><p>At this point, all that’s left is to modify the pllum-1.yaml file to use the new, modified model and 8 TPUs. Let’s save it as pllum-2.yaml:</p><pre>apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>  name: vllm-tpu<br>spec:<br>  replicas: 1<br>  selector:<br>    matchLabels:<br>      app: vllm-tpu<br>  template:<br>    metadata:<br>      labels:<br>        app: vllm-tpu<br>      annotations:<br>        gke-gcsfuse/volumes: &quot;true&quot;<br>    spec:<br>      nodeSelector:<br>        cloud.google.com/gke-tpu-topology: 2x4<br>        # Following values can be used to run v6e TPUs with a reservation.<br>        cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice<br>        cloud.google.com/reservation-affinity: &quot;specific&quot;<br>        cloud.google.com/reservation-name: RESERVATION_NAME<br>      containers:<br>      - name: vllm-tpu<br>        image: docker.io/vllm/vllm-tpu:latest<br>        command: [&quot;python3&quot;, &quot;-m&quot;, &quot;vllm.entrypoints.openai.api_server&quot;]<br>        args:<br>        - --host=0.0.0.0<br>        - --port=8000<br>        - --tensor-parallel-size=8<br>        - --max-model-len=4096<br>        - --model=/models/Llama-PLLuM-8B-instruct-2512-padded<br>        - --served-model-name=CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512<br>        - --max-num-batched-tokens=512<br>        - --max-num-seqs=128<br>        env:<br>        - name: HUGGING_FACE_HUB_TOKEN<br>          valueFrom:<br>            secretKeyRef:<br>              name: hf-secret<br>              key: hf_api_token<br>        ports:<br>        - containerPort: 8000<br>        resources:<br>          limits:<br>            google.com/tpu: 8<br>        readinessProbe:<br>          tcpSocket:<br>            port: 8000<br>          initialDelaySeconds: 1800<br>          periodSeconds: 60<br>          failureThreshold: 10<br>          timeoutSeconds: 5<br>        volumeMounts:<br>        - name: dshm<br>          mountPath: /dev/shm<br>        - name: gcs-fuse-csi-ephemeral<br>          mountPath: /models<br>          readOnly: true<br>      volumes:<br>      - name: dshm<br>        emptyDir:<br>          medium: Memory<br>      - name: gcs-fuse-csi-ephemeral<br>        csi:<br>          driver: gcsfuse.csi.storage.gke.io<br>          readOnly: true<br>          volumeAttributes:<br>            bucketName: padded-pllum-model<br>            mountOptions: &quot;implicit-dirs&quot;<br>---<br>apiVersion: v1<br>kind: Service<br>metadata:<br>  name: vllm-service<br>spec:<br>  selector:<br>    app: vllm-tpu<br>  type: LoadBalancer<br>  ports:<br>    - name: http<br>      protocol: TCP<br>      port: 8000<br>      targetPort: 8000</pre><p>Once again, remember to replace <em>RESERVATION_NAME</em> with the proper name of your TPU reservation. Then apply the new YAML with kubectl apply -f pllum-2.yaml. Make sure you trigger the recreation of the vLLM pod by manually deleting it with: kubectl delete pod -l app=vllm-tpu.</p><p>You can verify that it works by again using a simple curl command:</p><pre># Get the external ip from: kubectl get svc<br><br>curl http://${EXTERNAL_IP}:8000/v1/completions \<br>    -H &quot;Content-Type: application/json&quot; \<br>    -d &#39;{<br>        &quot;model&quot;: &quot;CYFRAGOVPL/Llama-PLLuM-8B-instruct-2512&quot;,<br>        &quot;prompt&quot;: &quot;Jak to jest być skrybą?&quot;,<br>        &quot;max_tokens&quot;: 150,<br>        &quot;temperature&quot;: 0.7<br>    }&#39;</pre><p>When you’re done with your experiments, you can delete the deployment, the cluster, and the storage bucket with the following commands:</p><pre># Delete the deployment<br>kubectl delete -f pllum-2.yaml<br><br># Delete the cluster<br>gcloud container clusters delete $CLUSTER_NAME --location=$REGION --project=$PROJECT_ID<br><br># Delete the storage bucket<br>gcloud storage rm --recursive gs://BUCKET_NAME</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=877478f77dda" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/serving-polish-large-language-model-pllum-on-tpus-877478f77dda">Serving Polish Large Language Model (PLLuM) on TPUs</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Real-Time CDC to the Open Lakehouse: Streaming Cloud SQL (PostgreSQL) Changes into Apache Iceberg…]]></title>
            <link>https://medium.com/google-cloud/real-time-cdc-to-the-open-lakehouse-streaming-cloud-sql-postgresql-changes-into-apache-iceberg-6e114861469e?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/6e114861469e</guid>
            <category><![CDATA[data-lakehouse]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[apache-iceberg]]></category>
            <category><![CDATA[cdc]]></category>
            <category><![CDATA[streaming-pipeline]]></category>
            <dc:creator><![CDATA[Muhammad Saipul Rohman]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:45:45 GMT</pubDate>
            <atom:updated>2026-10-13T11:45:43.814Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Real-Time CDC to the Open Lakehouse: Streaming Cloud SQL (PostgreSQL) Changes into Apache Iceberg on GCP</strong></h3><p><em>How to build a zero-lock-in Change Data Capture pipeline using Cloud SQL, Datastream, BigQuery Managed Iceberg Tables, and Cloud Storage — plus the real-world engineering patterns for turning Append-Only CDC logs into upserted Silver Iceberg tables.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xIk_3YvKJk5gRYhN7QZX-w.jpeg" /><figcaption><em>Figure 1: End-to-end serverless CDC architecture streaming Cloud SQL for PostgreSQL changes into BigQuery Managed Apache Iceberg tables on Google Cloud Storage via Datastream.</em></figcaption></figure><h4><strong>Introduction: The Tension Between Real-Time CDC and Open Table Formats</strong></h4><p>Modern data engineering teams usually want two things that historically required painful trade-offs:</p><p>1. <strong>Real-Time Operational Analytics (CDC)</strong>: Replicating INSERT, UPDATE, and DELETE transactions from operational PostgreSQL databases within seconds so business dashboards and AI agents never run on stale data.</p><p>2. <strong>An Open Data Lakehouse Architecture</strong>: Storing analytical tables in open formats — specifically <strong>Apache Iceberg</strong> backed by <strong>Parquet</strong> files in customer-owned <strong>Google Cloud Storage (GCS)</strong> buckets — so data is never locked into a single proprietary storage engine.</p><p>Traditionally, combining real-time PostgreSQL CDC with Apache Iceberg meant managing a complex, heavy stack: Debezium, Apache Kafka, Schema Registry, Spark Structured Streaming or Apache Flink, and custom compaction cron jobs to prevent the notorious Iceberg “small file problem.”</p><p>With <strong>Google Cloud Datastream</strong> and <strong>BigQuery Managed Tables for Apache Iceberg (BigLake)</strong>, you can now stream PostgreSQL Write-Ahead Log (WAL) changes directly into governed Apache Iceberg tables in your own GCS bucket — completely serverless.</p><p>In this hands-on guide, we will build this pipeline end-to-end on Google Cloud and solve three real-world engineering nuances you will encounter in production:</p><ul><li>Configuring Cloud SQL for PostgreSQL logical decoding and Datastream blmtConfig (BigLake Managed Tables) via the gcloud CLI.</li><li>Understanding why Datastream writes to Apache Iceberg in <strong>Append-Only mode</strong> (and how its datastream_metadata struct differs from native BigQuery Merge tables).</li><li>Implementing both a <strong>Zero-Latency Deduplicated View</strong> and an atomic <strong>ACID </strong><strong>MERGE INTO Silver Iceberg Table</strong> to handle row-level updates and deletes.</li></ul><h4><strong>Architecture Overview</strong></h4><p>Our pipeline consists of four layers:</p><p>1. <strong>Operational Source (OLTP): </strong>A <strong>Cloud SQL for PostgreSQL 15</strong> instance configured with cloudsql.logical_decoding=on, a PUBLICATION, and a pgoutput logical replication slot.</p><p>2. <strong>Serverless CDC Ingestion:</strong> <strong>Google Cloud Datastream</strong> reads the PostgreSQL WAL stream and writes via the BigQuery Storage Write API.</p><p>3. <strong>Bronze Open Lakehouse Layer (</strong><strong>lakehouse_bronze.public_orders):</strong> Configured via Datastream’s blmtConfig (tableFormat: ICEBERG, fileFormat: PARQUET), Datastream lands every change event (INSERT, UPDATE-INSERT, UPDATE-DELETE, DELETE) into an Apache Iceberg table stored in our GCS bucket via a <strong>BigQuery Cloud Resource Connection</strong>.</p><p>4. <strong>Silver Current-State Layer (</strong><strong>lakehouse_silver):</strong></p><ul><li><strong>Pattern A (</strong><strong>v_orders_current):</strong> A real-time SQL View using QUALIFY ROW_NUMBER() that resolves the latest state per primary key on the fly.</li><li><strong>Pattern B (</strong><strong>lakehouse_silver.orders):</strong> A materialized Apache Iceberg table updated via BigQuery’s native ACID MERGE INTO DML, with exported Iceberg V2 metadata (metadata.json &amp; Avro manifest lists) ready for Apache Spark, Trino, or PyIceberg.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GwixSanwOLUqjFn32TNW4g.jpeg" /><figcaption><em>End-to-end CDC transaction lifecycle from Cloud SQL PostgreSQL WAL to Bronze and Silver Apache Iceberg tables on Google Cloud Storage.</em></figcaption></figure><p><strong>Step 1: Provisioning the Open Lakehouse Foundation &amp; Cloud SQL</strong></p><p>First, let’s define our environment variables and enable the required Google Cloud APIs (sqladmin, datastream, bigquery, bigqueryconnection, biglake, and storage):</p><pre>export PROJECT_ID=&quot;your-gcp-project-id&quot;<br>export REGION=&quot;us-central1&quot;<br>export GCS_BUCKET=&quot;${PROJECT_ID}-iceberg-lakehouse&quot;<br>export BQ_CONNECTION_ID=&quot;iceberg-gcs-connection&quot;<br>export BQ_BRONZE_DATASET=&quot;lakehouse_bronze&quot;<br>export BQ_SILVER_DATASET=&quot;lakehouse_silver&quot;<br>export CLOUDSQL_INSTANCE=&quot;pg-cdc-source&quot;<br>export PG_DATABASE=&quot;ecommerce&quot;<br>export PG_USER=&quot;postgres&quot;<br>export PG_PASSWORD=&quot;YourStrongPassword2026!&quot;</pre><h4><strong>1.1 Create the GCS Bucket &amp; BigQuery Cloud Resource Connection</strong></h4><p>BigQuery Managed Iceberg tables require a **Cloud Resource Connection** whose service account has `roles/storage.admin` on your GCS bucket:</p><pre># Create the GCS bucket for Apache Iceberg data and metadata<br>gcloud storage buckets create &quot;gs://${GCS_BUCKET}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - location=&quot;${REGION}&quot; \<br> - uniform-bucket-level-access<br># Create the BigQuery Cloud Resource Connection<br>bq mk - connection \<br> - connection_type=CLOUD_RESOURCE \<br> - project_id=&quot;${PROJECT_ID}&quot; \<br> - location=&quot;${REGION}&quot; \<br>&quot;${BQ_CONNECTION_ID}&quot;<br># Retrieve the Connection&#39;s Service Account and grant Storage Admin on the bucket<br>BQ_CONN_SA=$(bq show - format=json - connection &quot;${PROJECT_ID}.${REGION}.${BQ_CONNECTION_ID}&quot; \<br>| python3 -c &quot;import sys, json; print(json.load(sys.stdin)[&#39;cloudResource&#39;][&#39;serviceAccountId&#39;])&quot;)<br>gcloud storage buckets add-iam-policy-binding &quot;gs://${GCS_BUCKET}&quot; \<br> - member=&quot;serviceAccount:${BQ_CONN_SA}&quot; \<br> - role=&quot;roles/storage.admin&quot; \<br> - project=&quot;${PROJECT_ID}&quot;<br># Create Bronze and Silver datasets in BigQuery<br>bq - location=&quot;${REGION}&quot; mk - dataset &quot;${PROJECT_ID}:${BQ_BRONZE_DATASET}&quot;<br>bq - location=&quot;${REGION}&quot; mk - dataset &quot;${PROJECT_ID}:${BQ_SILVER_DATASET}&quot;</pre><h4><strong>1.2 Create the Cloud SQL for PostgreSQL Instance with Logical Decoding</strong></h4><blockquote><strong><em>Engineering Gotcha #1:</em></strong><em> On Cloud SQL for PostgreSQL, the database flag that sets </em><em>wal_level = logical is named </em><strong><em>cloudsql.logical_decoding=on</em></strong><em> (not </em><em>cloudsql.logical_replication=on, which is used by AlloyDB).</em></blockquote><p>We also dynamically fetch Datastream’s regional static IPs so Datastream can connect to Cloud SQL:</p><pre>DS_IPS=$(gcloud datastream locations fetch-static-ips &quot;${REGION}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - format=&quot;value(staticIps)&quot; | tr &#39;;&#39; &#39;,&#39; | tr &#39;\n&#39; &#39;,&#39; | sed &#39;s/,$//&#39;)<br>gcloud sql instances create &quot;${CLOUDSQL_INSTANCE}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - database-version=POSTGRES_15 \<br> - edition=ENTERPRISE \<br> - tier=db-custom-2–7680 \<br> - region=&quot;${REGION}&quot; \<br> - root-password=&quot;${PG_PASSWORD}&quot; \<br> - database-flags=cloudsql.logical_decoding=on,max_replication_slots=10,max_wal_senders=10 \<br> - authorized-networks=&quot;${DS_IPS}&quot; \<br> - storage-type=SSD \<br> - storage-size=20GB<br>gcloud sql databases create &quot;${PG_DATABASE}&quot; \<br> - instance=&quot;${CLOUDSQL_INSTANCE}&quot; \<br> - project=&quot;${PROJECT_ID}&quot;</pre><h4><strong>Step 2: Preparing PostgreSQL Publication &amp; Logical Replication Slot</strong></h4><p>Next, connect to the ecommerce database on Cloud SQL and create an operational public.orders table, insert 5 initial seed rows, create a PostgreSQL publication, and initialize a pgoutput logical replication slot:</p><pre>- 1. Create the operational `orders` table<br>CREATE TABLE IF NOT EXISTS public.orders (<br>order_id VARCHAR(20) PRIMARY KEY,<br>customer_id VARCHAR(50) NOT NULL,<br>product_category VARCHAR(50) NOT NULL,<br>order_status VARCHAR(30) NOT NULL,<br>total_amount NUMERIC(12, 2) NOT NULL,<br>shipping_city VARCHAR(50) NOT NULL,<br>updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP<br>);<br> - 2. Configure REPLICA IDENTITY DEFAULT so primary keys are logged on UPDATE/DELETE<br>ALTER TABLE public.orders REPLICA IDENTITY DEFAULT;<br> - 3. Insert 5 seed orders (to test Datastream initial backfill into Iceberg)<br>INSERT INTO public.orders (order_id, customer_id, product_category, order_status, total_amount, shipping_city, updated_at)<br>VALUES<br>(&#39;ORD-1001&#39;, &#39;CUST-01&#39;, &#39;Electronics&#39;, &#39;PENDING&#39;, 450.00, &#39;Jakarta&#39;, NOW() - INTERVAL &#39;2 hours&#39;),<br>(&#39;ORD-1002&#39;, &#39;CUST-02&#39;, &#39;Home Office&#39;, &#39;PROCESSING&#39;, 180.50, &#39;Surabaya&#39;, NOW() - INTERVAL &#39;90 minutes&#39;),<br>(&#39;ORD-1003&#39;, &#39;CUST-03&#39;, &#39;Books&#39;, &#39;PENDING&#39;, 35.00, &#39;Bandung&#39;, NOW() - INTERVAL &#39;60 minutes&#39;),<br>(&#39;ORD-1004&#39;, &#39;CUST-04&#39;, &#39;Gaming&#39;, &#39;SHIPPED&#39;, 890.00, &#39;Singapore&#39;, NOW() - INTERVAL &#39;30 minutes&#39;),<br>(&#39;ORD-1005&#39;, &#39;CUST-05&#39;, &#39;Audio&#39;, &#39;DELIVERED&#39;, 220.00, &#39;Jakarta&#39;, NOW() - INTERVAL &#39;15 minutes&#39;)<br>ON CONFLICT (order_id) DO NOTHING;<br> - 4. Grant replication role and create Publication + Replication Slot<br>ALTER USER postgres WITH REPLICATION;<br>CREATE PUBLICATION datastream_iceberg_pub FOR TABLE public.orders;<br>SELECT pg_create_logical_replication_slot(&#39;datastream_iceberg_slot&#39;, &#39;pgoutput&#39;);</pre><h4><strong>Step 3: Configuring Datastream for BigQuery Apache Iceberg Tables (</strong><strong>blmtConfig)</strong></h4><p>Now we create the Datastream Source and Destination Connection Profiles:</p><pre>PG_PUBLIC_IP=$(gcloud sql instances describe &quot;${CLOUDSQL_INSTANCE}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - format=&quot;value(ipAddresses[0].ipAddress)&quot;)<br>gcloud datastream connection-profiles create pg-source-profile \<br> - location=&quot;${REGION}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - display-name=&quot;pg-source-profile&quot; \<br> - type=POSTGRESQL \<br> - postgresql-hostname=&quot;${PG_PUBLIC_IP}&quot; \<br> - postgresql-port=5432 \<br> - postgresql-username=&quot;${PG_USER}&quot; \<br> - postgresql-password=&quot;${PG_PASSWORD}&quot; \<br> - postgresql-database=&quot;${PG_DATABASE}&quot; \<br> - static-ip-connectivity<br>gcloud datastream connection-profiles create bq-iceberg-dest-profile \<br> - location=&quot;${REGION}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - display-name=&quot;bq-iceberg-dest-profile&quot; \<br> - type=BIGQUERY \<br> - static-ip-connectivity</pre><h4><strong>Configuring </strong><strong>blmtConfig (BigLake Managed Tables for Apache Iceberg)</strong></h4><p>To instruct Datastream to create and stream into <strong>Apache Iceberg tables</strong> on GCS rather than proprietary BigQuery native storage, we pass a blmtConfig block inside bigquery_iceberg_destination_config.json.</p><blockquote><strong><em>Engineering Gotcha #2:</em></strong><em> When configuring </em><em>blmtConfig via the Datastream API/CLI:</em></blockquote><blockquote><em>1. </em><em>connectionName must follow the compact </em><em>{project}.{location}.{name} format (not the full </em><em>projects/…/locations/…/connections/… resource URI).</em></blockquote><blockquote><em>2. </em><em>rootPath must begin with a leading slash </em><em>/ (e.g., </em><em>&quot;/bronze&quot;).</em></blockquote><blockquote><em>3. Streaming into Apache Iceberg tables requires </em><em>&quot;appendOnly&quot;: {} write mode.</em></blockquote><pre>{<br>  &quot;singleTargetDataset&quot;: {<br>    &quot;datasetId&quot;: &quot;YOUR_PROJECT_ID:lakehouse_bronze&quot;<br>  },<br>  &quot;appendOnly&quot;: {},<br>  &quot;blmtConfig&quot;: {<br>    &quot;bucket&quot;: &quot;YOUR_PROJECT_ID-iceberg-lakehouse&quot;,<br>    &quot;rootPath&quot;: &quot;/bronze&quot;,<br>    &quot;connectionName&quot;: &quot;YOUR_PROJECT_ID.us-central1.iceberg-gcs-connection&quot;,<br>    &quot;fileFormat&quot;: &quot;PARQUET&quot;,<br>    &quot;tableFormat&quot;: &quot;ICEBERG&quot;<br>  }<br>}</pre><p>Create and start the stream:</p><pre>gcloud datastream streams create pg-to-iceberg-cdc-stream \<br> - location=&quot;${REGION}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - display-name=&quot;pg-to-iceberg-cdc-stream&quot; \<br> - source=&quot;pg-source-profile&quot; \<br> - postgresql-source-config=&quot;configs/postgresql_source_config.json&quot; \<br> - destination=&quot;bq-iceberg-dest-profile&quot; \<br> - bigquery-destination-config=&quot;configs/bigquery_iceberg_destination_config.json&quot; \<br> - backfill-all<br># Once the stream reaches NOT_STARTED state, start it:<br>gcloud datastream streams update pg-to-iceberg-cdc-stream \<br> - location=&quot;${REGION}&quot; \<br> - project=&quot;${PROJECT_ID}&quot; \<br> - state=RUNNING \<br> - update-mask=state</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o_-y7cqxoaeuTQANJSh0gA.png" /><figcaption><em>Figure 2: Google Cloud Datastream configured to stream PostgreSQL WAL changes directly into BigQuery Apache Iceberg tables on Cloud Storage in Append-Only mode.</em></figcaption></figure><h4><strong>Step 4: Simulating Real-Time OLTP Changes (</strong><strong>INSERT, </strong><strong>UPDATE, </strong><strong>DELETE)</strong></h4><p>Once the initial 5 seed rows are backfilled into lakehouse_bronze.public_orders, let’s simulate real-time operational transactions in PostgreSQL:</p><ul><li><strong>INSERT</strong> 2 new orders (ORD-1006 and ORD-1007)</li><li><strong>UPDATE</strong> ORD-1001 (PENDING → SHIPPED) and ORD-1002 (PROCESSING → DELIVERED, amount updated to 195.00)</li><li><strong>DELETE</strong> cancelled order ORD-1003</li></ul><pre>INSERT INTO public.orders (order_id, customer_id, product_category, order_status, total_amount, shipping_city, updated_at)<br>VALUES<br>(&#39;ORD-1006&#39;, &#39;CUST-06&#39;, &#39;Smartphones&#39;, &#39;PENDING&#39;, 699.00, &#39;Jakarta&#39;, NOW()),<br>(&#39;ORD-1007&#39;, &#39;CUST-07&#39;, &#39;Wearables&#39;, &#39;PAID&#39;, 149.90, &#39;Medan&#39;, NOW());<br>UPDATE public.orders<br>SET order_status = &#39;SHIPPED&#39;, updated_at = NOW()<br>WHERE order_id = &#39;ORD-1001&#39;;<br>UPDATE public.orders<br>SET order_status = &#39;DELIVERED&#39;, total_amount = 195.00, updated_at = NOW()<br>WHERE order_id = &#39;ORD-1002&#39;;<br>DELETE FROM public.orders<br>WHERE order_id = &#39;ORD-1003&#39;;</pre><h4><strong>Inspecting the Bronze Apache Iceberg Change Log in BigQuery</strong></h4><blockquote><strong><em>Engineering Gotcha #3:</em></strong><em> Unlike Datastream’s </em><em>MERGE mode (which adds a boolean </em><em>datastream_metadata.is_deleted field), Datastream’s </em><strong><em>APPEND_ONLY mode for Apache Iceberg</em></strong><em> uses the schema:</em></blockquote><blockquote><em>STRUCT&lt;uuid STRING, source_timestamp INT64, change_sequence_number STRING, change_type STRING, sort_keys ARRAY&lt;STRING&gt;&gt;.</em></blockquote><blockquote><em>Every mutation is recorded via </em><strong><em>datastream_metadata.change_type</em></strong><em> (</em><em>INSERT, </em><em>UPDATE-INSERT, </em><em>UPDATE-DELETE, or </em><em>DELETE).</em></blockquote><p>Let’s query our Bronze Iceberg table:</p><pre>SELECT<br>order_id,<br>customer_id,<br>order_status,<br>total_amount,<br>datastream_metadata.change_type,<br>TIMESTAMP_MICROS(datastream_metadata.source_timestamp) AS source_ts,<br>datastream_metadata.change_sequence_number AS csn<br>FROM `YOUR_PROJECT_ID.lakehouse_bronze.public_orders`<br>ORDER BY order_id, datastream_metadata.source_timestamp DESC, csn DESC;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bUlgC4KFVLFVbyG-XcpzCQ.png" /><figcaption><em>Figure 3: Querying the Bronze Apache Iceberg table (</em><em>lakehouse_bronze.public_orders) in BigQuery, showing the immutable CDC event log (INSERT, UPDATE-INSERT, UPDATE-DELETE, and DELETE) captured in datastream_metadata.</em></figcaption></figure><h4><strong>Step 5: Turning Append-Only Bronze into an Upserted Silver Iceberg Table</strong></h4><p>Because our Bronze Iceberg table is an immutable append-only change log, downstream BI tools and AI agents need a clean, deduplicated <strong>Current State (Silver)</strong> layer where updates overwrite older versions and DELETE events remove deleted records.</p><p>Here are the two production patterns you can use on Google Cloud:</p><h4><strong>Pattern A: Zero-Latency Real-Time Deduplicated View</strong></h4><p>If you want zero replication delay between Bronze and Silver without storing a second copy of the data, create a view using QUALIFY ROW_NUMBER():</p><pre>CREATE OR REPLACE VIEW `YOUR_PROJECT_ID.lakehouse_silver.v_orders_current` AS<br>SELECT<br>order_id,<br>customer_id,<br>product_category,<br>order_status,<br>total_amount,<br>shipping_city,<br>updated_at,<br>TIMESTAMP_MICROS(datastream_metadata.source_timestamp) AS last_cdc_timestamp,<br>datastream_metadata.change_type AS last_change_type<br>FROM `YOUR_PROJECT_ID.lakehouse_bronze.public_orders`<br>WHERE datastream_metadata.change_type != &#39;UPDATE-DELETE&#39;<br>QUALIFY ROW_NUMBER() OVER (<br>PARTITION BY order_id<br>ORDER BY<br>datastream_metadata.source_timestamp DESC,<br>datastream_metadata.change_sequence_number DESC<br>) = 1<br>AND datastream_metadata.change_type != &#39;DELETE&#39;;</pre><h4><strong>Pattern B: Materialized Silver Apache Iceberg Table via ACID </strong><strong>MERGE INTO</strong></h4><p>When external engines (like <strong>Apache Spark</strong>, <strong>Trino</strong>, or <strong>PyIceberg</strong>) need to read the curated Silver table directly from GCS without executing a BigQuery SQL view, or when your Bronze log grows to hundreds of millions of events, materialize a <strong>Silver Managed Iceberg Table</strong> and run an incremental MERGE INTO:</p><pre>-- 1. Create the Silver Managed Apache Iceberg Table on GCS<br>CREATE TABLE IF NOT EXISTS `YOUR_PROJECT_ID.lakehouse_silver.orders` (<br>order_id STRING,<br>customer_id STRING,<br>product_category STRING,<br>order_status STRING,<br>total_amount NUMERIC,<br>shipping_city STRING,<br>updated_at TIMESTAMP,<br>last_cdc_timestamp TIMESTAMP<br>)<br>CLUSTER BY order_id<br>WITH CONNECTION `YOUR_PROJECT_ID.us-central1.iceberg-gcs-connection`<br>OPTIONS (<br>file_format = &#39;PARQUET&#39;,<br>table_format = &#39;ICEBERG&#39;,<br>storage_uri = &#39;gs://YOUR_PROJECT_ID-iceberg-lakehouse/silver/orders&#39;<br>);<br> <br>-- 2. Execute Atomic ACID MERGE INTO (Upserts + Physical Deletes)<br>MERGE `YOUR_PROJECT_ID.lakehouse_silver.orders` AS T<br>USING (<br>SELECT<br>order_id,<br>customer_id,<br>product_category,<br>order_status,<br>total_amount,<br>shipping_city,<br>updated_at,<br>TIMESTAMP_MICROS(datastream_metadata.source_timestamp) AS source_ts,<br>datastream_metadata.change_type AS change_type<br>FROM `YOUR_PROJECT_ID.lakehouse_bronze.public_orders`<br>WHERE datastream_metadata.change_type != &#39;UPDATE-DELETE&#39;<br>QUALIFY ROW_NUMBER() OVER (<br>PARTITION BY order_id<br>ORDER BY<br>datastream_metadata.source_timestamp DESC,<br>datastream_metadata.change_sequence_number DESC<br>) = 1<br>) AS S<br>ON T.order_id = S.order_id<br>WHEN MATCHED AND S.change_type = &#39;DELETE&#39; THEN<br>DELETE<br>WHEN MATCHED THEN<br>UPDATE SET<br>customer_id = S.customer_id,<br>product_category = S.product_category,<br>order_status = S.order_status,<br>total_amount = S.total_amount,<br>shipping_city = S.shipping_city,<br>updated_at = S.updated_at,<br>last_cdc_timestamp = S.source_ts<br>WHEN NOT MATCHED AND S.change_type != &#39;DELETE&#39; THEN<br>INSERT (<br>order_id, customer_id, product_category, order_status,<br>total_amount, shipping_city, updated_at, last_cdc_timestamp<br>)<br>VALUES (<br>S.order_id, S.customer_id, S.product_category, S.order_status,<br>S.total_amount, S.shipping_city, S.updated_at, S.source_ts<br>);<br><br>-- 3. Export Iceberg Metadata Snapshot for Multi-Engine Readers (Spark / PyIceberg)<br>EXPORT TABLE METADATA FROM `YOUR_PROJECT_ID.lakehouse_silver.orders`;</pre><p>Querying lakehouse_silver.orders now returns the exact 6 active orders matching Cloud SQL (ORD-1001 as SHIPPED, ORD-1002 as DELIVERED at 195.00, ORD-1003 deleted, and ORD-1006/ORD-1007 inserted)!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zYHOT7ty70qxc4wC5KHdpA.png" /><figcaption><em>Figure 4: The curated Silver Apache Iceberg table (lakehouse_silver.orders) after atomic ACID MERGE INTO, reflecting updated order statuses (ORD-1001, ORD-1002), newly inserted orders (ORD-1006, ORD-1007), and physical removal of deleted order ORD-1003.</em></figcaption></figure><h4><strong>Verifying Physical Open Parquet &amp; Iceberg Metadata Files in Cloud Storage</strong></h4><p>Finally, let’s verify that both our Bronze and Silver tables are stored as genuine open-format <strong>Apache Iceberg</strong> tables inside our customer-owned GCS bucket (gs://${GCS_BUCKET}/):</p><pre>gcloud storage ls - recursive &quot;gs://${GCS_BUCKET}/&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Lq58SQ1KqQOF2yDp60c5tA.png" /><figcaption><em>Figure 5: Inside our Google Cloud Storage bucket (`gs://…/bronze` and `gs://…/silver/orders`), verifying open Parquet data files and exported Apache Iceberg V2 metadata snapshots.</em></figcaption></figure><h4><strong>When to Use Pattern A (View) vs. Pattern B (Materialized Iceberg </strong><strong>MERGE)</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2OGfHH8e_GFrAjjCkB-KRw.jpeg" /></figure><h4><strong>Conclusion</strong></h4><p>By combining <strong>Cloud SQL for PostgreSQL</strong>, <strong>Google Cloud Datastream</strong>, and <strong>BigQuery Managed Tables for Apache Iceberg</strong>, you get the best of both worlds: a zero-maintenance, serverless CDC pipeline and an open Parquet/Iceberg lakehouse stored in your own Cloud Storage bucket.</p><p>You can find all the complete, reproducible bash and SQL scripts for this tutorial in my GitHub repository:</p><p>- <strong>GitHub Repository:</strong> <a href="https://github.com/saipulrx/cdc_lakehouse_gcp">https://github.com/saipulrx/cdc_lakehouse_gcp</a></p><p><em>Built for the Google Cloud Lakehouse/Iceberg Content Sprint 2026</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e114861469e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/real-time-cdc-to-the-open-lakehouse-streaming-cloud-sql-postgresql-changes-into-apache-iceberg-6e114861469e">Real-Time CDC to the Open Lakehouse: Streaming Cloud SQL (PostgreSQL) Changes into Apache Iceberg…</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Should You Give Your AI Coding Agent Access to Your Production Database?]]></title>
            <link>https://medium.com/google-cloud/should-you-give-your-ai-coding-agent-access-to-your-production-database-3758d83dfe12?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/3758d83dfe12</guid>
            <category><![CDATA[database]]></category>
            <category><![CDATA[ai-coding-agent]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Martin Omander]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:44:36 GMT</pubDate>
            <atom:updated>2026-10-13T11:44:34.752Z</atom:updated>
            <content:encoded><![CDATA[<h4>AI coding agents are the latest superpower in a developer’s toolkit. It’s tempting to hand them the keys to our live database so they can diagnose production bugs. Should you?</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q8E0HFKEHqVuVKQdz-HoUA.png" /></figure><p>The short answer is: <strong>Absolutely not!</strong></p><p>Allowing an AI to interact directly with your production data is a disaster waiting to happen. Here’s why, and how you can get the benefit of an AI using a database while keeping your data safe.</p><h3>The Dual Nightmares: Hallucinations and Compliance</h3><p>When you give an AI agent direct access to your live database, you open yourself up to two risks:</p><h4>1. The Rogue Deletion Command</h4><p>AI models are highly capable, but they are also prone to hallucinations. An agent trying to resolve a simple schema issue might misinterpret a prompt and generate a destructive query. One unintended bulk deletion or DROP TABLE command later, and your entire production database is history. Reddit is full of horror stories from vibe coders who lost production data.</p><blockquote><strong><em>Giving an AI agent production write-access is like writing critical code and just leaving a comment:</em></strong></blockquote><blockquote><strong><em>// Please do not crash!</em></strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/988/1*PLVHI3H4tm_dpLeTb8UkMg.png" /></figure><h4>2. The Privacy and Regulatory Trap</h4><p>Even if your agent only has read-only access, feeding live personal data (PII) to an AI model can violate laws like GDPR, HIPAA, or CCPA. Passing real names, email addresses, and financial transactions to an external API is an easy way to trigger compliance audits and eye-watering fines.</p><blockquote><strong><em>Passing raw customer PII to an external AI API is the fastest way to turn your database into a GDPR horror story.</em></strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/968/1*2-_ZDPJOtqkdoBT77RWZcA.png" /></figure><h3>The Clean Pattern: A Safe Sandbox</h3><p>Fortunately, the fix is simple. You don’t have to lock your AI agent in a closet. Instead, you can give it access to a development database.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JrZKp7wG-V_uRF-sz4XTgA.png" /></figure><p>To make the data realistic you can copy production data into the development database, as long as you have a script that anonymizes any personal data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dvmGeVFgCaUZWZolkXkR6Q.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hxa64v7UKjuf2M3ifPgfrg.png" /></figure><p>And Bob’s your uncle! Lower risk, greater security, and no more sleepless nights.</p><h3>Over to You!</h3><p>Take a look at your cloud console and revoke production access for your agents today.</p><p>Do you let your coding agent read or write to a database? What has worked well for you?</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3758d83dfe12" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/should-you-give-your-ai-coding-agent-access-to-your-production-database-3758d83dfe12">Should You Give Your AI Coding Agent Access to Your Production Database?</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Grounded Agentic Analytics in BigQuery: Augmented Analytics TVFs, Knowledge Catalog, and…]]></title>
            <link>https://medium.com/google-cloud/grounded-agentic-analytics-in-bigquery-augmented-analytics-tvfs-knowledge-catalog-and-f92df6bd17e4?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/f92df6bd17e4</guid>
            <category><![CDATA[bqml]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[bigquery]]></category>
            <category><![CDATA[analytics]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Rajdip Chaudhuri]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:42:48 GMT</pubDate>
            <atom:updated>2026-10-13T11:42:47.635Z</atom:updated>
            <content:encoded><![CDATA[<h3>Grounded Agentic Analytics in BigQuery: Augmented Analytics TVFs, Knowledge Catalog, and Conversational Analytics</h3><blockquote>Running change-point detection, key-driver attribution, and counterfactual impact analysis inside BigQuery on CFPB complaints, and what it actually took to stop a conversational agent from guessing.</blockquote><p>Every Monday in a consumer bank’s Dispute &amp; Fraud Risk Operations center, someone in leadership asks a version of the same three questions:</p><ol><li><em>“Are Regulation E disputes on our deposit, prepaid, wallet, and money-transfer products growing structurally, or are we just looking at normal weekly and seasonal cycles? And what is driving our handling cost?”</em></li><li><em>“When Reg E disputes spiked, when did that surge actually start and end, and which combination of product, sub-product, issue, channel, and state drove it?”</em></li><li><em>“We tightened our fraud controls on a given date. Did that change actually reduce disputes and cost, or was volume already moving on its own?”</em></li></ol><p>In most banks, answering those questions sets off a slow relay race between BI analysts, data scientists, and governance teams.</p><p>First, an analyst builds a GROUP BY dashboard. Standard SQL aggregations are great at counting rows, but they cannot separate a multi-week regime shift from a routine Monday rebound, and they cannot separate the effect of a policy change from a trend that was already under way. So the dashboard reports an eyeballed spike window and a raw before/after average. Next, a data scientist exports millions of regulated complaint rows to a Python notebook to run ruptures, custom slice-attribution code, and CausalImpact. Meanwhile, an executive opens a text-to-SQL assistant directly, and the assistant has to guess what legacy column names like reg_e_elig_flg, cust_esc_tier2_flg, and mon_rel_ind mean.</p><p>That workflow breaks down in three places:</p><ol><li><strong>Regulated data leaves the warehouse just to run basic statistics.</strong> Exporting dispute tables to run change-point detection or counterfactual time-series models adds egress, notebook drift, and extra security reviews.</li><li><strong>Manual slicing turns into </strong><strong>GROUP BY CUBE spaghetti.</strong> Finding which combination of Product × Sub-product × Issue × Channel × State drove a surge means sifting through dozens of overlapping parent and child slices.</li><li><strong>Naive before/after averages get causal questions backwards.</strong> If dispute volume was already climbing before an intervention date, comparing AVG(daily_disputes) before and after that date confuses the pre-existing trend with the effect of your rollout. In the 2020 CFPB data below, a plain pre/post average says disputes went <strong>up 22.5%</strong>, while a proper counterfactual shows no statistically significant portfolio effect at all (p = 0.59).</li></ol><p>I wanted to see how far we could push this entire workflow <strong>inside BigQuery</strong> without exporting a single row to Python.</p><p>This post walks through what I built and tested on public CFPB complaints (2018–2022):</p><ul><li><strong>Six </strong><a href="https://cloud.google.com/blog/products/data-analytics/bigquery-augmented-analytics-tvfs"><strong>BigQuery Augmented Analytics Table-Valued Functions (TVFs)</strong></a> (ML.TREND, ML.SEASONALITY, ML.CORRELATION, ML.DETECT_CHANGE_POINTS, AI.KEY_DRIVERS, and AI.CAUSAL_EFFECT) chained together in GoogleSQL, scanning about <strong>1.9 GiB</strong> in total.</li><li><strong>Knowledge Catalog</strong> column descriptions, profiling scans, and a 6-term Business Glossary linked to 13 physical columns across two tables.</li><li><strong>BigQuery Conversational Analytics (BQCA)</strong> configured with domain instructions and three Verified Queries, tested side by side against an ungrounded agent on the exact same question, and audited through region-us.INFORMATION_SCHEMA.JOBS.</li></ul><blockquote><em>🔗 </em><strong><em>Code, SQL scripts, and Console runbook:</em></strong><em> </em><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs"><strong><em>github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs</em></strong></a></blockquote><h3>How the Pieces Fit Together</h3><p>The core idea is simple: keep <strong>semantic interpretation</strong> in the catalog and agent layer, and keep <strong>statistical math</strong> inside BigQuery’s SQL engine.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ScHIIwEwt4Sy-wJRpqG3wQ.png" /></figure><p>Three practical rules govern how this works:</p><ol><li><strong>The agent writes SQL; the TVFs do the math.</strong> An LLM should never estimate p-values or invent attribution logic in token space. Its job is to resolve banking terms and emit SQL that calls BigQuery’s statistical TVFs where the rows already live.</li><li><strong>Definitions live in Knowledge Catalog, not in someone’s scratchpad.</strong> Core-banking schemas are full of cryptic abbreviations. Enriched column descriptions and a Business Glossary (FinServ_Risk_Glossary) attach regulatory definitions directly to the physical columns.</li><li><strong>Verified Queries lock in the statistical method for high-stakes questions.</strong> As we will see later, BigQuery’s conversational agent already knows the syntax of AI.KEY_DRIVERS. What it does <em>not</em> know on its own is your team&#39;s analytical methodology—such as finding the surge window with ML.DETECT_CHANGE_POINTS before slicing drivers, or using AI.CAUSAL_EFFECT instead of a before/after average.</li></ol><p>Here is the end-to-end flow across the six SQL scripts in the repository:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tzfgmaYCUmhJ0qDh0_i8qQ.png" /></figure><h3>When to Use These TVFs (and When I’d Still Use Python)</h3><p>Whenever a data warehouse adds statistical functions in SQL, the fair question is: <em>why not just pull the aggregated daily series into a Python notebook?</em></p><p>If you are doing a one-off study on an 1,800-row daily time series on your laptop, statsmodels, ruptures, and CausalImpact in Python are great. Where the in-warehouse TVFs genuinely earn their keep is when three requirements meet:</p><ul><li><strong>You want to chain time-series change-point detection directly into million-row multidimensional slicing.</strong> ML.DETECT_CHANGE_POINTS runs on the daily series, and AI.KEY_DRIVERS immediately slices the <strong>row fact table</strong> across five dimensions (prod_line_cd × sub_prod_cd × iss_cat_cd × subm_chnl_cd × jur_state_cd) with automatic redundancy pruning. Doing both inside one SQL CTE means zero data export and one IAM boundary.</li><li><strong>You want a conversational agent to use real statistical methods.</strong> A BigQuery Data Agent cannot spin up a custom Python container mid-chat to run CausalImpact when a risk officer asks a question in BigQuery Studio. It <em>can</em> execute a single SQL query that calls AI.CAUSAL_EFFECT or chains ML.DETECT_CHANGE_POINTS into AI.KEY_DRIVERS.</li><li><strong>You want to retire brittle window-function SQL.</strong> Approximating step-adjusted trend smoothing or segmented correlation matrices in handwritten SQL takes dozens of lines of window functions. A single TVF call is much easier to read and review.</li></ul><p><strong>Where I would still reach for Python instead:</strong></p><ul><li><strong>User-level or cross-sectional causal inference.</strong> AI.CAUSAL_EFFECT is a time-series counterfactual tool built around an intervention_timestamp. If you need propensity-score matching, doubly robust uplift modeling, or instrumental variables across users with staggered treatment dates, use a dedicated Python econometrics pipeline.</li><li><strong>Sub-second application serving.</strong> These TVFs fit statistical models at query time and take a few seconds to run. They are designed for interactive analytics and decision support, not 50 ms checkout APIs.</li></ul><h3>The Data (and Three Traps I Hit Building the Mart)</h3><p>To keep everything reproducible on real public data, <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/01_setup_finserv_risk_mart.sql">sql/01_setup_finserv_risk_mart.sql</a> transforms five years of the <strong>CFPB Consumer Complaint Database</strong> (bigquery-public-data.cfpb_complaints.complaint_database, 2018-01-01 to 2022-12-31, US multi-region) into two tables in finserv_risk_ops:</p><ol><li><strong>finserv_risk_ops.fct_consumer_disputes</strong> : partitioned by month on intake_dt and clustered by prod_line_cd, subm_chnl_cd, jur_state_cd. Across the five-year window, it contains <strong>182,279</strong> Reg E-scope disputes, <strong>56,746</strong> Tier-2 escalation-proxy cases, <strong>67,220</strong> monetary-relief cases, <strong>24,932</strong> SLA breaches, and <strong>$144.26M</strong> of modeled handling cost.</li><li><strong>finserv_risk_ops.agg_daily_risk_kpis</strong> : one row per intake_dt × prod_line_cd across 9 product lines, rolling up daily_dispute_vol, reg_e_dispute_vol, avg_intake_fwd_lag_days, tier2_escalation_rate_pct, monetary_relief_rate_pct, sla_breach_rate_pct, total_ops_cost_usd, and avg_ops_cost_per_dispute_usd.</li></ol><p>To mimic the tables you actually run into at a bank, sql/01 creates both tables with <strong>abbreviated column codes</strong> and <strong>blank schema descriptions</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ggJ5x9zCvDkJPhjsUNwO5A.png" /></figure><p>Building this mart on real CFPB data surfaced three data traps right away.</p><h3>Trap 1: Regulation E is not “any complaint mentioning fraud”</h3><p>Regulation E (12 CFR 1005) covers electronic fund transfers to or from consumer <strong>asset</strong> accounts. In my first draft of sql/01, I took a lazy shortcut: I flagged any complaint mentioning fraud or unauthorized transactions, plus any card or money-transfer product. That quietly swept in <strong>credit cards</strong>—which are governed by <strong>Regulation Z (Truth in Lending / Fair Credit Billing Act)</strong>, not Regulation E—along with gift cards, virtual currency, and mortgage or credit-bureau fraud complaints.</p><p>Tightening reg_e_elig_flg in <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/01_setup_finserv_risk_mart.sql">sql/01_setup_finserv_risk_mart.sql</a> to true Reg E deposit, prepaid, wallet, and money-transfer accounts cut the qualifying dispute count and changed every change-point and causal number downstream. That experience is the best argument I know for a Business Glossary: a scoping rule that specific belongs in governed metadata, not buried in a CASE WHEN block that every analyst rewrites differently.</p><h3>Trap 2: consumer_disputed looks ideal until you profile it</h3><p>The public CFPB table has a boolean column named consumer_disputed. From the schema alone, it looks like the exact flag you would use for Tier-2 customer escalations. When I profiled our 2018–2022 cohort, <strong>consumer_disputed was </strong><strong>NULL across all rows</strong>—the CFPB retired that field in April 2017.</p><p>In <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/01_setup_finserv_risk_mart.sql">sql/01</a>, I dropped consumer_disputed and defined cust_esc_tier2_flg as an explicit <strong>operational proxy</strong> (SLA breach, untimely response, or fraud/scam/unauthorized issue), and called out that proxy definition in both the column description and the Knowledge Catalog glossary.</p><h3>Trap 3: Spring 2020 breaks naive before/after comparisons</h3><p>If you pick an intervention date in mid-2020 and compare average daily disputes before vs. after that date, the spring 2020 pandemic wave poisons the baseline. Because January through April 2020 had lower volume than May and June 2020, a raw pre/post average across 2020-01-01 to 2020-10-01 claims Reg E disputes jumped <strong>+22.5%</strong> after July 1, 2020—even though volume had already plateaued before July 1. We will see how AI.CAUSAL_EFFECT handles that in Act 3.</p><h3>Running the Six TVFs Across Three Acts</h3><p>Rather than running six disconnected toy examples, <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/02_act1_baseline_trend_seasonality_correlation.sql">sql/02</a>, <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/03_act2_change_points_and_key_drivers_chain.sql">sql/03</a>, and <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/04_act3_intervention_causal_effect.sql">sql/04</a> use the six TVFs to answer the three Monday-morning questions in sequence.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eAsHgM6URUlhbCIbnY_u2g.png" /></figure><h3>Act 1: Trend, Seasonality, and Cost Correlations (ML.TREND, ML.SEASONALITY, ML.CORRELATION)</h3><p>In <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/02_act1_baseline_trend_seasonality_correlation.sql">sql/02_act1_baseline_trend_seasonality_correlation.sql</a>, <strong>Query 1A</strong> extracts the long-term trend of daily Reg E disputes using ML.TREND with adjust_step_changes =&gt; TRUE so sudden level shifts do not smear the 14-day smoothing window:</p><pre>WITH daily_reg_e AS (<br>  SELECT<br>    intake_dt,<br>    SUM(reg_e_dispute_vol) AS total_reg_e_disputes<br>  FROM finserv_risk_ops.agg_daily_risk_kpis<br>  GROUP BY intake_dt<br>)<br>SELECT<br>  intake_dt,<br>  total_reg_e_disputes,<br>  ROUND(trend, 2) AS smoothed_long_term_trend,<br>  time_series_type,<br>  status<br>FROM ML.TREND(<br>  TABLE daily_reg_e,<br>  data_col =&gt; &#39;total_reg_e_disputes&#39;,<br>  timestamp_col =&gt; &#39;intake_dt&#39;,<br>  smoothing_window_size =&gt; 14,<br>  adjust_step_changes =&gt; TRUE<br>)<br>ORDER BY intake_dt;</pre><p><strong>Query 1B</strong> runs ML.SEASONALITY (seasonalities =&gt; [&#39;Yearly&#39;, &#39;Monthly&#39;, &#39;Weekly&#39;]) on the same daily series. That weekly component turns out to be critical later: complaint intake drops sharply every Saturday and Sunday and rebounds every Monday.</p><p><strong>Query 1C</strong> runs ML.CORRELATION across agg_daily_risk_kpis to measure how strongly each operational metric correlates with daily handling cost (total_ops_cost_usd), both across the whole portfolio and within each product line:</p><pre>SELECT<br>  CASE<br>    WHEN prod_line_cd IS NULL<br>      AND NOT EXISTS (SELECT 1 FROM UNNEST(segment) s WHERE s.dimension_col = &#39;prod_line_cd&#39;)<br>      THEN &#39;ALL PRODUCT LINES (Global Aggregate)&#39;<br>    WHEN prod_line_cd IS NULL<br>      THEN &#39;UNSPECIFIED PRODUCT&#39;<br>    ELSE prod_line_cd<br>  END AS product_line_scope,<br>  target_col,<br>  corr_col AS correlated_operational_metric,<br>  ROUND(correlation, 4) AS pearson_correlation,<br>  segment_size AS days_observed,<br>  ROUND(segment_proportion * 100, 2) AS pct_of_total_observations<br>FROM ML.CORRELATION(<br>  TABLE finserv_risk_ops.agg_daily_risk_kpis,<br>  target_col =&gt; &#39;total_ops_cost_usd&#39;,<br>  target_correlation_cols =&gt; [<br>    &#39;reg_e_dispute_vol&#39;,<br>    &#39;avg_intake_fwd_lag_days&#39;,<br>    &#39;tier2_escalation_rate_pct&#39;,<br>    &#39;monetary_relief_rate_pct&#39;,<br>    &#39;sla_breach_rate_pct&#39;<br>  ],<br>  dimension_cols =&gt; [&#39;prod_line_cd&#39;],<br>  method =&gt; &#39;PEARSON&#39;<br>)<br>WHERE segment_size &gt;= 100<br>  AND NOT IS_NAN(correlation)<br>ORDER BY<br>  ARRAY_LENGTH(segment) ASC,<br>  ABS(correlation) DESC;</pre><p>Here are the key rows from BigQuery:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/663/1*soORgnN93gSZBasFLYFvAw.png" /></figure><p>Notice the contrast between the bottom row and the top two rows. Across all nine product lines pooled together, the correlation between reg_e_dispute_vol and cost is basically zero (r = 0.0445) because high-volume non-Reg E categories like credit reporting dominate total row counts. Once you pass dimension_cols =&gt; [&#39;prod_line_cd&#39;], ML.CORRELATION computes both the global rollup and every product slice in a single pass, showing r = 0.9405 in money transfers and r = 0.9141 in checking and savings. (Because total_ops_cost_usd is a modeled cost formula built from our operational flags, high correlation within Reg E products partly reflects the cost weights by construction; the useful diagnostic is where products diverge, such as Mortgage, where routing lag tracks cost at r = 0.5573.)</p><h3>Act 2: Finding the Surge and What Drove It (ML.DETECT_CHANGE_POINTS → AI.KEY_DRIVERS)</h3><p>In <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/03_act2_change_points_and_key_drivers_chain.sql">sql/03_act2_change_points_and_key_drivers_chain.sql</a>, <strong>Query 2A</strong> runs ML.DETECT_CHANGE_POINTS on the five-year daily Reg E series (overall mean: <strong>99.9 disputes/day</strong>). It identifies four distinct regimes:</p><p>regime_start_dateregime_end_dateAvg / dayMinMaxDays2020-05-252020-05-28111.5271744<strong>2021-05-042021-05-19161.1</strong>66228<strong>16</strong>2022-05-022022-05-16123.42203152022-06-252022-07-0597.25119711</p><p>Instead of copy-pasting &#39;2021-05-04&#39; and &#39;2021-05-19&#39; into a second query, <strong>Query 2B</strong> pipes the top regime from ML.DETECT_CHANGE_POINTS directly into AI.KEY_DRIVERS inside a single CTE chain. It labels rows inside the 16-day surge (2021-05-04 to 2021-05-19) as is_surge = TRUE and compares them against the immediately preceding 16 days (2021-04-18 to 2021-05-03, is_surge = FALSE):</p><pre>WITH daily_reg_e AS (<br>  SELECT<br>    TIMESTAMP(intake_dt) AS intake_ts,<br>    SUM(reg_e_dispute_vol) AS total_reg_e_disputes<br>  FROM finserv_risk_ops.agg_daily_risk_kpis<br>  GROUP BY intake_ts<br>),<br>detected_regimes AS (<br>  SELECT<br>    DATE(begin_timestamp) AS regime_start,<br>    DATE(end_timestamp) AS regime_end,<br>    metrics.avg AS regime_avg<br>  FROM ML.DETECT_CHANGE_POINTS(<br>    TABLE daily_reg_e,<br>    data_col =&gt; &#39;total_reg_e_disputes&#39;,<br>    timestamp_col =&gt; &#39;intake_ts&#39;<br>  )<br>  WHERE begin_timestamp IS NOT NULL<br>),<br>primary_surge_regime AS (<br>  SELECT<br>    regime_start,<br>    regime_end,<br>    DATE_DIFF(regime_end, regime_start, DAY) + 1 AS regime_days<br>  FROM detected_regimes<br>  ORDER BY regime_avg DESC<br>  LIMIT 1<br>),<br>dispute_comparison_window AS (<br>  SELECT<br>    f.prod_line_cd,<br>    f.sub_prod_cd,<br>    f.iss_cat_cd,<br>    f.subm_chnl_cd,<br>    f.jur_state_cd,<br>    1 AS dispute_count,<br>    f.intake_dt &gt;= r.regime_start AS is_surge<br>  FROM finserv_risk_ops.fct_consumer_disputes AS f<br>  CROSS JOIN primary_surge_regime AS r<br>  WHERE f.reg_e_elig_flg = 1<br>    AND f.intake_dt BETWEEN DATE_SUB(r.regime_start, INTERVAL r.regime_days DAY)<br>                        AND r.regime_end<br>)<br>SELECT<br>  drivers AS contributing_segment_drivers,<br>  metric_interest AS surge_window_disputes,<br>  metric_reference AS baseline_window_disputes,<br>  difference AS net_dispute_change,<br>  ROUND(relative_difference * 100, 1) AS pct_growth,<br>  ROUND(unexpected_difference, 1) AS unexpected_excess_disputes,<br>  ROUND(contribution, 1) AS absolute_contribution,<br>  ROUND(apriori_support * 100, 2) AS population_support_pct<br>FROM AI.KEY_DRIVERS(<br>  (SELECT * FROM dispute_comparison_window),<br>  metric_col =&gt; &#39;dispute_count&#39;,<br>  interest_label_col =&gt; &#39;is_surge&#39;,<br>  dimension_cols =&gt; [<br>    &#39;prod_line_cd&#39;,<br>    &#39;sub_prod_cd&#39;,<br>    &#39;iss_cat_cd&#39;,<br>    &#39;subm_chnl_cd&#39;,<br>    &#39;jur_state_cd&#39;<br>  ],<br>  top_k =&gt; 15,<br>  enable_pruning =&gt; TRUE<br>)<br>ORDER BY unexpected_difference DESC;</pre><p>Here are the top slices returned by AI.KEY_DRIVERS for the <strong>2021-05-04 → 2021-05-19</strong> surge:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/818/1*5hAJ5Word_7CG-A8g9t1Fg.png" /></figure><p>Across the entire Reg E portfolio, disputes rose by <strong>+423 (+19.6%)</strong> over the prior 16 days. <strong>Checking or savings account accounts for +424 of that net increase (+33.6%)</strong>, concentrated specifically in Checking account × Managing an account (+365 disputes, +61.0%) and Referral submissions (+49.5%). Meanwhile, money-transfer and wallet complaints barely moved (+4.4%). Passing enable_pruning =&gt; TRUE drops redundant parent/child slices so the top 15 results stay readable.</p><blockquote><em>💡 </em><strong><em>A bug I hit while writing Query 2B:</em></strong><em> In my first attempt, I hardcoded a </em><em>±60-day comparison window around </em><em>regime_start. Because the May 2021 surge only lasted 16 days, a 60-day post-window diluted those 16 high-volume days with 44 normal days and made total Reg E disputes look like they </em>fell<em> by 5.2%. Computing </em><em>DATE_DIFF(regime_end, regime_start, DAY) + 1 AS regime_days and comparing the surge against an equal-length baseline window fixed the dilution immediately.</em></blockquote><h3>Act 3: Did the July 2020 Control Change Work? (AI.CAUSAL_EFFECT vs. Naive AVG())</h3><p>Now suppose risk leadership rolled out tightened step-up authentication controls on <strong>July 1, 2020</strong> (a hypothetical intervention date overlaid on the public CFPB timeline) and wants to know whether it reduced Reg E disputes and handling cost over the next three months.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XWO9Guz3SlzTj1r0_GVgqg.png" /></figure><p><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/04_act3_intervention_causal_effect.sql">sql/04_act3_intervention_causal_effect.sql</a> runs both the naive before/after average (<strong>Query 3D</strong>) and AI.CAUSAL_EFFECT (<strong>Queries 3A, 3B, 3C</strong>) on the same 9-month window (2020-01-01 to 2020-06-30 pre-intervention baseline, 2020-07-01 to 2020-10-01 evaluation period):</p><pre>WITH daily_reg_e_window AS (<br>  SELECT<br>    TIMESTAMP(intake_dt) AS intake_ts,<br>    SUM(reg_e_dispute_vol) AS total_reg_e_disputes<br>  FROM finserv_risk_ops.agg_daily_risk_kpis<br>  WHERE intake_dt BETWEEN &#39;2020-01-01&#39; AND &#39;2020-10-01&#39;<br>  GROUP BY intake_ts<br>)<br>SELECT<br>  ROUND(absolute_effect, 0) AS cumulative_incremental_disputes,<br>  ROUND(relative_effect * 100, 2) AS relative_effect_pct,<br>  ROUND(prob_causal_effect * 100, 2) AS causal_confidence_pct,<br>  ROUND(p_value, 6) AS p_value,<br>  status<br>FROM AI.CAUSAL_EFFECT(<br>  (SELECT * FROM daily_reg_e_window),<br>  data_col =&gt; &#39;total_reg_e_disputes&#39;,<br>  timestamp_col =&gt; &#39;intake_ts&#39;,<br>  intervention_timestamp =&gt; &#39;2020-07-01 00:00:00&#39;,<br>  confidence_level =&gt; 0.95,<br>  output_time_series =&gt; FALSE<br>);</pre><p><strong>Query 3B</strong> flips output_time_series =&gt; TRUE to plot the daily actuals against the predicted counterfactual and 95% confidence bounds, and <strong>Query 3C</strong> adds id_cols =&gt; [&#39;prod_line_cd&#39;] to evaluate daily operational cost per product line in parallel.</p><p>Here is what the four queries return:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/652/1*lWaidx4WLbzUWO7ltul5Rw.png" /></figure><p>The naive AVG() query says daily Reg E disputes rose <strong>+22.47%</strong> after July 1 (89.9/day in Jan–Jun vs. 110.1/day in Jul–Sep), which would make a leadership team think the July 1 rollout backfired.</p><p>AI.CAUSAL_EFFECT tells the real story. Because dispute volume had already stepped up in May 2020, the post-July trajectory sits well inside the 95% counterfactual band (−13.31%, p = 0.5948). There is <strong>no statistically credible portfolio-wide effect</strong>. And when Query 3C breaks cost out by product line via id_cols =&gt; [&#39;prod_line_cd&#39;], only Money transfer, virtual currency, or money service shows a notable post-July drop (<strong>−$188,165, −33.42%</strong>), which at p = 0.0907 (90.93% posterior probability) is worth investigating further, not declaring a win at alpha = 0.05$.</p><h3>What Actually Happened When I Tested the Agent Before and After Grounding</h3><p>Running the TVFs in SQL is half the problem. The other half is what happens when a risk officer asks a <strong>BigQuery Conversational Analytics (BQCA)</strong> Data Agent a question in plain English.</p><p>When I first set up the ungrounded agent (Demo-FinservRiskOps-Before-Grounding) pointed at the raw fct_consumer_disputes table with blank descriptions and zero Verified Queries, I asked it:</p><blockquote>“When did daily unauthorized electronic fund transfer claims under 12 CFR 1005 experience their biggest surge, and which product, sub-product, issue, submission channel, and state drove that surge compared to the baseline right before it?”</blockquote><p>I expected the ungrounded agent to fail on the cryptic column name reg_e_elig_flg and write a plain GROUP BY. It didn’t. On its very first attempt, the ungrounded agent filtered WHERE reg_e_elig_flg = 1 and called AI.KEY_DRIVERS.</p><p>Curious how it knew AI.KEY_DRIVERS with zero instructions from me, I ran <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/06_audit_bqca_agent_jobs.sql">sql/06_audit_bqca_agent_jobs.sql</a> against region-us.INFORMATION_SCHEMA.JOBS. Every query submitted by Conversational Analytics carried a label I hadn&#39;t noticed before: <strong>ca-skill: bigquery-ai-ml-ca-rules</strong>. Out of the box, BQCA attaches a built-in system skill that teaches the agent the SQL syntax for ML.TREND, ML.SEASONALITY, AI.KEY_DRIVERS, and AI.CAUSAL_EFFECT. And because the physical column identifier contains the substring reg_e, the LLM connected &quot;12 CFR 1005&quot; to Regulation E and guessed reg_e_elig_flg = 1.</p><p><strong>So the syntax worked. The analytical answer was still completely wrong.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m2UlL0OoNPQHmkRdHTI2fQ.png" /></figure><p>When I inspected the four SQL queries that Demo-FinservRiskOps-Before-Grounding executed, here is what it actually did:</p><ol><li><strong>Query 1 — It skipped </strong><strong>ML.DETECT_CHANGE_POINTS and used a 1-day </strong><strong>LAG() difference: </strong>Remember from Act 1 that dispute intake drops every Sunday and rebounds every Monday. A 1-day LAG() difference does not find structural surges; it finds Mondays. Every single top date in the ungrounded agent&#39;s output (2022-02-28, 2022-04-11, 2022-07-11) was a Monday rebounding off a quiet Sunday (212 claims on Monday 2022-02-28 vs. 42 on Sunday 2022-02-27 = +170).</li><li><strong>Query 2 — It pulled two weeks of daily counts around February 28, 2022</strong> to eyeball its own LAG() result.</li><li><strong>Query 3 — It fed a 2-day Sunday-vs-Monday window into </strong><strong>AI.KEY_DRIVERS with pruning turned off: </strong>It filtered WHERE intake_dt IN (&#39;2022-02-27&#39;, &#39;2022-02-28&#39;) with (intake_dt = &#39;2022-02-28&#39;) AS IS_SURGE and ran AI.KEY_DRIVERS without enable_pruning =&gt; TRUE. Because pruning was off, Row 1 was the trivial all slice (212 vs 42), and redundant parent/child combinations crowded out every single jur_state_cd slice from LIMIT 10.</li><li><strong>Query 4 — It fell back to a manual </strong><strong>GROUP BY jur_state_cd query</strong> between Sunday 2022-02-27 and Monday 2022-02-28 (CA: +21, FL: +20) because no state had appeared in Query 3.</li></ol><h3>What It Took to Ground the Agent</h3><p>To fix that, I applied three layers of context (<a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/05_enrich_metadata_for_knowledge_catalog.sql">sql/05_enrich_metadata_for_knowledge_catalog.sql</a>, <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/scripts/link_knowledge_catalog_glossary.py">scripts/link_knowledge_catalog_glossary.py</a>, and <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/docs/knowledge_catalog_and_bqca_assets.md">docs/knowledge_catalog_and_bqca_assets.md</a>):</p><ol><li><strong>Column descriptions on all 29 columns (</strong><strong>sql/05):</strong> Populated descriptions on all 19 columns of fct_consumer_disputes and all 10 columns of agg_daily_risk_kpis, including which columns map to timestamp_col, data_col, and dimension_cols in the six TVFs.</li><li><strong>Knowledge Catalog scans + 6 Business Glossary terms linked to 13 physical columns (</strong><strong>scripts/link_knowledge_catalog_glossary.py):</strong> Generated Data Profile and Data Insights scans on both tables, created FinServ_Risk_Glossary with six terms (<em>Reg E Qualifying Dispute</em>, <em>Tier-2 Customer Escalation</em>, <em>Monetary Relief Ratio</em>, <em>Intake-to-Network Routing Lag</em>, <em>Regulatory SLA Breach</em>, and <em>Dispute Operational Cost</em>), and attached those terms to 13 physical columns across fct_consumer_disputes and agg_daily_risk_kpis using Dataplex EntryLink resources (entryLinkTypes/definition).</li><li><strong>Agent instructions + 3 Verified Queries (</strong><strong>docs/knowledge_catalog_and_bqca_assets.md):</strong> Added explicit table-routing and TVF rules (agg_daily_risk_kpis for daily time-series TVFs, fct_consumer_disputes for 5-dimension AI.KEY_DRIVERS), plus three Verified Queries covering the chained ML.DETECT_CHANGE_POINTS → AI.KEY_DRIVERS workflow (VQ-1), segmented ML.CORRELATION (VQ-2), and AI.CAUSAL_EFFECT (VQ-3).</li></ol><p>When I created the grounded agent (Demo-FinservRiskOps-After-Grounding) and asked the exact same prompt word-for-word (images/07_bqca_after_grounding_tvf.png), it matched <strong>Verified Query 1 (</strong><strong>✓)</strong> and ran two clean queries:</p><ul><li><strong>Query 1:</strong> Chained ML.DETECT_CHANGE_POINTS on agg_daily_risk_kpis into AI.KEY_DRIVERS(..., enable_pruning =&gt; TRUE) on fct_consumer_disputes, isolating the true 16-day surge (<strong>May 4 – May 19, 2021</strong>) against the prior 16 days (April 18 – May 3, 2021), attributing the +423 net jump to <strong>Checking or savings account (</strong><strong>1,685 vs. </strong><strong>1,261 = </strong><strong>+424, </strong><strong>+33.6%)</strong>, and automatically rendering a horizontal bar chart of the top drivers.</li><li><strong>Query 2:</strong> Extracted the ML.DETECT_CHANGE_POINTS CTE as a standalone query (ORDER BY regime_avg DESC) so it could display the full table of four detected regimes (<strong>2021-05-04 → 2021-05-19 at </strong><strong>161.06 disputes/day</strong>) right above the driver breakdown.</li></ul><h3>Reproducing it in the Cloud Console</h3><p>You can run the entire sequence from the <strong>Google Cloud Console</strong> (<strong>BigQuery Studio</strong>, <strong>Knowledge Catalog</strong>, and <strong>BigQuery Conversational Analytics</strong>). All eight screenshots from our test run in rajdipch-project-2 are in <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/tree/main/images">images/</a>.</p><h3>Step 1: Build the Mart &amp; Inspect the Un-Enriched Schema</h3><p>In <strong>BigQuery → Studio</strong>, set your query location to <strong>US (multiple regions in United States)</strong> and run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/01_setup_finserv_risk_mart.sql">sql/01_setup_finserv_risk_mart.sql</a>. When it finishes, open finserv_risk_ops.fct_consumer_disputes → <strong>Schema</strong> to confirm the cryptic column names and empty Description fields (-).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/619/1*w2lD-17MlkZmehl5Z8iN_Q.png" /></figure><h3>Step 2: Run Act 1 — Trend, Seasonality &amp; Cost Correlations</h3><p>Run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/02_act1_baseline_trend_seasonality_correlation.sql">sql/02_act1_baseline_trend_seasonality_correlation.sql</a> in BigQuery Studio. Inspect the three result tabs for ML.TREND (Query 1A), ML.SEASONALITY (Query 1B), and segmented ML.CORRELATION (Query 1C).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K3cnogdmN3jat_xX07z22A.png" /></figure><h3>Step 3: Run Act 2 — Chained ML.DETECT_CHANGE_POINTS → AI.KEY_DRIVERS</h3><p>Run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/03_act2_change_points_and_key_drivers_chain.sql">sql/03_act2_change_points_and_key_drivers_chain.sql</a>. Result Tab 1 shows the four change-point regimes led by 2021-05-04 → 2021-05-19 (161.1/day); Result Tab 2 shows the pruned AI.KEY_DRIVERS attribution table.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pH9HRelJ6_OAfIOWn2XMBw.png" /></figure><h3>Step 4: Run Act 3 — Counterfactual AI.CAUSAL_EFFECT vs. Naive Average</h3><p>Run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/04_act3_intervention_causal_effect.sql">sql/04_act3_intervention_causal_effect.sql</a>. On Result Tab 2 (Query 3B), switch to <strong>Chart → Line chart</strong> with dispute_date on the X-axis and total_reg_e_disputes, predicted_counterfactual_disputes, counterfactual_lower_95, and counterfactual_upper_95 on the Y-axis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zm9kpyVATtmOSjiTTjnWiA.png" /></figure><h3>Step 5: Test the Ungrounded Agent (“Before Grounding”)</h3><p>Before running sql/05, create a Data Agent (Demo-FinservRiskOps-Before-Grounding) in <strong>BigQuery → Agents</strong> pointed only at finserv_risk_ops.fct_consumer_disputes, with empty instructions and no Verified Queries, and ask the surge-and-drivers prompt from <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/docs/knowledge_catalog_and_bqca_assets.md">docs/knowledge_catalog_and_bqca_assets.md</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iZWtl9sysGRVkIYA1OLM5A.png" /></figure><h3>Step 6: Enrich Metadata, Run Scans &amp; Link the Business Glossary</h3><ol><li>Run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/05_enrich_metadata_for_knowledge_catalog.sql">sql/05_enrich_metadata_for_knowledge_catalog.sql</a> in BigQuery Studio to populate descriptions on both tables and all 29 columns.</li><li>Trigger <strong>Data profile</strong> and <strong>Insights</strong> scans on both tables from their BigQuery Studio tabs.</li><li>Create FinServ_Risk_Glossary in <strong>Dataplex → Knowledge Catalog → Glossaries</strong> (US multi-region) and run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/scripts/link_knowledge_catalog_glossary.py">python3 scripts/link_knowledge_catalog_glossary.py</a> in Cloud Shell to create the 6 Business Glossary terms and attach them to all 13 physical BigQuery columns.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BZOUoisHh_hicVp1-cLC3w.png" /></figure><h3>Step 7: Configure &amp; Test the Grounded Agent (“After Grounding”)</h3><p>Create a new Data Agent (Demo-FinservRiskOps-After-Grounding or FinServ_Payments_Risk_Agent) connected to both fct_consumer_disputes and agg_daily_risk_kpis, paste the Agent Instructions and three Verified Queries from <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/docs/knowledge_catalog_and_bqca_assets.md">docs/knowledge_catalog_and_bqca_assets.md</a>, and run the exact same prompt from Step 5.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C0WuousZ43GFOtsvu6CSAQ.png" /></figure><h3>Step 8: Audit Both Agents in INFORMATION_SCHEMA.JOBS</h3><p>Run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/06_audit_bqca_agent_jobs.sql">sql/06_audit_bqca_agent_jobs.sql</a> in BigQuery Studio. Filtering region-us.INFORMATION_SCHEMA.JOBS on ca-bq-job = &#39;true&#39; shows every query executed by both the grounded agent and the ungrounded agent, along with ca_skill (bigquery-ai-ml-ca-rules), invoked_augmented_analytics_tvfs, mb_billed, and generated_sql.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MJ7K5052k_2m7K7EQW8dFA.png" /></figure><p>When you are finished testing, run <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/99_teardown.sql">sql/99_teardown.sql</a> (DROP SCHEMA IF EXISTS finserv_risk_ops CASCADE) and delete the Data Agents and glossary in the Console.</p><h3>Gotchas Worth Saving</h3><p>Every one of these tripped me up at least once while building and testing this project:</p><ol><li><strong>Set your query location to </strong><strong>US multi-region.</strong> The public CFPB dataset lives in US. If your BigQuery Studio tab defaults to a regional location like us-central1, cross-dataset queries fail with Not found: Dataset … was not found in location ….</li><li><strong>Do not prefix built-in TVFs with a project or dataset name.</strong> Writing my_project.AI.KEY_DRIVERS(...) or finserv_risk_ops.ML.TREND(...) fails with <em>Table-valued function not found</em>. Qualify only the input table inside TABLE my_dataset.my_table.</li><li><strong>AI.KEY_DRIVERS rejects </strong><strong>top_k and </strong><strong>min_apriori_support together.</strong> Those two arguments are mutually exclusive; pass one or the other.</li><li><strong>AI.KEY_DRIVERS needs both </strong><strong>TRUE and </strong><strong>FALSE rows in </strong><strong>interest_label_col.</strong> If your WHERE clause only includes the surge dates, AI.KEY_DRIVERS fails because the baseline reference group is empty.</li><li><strong>Match the baseline window length to the surge window length.</strong> Comparing a 16-day surge against a fixed ±60-day window dilutes the spike with 44 normal days. Compute DATE_DIFF(regime_end, regime_start, DAY) + 1 from ML.DETECT_CHANGE_POINTS so both windows have equal length.</li><li><strong>AI.CAUSAL_EFFECT requires </strong><strong>intervention_timestamp to be a compile-time literal.</strong> Subqueries and column expressions are rejected, which is why Verified Query 3 provides a literal timestamp template that the agent edits when a user asks about a different date.</li><li><strong>Filter out </strong><strong>NaN rows in </strong><strong>ML.CORRELATION.</strong> Product lines where a metric is constant (for example, reg_e_dispute_vol is 0 on every row of Mortgage and Student loan) return NaN; add WHERE NOT IS_NAN(correlation).</li><li><strong>Test ungrounded vs. grounded behavior in separate Data Agents.</strong> BQCA chat threads cache schema metadata and conversation history. Always create a fresh Data Agent (or start a new chat thread after saving instructions and Verified Queries) so earlier turns do not bleed into your test.</li></ol><h3>Closing Thoughts</h3><p>What stood out to me most while building this wasn’t that BigQuery could run ML.DETECT_CHANGE_POINTS, AI.KEY_DRIVERS, or AI.CAUSAL_EFFECT in SQL—it was seeing <em>why</em> an ungrounded agent gets analytical questions wrong even when it already knows the function syntax.</p><p>Thanks to BQCA’s built-in bigquery-ai-ml-ca-rules skill, our ungrounded agent had no trouble calling AI.KEY_DRIVERS. It still gave a misleading answer because it used a 1-day LAG() difference instead of change-point detection, mistook a routine Monday rebound (2022-02-28 vs. Sunday 2022-02-27) for the biggest surge in five years, and forgot to turn on slice pruning. Grounding an agent isn&#39;t just about teaching it what column names mean; it&#39;s about locking in the statistical workflow so the right functions get chained in the right order.</p><p>If you want to try this on your own GCP project, everything — the six SQL scripts, the Dataplex glossary linker script, the BQCA agent config, and the Console walkthrough — is in <a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs"><strong>github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs</strong></a>.</p><h3>References &amp; Further Reading</h3><h3>Companion GitHub Repository &amp; Runbooks</h3><ul><li><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs">GitHub Repository: </a><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs">Rajdipc/bq-agentic-analytics-finserv-tvfs</a></li><li><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/README.md">Console Runbook &amp; Architecture Guide (</a><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/README.md">README.md)</a></li><li><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/docs/knowledge_catalog_and_bqca_assets.md">Knowledge Catalog Glossary, Agent Instructions &amp; Verified Queries (</a><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/docs/knowledge_catalog_and_bqca_assets.md">docs/knowledge_catalog_and_bqca_assets.md)</a></li><li><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/scripts/link_knowledge_catalog_glossary.py">Automated Knowledge Catalog Glossary &amp; Column Linker (</a><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/scripts/link_knowledge_catalog_glossary.py">scripts/link_knowledge_catalog_glossary.py)</a></li><li><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/99_teardown.sql">Teardown script (</a><a href="https://github.com/Rajdipc/bq-agentic-analytics-finserv-tvfs/blob/main/sql/99_teardown.sql">sql/99_teardown.sql)</a></li></ul><h3>BigQuery Augmented Analytics TVFs</h3><ul><li><a href="https://cloud.google.com/blog/products/data-analytics/bigquery-augmented-analytics-tvfs">Streamline data analysis with BigQuery Augmented Analytics TVFs (Google Cloud Blog)</a></li><li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-trend">ML.TREND Function Reference</a></li><li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-seasonality">ML.SEASONALITY Function Reference</a></li><li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-correlation">ML.CORRELATION Function Reference</a></li><li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-detect-change-points">ML.DETECT_CHANGE_POINTS Function Reference</a></li><li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-key-drivers">AI.KEY_DRIVERS Function Reference</a></li><li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-causal-effect">AI.CAUSAL_EFFECT Function Reference</a></li></ul><h3>Knowledge Catalog &amp; BigQuery Conversational Analytics</h3><ul><li><a href="https://cloud.google.com/dataplex/docs/introduction">Knowledge Catalog / Dataplex Universal Catalog Overview</a></li><li><a href="https://cloud.google.com/dataplex/docs/data-insights">Generate Automated Data Insights in BigQuery &amp; Knowledge Catalog</a></li><li><a href="https://cloud.google.com/dataplex/docs/business-glossary-overview">Create and Manage a Business Glossary in Knowledge Catalog</a></li><li><a href="https://cloud.google.com/dataplex/docs/data-profiling-overview">Configure Automated Data Profiling Scans</a></li><li><a href="https://cloud.google.com/bigquery/docs/conversational-analytics">BigQuery Conversational Analytics Overview</a></li><li><a href="https://cloud.google.com/bigquery/docs/data-agents">Create and Configure Data Agents with Verified Queries in BigQuery</a></li><li><a href="https://cloud.google.com/bigquery/docs/information-schema-jobs">INFORMATION_SCHEMA.JOBS View Reference</a></li><li><a href="https://console.cloud.google.com/marketplace/product/cfpb/complaint-database">CFPB Consumer Complaint Database on BigQuery Public Datasets</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f92df6bd17e4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/grounded-agentic-analytics-in-bigquery-augmented-analytics-tvfs-knowledge-catalog-and-f92df6bd17e4">Grounded Agentic Analytics in BigQuery: Augmented Analytics TVFs, Knowledge Catalog, and…</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Writing for machine translation: Seven tips to make your docs translation friendly]]></title>
            <link>https://medium.com/google-cloud/writing-for-machine-translation-seven-tips-to-make-your-docs-translation-friendly-930d6a8d9c5c?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/930d6a8d9c5c</guid>
            <category><![CDATA[machine-translation]]></category>
            <category><![CDATA[technical-writing]]></category>
            <category><![CDATA[editing]]></category>
            <category><![CDATA[google-cloud]]></category>
            <category><![CDATA[writing-tips]]></category>
            <dc:creator><![CDATA[Isaac Huben]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:42:35 GMT</pubDate>
            <atom:updated>2026-10-13T13:53:55.598Z</atom:updated>
            <content:encoded><![CDATA[<p><em>A lesson about clear writing from an editor at Google.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o-vpd3yHJMWRKbTOow16-g.jpeg" /><figcaption>Image from<a href="https://gemini.google.com/?utm_campaign=cloud-ix-social-media-post_510896088"> </a><a href="http://unsplash.com">Unsplash.com</a>.</figcaption></figure><p>Optimizing your docs for translation is an important and necessary part of increasing your global reach and safeguarding your brand reputation. Even though you cannot fully eliminate translation errors from technical documentation, by following a few simple tips like the ones in this post, you can make your content significantly more translation friendly.</p><h3><strong>Let’s discuss machine translation, its pros and cons</strong></h3><p>Ask any translation or localization professional and they’ll likely tell you: mistranslations are extremely common, even with advanced translation engines like Google Translate and Microsoft Translator. Errors can range from wrong words or awkward phrasing to high-stakes blunders with the potential to be extremely expensive and damage your brand reputation.</p><p>Just ask HSBC. In the 2000s, this global bank launched a worldwide marketing campaign built around the catchphrase: “Assume Nothing.” But likely because “assume” in this sense has false friends in languages like French and because HSBC may have used machine translation without proper human review, the phrase effectively turned into “Do Nothing” in several languages. To fix this blunder, HSBC reportedly spent up to $10 million on an emergency rebranding campaign.</p><p>So mistranslations are common and can be expensive, and machine translation might be flawed. But there’s a flip side to this. Not only is machine translation improving as the technology advances, but it also gives us the opportunity to enhance our technical documentation at scale.</p><p>Because machine translation is fast and cheap, as documentation teams we no longer have to split hairs about how expensive fixing typos or inaccuracies might be given the downstream localization costs. In other words, with machine translation, we have a real opportunity to iteratively and continually improve our English docs to be more compatible with an ever-growing number of machine-assisted technologies and AI tools and agents, and, of course, to be more translation friendly.</p><h3>How to make tech docs more translation friendly</h3><p>Given how inaccurate machine translations can be, improving your translation outcomes can seem like a big ask. But a good start to improving translations is by preventing ambiguity in the English source. There are two major types of ambiguity that frequently creep up into our writing:</p><ul><li><strong>Lexical ambiguity</strong>, where a word has multiple meanings or interpretations—for example: “<em>Verify the </em><strong><em>driver</em></strong><em> is </em><strong><em>running</em></strong>.”<em> </em>Here, both <strong><em>driver</em></strong> and <strong><em>running</em></strong><em> </em>are ambiguous. <strong><em>Driver</em></strong> could refer either to a hardware or software driver or to a person who operates a motor vehicle. <strong><em>Running</em></strong> could refer to software or hardware processes or to a person running. This could cause mistranslations in Spanish, for instance, where many of these meanings map to very different words.</li><li><strong>Structural ambiguity</strong>, where the grammar or word order is ambiguous—for example: “<strong><em>Running containers</em></strong><em> without resource limits</em>.”<em> </em>Here, <strong><em>running containers</em></strong> could refer to “containers that are running” or “the act of running containers.” In addition, it’s unclear if “<em>without resource limits</em>” describes the act of running containers or the containers themselves. Phrases like this can easily cause confusion and mistranslations in many languages including German and Japanese.</li></ul><h3>Seven tips to write more translation friendly</h3><p>Here are seven simple tips to help prevent ambiguity and mistranslations. This isn’t an exhaustive list but should give you an idea of what to look for.</p><p>Beyond ambiguity, keeping your content clear and concise can also improve machine translations. Check out my post <a href="https://medium.com/google-cloud/simple-formulas-for-concise-writing-7cee8804081e">Simple formulas for concise writing</a>.</p><h3>How to prevent lexical ambiguity</h3><p>These tips help you to avoid using ambiguous words.</p><p><strong>1. Use words in their primary sense. </strong>A golden rule of simplified technical communications is “one word, one part of speech, one meaning.” Communication is often much more context dependent than you may realize. Machine translation errors help to reveal this underlying ambiguity. To prevent mistranslations, aim to limit your use of words to their primary industry-relevant sense and use those words consistently. Avoid unnecessary variation or synonyms. Consider these examples:</p><ul><li><em>detail</em> → <strong>detail</strong> (an item or fact) or <strong>describe</strong> (to explain)</li><li><em>secure</em> → <strong>secure</strong> (to protect) or <strong>obtain</strong> (to acquire, as in <em>secure funding</em>)</li><li><em>leave</em> → <strong>leave</strong><em> </em>(to depart) or <strong>keep</strong> (to allow to remain, as in <em>leave blank</em>)</li><li><em>since</em> → <strong>since</strong> (from the time that) or <strong>because</strong> (for the reason that)</li><li><em>type</em> → <strong>type</strong> (a kind or category) or <strong>enter</strong> (to input text using a keyboard)</li></ul><p>These are all examples of <a href="https://en.wikipedia.org/wiki/Polysemy"><em>polysemy</em></a>, which is when one word has multiple meanings or uses. Yes, they’re not always ambiguous. But by limiting how you use these words, you can help prevent ambiguity.</p><p>In addition, it’s also helpful to provide clear context clues. Consider this scenario. The Chinese translation of your cloud infrastructure docs seemingly out of nowhere tells customers to “publish the picture to the registry,” where it should’ve said “deploy the container image to the container registry.” This could happen because Chinese uses different words for <em>visual images</em> and <em>container images</em>. Without keywords like <em>container</em>, words like “image” could be mistranslated.</p><p><strong>2. Avoid homographs. </strong><a href="https://www.english-efl.com/lessons/homonyms-homographs-homophones-heteronyms/">Homographs</a> are unrelated words that are spelled the same. Because we’re typically working with written translation, homographs can often be at the root of many mistranslations. Let’s go over a few likely culprits:</p><ul><li><strong>resume </strong>(ˈrɛzəmeɪ, a work summary) or <strong>resume </strong>(rɪˈzuːm, to restart)</li><li><strong>record</strong> (ˈrɛkərd, a stored item or entry) or <strong>record</strong> (rɪˈkɔrd, to capture)</li><li><strong>default</strong> (ˈdiːfɔlt, a preselected option) or <strong>default</strong> (dɪˈfɔlt, to fail to pay)</li><li><strong>close</strong> (kloʊz, to shut or seal) or <strong>close</strong> (kloʊs, near or nearby)</li></ul><p>Keep in mind that these words are likely completely unrelated words in other languages, so mistranslations may feel like nonsense. Of these, perhaps the most commonly mistranslated is <em>close</em>, which can cause a lot of frustration. <a href="https://www.reddit.com/r/brave_browser/comments/fpl32t/braves_spanish_ui_is_really_badly_translated_why/">Consider this complaint on Reddit</a>.</p><p>To address this issue, try to avoid homographs where possible or at least clarify the context. For example, consider changing <em>Resume</em> to a less ambiguous <em>Play/Pause</em>. Or expand <em>click </em><strong><em>Resume</em></strong> to <em>click the </em><strong><em>Resume</em></strong><em> button</em>. The second might not prevent mistranslation but it’ll at least provide users with more context, say if the translation is incorrect elsewhere.</p><p><strong>3. Be careful how you use abbreviations. </strong>Not only can abbreviations, acronyms, or initialisms be unfamiliar to some users but they can be ambiguous or have different meanings in other languages with some potentially inappropriate meanings. For example, “TOC” in Spanish is actually the Spanish version of “OCD,” and while “AI” is the abbreviation we use in English, in French it’s “IA.” And abbreviating “cryptography user” to “CU” may seem perfectly fine in English, but <a href="https://lnkd.in/p/gee77BFb">it would likely cause a few dirty looks in Brazil</a>. To prevent such problems, consider potential localization issues before you create a new term or abbreviation, determine if abbreviation is even necessary, and follow recommended best practices for spelling out and using abbreviations.</p><h3>How to prevent structural ambiguity</h3><p>These tips help you to avoid using ambiguous grammar.</p><p><strong>4. Add structure with “of,” “that,” “the.” </strong>Think of this tip as a golden rule of translation-friendly grammar: When in doubt, use helper words, also called function words, which help to prevent ambiguity by clarifying the structure or focus of your sentences. Without them, sentences can often be misunderstood. Put another way, helper words can make your writing more readable and easier to parse, which matters especially when your reader is a machine. Some helper words to remember:</p><ul><li><strong>Articles</strong> like <em>a</em>, <em>an</em>, and <em>the</em>.</li><li><strong>Prepositions</strong> like <em>to</em>, <em>in</em>, <em>for</em>, <em>on</em>, and <em>of</em>.</li><li><strong>Conjunctions</strong> and <strong>adverbs</strong> like <em>and</em>, <em>that</em>, and <em>then</em>.</li></ul><p>Helper words allow machines to parse your writing correctly, clarifying the intended meaning. Some examples of helper words in action:</p><ul><li><em>empty trash </em>→ Empty <strong>the</strong> trash</li><li><em>Verify target host connection</em> → Verify <strong>that the</strong> target host <strong>is</strong> connected</li></ul><p><strong>5. Watch out for invisible plurals. </strong>In English, modifiers that occur in front of a noun are typically grammatically singular–that is, they usually can’t take a <em>plural s</em>–even if they’re conceptually plural, creating the grammatical mismatch called <em>invisible plural</em>. Invisible plurals are ambiguous and can cause mistranslations in languages that have plural modifiers like Spanish, Polish, and Finnish.</p><p>To help prevent mistranslations, reword the sentence to make clear whether the invisible plural is singular or plural:</p><ul><li>Verify the <em>user account credentials</em>.<em> </em>→ Verify <strong>the credentials of the user accounts</strong>.</li><li>After <em>file recovery</em>,.… → After <strong>you recover the files</strong>, …</li></ul><p><strong>6. Stop “verbing” around! </strong>In English,<strong> </strong><em>verbing</em> can make your sentence harder to parse or ambiguous (remember <strong><em>running containers</em></strong><em>?</em>) because <em>verbing</em> can act as different parts of speech. As a result, natural language processing systems like translation engines can struggle to figure out the precise meaning. Let’s go over how <em>verbing</em> is used in English:</p><ul><li>As a <strong>verb</strong> (<em>present participle</em>) in “I’m <em>going</em> to work.”</li><li>As a <strong>noun</strong> (<em>gerund</em>) in “<em>Smoking</em>’s bad for you.”</li><li>As an <strong>adjective</strong> (<em>present participle</em>) in “That’s <em>interesting</em>.”</li></ul><p>Besides this, <em>verbing</em>, as a gerund, is a type of <a href="https://medium.com/google-cloud/when-writing-beware-of-zombies-f6450726a9be">zombie noun</a>, which can make your writing indirect by displacing concrete subjects. So, to prevent mistranslations, avoid <em>verbing</em> and add back concrete subjects or fold in helper words. For example, change<em> using</em> to <em>by using</em>, <em>that use</em>, or <em>you use</em>. Seem complicated? Consider these examples:</p><ul><li><em>Configuring</em> the VPC firewall rules is required before <em>deploying</em> the VM instance. → <strong>You must configure</strong> the VPC firewall rules before <strong>you deploy</strong> the VM instance.</li><li>the folder <em>containing</em> the configuration details → the folder <strong>that contains</strong> the configuration details</li></ul><p><strong>7. Be careful what you omit. </strong>Not all languages allow for the same patterns of omission. What sounds natural in English might sound strange in another language. If literally translated, the first example below would be awkward in Chinese or Spanish, which typically can’t reduce a predicate to just an auxiliary verb alone. In addition, omitting words can also create ambiguous sentences, like in the second example where it’s unclear whether we’re referring to “database” alone or “database encryption.” Let’s go over the examples:</p><ul><li>A Standard tier Compute Engine instance supports automated backups, but a Basic tier instance <em>doesn’t</em>. → A Standard tier Compute Engine instance supports automated backups, but a Basic tier instance <strong>doesn’t support automated backups</strong>.</li><li>Enable <em>database and storage encryption</em>. → Enable <strong>database encryption and storage encryption</strong>.</li></ul><p>The tip here is, when in doubt, use grammatically complete sentences and repeat words to prevent ambiguity and mistranslations.</p><p>Want to learn more about how to write translation-friendly documentation? Check out the <a href="https://developers.google.com/style/translation?utm_campaign=cloud-ix-social-media-post_460789162">Write for a global audience</a> page of the Google developer documentation style guide.</p><h3>References</h3><ul><li><a href="https://docs.document360.com/docs/writing-for-translation">Document 360. “Writing for translation.” Published July 2026.</a></li><li><a href="https://translated.com/resources/translation-mistakes-cost-millions-real-cases-lessons">Patrioli, Daniele. “Translation Mistakes That Cost Millions: Real Cases and What They Can Teach Your Business.” Translated.</a></li><li><a href="https://www.cdt.europa.eu/sites/default/files/documentation/pdf/cdt-writing-for-mt-en.pdf">Translation Centre for the Bodies of the European union. “Writing for machine translation.” Cdt.europa.eu. Last updated December 2021. Accessed October 2026.</a></li><li><a href="https://cornerstone.lib.mnsu.edu/cgi/viewcontent.cgi?article=1020&amp;context=eng_tech_comm_capstone_course">Urban, Adrienne. “Don’t Get Lost in Translation: A Discussion of Best Practices for Creating Translation-Friendly Text and Related Curriculum for Technical Communication.” Cornerstone.lib.mnsu.edu. Published in 2018.</a></li></ul><h3>About the author</h3><p>Isaac is a technical editor at Google who works on content for the Cloud Security services and helps maintain the Google developer documentation style guide. Passionate about creating superior content experiences, Isaac enjoys sharing his knowledge about language and grammar to help technical writers and other content creators create a frictionless documentation experience for Google customers.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=930d6a8d9c5c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/writing-for-machine-translation-seven-tips-to-make-your-docs-translation-friendly-930d6a8d9c5c">Writing for machine translation: Seven tips to make your docs translation friendly</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[E Ink AI Assistant with Gemini Live]]></title>
            <link>https://medium.com/google-cloud/e-ink-ai-assistant-with-gemini-live-7cf88cb1c0d1?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/7cf88cb1c0d1</guid>
            <category><![CDATA[google-gemini]]></category>
            <category><![CDATA[esp32]]></category>
            <category><![CDATA[gemini-live]]></category>
            <category><![CDATA[google]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Anand Kumar]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:42:06 GMT</pubDate>
            <atom:updated>2026-10-13T11:42:05.050Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XOE2j6uxHaCaZYzg.png" /></figure><p>I wanted an AI assistant without another distracting screen.</p><p>So, I built a small, quiet E ink device. E Ink. No feed to refresh. No apps to browse. Just enough information to tell me what’s happening, and then leave me alone but with the power of an agent behind it.</p><p>My <a href="https://www.cloud-monk.com/blog/going-screenless-using-ai/">earlier screenless experiment</a> used Gemini Live and a Hermes agent to handle everyday tasks through voice using raspberry Pi. The one thing that made it limited was it was wired in and lets face it — the form factor wasn’t great :).</p><p>This is the next version: a tiny wireless device I can carry in my pocket.</p><p>The question is still the same: can I keep the useful parts of technology while spending less time looking at it?</p><h3>What can it do?</h3><p>Press a button and talk to it. Ask a question, post a thought to X, create slides and email them to me, or find a coloring page and print it on my home printer.</p><p>Gemini Live handles the conversation. Hermes handles the tasks that need my tools and workflows. And I don’t have to stand there waiting.</p><p><a href="https://www.youtube.com/shorts/2kSqseHDv_8">Watch the live demo on YouTube →</a></p><h3>Gemini Live: just talk to it</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XnMiFXtmPwSgBb5l.png" /></figure><p>The conversational side uses Gemini Live. I can speak naturally instead of typing a prompt into an app.</p><p>In this setup, Google Search is enabled too. In the demo, I ask for Google’s latest stock price while another task is being processed. The useful part is being able to ask for current information without opening a browser.</p><h3>It talks back-and I can keep talking</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*70PpIhYEFbc3VM-N.png" /></figure><p>Listening and speaking have their own screen states. The little face changes expression, so I can tell what the device is doing at a glance.</p><p>I can also continue having a live conversation while Hermes works in the background. Asking it to make a presentation doesn’t mean I have to wait before asking my next question.</p><h3>Hermes: where things actually get done</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IgoJY-nDiAvGJBaN.png" /></figure><p>Gemini Live hands tool-based requests to my Hermes agent, which is connected to my tools, connectors, skills, and home network.</p><p>For example: “Create five slides on AI agents and send them to my email.”</p><p>Hermes (with gemini as the brain) researches the topic, creates the slides using my existing slide creation skills, uploads the result, and emails me the link. When I open it, the presentation follows the style I already use.</p><p>That’s the useful part for me: starting an existing workflow with my voice.</p><h3>Post a thought to X</h3><p>In the demo, I ask it to post a thought about AI requiring less of our attention.</p><p>It acknowledges the request and hands it to the agent. When the task finishes, I can hear the completion result, then check the published post.</p><h3>Ask, then carry on</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4ukrqG4Ts2bariFL.png" /></figure><p>Longer tasks run asynchronously. I can keep talking to Gemini Live, or end the conversation while the agent continues working.</p><p>The live WebSocket connection doesn’t need to stay open just because a background task is still running. Keeping those separate avoids holding an unnecessary live session while waiting.</p><p>When a result arrives, a bell appears above the home character. I can press BOOT to start a conversation and hear the result.</p><p>A small visual signal is enough to tell me something is ready.</p><h3>A voice request that turns into something on paper</h3><p>“Look for a cute coloring page picture and print it out on my printer.”</p><p>Because Hermes is connected to my home network, it can handle finding the picture and sending it to my printer.</p><p>A little later, there’s an actual coloring page on paper. That was probably my favorite part of the demo. :)</p><h3>The sleep screen actually means sleep</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fHk24LnxlolpCLxn.png" /></figure><p>E Ink can retain the image without continuously powering the display. But a sleeping character alone wouldn’t tell us much about the rest of the device.</p><p>In this firmware, idle light sleep closes the microphone and speaker, turns Wi-Fi off, puts the display into sleep, and then enters ESP32 light sleep. Pressing BOOT wakes it and the device reconnects.</p><p>The firmware checks for pending agent work before entering idle sleep. While asleep, Wi-Fi is off, so it isn’t receiving new network notifications.</p><p>I haven’t measured battery life yet. But the power-saving behavior is real, rather than just a different picture on the screen.</p><h3>Why E Ink?</h3><p>I wanted something that could sit quietly until I needed it. No glowing display. No scrolling. No stream of unrelated notifications competing for my attention.</p><p>Gemini Live and the remote agent still need network access. But around the house, this gives me a deliberate way to reach the assistant without picking up my phone.</p><p>Could I do many of these things from a phone? Yes. But I know what happens when I pick it up for one small thing. There’s usually a second thing waiting. And a third. :)</p><h3>Build your own version</h3><p>The <a href="https://github.com/ranand12/gemini-charm-esp32-eink">source code and complete build guide are on GitHub</a>.</p><p>I chose E Ink because a quiet display was the point. You could use another ESP32 device, including one with an AMOLED display, and build on the same idea.</p><p>For me, the interesting question is how little interface I need before the assistant becomes useful.</p><p><strong>AI should require less of our attention, not more.</strong></p><p>Originally published on <a href="https://www.linkedin.com/pulse/going-screenless-again-tiny-e-ink-ai-assistant-my-pocket-anand-kumar-uvv1c/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7cf88cb1c0d1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/e-ink-ai-assistant-with-gemini-live-7cf88cb1c0d1">E Ink AI Assistant with Gemini Live</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automating Snowflake-to-BigQuery dbt Migrations with AI Skills]]></title>
            <link>https://medium.com/google-cloud/automating-snowflake-to-bigquery-dbt-migrations-with-ai-skills-698586704570?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/698586704570</guid>
            <category><![CDATA[snowflake-migration]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[dbt]]></category>
            <category><![CDATA[bigquery]]></category>
            <dc:creator><![CDATA[Prabha Arya]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 11:41:12 GMT</pubDate>
            <atom:updated>2026-10-13T11:41:11.748Z</atom:updated>
            <content:encoded><![CDATA[<p>Migrating a mature data warehouse from Snowflake to Google BigQuery is rarely just a copy-and-paste exercise. When your analytics engineering workflow is anchored in <strong>dbt (data build tool)</strong>, the challenge scales rapidly. You aren’t just translating SQL dialects; you are mapping custom Jinja macros, handling implicit type casting (::date, ::timestamp), refactoring configuration blocks (copy_grants, transient, secure), and ensuring that complex model lineagereferences ({{ ref() }} and {{ source() }}) survive the trip intact.</p><p>Enter <strong>AI Skills</strong> — specifically, Google’s open-source <a href="https://github.com/google/skills/tree/main/skills/cloud/dbt-sf-to-bq-translator">dbt-sf-to-bq-translator agent skill</a>.</p><p>In this post, we’ll explore how this specialized AI skill works under the hood, why traditional transpilation falls short for dbt projects, and how you can orchestrate a seamless Snowflake-to-BigQuery migration using your favorite LLM.</p><h3>Why Dbt Migrations Break Traditional Translators</h3><p>Automated translation tools (like the BigQuery Translation Service) are incredible at converting raw Snowflake SQL into standard BigQuery-compliant syntax. However, they hit a wall when introduced to <strong>dbt projects</strong> for two primary reasons:</p><ol><li><strong>Jinja Macro Collisions:</strong> Standard transpilers don’t understand dbt templating tags ({{ config(...) }}, {{ ref() }}, {{ source() }}, and custom macros). Feeding raw dbt .sql files directly into a transpiler usually results in syntax errors or mangled references.</li><li><strong>Dialect-Specific Edge Cases:</strong> Snowflake allows shorthand syntactic sugar like column::date, dateadd(), negative intervals (- interval &#39;5 month&#39;), and specific JSON extraction patterns that BigQuery handles differently via standard functions like CAST(... AS DATE) and DATE_ADD().</li></ol><p>The dbt-sf-to-bq-translator skill solves this by establishing a rigorous, multi-stage <strong>pre-processing, translation, and post-processing lifecycle</strong>.</p><h3>Inside the Translation Lifecycle</h3><p>When an AI agent loads the dbt-sf-to-bq-translator skill, it doesn’t just guess how to rewrite your code. It follows a structured, script-driven workflow designed to protect your project architecture:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NuSfGUzmT1hbKkPcJBFmjA.png" /><figcaption>From Snowflake to BigQuery: how raw code is split, masked, translated, and safely reassembled.</figcaption></figure><h3>1. Pre-Processing &amp; Placeholder Masking</h3><p>Before touching any translation engine, the skill processes your source .sql files:</p><ul><li><strong>Strips and isolates</strong> the {{ config(...) }} header block from the top of each model.</li><li><strong>Masks dbt macros</strong> with safe placeholder identifiers. For example:</li><li>{{ ref(&#39;my_model&#39;) }} becomes _DBT_REF_my_model_DBTSEP_</li><li>{{ source(&#39;src&#39;, &#39;tbl&#39;) }} becomes _DBT_SOURCE_src_DBTSEP_tbl_</li><li><strong>Eliminates Jinja curly braces</strong> so the underlying transpiler can process 100% valid, clean SQL.</li></ul><h3>2. The Migration Execution</h3><p>The isolated, Jinja-free files are staged and passed through the BigQuery migration workflow. Because the syntax is now standardized, the translation service executes cleanly without choking on custom templating tokens.</p><h3>3. Post-Processing &amp; AST Reconstruction</h3><p>Once the BigQuery-translated SQL is returned, the skill performs deep reconstruction:</p><ul><li><strong>AST-Based Config Transformation:</strong> Parses the original {{ config(...) }} blocks, safely stripping Snowflake-specific properties (copy_grants, transient, secure) and cleaning up incompatible hooks (pre_hook, post_hook).</li><li><strong>Reference Resolution:</strong> Scans the translated SQL for hardcoded paths in FROM and JOIN clauses, automatically mapping them back to native dbt {{ ref() }} and {{ source() }} macros.</li></ul><p><strong>Syntax &amp; Edge-Case Auditing:</strong> Converts type-cast suffixes (::date— &gt;CAST(... AS DATE)), handles nested dateadd() operations, and logs warnings for any un-allowlisted or custom database-specific macros.</p><ul><li><strong>File Assembly:</strong> Prepends mandatory headers, preserves your exact directory structure, and copies over all associated .yml / .yaml configuration files.</li></ul><h3>Step-by-Step: How to Use the Skill</h3><p>Using the skill is straightforward whether you are working inside an AI coding assistant or an agent-enabled interface like TypingMind:</p><ol><li><strong>Install the Skill:<br></strong>Add the skill folder directly from the public repository:</li></ol><pre>npx skills add https://github.com/google/skills --skill dbt-sf-to-bq-translator</pre><ol><li><strong>Configure Your Environment:<br></strong>Ensure your Google Cloud CLI session and billing are active:</li></ol><pre>gcloud auth login gcloud auth application-default login <br>gcloud config set project {project_id} <br>gcloud services enable bigquerymigration.googleapis.com storage.googleapis.com bigquery.googleapis.com</pre><ol><li><strong>Run the Migration Prompt:<br></strong>Point your AI assistant to your dbt project directory and invoke the skill by requesting a migration. The agent will initialize a task tracker (tasks.md), verify your prerequisites, and guide you through transforming your models layer-by-layer (staging $\rightarrow$ intermediate →marts).</li></ol><h3>Conclusion</h3><p>Migrating data stacks used to mean weeks of tedious, manual regex-replacement and debugging broken Jinja templates. By packaging expert migration workflows into open-source <strong>AI Skills</strong>, teams can now automate the tedious boilerplate of dialect conversion while maintaining strict control over their dbt project structure, lineage, and configurations.</p><p><em>Have you tackled a Snowflake-to-BigQuery migration recently? Check out the </em><a href="https://github.com/google/skills"><em>google/skills repository on GitHub</em></a><em> to try out the translator skill on your own codebase.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=698586704570" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/automating-snowflake-to-bigquery-dbt-migrations-with-ai-skills-698586704570">Automating Snowflake-to-BigQuery dbt Migrations with AI Skills</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automating Agentic Security Reviews: Running CodeMender CLI in GitHub Actions]]></title>
            <link>https://medium.com/google-cloud/automating-agentic-security-reviews-running-codemender-cli-in-github-actions-03ba291ac0a6?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/03ba291ac0a6</guid>
            <category><![CDATA[sdlc]]></category>
            <category><![CDATA[ai-code-security]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[codemender]]></category>
            <category><![CDATA[code-security]]></category>
            <dc:creator><![CDATA[Tianzi Cai]]></dc:creator>
            <pubDate>Mon, 12 Oct 2026 19:13:42 GMT</pubDate>
            <atom:updated>2026-10-12T19:13:42.090Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/988/1*OO2bf8Zg_jQZIpHhC1g7Jg.png" /></figure><h3>From Local Conductor Loops to CI Governance</h3><p>In two previous deep dives, I explored running code reviews in two distinct domains of the modern software development lifecycle:</p><p>• <strong>DIY PR Code Reviews with Antigravity CLI in GitHub Actions: </strong><em>In this </em><a href="https://medium.com/google-cloud/diy-pr-code-reviews-with-antigravity-cli-in-github-action-47075c4ba492"><strong>earlier post</strong></a>, I walked through building automated, stateless pull request code reviews in the <strong>outer loop</strong> (CI/CD pipeline) using Google Antigravity CLI (<a href="https://github.com/anguillanneuf/run-antigravity-cli">run-antigravity-cli</a>). I demonstrated how to leverage Workload Identity Federation and service account impersonation to let an agent inspect incoming PR diffs via the GitHub REST API and post review feedback without needing local checkouts or stored secrets. A small note: I have since updated the code to <a href="https://github.com/anguillanneuf/run-antigravity-cli#step-1-configure-gcp-workload-identity-pool">direct Workload Identity Federation</a>, eliminating the need for service account impersonation.</p><p>• <strong>Extending Conductor with CodeMender: AI-Powered Threat Modeling and Remediation: </strong><em>In this </em><a href="https://medium.com/google-cloud/extending-conductor-with-codemender-ai-powered-threat-modeling-and-remediation-in-the-autonomous-db5e099315f6"><strong>follow-up article</strong></a>, I shifted security left into the <strong>inner loop</strong> (the local developer environment). I showed how embedding CodeMender CLI ( cm ) into Conductor’s review phase enabled deep threat modeling, trust boundary exploration, sandboxed proof-of-concept exploit verification, and automated patching ( cm fix ) directly on the engineer’s workstation before code is committed.</p><p>In the same Conductor post, I posed a central architectural thesis: catching security defects locally in the inner loop is essential for developer velocity, but robust enterprise governance still demands an unbypassable gatekeeper in the outer loop (CI). No matter how diligent developers are locally, pull request pipelines must independently verify that incoming code changes introduce zero security regressions.</p><p><em>Naturally, the logical next step was: </em><strong>Why not also run CodeMender CLI directly inside GitHub Actions alongside Antigravity CLI?</strong></p><h3>What’s New in This Blog Post</h3><p>This post bridges the inner and outer loops by documenting the exact configurations required to run CodeMender CLI in GitHub Actions. Specifically, I cover:</p><p>• <strong>Auth Reality Check (Direct WIF vs. ADC File Impersonation): </strong>Why the pure 1-hop Direct WIF architecture celebrated in the Antigravity CLI post does not work out-of-the-box for CodeMender CLI, and how to use the 2-hop WIF with service account impersonation and physical ADC credentials generation together.</p><p>• <strong>The CI Sandboxing Dilemma &amp; Path Constriction: </strong>Why the Linux namespace and bubblewrap sandboxing highlighted in our Conductor post fails inside CI runners. We dissect how passing relative paths triggers severe <strong>🚫 SANDBOX VIOLATION</strong> errors that blind the agent, how I solved it with workspace root declarations and absolute paths, and when it is safe (or unsafe) to disable sandboxing in CI.</p><p>• <strong>Hardening Cross-Repository PRs Against Fork Abuse: </strong>How to safely allow CodeMender scans on open-source fork PRs using pull_request_target without opening your Google Cloud WIF pool to credential exfiltration.</p><p>• <strong>Operating in Gated Public Preview: </strong>How to manage CodeMender CLI’s gated preview status, Artifact Registry distribution, project allowlisting, and automated preflight dry-run validation.</p><h3>Cloud Authentication Architecture: Direct WIF vs. ADC File</h3><p>One of the most consequential differences between running Antigravity CLI alone and orchestrating CodeMender CLI in CI lies in how each tool handles Google Cloud authentication.</p><h3>Antigravity CLI: Stateless 1-Hop Direct Workload Identity Federation</h3><p>When running Antigravity CLI in consumer repositories, such as in <a href="https://github.com/nedtargaryen243870-code/modern-app"><strong>nedtargaryen243870-code/modern-app</strong></a>, I make use of <strong>Direct Workload Identity Federation (Direct WIF)</strong>. Google Cloud IAM permissions are bound directly to the federated external GitHub identity without needing any intermediate service account.</p><p>As detailed in <a href="https://github.com/nedtargaryen243870-code/modern-app/pull/10"><strong>modern-app PR #10 (Remove service account email from GCP auth)</strong></a>, eliminating the service account compressed the authentication handshake from a traditional 2-hop flow down to a direct 1-hop flow:</p><p>• <strong>Legacy 2-Hop Impersonation: </strong>GitHub OIDC Token → Security Token Service (STS) → IAM Credentials API (impersonating a service account) → Access Google Cloud Agent Platform.</p><p>• <strong>Direct 1-Hop WIF: </strong>GitHub OIDC Token → Security Token Service (STS) → Direct Access to Google Cloud Agent Platform.</p><p>By granting roles/aiplatform.user directly to the federated principal set:</p><p>principalSet://iam.googleapis.com/projects/&lt;PROJECT_NUM&gt;/locations/global/workloadIdentityPools/&lt;POOL_ID&gt;/attribute.repository/&lt;REPO_OWNER&gt;/&lt;REPO_NAME&gt;</p><p>the workflow in <a href="https://github.com/anguillanneuf/run-antigravity-cli/blob/master/.github/workflows/test.yml">.github/workflows/test.yml</a> and consumer repos no longer specifies a service_account parameter in google-github-actions/auth@v3 (formalized in <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/14"><strong>run-antigravity-cli PR #14</strong></a>).</p><h3>CodeMender CLI: Standard Application Default Credentials (ADC) Requirements</h3><p>Unlike lightweight API clients, CodeMender CLI ( cm) is an integrated, packaged command-line binary. Under the hood, it utilizes Google Cloud client libraries that expect standard <strong>Application Default Credentials (ADC)</strong> materialized as a file on disk.</p><p>When running CodeMender CLI in CI, attempting to use pure in-memory Direct WIF environment variables causes CodeMender to fail with missing credentials. As resolved in <a href="https://github.com/sansafrey261045-coder/run-antigravity-cli/pull/2"><strong>Fork PR #2 (fix: configure ADC credentials file and export GOOGLE_CLOUD_PROJECT)</strong></a> and upstreamed in <a href="https://github.com/anguillanneuf/run-antigravity-cli/blob/master/.github/workflows/test-cm.yml">.github/workflows/test-cm.yml</a>, the auth step must generate a physical credentials file and export environment variables:</p><pre>      # -------------------------------------------------------------------------<br>      # Authenticate to Google Cloud via Workload Identity Federation (OIDC)<br>      # -------------------------------------------------------------------------<br>      - name: Authenticate to Google Cloud (OIDC)<br>        id: gcp-auth<br>        uses: google-github-actions/auth@v3<br>        continue-on-error: ${{ inputs.dry_run == true }}<br>        with:<br>          project_id: ${{ vars.GCP_PROJECT_ID }}<br>          workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}<br>          service_account: ${{ vars.GCP_SERVICE_ACCOUNT_EMAIL }}<br>          create_credentials_file: true<br>          export_environment_variables: true</pre><p>Additionally, CodeMender reads project settings from ~/.codemender/config.yaml. The automated setup script (<a href="https://github.com/anguillanneuf/run-antigravity-cli/blob/master/scripts/setup_cm_config.py">scripts/setup_cm_config.py</a>) writes the required server.project and server.endpoint fields, ensuring CodeMender CLI has full ADC alignment before scanning starts.</p><h3>The Sandboxing Dilemma: Container Namespaces vs. Ephemeral CI Runners</h3><p>Sandboxing is a core architectural pillar of autonomous coding agents. When an AI agent executes autonomously on your workstation, you do not want it reading your private SSH keys ( ~/.ssh ), querying sensitive credentials, or tampering with system binaries. CodeMender enforces this locally using Linux unprivileged user namespaces and bubblewrap isolation.</p><h3>The Relative-Path Constriction Trap</h3><p>However, running that same sandbox inside GitHub Actions runners created unexpected failures. When my initial CI workflow attempted to scan changed files in a pull request by passing relative paths (e.g., cm find tests/test_workflow_cm.py ) with an empty default project_paths” [] , CodeMender’s internal agent sandbox constricted allowed roots strictly to:</p><ul><li>/home/runner/work/run-antigravity-cli/run-antigravity-cli/tests/test_workflow_cm.py</li><li>/home/runner/.codemender</li><li>/tmp</li></ul><p>As the agent reasoned through the test file, it naturally attempted to inspect project imports, configuration helpers in scripts/, and workflow files in .github/. CodeMender intercepted these calls as sandbox violations. Citing the exact log preserved in <a href="https://github.com/sansafrey261045-coder/run-antigravity-cli/pull/2#issuecomment-5346219652"><strong>Fork PR #2 comment #5346219652</strong></a>:</p><pre>2026–08–20T21:18:02Z [WARN] 🚫 SANDBOX VIOLATION #1: Agent requested path &quot;/home/runner/work/run-antigravity-cli/run-antigravity-cli&quot; - ⛔ blocked (allowed roots: [/home/runner/work/run-antigravity-cli/run-antigravity-cli/tests/test_workflow_cm.py /home/runner/.codemender /tmp])<br>2026–08–20T21:18:17Z [WARN] 🚫 SANDBOX VIOLATION #2: Agent requested path &quot;..&quot; - ⛔ blocked<br>2026–08–20T21:18:38Z [WARN] 🚫 SANDBOX VIOLATION #4: Agent requested path &quot;../scripts/setup_cm_config.py&quot; - ⛔ blocked<br>2026–08–20T21:18:54Z [WARN] 🚫 SANDBOX VIOLATION #5: Agent requested path &quot;../.github/workflows/test-cm.yml&quot; - ⛔ blocked</pre><p>Because the sandbox roots were too narrow, the agent was blinded to the broader codebase, unable to follow cross-file references or inspect schemas.</p><h3>The Fix: Explicit Workspace Roots and CI Sandbox Disabling</h3><p>In <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/12"><strong>run-antigravity-cli PR #12 (fix: disable namespace sandbox in CI and pass absolute paths)</strong></a>, I let my coding agent engineer a threefold fix:</p><ul><li><strong>Declared Workspace Roots: </strong>Updated <a href="https://github.com/anguillanneuf/run-antigravity-cli/blob/master/scripts/setup_cm_config.py">scripts/setup_cm_config.py</a> to default project_paths to [“.”], ensuring the entire repository root is registered inside the agent’s allowed boundaries.</li><li><strong>Absolute Path Normalization: </strong>Added the --absolute flag to <a href="https://github.com/anguillanneuf/run-antigravity-cli/blob/master/scripts/resolve_pr_files.py">scripts/resolve_pr_files.py</a>, ensuring target paths are fully qualified and never accidentally restrict directory traversal.</li><li><strong>Disabled Namespace Sandbox in CI: </strong>Set sandbox.enabled: false in the headless CI configuration.</li></ul><p>With these changes, CodeMender executed flawlessly across 5 target files with zero boundary violations, publishing the clean <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/12#issuecomment-5373510679"><strong>PR #12 Security Scan Report</strong></a><strong>.</strong></p><p>In <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/19"><strong>PR #19 feat(ci): adopt CodeMender native find — diff mode with impact analysis</strong></a>, the workflow further evolved to use CodeMender’s native Git diff mode, i.e. cm find . --diff=... --sandbox=false, scanning from the repository root while retaining --sandbox=false. It also produced a clean <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/19#issuecomment-6087818153"><strong>PR #19 Security Scan Report</strong></a><strong>.</strong></p><h3>Security Analysis: Why It Is Safe in CI (And When It Isn’t)</h3><p>Disabling a security sandbox should never be done lightly. Why is it acceptable here?</p><p>• <strong>Why It Is Safe on GitHub-Hosted Runners: </strong>GitHub-hosted runners ( ubuntu-latest ) are ephemeral, isolated virtual machines provisioned on demand and destroyed immediately after job completion. No persistent secrets, credentials, or state linger after the workflow finishes. Furthermore, cm find is a read-only detection scan. It analyzes ASTs, code paths, and data flows to detect flaws without compiling, executing untrusted binaries, or modifying the host filesystem. Combined with short-lived Workload Identity Federation (OIDC) tokens and isolated CI scripts, there are no static credentials for untrusted code to exfiltrate.</p><p>• <strong>When It Is NOT Safe (Cautionary Scenarios): </strong>If your organization operates <strong>self-hosted or persistent CI runners</strong> that run multiple jobs sequentially, disabling sandboxing is dangerous. A malicious dependency or prompt injection could inspect neighboring repositories or pivot across the runner host. Similarly, if you execute active remediation ( cm verify and cm fix ) or run build scripts on untrusted fork code, namespace or container isolation is mandatory to prevent arbitrary code execution.</p><h3>Securing Cross-Repository PRs: Preventing Fork Abuse</h3><p>A recurring vulnerability in CI pipelines involves handling pull requests submitted from external forks. In public open-source projects, anyone can fork the repository, modify code, and open a PR.</p><p>Standard GitHub Actions pull_request triggers run in the context of the fork with read-only repository tokens. Crucially, their OIDC tokens possess external repository claims, preventing them from minting Google Cloud WIF credentials for your cloud project. If you switch blindly to pull_request_target to grant WIF access, a malicious contributor could modify workflow steps to exfiltrate tokens or abuse cloud quota.</p><p>To resolve this safely, I approved to have my coding agent implement a defense-in-depth architecture in <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/13"><strong>run-antigravity-cli PR #13</strong></a> and <a href="https://github.com/anguillanneuf/run-antigravity-cli/pull/18"><strong>run-antigravity-cli PR #18</strong></a>:</p><ul><li><strong>Dual-Trigger Architecture &amp; Declarative Association Gating: </strong>Workflows listen to both pull_request for internal branches and pull_request_target for external forks. Internal PRs and trusted collaborators ( COLLABORATOR, MEMBER, OWNER ) are evaluated natively in GitHub Actions if: expressions before runners are provisioned, avoiding unnecessary credential minting.</li><li><strong>TOCTOU-Resistant Label Gating ( </strong><strong>safe-to-test ): </strong>External fork PRs from non-collaborators run when an authorized maintainer explicitly attaches the safe-to-test label after reviewing the diff. To eliminate Time-of-Check to Time-of-Use (TOCTOU) exploits, where an attacker pushes malicious code ( synchronize ) after receiving approval, the scan job only triggers on action == ‘labeled’. In addition, an automated remove-stale-approval job immediately revokes the safe-to-test lable upon any subsequent commit push or PR reopen.</li><li><strong>Trusted Script Isolation ( </strong><strong>$RUNNER_TEMP ): </strong>Under pull_request_target, the workflow YAML executes from the trusted base branch. However, because CodeMender must inspect the actual PR commit ( head.sha ), untrusted workspace code is checked out on the runner. To prevent the runner from executing attacker-tampered helper scripts, the workflow performs a sparse checkout of scripts/ from the trusted base ref into .trusted-ci, and executes all CI tooling ( setup_cm_config.py, resolve_pr_files.py, report_cm_results.py ) strictly from this isolated directory.</li></ul><h3>The Operational Reality: Gated Public Preview</h3><p>For teams eager to adopt CodeMender CLI in their CI pipelines today, there is an important operational prerequisite: <strong>CodeMender CLI is currently in a Gated Public Preview</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1009/1*h7ARKu6Whv0j4oT7BpBSHg.png" /></figure><p>While the CLI binary is distributed via Google Cloud Artifact Registry, invoking the backend scanning and verification services on Google Cloud Agent Platform requires the calling Google Cloud Project to be allowlisted.</p><p>To enable teams to build, test, and wire their GitHub Actions workflows before allowlisting is complete, my workflow and scripts provide a built-in preflight mode:</p><pre>python3 scripts/setup_cm_config.py - project-id &quot;${{ vars.GCP_PROJECT_ID }}&quot; - project-paths &quot;.&quot; - sync-workspace<br>python3 scripts/resolve_pr_files.py - base-ref &quot;${{ github.base_ref }}&quot; - absolute - dry-run</pre><p>This allows continuous integration teams to validate WIF token issuance, ADC file materialization, configuration generation, and target file resolution without triggering backend permission rejections.</p><h3>Key Takeaways &amp; Best Practices Checklist</h3><p>Deploying autonomous AI security reviewers in GitHub Actions bridges the gap between passive code review and proactive defense. Here is the operational checklist for success:</p><ul><li><strong>Choose the Right Auth Model: </strong>Use Direct WIF (1-hop) for stateless API tools like Antigravity CLI to eliminate service accounts and keys; use ADC with service account impersonation (2-hop) with create_credentials_file: true for CodeMender CLI.</li><li><strong>Declare Project Roots &amp; Normalize Paths: </strong>Avoid relative path constriction by setting project_path: [“.”] and passing fully qualified absolute paths to cm find.</li><li><strong>Evaluate Sandbox Safety: </strong>Disabling namespace sandboxing is safe on ephemeral, single-use GitHub-hosted VMs for read-only scans, but must be strictly preserved or containerized on persistent self-hosted runners.</li><li><strong>Protect Against Untrusted Forks: </strong>Always gate external PR execution under pull_request_target with manual review guardrails.</li><li><strong>Plan for Preview Allowlisting: </strong>Validate your CI pipeline using dry-run preflights while coordinating Google Cloud Project allowlisting for CodeMender CLI access.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=03ba291ac0a6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/automating-agentic-security-reviews-running-codemender-cli-in-github-actions-03ba291ac0a6">Automating Agentic Security Reviews: Running CodeMender CLI in GitHub Actions</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Does the “Catchment Premium” Really Exist?]]></title>
            <link>https://medium.com/google-cloud/does-the-catchment-premium-really-exist-d7a22438498f?source=rss----e52cf94d98af---4</link>
            <guid isPermaLink="false">https://medium.com/p/d7a22438498f</guid>
            <category><![CDATA[bigquery]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[data-analytics]]></category>
            <category><![CDATA[gis]]></category>
            <category><![CDATA[looker-studio]]></category>
            <dc:creator><![CDATA[Maya Hori]]></dc:creator>
            <pubDate>Mon, 12 Oct 2026 11:22:35 GMT</pubDate>
            <atom:updated>2026-10-12T11:22:33.987Z</atom:updated>
            <content:encoded><![CDATA[<h3>Does the “Catchment Premium” Really Exist? Unmasking Spatial Reality in Shrewsbury with BigQuery GIS &amp; Looker Studio</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kwU_eMe4UQ4xguAECO5peg.jpeg" /></figure><h3>Introduction: The Catchment Myth vs. Regional Reality</h3><p>In the UK housing market, few narratives are as deeply entrenched as the “Catchment Area Premium.” Across London and major metropolitan hubs, property listings routinely trumpet proximity to “Outstanding” primary schools, commanding eye-watering price premiums. Families stretch their borrowing capacity simply to fall on the fortunate side of an administrative line.</p><p>In my <a href="https://medium.com/google-cloud/mapping-local-flood-risks-in-looker-studio-using-defra-open-data-and-bigquery-db2571a7dcd0">previous article on mapping Defra flood risks,</a> I explored the physical geography of Shrewsbury — a historic county town bounded and periodically besieged by the unembanked River Severn. Having engineered a fast GeoParquet pipeline to map Flood Zones 2 and 3 in Looker Studio, a natural economic question emerged:</p><blockquote>In a historic regional town facing recurring flood risks, does the primary school catchment premium still reign supreme, or is it eclipsed by environmental vulnerability and urban geography?</blockquote><p>To answer this, I extended our spatial architecture in Google BigQuery GIS, joining HM Land Registry transaction data across 22 primary school catchments, 3 secondary school zones, and localized flood boundaries. What the data revealed was not a triumph of school branding, but a masterclass in urban economics and topography.</p><h3>Data Architecture: Extending the Spatial Pipeline</h3><p>Rather than visualizing individual, noisy property sales on a raw map, my objective was to design a clean, high-performance analytical model in BigQuery that preserves both property-type distinctions and environmental context.</p><p>Our data stack incorporates four foundational layers:</p><ol><li><strong>HM Land Registry (Price Paid Data)</strong>: Transaction records including sale prices, transfer dates, and property classifications: Detached (D), Semi-detached (S), Terraced (T), and Flat (F).</li><li><strong>ONS Postcode Directory (ONSPD)</strong>: Providing precise geographic coordinates (ST_GEOGPOINT) for postcode centroids.</li><li><strong>Shropshire Council School Catchments</strong>: Shapefiles defining 22 localized primary catchments and 3 broader secondary school zones.</li><li><strong>Environment Agency Flood Zones 2 &amp; 3</strong>: Our pre-optimized flood boundary table (shrewsbury_flood_zones), processed via QGIS and GeoParquet in the previous project.</li></ol><h4>Balancing Performance and Granularity</h4><p>Directly piping hundreds of thousands of individual transactions into Looker Studio introduces latency and skews aggregate calculations. Conversely, rolling up metrics strictly by school name discards vital property characteristics.</p><p>To solve this, I engineered an intermediate aggregation layer grouped by postcode × property_type × is_flood_area, retaining sample size (total_transactions) and baseline prices to enable dynamic weighted averaging downstream.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/655/1*lOVkIAFFINmSxiv8XCfbHw.png" /><figcaption><em>Figure 1: Final table schema in BigQuery (</em><em>shrewsbury_property_primary_secondary_final) balancing transaction sample sizes with spatial dimensions.</em></figcaption></figure><pre>WITH property_points AS (<br>  -- 1. Geocode transaction records to Point geometries<br>  SELECT<br>    lr.price,<br>    lr.property_type,<br>    lr.postcode,<br>    ST_GEOGPOINT(ons.longitude, ons.latitude) AS geom_point<br>  FROM<br>    `your_project.land_registry.price_paid` AS lr<br>  JOIN<br>    `your_project.geo_reference.onspd` AS ons<br>    ON lr.postcode = ons.postcode<br>  WHERE<br>    ons.outcode IN (&#39;SY1&#39;, &#39;SY2&#39;, &#39;SY3&#39;)<br>),<br>joined_catchments AS (<br>  -- 2. Spatial Join with Primary &amp; Secondary boundaries<br>  SELECT<br>    p.price,<br>    p.property_type,<br>    p.postcode,<br>    p.geom_point,<br>    pri.school_name AS School_name,<br>    sec.school_name AS secondary_school_name<br>  FROM<br>    property_points AS p<br>  LEFT JOIN<br>    `your_project.catchments.primary_schools` AS pri<br>    ON ST_CONTAINS(pri.geometry, p.geom_point)<br>  LEFT JOIN<br>    `your_project.catchments.secondary_schools` AS sec<br>    ON ST_CONTAINS(sec.geometry, p.geom_point)<br>)<br>-- 3. Intersect with Flood Zones and generate pre-aggregated analytical table<br>SELECT<br>  jc.School_name,<br>  jc.secondary_school_name,<br>  jc.property_type,<br>  jc.postcode,<br>  CASE <br>    WHEN fz.geometry IS NOT NULL THEN TRUE <br>    ELSE FALSE <br>  END AS is_flood_area,<br>  ROUND(AVG(jc.price), 0) AS average_sale_price,<br>  COUNT(1) AS total_transactions<br>FROM<br>  joined_catchments AS jc<br>LEFT JOIN<br>  `your_project.environment_agency.shrewsbury_flood_zones` AS fz<br>  ON ST_INTERSECTS(fz.geometry, jc.geom_point)<br>WHERE<br>  jc.School_name IS NOT NULL<br>GROUP BY<br>  1, 2, 3, 4, 5;</pre><h3>Visualization in Looker Studio: Uncovering the Baseline</h3><p>To prevent sampling skew across postcodes with unequal turnover, I configured a custom calculated metric in Looker Studio for weighted average pricing:</p><pre>Weighted Average Price = SUM(average_sale_price * total_transactions) / SUM(total_transactions)</pre><p>When rendered across all 22 primary school catchments, the property hierarchy behaves impeccably: Detached houses sit firmly at the top, followed by Semi-detached, Terraced, and Flats.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/955/1*ZaSF8MO9iSQozr1GfC9RIQ.png" /><figcaption><em>Figure 2: Distribution of property values across primary catchments in Shrewsbury by housing typology (Detached, Semi-Detached, Terraced, Flat).</em></figcaption></figure><p>At first glance, significant price gaps appear between catchments. Rural and edge-of-town schools like <strong>Longden CofE</strong> show massive detached averages (£650k+) driven by expansive rural parcels. Within town boundaries, <strong>St George’s Junior</strong> and <strong>Coleham Primary</strong> lead the urban pack, maintaining semi-detached averages near £400k.</p><h3>The Reality Check: Flood Risk as the Ultimate Arbitrator</h3><p>The dynamic shifts dramatically the moment we apply our is_flood_area boolean filter.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gl732x5G1uTN0SyyUF_eUQ.png" /><figcaption>Figure 3: Valuation impact of EA Flood Zones 2 &amp; 3 on core housing typologies (Semi-detached &amp; Terraced) across primary school catchments.</figcaption></figure><p>By isolating properties directly touching EA Flood Zones 2 and 3, an unyielding market penalty surfaces:</p><p><strong>Coleham Primary School:</strong><br> • Non-Flood Semi-detached: ~£390k<br> • Flood Zone Semi-detached: ~£300k<br> • Net Discount: -£89,854 (-23%)</p><p><strong>St George’s Junior School (Frankwell Lowlands):</strong><br>• Non-Flood Semi-detached: ~£400k<br>• Flood Zone Semi-detached: ~£295k <br>• Net Discount: -£105,022 (-26%)</p><p><strong>Greenfields Primary School:</strong> <br>• Non-Flood Semi-detached: ~£274k <br>• Flood Zone Semi-detached: ~£226k <br>• Net Discount: -£47,336 (-17%)</p><p><strong>The Martin Wilson School (Castlefields): </strong><br>• Non-Flood Terraced: ~£222k <br>• Flood Zone Terraced: ~£192k <br>• Net Discount: -£29,965 (-13%)</p><p>Regardless of academic accolades or community reputation, crossing the threshold into the River Severn’s flood path imposes an immediate, non-negotiable <strong>20% to 25% price discount</strong> within the very same school boundary.</p><h3>Deconstructing the Myth: Why Geography Beats Catchments</h3><p>Seeing Coleham’s flooded semi-detached homes hold a £300k baseline — still higher than non-flooded homes in outer suburbs like Mereside (~£270k) — one might be tempted to argue: <em>“The catchment premium is so strong it cushions the flood hit!”</em></p><p>However, looking at the structural geography of Shrewsbury completely disproves this assumption.</p><h4>1. Urban Gravity vs. Academic Prestige</h4><p>Coleham sits immediately across the English Bridge from the town center. Its price floor is sustained not by school gates, but by walkable lifestyle amenities, independent shops, and direct pedestrian access to the town core.</p><p>When comparing Coleham to <strong>Castlefields (The Martin Wilson School catchment)</strong> — which shares an identical urban-edge location and River Severn flood exposure — flooded terraced homes in Castlefields trade around <strong>£190k</strong>, compared to <strong>£280k</strong> in Coleham. This £90k chasm is rooted in housing typology: Castlefields comprises compact, industrial 19th-century worker terraces, whereas Belle Vue and Coleham boast wider Victorian and Edwardian villas with substantial gardens.</p><h4>2. The Absence of Feeder Pressure</h4><p>In London, primary catchments matter immensely because admissions funnel into competitive secondary schools. In Shrewsbury, secondary education across the urban core is unified under a single <strong>shared catchment (Shrewsbury Schools)</strong> encompassing The Priory, Meole Brace, Belvidere, and Shrewsbury Academy. With no hyper-localized secondary gating mechanism, the primary school “feeder race” largely evaporates.</p><h4>3. Stock Eras Across Suburban Rings</h4><p>Comparing adjacent southwestern residential suburbs further confirms the absence of school-driven pricing:</p><ul><li><strong>Radbrook Primary vs. Meole Brace CE Primary</strong>: Detached homes in both catchments trade almost identically at <strong>~£460k–£470k</strong>. Yet, semi-detached properties in Meole Brace trade ~£70k higher than Radbrook. This reflects architectural epochs: Radbrook’s housing stock is dominated by mature, uniform 1980s–90s suburban estates, whereas Meole Brace’s catchment captures a significant concentration of recent high-spec new-build developments (particularly along the southern urban extensions) alongside historic village plots, commanding a notable new-build premium.</li></ul><h3>Conclusion: Spatial Intelligence Over Conventional Wisdom</h3><p>When I set out to analyze the relationship between school catchments and property values, conventional wisdom suggested I would find a distinct educational premium.</p><p>Instead, my BigQuery GIS pipeline uncovered an entirely different truth:</p><ul><li><strong>Physical hazard is absolute</strong>: Flood risk enforces an undeniable 20–25% valuation cut within identical school boundaries.</li><li><strong>School catchments are secondary to urban structure</strong>: In regional towns with unified secondary systems, micro-catchment premiums are practically non-existent. Market prices are driven overwhelmingly by <strong>walkability to town, architectural epoch, and topography</strong>.</li></ul><p>Relying on high-level averages often reinforces market myths. By joining open geospatial datasets at granular resolution, spatial data analysts can cut through generalized assumptions and illuminate the true mechanics of local real estate.</p><h3>Looking Ahead: The Final Synthesis</h3><p>Over the course of my spatial data projects, I have dissected Shrewsbury from multiple angles:</p><p>• <strong>Environmental Risk</strong>: Engineering optimized Defra Flood Zone boundaries via GeoParquet.</p><p>• <strong>Healthcare Infrastructure</strong>: Mapping travel-time access to GP clinics and hospitals.</p><p><strong>・Urban Development</strong>: Tracking planning permissions and heritage conservation in real time.</p><p><strong>・Social Economics</strong>: Evaluating school catchments against property market fundamentals.</p><p>In the upcoming capstone article of my spatial analytics work, I will bring all these disparate spatial layers together into a comprehensive Urban Spatial Intelligence Framework — demonstrating how modern data engineering can build a holistic, 360-degree digital twin of a regional town. Stay tuned!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d7a22438498f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/does-the-catchment-premium-really-exist-d7a22438498f">Does the “Catchment Premium” Really Exist?</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>