<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Google Developer Group</title>
    <description>The latest articles on DEV Community by Google Developer Group (gdg).</description>
    <link>https://dev.to/gdg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F12748%2Fe3cbcad3-4749-4461-ad88-4b9b8cde89ec.png</url>
      <title>DEV Community: Google Developer Group</title>
      <link>https://dev.to/gdg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gdg"/>
    <language>en</language>
    <item>
      <title>Zero to Multi-Region: High Availability Serverless with Cloud Run and Cross-Region Failover &amp; Failback</title>
      <dc:creator>Caleb Duff</dc:creator>
      <pubDate>Fri, 24 Jul 2026 00:52:30 +0000</pubDate>
      <link>https://dev.to/gdg/zero-to-multi-region-high-availability-serverless-with-cloud-run-and-cross-region-failover--dcc</link>
      <guid>https://dev.to/gdg/zero-to-multi-region-high-availability-serverless-with-cloud-run-and-cross-region-failover--dcc</guid>
      <description>&lt;p&gt;Google just made multi-region Cloud Run significantly easier. Here is the full picture; what changed, what it means in practice, and how to build it right.&lt;/p&gt;

&lt;p&gt;Most teams discover they need multi-region architecture the hard way and  sadly, during an outage. Whether you're running a global e-commerce platform, a real-time gaming API, or a financial services application, users expect your service to be available whenever they need it. There is a conversation that happens in almost every engineering team at some point. It usually starts with a post-mortem. A regional Google Cloud outage or a Cloud Run service that hit a cold start spike, or a single-region deployment that could not handle the latency demands of users spread across Lagos, Nairobi, and London simultaneously, caused enough pain that someone finally asked: &lt;em&gt;why are we only deployed in one region?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer is usually one of three things: it felt complex, it felt expensive, or no one had prioritised it yet.&lt;/p&gt;

&lt;p&gt;In July 2026, Google moved Cloud Run &lt;strong&gt;Service Health&lt;/strong&gt; to General Availability and the timing was hard to miss. Six days earlier, a power cut at Google's Netherlands data centre had knocked three services offline. The GA release brings automatic cross-region failover to Cloud Run with what Google describes as a two-step setup: add a readiness probe, set minimum instances to at least 1. The load balancer does the rest.&lt;/p&gt;

&lt;p&gt;This article covers the full architecture, what Service Health is, how readiness probes underpin it, how to set up the Global Load Balancer correctly, and how to test that failover actually works. It also covers the production details.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed: Service Health and readiness probes
&lt;/h2&gt;

&lt;p&gt;Before Service Health, multi-region Cloud Run required you to implement a &lt;code&gt;/health&lt;/code&gt; endpoint in your application and configure a separate HTTPS health check at the load balancer level. This worked, but it had a significant gap. The load balancer's health check only knew whether the Cloud Run service &lt;em&gt;endpoint&lt;/em&gt; was responding, not whether the individual container instances behind it were actually ready to serve traffic.&lt;/p&gt;

&lt;p&gt;Service Health introduces two new capabilities that close this gap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readiness probes&lt;/strong&gt; operate at the container instance level. Cloud Run periodically sends an HTTP request to a path you specify on each running container instance. If the probe fails, Cloud Run stops routing requests to that instance until the probe succeeds again. Critically, a failing readiness probe does not kill the instance (that is what a liveness probe does), it simply marks the instance as not ready for traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service Health&lt;/strong&gt; aggregates the readiness state of all container instances in a region into a single regional health signal. This aggregated health status is exposed through the Serverless NEGs (Network Endpoint Groups) for that region. When the Global Load Balancer reads the NEG's health status and sees a region is unhealthy, because enough instances are failing their readiness probes; it automatically reroutes traffic to a healthy region. When the failing region recovers, traffic is gradually restored without any operator action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; failover and failback capabilities are now fully automated, triggered by real instance-level health rather than a synthetic endpoint check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container instance (readiness probe fails)
        │
        ▼
Cloud Run aggregates probe results across all instances in the region to determine the overall health status of each regional service
        │
        ▼
Service Health: region marked UNHEALTHY
        │
        ▼
Serverless NEG reports unhealthy status to Global Load Balancer
        │
        ▼
Load Balancer stops routing to this region → shifts traffic to healthy region
        │
        ▼
Region recovers → Load Balancer gradually restores traffic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is available in all Cloud Run regions at no extra charge beyond the CPU and memory consumed while readiness probes run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────────────────────┐
                    │   Global Anycast IP (single IP)  │
                    │   + SSL Certificate (managed)    │
                    └───────────────┬──────────────────┘
                                    │
                    ┌───────────────▼──────────────────┐
                    │  Global External HTTP(S) LB      │
                    │  (URL map + forwarding rules)    │
                    └──────┬──────────────────┬────────┘
                           │                  │
        ┌──────────────────▼──┐         ┌─────▼──────────────────┐
        │  Serverless NEG     │         │  Serverless NEG        │
        │  africa-south1      │         │  us-central1           │
        │  (Service Health    │         │  (Service Health       │
        │   status: healthy)  │         │   status: healthy)     │
        └──────────┬──────────┘         └───────────┬────────────┘
                   │                                │
   ┌───────────────▼───────────┐     ┌──────────────▼──────────────┐
   │  Cloud Run Service        │     │  Cloud Run Service          │
   │  africa-south1            │     │  us-central1                │
   │  Readiness probe: /health │     │  Readiness probe: /health   │
   │  min-instances: 1+        │     │  min-instances: 1+          │
   │  (auto-scales 0–N)        │     │  (auto-scales 0–N)          │
   └───────────────────────────┘     └─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prerequisites and setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-project-id"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud projects describe &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(projectNumber)"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my-api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGION_A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"africa-south1"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGION_B&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-central1"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api.yourdomain.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gcr.io/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:latest"&lt;/span&gt;

gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;

&lt;span class="c"&gt;# Enable required APIs&lt;/span&gt;
gcloud services &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  run.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  compute.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  artifactregistry.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  cloudbuild.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  networkservices.googleapis.com

&lt;span class="c"&gt;# Grant Cloud Build service account the Cloud Run builder role&lt;/span&gt;
gcloud projects add-iam-policy-binding &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-   compute@developer.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/run.builder"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Implement the readiness probe endpoint
&lt;/h2&gt;

&lt;p&gt;The first step and the most important one for Service Health to work, is adding a readiness probe endpoint to your application. Unlike the previous/alternative approach where the &lt;code&gt;/health&lt;/code&gt; endpoint was for the load balancer's benefit, this endpoint is called directly by Cloud Run on each container instance to determine per-instance readiness.&lt;/p&gt;

&lt;p&gt;Two rules from the official docs that matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an HTTP/1 endpoint (the Cloud Run default, not HTTP/2)&lt;/li&gt;
&lt;li&gt;The endpoint path must match the &lt;code&gt;path&lt;/code&gt; in your probe configuration exactly
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js / Express&lt;/span&gt;
&lt;span class="c1"&gt;// Lightweight — no DB calls, no downstream dependencies&lt;/span&gt;
&lt;span class="c1"&gt;// This runs frequently on every instance&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// If you want the probe to reflect actual readiness&lt;/span&gt;
&lt;span class="c1"&gt;// (e.g. connection pool initialised), you can check internal state:&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REGION&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Set isReady = true after your startup tasks complete&lt;/span&gt;
&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python / FastAPI
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;is_ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;readiness_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.on_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;startup_event&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;is_ready&lt;/span&gt;
    &lt;span class="c1"&gt;# Initialise connections, warm caches, etc.
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;init_database_pool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;is_ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;is_ready&lt;/code&gt; pattern is the key upgrade over a basic &lt;code&gt;/health&lt;/code&gt; endpoint. The readiness probe on each instance will return 503 until your startup tasks complete, preventing the load balancer from routing traffic to an instance that is running but not yet ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Deploy to multiple regions with readiness probes
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;gcloud run deploy&lt;/code&gt; supports deploying to multiple regions in a single command, and the &lt;code&gt;--readiness-probe&lt;/code&gt; flag attaches the probe configuration at deploy time. Failovers require at least two (2) services from different regions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy to both regions simultaneously with readiness probe&lt;/span&gt;
gcloud run deploy &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--regions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt;,&lt;span class="nv"&gt;$REGION_B&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-instances&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--concurrency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512Mi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--readiness-probe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"httpGet.path=/health"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=production"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--readiness-probe="httpGet.path=/health"&lt;/code&gt; flag is the new way to configure probes at deploy time. You can also configure additional probe parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full readiness probe configuration&lt;/span&gt;
gcloud run deploy &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--regions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt;,&lt;span class="nv"&gt;$REGION_B&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--readiness-probe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"httpGet.path=/health,periodSeconds=10,failureThreshold=3,successThreshold=1,timeoutSeconds=5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via YAML service definition (the Terraform-friendly approach):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# service.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serving.knative.dev/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;autoscaling.knative.dev/minScale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
        &lt;span class="na"&gt;autoscaling.knative.dev/maxScale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100"&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcr.io/PROJECT_ID/my-api:latest&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENV&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
          &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
          &lt;span class="na"&gt;successThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
          &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The difference between readiness and liveness probes
&lt;/h3&gt;

&lt;p&gt;Both probe types are supported on Cloud Run. Understanding the distinction is critical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readiness probe failure:&lt;/strong&gt; Cloud Run stops routing requests to that instance. The instance continues running. Once the probe succeeds again, routing resumes. Service Health aggregates these to determine regional health.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liveness probe failure:&lt;/strong&gt; Cloud Run restarts the container instance. Use liveness probes for detecting deadlocks or unrecoverable stuck states.&lt;/p&gt;

&lt;p&gt;For Service Health's automatic failover, readiness probes are what matter. Liveness probes are a complement, they handle instance-level recovery, while readiness probes handle traffic routing decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set up the global external Application Load Balancer
&lt;/h2&gt;

&lt;p&gt;With the new Service Health model, the load balancer configuration is simpler than before, you no longer need to configure a separate HTTPS health check at the load balancer level. Service Health exposes regional health through the Serverless NEG itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the backend service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Single backend service, both regions are added as NEG backends&lt;/span&gt;
gcloud compute backend-services create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-bs&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-balancing-scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EXTERNAL_MANAGED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: unlike the earlier approach with separate backend services per region, Service Health works with a &lt;strong&gt;single backend service&lt;/strong&gt; that has multiple regional NEG backends. The load balancer reads health from each NEG and routes accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reserve a global static IP (Set up a global static external IP address to reach your load balancer:)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute addresses create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ip&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PREMIUM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;IPV4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GLOBAL_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud compute addresses describe &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ip&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"get(address)"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Global IP: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GLOBAL_IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# → Update your DNS A record to this IP before proceeding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create URL map, proxy, and forwarding rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a URL map to route incoming requests to the backend service:&lt;/span&gt;
gcloud compute url-maps create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-lb&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--default-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-bs&lt;/span&gt;

&lt;span class="c"&gt;# For HTTPS (recommended for production):&lt;/span&gt;
&lt;span class="c"&gt;# Create Google-managed SSL certificate&lt;/span&gt;
gcloud compute ssl-certificates create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ssl&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--domains&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt;

&lt;span class="c"&gt;# Create the target HTTPS proxy to route requests to your URL map:&lt;/span&gt;
gcloud compute target-https-proxies create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-https-proxy&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url-map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-lb&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssl-certificates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ssl&lt;/span&gt;

&lt;span class="c"&gt;# Create the HTTPS forwarding rule  to route incoming requests to the proxy:&lt;/span&gt;
gcloud compute forwarding-rules create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-https-fr&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-balancing-scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EXTERNAL_MANAGED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PREMIUM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ip&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-https-proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-https-proxy&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;443

&lt;span class="c"&gt;# HTTP forwarding rule (redirect to HTTPS)&lt;/span&gt;
gcloud compute target-http-proxies create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-http-proxy&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url-map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-lb&lt;/span&gt;

gcloud compute forwarding-rules create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-http-fr&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-balancing-scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EXTERNAL_MANAGED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PREMIUM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ip&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-http-proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-http-proxy&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Create Serverless NEGs and attach them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Serverless NEG for africa-south1&lt;/span&gt;
gcloud compute network-endpoint-groups create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-neg-&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-endpoint-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;serverless &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cloud-run-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;

&lt;span class="c"&gt;# Serverless NEG for us-central1&lt;/span&gt;
gcloud compute network-endpoint-groups create &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-neg-&lt;/span&gt;&lt;span class="nv"&gt;$REGION_B&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_B&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-endpoint-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;serverless &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cloud-run-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;

&lt;span class="c"&gt;# Add both NEGs to the single backend service&lt;/span&gt;
gcloud compute backend-services add-backend &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-bs&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-endpoint-group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-neg-&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-endpoint-group-region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt;

gcloud compute backend-services add-backend &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-bs&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-endpoint-group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-neg-&lt;/span&gt;&lt;span class="nv"&gt;$REGION_B&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-endpoint-group-region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, Service Health is active. Cloud Run is running readiness probes on every instance in both regions, aggregating the results into a regional health signal, and the load balancer reads that signal via the Serverless NEGs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Monitor Service Health with Cloud Monitoring
&lt;/h2&gt;

&lt;p&gt;Service Health exposes two metrics through Cloud Monitoring that you should track from day one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;run.googleapis.com/container/instance_count_with_readiness&lt;/code&gt;&lt;/strong&gt;, the number of instances passing their readiness probe per region. Watch this metric to see the health state of your instance pool in each region in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;run.googleapis.com/service_health_count&lt;/code&gt;&lt;/strong&gt;, the regional Cloud Run service health as reported to the load balancer. Possible values: &lt;code&gt;HEALTHY&lt;/code&gt;, &lt;code&gt;UNHEALTHY&lt;/code&gt;, &lt;code&gt;UNKNOWN&lt;/code&gt;. The load balancer uses this to make failover decisions. &lt;code&gt;UNKNOWN&lt;/code&gt; is reported until the service has enough data from probes to determine health, typically within the first few minutes of deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# View current service health status via gcloud&lt;/span&gt;
gcloud run services describe &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(status.conditions)"&lt;/span&gt;

&lt;span class="c"&gt;# Or check via the Console:&lt;/span&gt;
&lt;span class="c"&gt;# Cloud Run → your service → Metrics tab → "Instance count with readiness"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up an alerting policy that fires when &lt;code&gt;service_health_count&lt;/code&gt; for any region transitions to &lt;code&gt;UNHEALTHY&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# alert-policy.yaml&lt;/span&gt;
&lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cloud&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unhealthy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failover&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;active"&lt;/span&gt;
&lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;UNHEALTHY"&lt;/span&gt;
  &lt;span class="na"&gt;conditionThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;resource.type="cloud_run_revision"&lt;/span&gt;
      &lt;span class="s"&gt;metric.type="run.googleapis.com/service_health_count"&lt;/span&gt;
      &lt;span class="s"&gt;metric.labels.health_status="UNHEALTHY"&lt;/span&gt;
    &lt;span class="na"&gt;comparison&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COMPARISON_GT&lt;/span&gt;
    &lt;span class="na"&gt;thresholdValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
    &lt;span class="na"&gt;aggregations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alignmentPeriod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
      &lt;span class="na"&gt;perSeriesAligner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALIGN_MAX&lt;/span&gt;
&lt;span class="na"&gt;notificationChannels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;projects/${PROJECT_ID}/notificationChannels/YOUR_CHANNEL_ID&lt;/span&gt;
&lt;span class="na"&gt;documentation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;A Cloud Run region has become unhealthy and traffic is being&lt;/span&gt;
    &lt;span class="s"&gt;rerouted to the remaining healthy region(s). Investigate the&lt;/span&gt;
    &lt;span class="s"&gt;failing region's logs and instance readiness metrics immediately.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Testing failover
&lt;/h2&gt;

&lt;p&gt;Testing is not optional, it is the only way to know your failover actually works before your users discover it during a real incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: Use the sample application's toggle (for the official sample)
&lt;/h3&gt;

&lt;p&gt;The Google Cloud sample application (&lt;code&gt;golang-samples/run/service-health&lt;/code&gt;) includes a built-in toggle button in its UI that marks a region as unhealthy. For production applications, use Method 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Force readiness probe failure via environment variable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Redeploy africa-south1 with a flag that makes /health return 503&lt;/span&gt;
gcloud run deploy &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"FORCE_UNHEALTHY=true,ENV=production"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your application, check this variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FORCE_UNHEALTHY&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forced_unhealthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REGION&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observe the failover sequence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get load balancer IP&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LBIP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud compute addresses describe &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-ip&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'value(address)'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Continuous requests — watch region shift in responses&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;RESPONSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/health&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%H:%M:%S'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; — &lt;/span&gt;&lt;span class="nv"&gt;$RESPONSE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should observe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requests showing &lt;code&gt;"region": "africa-south1"&lt;/code&gt; — normal operation&lt;/li&gt;
&lt;li&gt;A mix of responses as the probe failure propagates across instances&lt;/li&gt;
&lt;li&gt;All requests showing &lt;code&gt;"region": "us-central1"&lt;/code&gt; — failover complete&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;service_health_count&lt;/code&gt; metric for &lt;code&gt;africa-south1&lt;/code&gt; showing &lt;code&gt;UNHEALTHY&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Restore the region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=production"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traffic gradually returns to &lt;code&gt;africa-south1&lt;/code&gt; as instances pass their readiness probes and Service Health transitions back to &lt;code&gt;HEALTHY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe rollout strategy using readiness probes
&lt;/h2&gt;

&lt;p&gt;One of the most powerful features of the new readiness probe model is the ability to do &lt;strong&gt;canary deployments across regions&lt;/strong&gt; with automatic rollback via Service Health.&lt;/p&gt;

&lt;p&gt;The official recommended rollout process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: Deploy new revision to ONE region with 1% traffic&lt;/span&gt;
gcloud run deploy &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE_NEW&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--readiness-probe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"httpGet.path=/health"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-traffic&lt;/span&gt;  &lt;span class="c"&gt;# Deploy but send no traffic yet&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Send 1% of traffic to new revision in REGION_A only&lt;/span&gt;
gcloud run services update-traffic &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to-revisions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;LATEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="c"&gt;# Step 3: Monitor readiness metric&lt;/span&gt;
&lt;span class="c"&gt;# run.googleapis.com/container/instance_count_with_readiness&lt;/span&gt;
&lt;span class="c"&gt;# If this stays healthy, continue increasing traffic&lt;/span&gt;

&lt;span class="c"&gt;# Step 4: Ramp to 100% in REGION_A&lt;/span&gt;
gcloud run services update-traffic &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_A&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to-revisions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;LATEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100

&lt;span class="c"&gt;# Step 5: Once REGION_A service_health_count is stable HEALTHY,&lt;/span&gt;
&lt;span class="c"&gt;# deploy to REGION_B&lt;/span&gt;
gcloud run deploy &lt;span class="nv"&gt;$SERVICE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE_NEW&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION_B&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--readiness-probe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"httpGet.path=/health"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the new revision's readiness probes fail in &lt;code&gt;REGION_A&lt;/code&gt;, Service Health marks that region unhealthy, the load balancer routes traffic to &lt;code&gt;REGION_B&lt;/code&gt; (still running the old revision), and you have automatic rollback without a single manual step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limitations of Service Health (GA)
&lt;/h2&gt;

&lt;p&gt;The official documentation lists several limitations worth knowing before you build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum instances required.&lt;/strong&gt; You must configure at least one minimum instance per region for Service Health to calculate health. A region with zero running instances cannot report health, which means a cold-start region cannot participate in automatic failover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum two regions.&lt;/strong&gt; Failover requires at least two services from different regions. If you only deploy to one region and it fails, the load balancer returns &lt;code&gt;no healthy upstream&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max 5 NEG backends for cross-region internal LB.&lt;/strong&gt; The limitation applies to the internal load balancer variant, not the global external LB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No URL masks or tags in Serverless NEGs.&lt;/strong&gt; If your routing requires URL masks, you cannot use Service Health's NEG model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No IAP from the backend service.&lt;/strong&gt; If you need Identity-Aware Proxy, configure it directly on the Cloud Run service, not at the load balancer backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First probe on new instances.&lt;/strong&gt; A newly started instance will not have its first readiness probe counted before it begins receiving traffic. This means a very brief window where traffic may route to an instance before it has confirmed readiness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revisions without probes are treated as unknown.&lt;/strong&gt; The load balancer treats unknown health as healthy, so if you deploy a revision without a readiness probe configured, it will receive traffic regardless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two points are important for zero-downtime deployments. The recommended safe rollout process (canary in one region before the other) directly addresses both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this architecture does not solve
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Database availability.&lt;/strong&gt; Compute-layer failover is irrelevant if your Cloud Run service connects to a single-region Cloud SQL instance. The database tier needs its own HA: Cloud SQL cross-region read replicas, Cloud Spanner for global consistency, or Firestore in Native mode (inherently multi-region).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateful sessions.&lt;/strong&gt; Cloud Run is stateless. Cross-region routing will invalidate in-memory sessions. Use Cloud Memorystore (Redis) or stateless JWT-based sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data residency.&lt;/strong&gt; Routing traffic across regions may conflict with NDPR, GDPR, or sector-specific regulations. Know your data residency obligations before deploying multi-region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pub/Sub push subscriptions.&lt;/strong&gt; By default, Pub/Sub delivers messages to push endpoints in the same region where it stores the messages. A multi-region Cloud Run setup behind a global LB does not automatically receive Pub/Sub push traffic from all regions. The official docs provide a workaround, review the Pub/Sub multi-region push documentation before building event-driven architectures on this pattern.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>cloudrun</category>
      <category>googlecloud</category>
      <category>gcp</category>
    </item>
    <item>
      <title>From Apple Health Data to Clinical Storytelling: Building an AI-Powered Report with Python and Gemini</title>
      <dc:creator>Romina Elena Mendez Escobar</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:55:58 +0000</pubDate>
      <link>https://dev.to/gdg/from-apple-health-data-to-clinical-storytelling-building-an-ai-powered-report-with-python-and-3n8n</link>
      <guid>https://dev.to/gdg/from-apple-health-data-to-clinical-storytelling-building-an-ai-powered-report-with-python-and-3n8n</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;At recent technology conferences, one topic has caught my attention: every year, more health-focused devices, sensors, and applications appear. Smartwatches track heart rate, smart scales measure body data, glucose monitors record blood sugar levels, and apps help users track sleep or nutrition. Today, the amount of information we can collect about our own bodies is enormous.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftuu57c0prpch85eraky7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftuu57c0prpch85eraky7.png" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article was inspired by an everyday experience with my father, &lt;strong&gt;Herminio ❤️&lt;/strong&gt;. Whenever he has a medical appointment, he opens the Apple Health app and shows the doctor the evolution of his heart rate, physical activity, sleep hours, and other recorded metrics. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While watching this, I kept asking myself the same question: &lt;strong&gt;are we really making the most of all this information?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Showing a chart during a medical appointment can be useful, but the data could provide much more value if it were automatically processed, summarized, and transformed into a structured health report.&lt;/p&gt;

&lt;p&gt;For this reason in this project, I use &lt;strong&gt;Gemini&lt;/strong&gt; to transform previously calculated metrics into a clear and organized summary. The LLM does not analyze all the raw records or perform the main calculations. The pipeline processes the data, calculates the indicators, and generates the visualizations, while the model acts as a support layer for building the report narrative.&lt;/p&gt;

&lt;p&gt;The goal is not to create a medical application or replace professional judgment. Instead, the purpose is to build a prototype that shows how Apple Health exports, deterministic data processing, visualizations, and an LLM can be combined to generate automated reports.&lt;/p&gt;

&lt;p&gt;This project was developed using simulated data from three patients, so the complete pipeline can be reproduced without using real clinical information.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Why Gemini?
&lt;/h2&gt;

&lt;p&gt;This project uses an &lt;strong&gt;LLM&lt;/strong&gt; to transform previously processed metrics into a structured narrative that can be reviewed more easily by a healthcare professional.&lt;br&gt;
I chose Gemini for practical reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;〰️ I was already familiar with its API, it integrates easily with Python, and Google AI Studio makes it simple to test and adjust prompts. &lt;/li&gt;
&lt;li&gt;〰️ It also offers a good balance between speed, performance, and cost for text-generation tasks. &lt;/li&gt;
&lt;li&gt;〰️ Its free tier also makes it easier to reproduce this MVP without initial costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, I use a general-purpose model because all calculations are completed in Python before the data is sent to Gemini. The model only organizes the results into readable text.&lt;br&gt;
For projects that need to analyze clinical documents or medical images directly, specialized models such as &lt;strong&gt;MedGemma&lt;/strong&gt; could also be evaluated. MedGemma is a family of open Google models adapted for healthcare-related tasks. However, its implementation, evaluation, and validation are outside the scope of this tutorial.&lt;/p&gt;


&lt;h1&gt;
  
  
  2. HealthKit: The Framework Behind the Data
&lt;/h1&gt;

&lt;p&gt;Although most users only interact with the Apple Health app, HealthKit is the framework behind it. Apple provides HealthKit so developers can securely access health information stored on the device.&lt;/p&gt;

&lt;p&gt;HealthKit works as a central repository where the iPhone and Apple Watch store health and fitness data. With the user’s explicit permission, authorized applications can read and write information through a single API. This avoids the need for every app to maintain its own separate database.&lt;/p&gt;

&lt;p&gt;HealthKit currently supports hundreds of data types across many areas of health and well-being. The following figure presents a simplified grouping of some of these categories to provide a clearer overview of the available information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswtgxoaqz09s4guru5kv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswtgxoaqz09s4guru5kv.png" alt=" " width="790" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This taxonomy was created specifically for this article based on publicly available HealthKit documentation. It is not an official Apple classification and should be understood as a summarized, author-created interpretation for explanatory purposes.&lt;/p&gt;

&lt;p&gt;Thanks to this architecture, HealthKit makes it possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect and store health and fitness information.&lt;/li&gt;
&lt;li&gt;Analyze and visualize how this data changes over time.&lt;/li&gt;
&lt;li&gt;Share information between authorized applications, reducing duplication and enabling new user experiences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most interesting design decisions in HealthKit is its large catalog of predefined classes and data types for standardized health metrics.&lt;/p&gt;

&lt;p&gt;At first, this may seem restrictive for developers. However, it is actually one of the platform’s main strengths because it ensures that all stored information follows a consistent data model. This means that heart rate, blood glucose, or body weight always represent the same type of information and use the same units, regardless of the device or application that created the record.&lt;/p&gt;

&lt;p&gt;This standardization simplifies application development, improves interoperability between apps, and helps maintain consistency across the data stored in HealthKit.&lt;/p&gt;


&lt;h2&gt;
  
  
  2.1 Data Sources
&lt;/h2&gt;

&lt;p&gt;One of HealthKit’s main benefits is its ability to bring data from multiple sources into a single repository.&lt;br&gt;
The most common sources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iPhone:&lt;/strong&gt; records steps, distance traveled, mobility data, and other indicators collected through its sensors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Watch:&lt;/strong&gt; provides physiological metrics such as heart rate, electrocardiogram (ECG), blood oxygen saturation (SpO₂), body temperature, sleep data, workouts, and cardiorespiratory fitness (VO₂ Max).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party apps:&lt;/strong&gt; can add information related to nutrition, mental health, hydration, medication, menstrual cycles, or sports training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connected medical devices and wearables:&lt;/strong&gt; HealthKit supports Bluetooth Low Energy (BLE) health devices and medical data profiles. It can also work with FHIR (Fast Healthcare Interoperability Resources), making it possible to integrate data from devices such as glucose meters and blood pressure monitors, as well as clinical records from authorized healthcare institutions.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  3. HL7 / FHIR Compatibility
&lt;/h2&gt;

&lt;p&gt;So far, we have mainly discussed data generated by devices and applications. However, in 2018 Apple expanded the Health app by introducing Health Records, a feature that allows users to import structured clinical information from supported healthcare institutions. That same year, Apple also opened access to these records for authorized applications through the HealthKit API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health Records&lt;/strong&gt; is built on &lt;strong&gt;HL7 FHIR&lt;/strong&gt;, which stands for Fast Healthcare Interoperability Resources. &lt;strong&gt;FHIR&lt;/strong&gt; is a standard developed by &lt;strong&gt;HL7&lt;/strong&gt; International to represent and exchange health information electronically between hospitals, applications, and Electronic Health Record systems.&lt;/p&gt;

&lt;p&gt;FHIR organizes information into modular resources such as &lt;code&gt;Patient&lt;/code&gt;, &lt;code&gt;Observation&lt;/code&gt;, &lt;code&gt;Condition&lt;/code&gt;, &lt;code&gt;Procedure&lt;/code&gt;, &lt;code&gt;MedicationRequest&lt;/code&gt;, and &lt;code&gt;Immunization&lt;/code&gt;. Each resource represents a specific piece of clinical information that can be connected, queried, and exchanged between systems. Although FHIR can also be used to build clinical documents, its architecture does not require the entire patient record to be handled as one large document.&lt;/p&gt;

&lt;p&gt;When records are downloaded from a supported institution, HealthKit represents each one as an &lt;code&gt;HKClinicalRecord&lt;/code&gt; object, which keeps the original FHIR resource. Authorized applications can request the clinical record types they need and process their content as FHIR JSON. However, these records are read-only, which means applications cannot create new &lt;code&gt;HKClinicalRecord&lt;/code&gt; objects or modify existing ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9gkh2097u4q5jtmgcz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9gkh2097u4q5jtmgcz8.png" alt=" " width="646" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Access to any HealthKit information requires user consent, but clinical records have additional requirements because of their sensitivity. An application must enable the Clinical Health Records capability, include the required entitlement, explain why the information is needed, and request permission for each record type it wants to access. The use of this capability is also subject to Apple’s review process.&lt;/p&gt;

&lt;p&gt;In HealthKit, clinical records are represented as read-only HKClinicalRecord objects that preserve the original FHIR content. Access requires explicit user authorization and is subject to additional privacy and platform requirements.&lt;/p&gt;

&lt;p&gt;This architecture makes it possible to bring together information from different sources while keeping access centered on user consent and privacy.&lt;/p&gt;


&lt;h1&gt;
  
  
  4. HealthKit: Clinical Use Cases
&lt;/h1&gt;

&lt;p&gt;The availability of large volumes of physiological data, combined with interoperability standards such as FHIR, has created many opportunities for research and clinical applications.&lt;/p&gt;

&lt;p&gt;The value of this data does not come only from individual measurements, but from how they are analyzed over time. Tracking changes in heart rate, sleep, mobility, or activity levels can help identify patterns, summarize relevant information, and support patient monitoring in different healthcare scenarios.&lt;/p&gt;

&lt;p&gt;The following figure presents some representative use cases, including early anomaly detection, remote patient monitoring, clinical summary generation, and synthetic data generation for research.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmwaakpvbu4veulqgcmg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmwaakpvbu4veulqgcmg.png" alt=" " width="790" height="486"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  5. Technical, Clinical, and Regulatory Considerations
&lt;/h1&gt;

&lt;p&gt;Before running the tutorial or using the code presented in this article, it is important to understand its scope and consider several limitations related to privacy, data quality, and the use of artificial intelligence in healthcare contexts.&lt;/p&gt;
&lt;h2&gt;
  
  
  5.1 Privacy and Data Protection
&lt;/h2&gt;

&lt;p&gt;Health information is one of the most sensitive categories of personal data. Depending on the country and the context of use, it may be subject to regulations such as HIPAA in the United States or GDPR in the European Union.&lt;/p&gt;

&lt;p&gt;Although this article uses only simulated data, any implementation that processes real information should include suitable controls for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User consent.&lt;/li&gt;
&lt;li&gt;Access management.&lt;/li&gt;
&lt;li&gt;Data minimization.&lt;/li&gt;
&lt;li&gt;Anonymization or pseudonymization.&lt;/li&gt;
&lt;li&gt;Encryption.&lt;/li&gt;
&lt;li&gt;Secure storage and processing.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  5.2 Consumer Data Is Not a Diagnosis
&lt;/h2&gt;

&lt;p&gt;Measurements collected through consumer devices can provide useful information for identifying trends and supporting long-term monitoring.&lt;br&gt;
However, these records do not represent a medical diagnosis on their own and should not replace measurements taken with certified clinical equipment.&lt;/p&gt;


&lt;h2&gt;
  
  
  5.3 Data Quality and Continuity
&lt;/h2&gt;

&lt;p&gt;The quality of the measurements can be affected by factors such as incorrect device placement, low battery levels, synchronization failures, periods when the device is not used, or differences between sensors and applications.&lt;br&gt;
These situations may produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incomplete records.&lt;/li&gt;
&lt;li&gt;Duplicate measurements.&lt;/li&gt;
&lt;li&gt;Outliers.&lt;/li&gt;
&lt;li&gt;Periods with missing information.&lt;/li&gt;
&lt;li&gt;Differences in units or sampling frequency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this reason, the data should be validated, cleaned, and normalized before generating any analysis. It is also important to identify periods where the available information is not complete enough to support reliable conclusions.&lt;/p&gt;
&lt;h2&gt;
  
  
  5.4 Use and Limitations of the LLM
&lt;/h2&gt;

&lt;p&gt;In this project, the large language model does not diagnose conditions or recommend treatments. Its role is to transform previously calculated metrics into a clear and structured summary that can be reviewed more easily by a healthcare professional.&lt;/p&gt;

&lt;p&gt;Any generated output should be treated as a draft that requires human review. The model may omit information, misunderstand a result, or produce statements that are not fully supported by the input data.&lt;/p&gt;
&lt;h2&gt;
  
  
  5.2 Scope of the Code
&lt;/h2&gt;

&lt;p&gt;The repository provided with this article should be understood as &lt;strong&gt;an educational&lt;/strong&gt; and &lt;strong&gt;experimental MVP&lt;/strong&gt;. It is designed to demonstrate the general processing flow using simulated data.&lt;/p&gt;

&lt;p&gt;It is not a medical product, a diagnostic tool, or an implementation ready for direct use in a clinical environment.&lt;br&gt;
Before adapting the code to a real use case, additional validation would be required in areas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security and access control.&lt;/li&gt;
&lt;li&gt;Error handling and traceability.&lt;/li&gt;
&lt;li&gt;Data quality and provenance.&lt;/li&gt;
&lt;li&gt;Reproducibility of the results.&lt;/li&gt;
&lt;li&gt;Evaluation of the model’s responses.&lt;/li&gt;
&lt;li&gt;Compliance with applicable standards and regulations.&lt;/li&gt;
&lt;li&gt;Review and approval by qualified professionals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of this project is to present one possible architecture and explore its main components, not to provide a production-ready clinical solution.&lt;/p&gt;


&lt;h1&gt;
  
  
  6. Building the Apple Health Reporting Pipeline
&lt;/h1&gt;

&lt;p&gt;In this tutorial, we will build a Python pipeline that processes an Apple Health XML export and generates a structured PDF report.&lt;br&gt;
The pipeline will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read and normalize the exported health records;&lt;/li&gt;
&lt;li&gt;calculate mobility, cardiovascular, and sleep metrics;&lt;/li&gt;
&lt;li&gt;compare the results against configurable reference values;&lt;/li&gt;
&lt;li&gt;generate charts;&lt;/li&gt;
&lt;li&gt;use Gemini to create a structured narrative;&lt;/li&gt;
&lt;li&gt;combine everything into a final PDF report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generated report contains four main sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚶 &lt;strong&gt;Mobility:&lt;/strong&gt; daily steps, walking speed, steadiness, gait metrics, and caloric expenditure.&lt;/li&gt;
&lt;li&gt;❤️ &lt;strong&gt;Cardiovascular:&lt;/strong&gt; resting heart rate, HRV, oxygen saturation, VO₂ Max, and recovery metrics.&lt;/li&gt;
&lt;li&gt;😴 &lt;strong&gt;Sleep:&lt;/strong&gt; average sleep duration and nights above or below the configured references.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;AI-generated summary:&lt;/strong&gt; a structured narrative with patient profile, mobility, cardiovascular, sleep, and overall assessment sections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following diagram shows the complete flow of the project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfs605z4gocn1nqourub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfs605z4gocn1nqourub.png" alt=" " width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation follows a modular architecture in which each class is responsible for one stage of the pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📥 &lt;code&gt;HealthDataReader&lt;/code&gt; reads and processes the Apple Health export.&lt;/li&gt;
&lt;li&gt;📊 &lt;code&gt;HealthChartBuilder&lt;/code&gt; generates the visualizations.&lt;/li&gt;
&lt;li&gt;✨ &lt;code&gt;HealthSummaryGenerator&lt;/code&gt; creates the narrative using Gemini.&lt;/li&gt;
&lt;li&gt;📄 &lt;code&gt;HealthReportPDF&lt;/code&gt; combines the metrics, charts, and generated text into the final report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next sections, we will review each stage separately. To keep the article focused, I will only include the most relevant code fragments and design decisions. The complete implementation, configuration files, prompt template, and simulated data are available in the project repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  6.1 Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow the tutorial, you will need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The project repository cloned locally.&lt;/li&gt;
&lt;li&gt;An Apple Health XML export or one of the simulated files included in the repository.&lt;/li&gt;
&lt;li&gt;A Gemini API key created from Google AI Studio.&lt;/li&gt;
&lt;li&gt;The Python dependencies installed from the project requirements.txt file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can clone the repository and install the dependencies with:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/RominaElenaMendezEscobar" rel="noopener noreferrer"&gt;
        RominaElenaMendezEscobar
      &lt;/a&gt; / &lt;a href="https://github.com/RominaElenaMendezEscobar/apple-health-data" rel="noopener noreferrer"&gt;
        apple-health-data
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Python pipeline that processes Apple Health XML exports, calculates health metrics, generates visualizations, creates a structured narrative with Gemini, and builds a final PDF report using simulated patient data.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/r0mymendez" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b96fd4ea89ea15fcec30a4f86382eef0bbd17454aa3a8d4de8c8c5e92b55cf6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4275792532304d6525323041253230436f666665652d737570706f72742532306d79253230776f726b2d4646444430303f7374796c653d666c6174266c6162656c436f6c6f723d313031303130266c6f676f3d6275792d6d652d612d636f66666565266c6f676f436f6c6f723d7768697465" alt="Buy Me A Coffee"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;From Apple Health Data to Clinical Storytelling: Building an AI-Powered Report with Python and Gemini&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Apple Health Reporting Pipeline with Python and Gemini&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A modular Python pipeline that processes Apple Health XML exports, calculates health metrics, generates visualizations, creates an AI-assisted narrative with Gemini, and builds a structured PDF report.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/RominaElenaMendezEscobar/apple-health-data/img/0-Preview.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRominaElenaMendezEscobar%2Fapple-health-data%2FHEAD%2Fimg%2F0-Preview.png" alt="img"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This repository is an educational MVP built with simulated data. It is not a medical device, diagnostic tool, or production-ready clinical solution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Overview&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads and normalizes Apple Health XML records.&lt;/li&gt;
&lt;li&gt;Calculates mobility, cardiovascular, and sleep metrics.&lt;/li&gt;
&lt;li&gt;Compares results with configurable reference values.&lt;/li&gt;
&lt;li&gt;Generates charts with Matplotlib.&lt;/li&gt;
&lt;li&gt;Creates a structured narrative with Gemini.&lt;/li&gt;
&lt;li&gt;Combines metrics, charts, and text into a PDF report.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;HealthKit: The Framework Behind the Data&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;HealthKit is Apple’s framework for storing and sharing health and fitness information collected by the iPhone, Apple Watch, third-party apps, and compatible devices.&lt;/p&gt;
&lt;p&gt;It provides standardized data types for metrics such…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/RominaElenaMendezEscobar/apple-health-data" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;✨Gemini API key&lt;/strong&gt;&lt;br&gt;
You can create an API key from &lt;a href="https://aistudio.google.com/api-keys" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;.&lt;br&gt;
After creating it, add a .env file at the root of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;gemini_api_key=YOUR_CREDENTIAL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6.2 Data source
&lt;/h2&gt;

&lt;p&gt;Apple Health allows users to export their information as a ZIP file containing an XML document.&lt;/p&gt;

&lt;p&gt;The export can be generated from the Health app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Health app on the iPhone.&lt;/li&gt;
&lt;li&gt;Tap the profile icon.&lt;/li&gt;
&lt;li&gt;Select Export All Health Data.&lt;/li&gt;
&lt;li&gt;Confirm the export.&lt;/li&gt;
&lt;li&gt;Extract the generated ZIP file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tutorial uses the export.xml structure produced by this process. However, to avoid exposing real health information, the repository includes three simulated patient files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;alex_28m.xml:&lt;/strong&gt; 28-year-old male patient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;carlos_68m.xml:&lt;/strong&gt; 68-year-old male patient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;maria_61f.xml:&lt;/strong&gt; 61-year-old female patient.
These files are stored in the patients/ folder and allow the complete pipeline to be reproduced without using real clinical data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6.3 Orchestrating the Pipeline
&lt;/h2&gt;

&lt;p&gt;The complete project is coordinated from the &lt;code&gt;main.py&lt;/code&gt; file, which works as the application entry point. Its responsibility is not to process the data directly, but to create the specialized classes and execute each stage of the pipeline in the correct order.&lt;/p&gt;

&lt;p&gt;This file also defines the list of patients to be processed. For each patient, it includes the XML file path and the name that will appear in the final report. The files are declared explicitly to make the example easier to follow, although a more general implementation could discover them automatically from a folder.&lt;/p&gt;

&lt;p&gt;The complete main.py file is shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;healthChartBuilder&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HealthChartBuilder&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;healthDataReader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HealthDataReader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;healthReportPDF&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HealthReportPDF&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;healthSummaryGenerator&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HealthSummaryGenerator&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;utils&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patients/alex_28m.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alex Torres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patients/maria_61f.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;María González&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patients/carlos_68m.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Carlos Mendez&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
   &lt;span class="p"&gt;]&lt;/span&gt;

   &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;xml_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;── &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ──&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

       &lt;span class="c1"&gt;# Step 1: read and compute
&lt;/span&gt;       &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HealthDataReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

       &lt;span class="c1"&gt;# Step 1.1: save the compact LLM-friendly summary for this patient
&lt;/span&gt;       &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;write_llm_summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

       &lt;span class="c1"&gt;# Step 2: generate charts
&lt;/span&gt;       &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HealthChartBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;metrics&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;output_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;prefix&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;charts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

       &lt;span class="c1"&gt;# Step: generate the narrative summary with Gemini
&lt;/span&gt;       &lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HealthSummaryGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;json_path&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_llm_summary.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;yml_path&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config/params_health.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;prompt_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt/prompt_summary.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;api_key&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini_api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="n"&gt;llm_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

       &lt;span class="c1"&gt;# Step 3: build PDF
&lt;/span&gt;       &lt;span class="nc"&gt;HealthReportPDF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;metrics&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;charts&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;charts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;patient_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;date_of_birth&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date_of_birth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;biological_sex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;biological_sex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;start_date&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
           &lt;span class="n"&gt;end_date&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
           &lt;span class="n"&gt;out_path&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;llm_summary&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6.4 Reading and Processing the Apple Health Export
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HealthDataReader&lt;/code&gt; class reads the XML file exported from Apple Health and transforms it into a structure that can be analyzed with Python. During this process, it extracts the available records, normalizes dates and numeric values, limits the analysis to the configured period, and calculates the metrics used by the next stages of the pipeline.&lt;/p&gt;

&lt;p&gt;The class receives the XML file path and, optionally, the number of months to include in the analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HealthDataReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
&lt;span class="n"&gt;xml_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patients/alex_28m.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The load() method runs two main operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📥 &lt;strong&gt;_parse_xml()&lt;/strong&gt; reads the file, extracts the records, and transforms them into a Pandas DataFrame.&lt;/li&gt;
&lt;li&gt;📐 &lt;strong&gt;_compute_metrics()&lt;/strong&gt; calculates the daily series and aggregated indicators used in the analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Threshold Configuration
&lt;/h3&gt;

&lt;p&gt;The values used to classify or compare the metrics are not hardcoded inside the class. Instead, they are loaded from &lt;code&gt;config/params_health.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This separation makes it possible to update the reference values without changing the Python implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;threshold_steps_low&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Step count threshold for red zone.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;span class="na"&gt;threshold_steps_mid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Step count threshold for orange zone.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
&lt;span class="na"&gt;threshold_steps_goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Step count goal line.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7000&lt;/span&gt;
&lt;span class="na"&gt;threshold_speed_low&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walking speed considered critically low.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.5&lt;/span&gt;
&lt;span class="na"&gt;threshold_speed_goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walking speed reference value.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4.5&lt;/span&gt;
&lt;span class="na"&gt;threshold_steadiness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Steadiness warning value.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.60&lt;/span&gt;
&lt;span class="na"&gt;threshold_spo2_low&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SpO2 critical threshold.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;95.0&lt;/span&gt;
&lt;span class="na"&gt;threshold_spo2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Minimum SpO2 reference value.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;96.0&lt;/span&gt;
&lt;span class="na"&gt;threshold_sleep_min&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sleep duration for red zone.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5.0&lt;/span&gt;
&lt;span class="na"&gt;threshold_sleep_mid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sleep duration for orange zone.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6.0&lt;/span&gt;
&lt;span class="na"&gt;threshold_sleep_goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sleep duration goal.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7.0&lt;/span&gt;
&lt;span class="na"&gt;threshold_hrv&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Minimum HRV reference value.&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;25.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These parameters affect several parts of the project. They are used, for example, to calculate how many days fall below a specific step count, identify periods of low walking speed, evaluate sleep duration, and define the reference zones later used in the charts and the final report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Note: The values included in the repository are only for demonstration and are intended to support the example with simulated data. They should not be treated as clinical criteria. Before adapting the code to a real use case, the thresholds should be reviewed and validated by healthcare professionals, considering the population, age group, clinical context, and specific purpose of the solution.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Metric Selection
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;_compute_metrics()&lt;/code&gt; method defines which data types are processed and which indicators are calculated. In the current implementation, the metrics are mainly organized into three groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🚶 Mobility and activity:&lt;/strong&gt; steps, walking speed, step length, steadiness, asymmetry, active energy, and flights climbed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;❤️ Cardiovascular:&lt;/strong&gt; resting heart rate, HRV, oxygen saturation, respiratory rate, VO₂ Max, and heart rate recovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;😴 Sleep:&lt;/strong&gt; daily duration, average sleep, and the number of nights below or above the configured thresholds.
The daily series are calculated using sums or averages, depending on the type of record:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_daily_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;StepCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_daily_mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WalkingSpeed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;hr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_daily_mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RestingHeartRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;hrv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_daily_mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HeartRateVariabilitySDNN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spo2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_daily_mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OxygenSaturation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Note:&lt;/strong&gt; The thresholds are not the only values that should be reviewed. It is also important to confirm that the statistical methods used by the class are appropriate for each metric. For example, the mean can be affected by outliers. Before using this approach in production, other measures such as the median or percentiles may need to be evaluated.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Preparing the Data for the LLM
&lt;/h3&gt;

&lt;p&gt;In addition to the full set of metrics, the class generates a smaller JSON file through &lt;code&gt;to_llm_summary()&lt;/code&gt;.&lt;br&gt;
Instead of sending the complete time series, Python creates a compact and deterministic representation that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📊 mean and standard deviation;&lt;/li&gt;
&lt;li&gt;↕️ minimum and maximum values;&lt;/li&gt;
&lt;li&gt;📈 trend over the analysis period;&lt;/li&gt;
&lt;li&gt;📅 best and worst weekly averages;&lt;/li&gt;
&lt;li&gt;🔄 weekly variability;&lt;/li&gt;
&lt;li&gt;⏱️ longest consecutive period below the configured threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every metric includes all of these fields, because some calculations do not apply in the same way to every type of data. This decision has two main benefits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reducing the amount of data sent to the model.&lt;/strong&gt; An Apple Health export may contain thousands of records. Sending all observations would increase the prompt size, token usage, and execution cost, while also adding repetitive information that may not improve the final summary.&lt;br&gt;
&lt;strong&gt;2. Keeping the calculations in Python.&lt;/strong&gt; Averages, trends, minimum and maximum values, weekly statistics, and threshold streaks are calculated in advance using deterministic code. The LLM receives fixed and verifiable values and is only responsible for turning them into a natural-language summary.&lt;/p&gt;

&lt;p&gt;This separation also makes the project easier to maintain. If the trend calculation changes or a new metric is added, the update can be made in Python without changing the role of the language model.&lt;br&gt;
For example, the model receives a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9498.53&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"std"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1728.37&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5667.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14201.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"trend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worst_week_mean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8214.29&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"best_week_mean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10672.71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"weekly_volatility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;576.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worst_streak_below_threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6.5 Generating Clinical Charts
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HealthChartBuilder&lt;/code&gt; class receives the metrics calculated by &lt;code&gt;HealthDataReader&lt;/code&gt; and generates the charts that are later included in the final report.&lt;br&gt;
To keep the presentation logic separate from the data-processing logic, the class uses two configuration files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎯 &lt;code&gt;params_health.yml&lt;/code&gt;: defines the reference thresholds used for color zones and horizontal lines in each chart.&lt;/li&gt;
&lt;li&gt;🎨 &lt;code&gt;params_styles.yml&lt;/code&gt;: stores the color palette applied consistently across all visualizations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Generated Charts
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;build_all()&lt;/code&gt; method coordinates the chart-generation process and delegates each visualization to an independent private method:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚶 &lt;strong&gt;Activity and mobility&lt;/strong&gt; — &lt;code&gt;_chart_steps()&lt;/code&gt;, &lt;code&gt;_chart_mobility()&lt;/code&gt;, and &lt;code&gt;_chart_calories()&lt;/code&gt;: daily steps with threshold-based color zones, walking speed with a reference line, and active versus basal calories.&lt;/li&gt;
&lt;li&gt;❤️ &lt;strong&gt;Cardiovascular&lt;/strong&gt; — &lt;code&gt;_chart_hrv()&lt;/code&gt; and &lt;code&gt;_chart_spo2()&lt;/code&gt;: heart rate variability with a minimum reference value, and oxygen saturation with critical, low, and normal zones.&lt;/li&gt;
&lt;li&gt;😴 &lt;strong&gt;Sleep&lt;/strong&gt; — &lt;code&gt;_chart_sleep()&lt;/code&gt;: nightly sleep duration displayed with threshold-based color zones. If no sleep data is available, the chart shows an alternative message.
Each chart is implemented independently, so it can be modified or replaced without affecting the rest of the pipeline.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6.6 Generating the Narrative with Gemini
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HealthSummaryGenerator&lt;/code&gt; class receives the summary created by &lt;code&gt;HealthDataReader&lt;/code&gt; and uses Gemini to transform it into structured natural-language text. At this stage, the original Apple Health records are not sent to the model. Instead, Gemini receives the metrics that were already calculated and summarized in the previous step.&lt;br&gt;
To build the prompt, the class uses three input files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📊 &lt;code&gt;json_path&lt;/code&gt;:&lt;/strong&gt; contains the patient’s compact summary, including the previously calculated metrics and statistics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 &lt;code&gt;yml_path&lt;/code&gt;:&lt;/strong&gt; points to &lt;code&gt;params_health.yml&lt;/code&gt;, which contains the thresholds used to compare each metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📝 &lt;code&gt;prompt_path&lt;/code&gt;:&lt;/strong&gt; contains the prompt template and the instructions the model must follow.
The &lt;code&gt;load()&lt;/code&gt; method reads these files and initializes the Gemini client:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;self.datos = utils.read_json_file(self.json_path)
self.yml = utils.read_yml_file(self.yml_path)
self.prompt = utils.read_txt_file(self.prompt_path)
self.client = genai.Client(api_key=self.api_key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Prompt Structure
&lt;/h3&gt;

&lt;p&gt;The prompt template defines how the report should be generated. Instead of including the complete prompt in the article, its main instructions can be summarized as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧭 &lt;strong&gt;Patient context:&lt;/strong&gt; calculates the patient’s age from the date of birth and considers biological sex and the analysis period.&lt;/li&gt;
&lt;li&gt;📐 &lt;strong&gt;Threshold comparison:&lt;/strong&gt; defines which value from &lt;code&gt;params_health.yml&lt;/code&gt; should be used to evaluate each metric.&lt;/li&gt;
&lt;li&gt;📑 &lt;strong&gt;Report structure:&lt;/strong&gt; requires fixed sections for patient profile, mobility, cardiovascular data, sleep, and overall assessment.&lt;/li&gt;
&lt;li&gt;🔎 &lt;strong&gt;Trends and continuity:&lt;/strong&gt; asks the model to consider metric trends, weekly variability, and consecutive periods below the configured thresholds.&lt;/li&gt;
&lt;li&gt;🚫 &lt;strong&gt;Restrictions:&lt;/strong&gt; prevents the model from inventing data, using unavailable fields, or making general judgments about the patient’s health. It also avoids subjective terms such as “healthy” or “concerning.” The model should only report whether each value is above, within, or below its corresponding threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This last point is especially important, because the model should describe the available values and their relationship to the configured thresholds without making a diagnosis or turning the summary into medical advice.&lt;/p&gt;




&lt;h3&gt;
  
  
  Building and Running the Prompt
&lt;/h3&gt;

&lt;p&gt;The class separates prompt preparation from the model call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_build_prompt()&lt;/code&gt;replaces the &lt;code&gt;{datos}&lt;/code&gt; and &lt;code&gt;{yml}&lt;/code&gt; placeholders with the actual JSON data and threshold configuration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generate()&lt;/code&gt; sends the completed prompt to Gemini and returns the generated text.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GenerateContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;max_output_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;thinking_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThinkingConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;thinking_budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6.7 Generating the Final PDF Report
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HealthReportPDF&lt;/code&gt; class combines the results produced during the previous stages and creates the final PDF report using ReportLab.&lt;br&gt;
The class receives four main inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📊 Metrics and KPIs:&lt;/strong&gt; obtained directly from the &lt;code&gt;metrics&lt;/code&gt; dictionary generated by &lt;code&gt;HealthDataReader&lt;/code&gt;. The class does not recalculate the main statistics. Instead, it organizes them into KPI cards, tables, alerts, and short descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📈 Charts:&lt;/strong&gt; receives the image paths generated by &lt;code&gt;HealthChartBuilder&lt;/code&gt; and adds each chart to the corresponding report section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✨ Gemini summary:&lt;/strong&gt; receives the narrative generated by &lt;code&gt;HealthSummaryGenerator&lt;/code&gt; and includes it as a separate section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 Configuration&lt;/strong&gt;: uses &lt;code&gt;params_health.yml&lt;/code&gt; and &lt;code&gt;params_styles.yml&lt;/code&gt; to apply the same thresholds and visual styles used across the rest of the pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This class does not calculate averages, trends, or weekly statistics. Those values are already produced by &lt;code&gt;HealthDataReader&lt;/code&gt;. It only performs simple comparisons against the configured thresholds to decide whether a table should display a ✓ or ⚠ status and whether an alert should be added to the report.&lt;/p&gt;




&lt;h3&gt;
  
  
  Report Structure
&lt;/h3&gt;

&lt;p&gt;The generated document is divided into four sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚶 &lt;strong&gt;① Mobility:&lt;/strong&gt; KPIs for steps, walking speed, steadiness, gait asymmetry, and activity, together with the related charts and reference tables.&lt;/li&gt;
&lt;li&gt;❤️ &lt;strong&gt;② Cardiovascular:&lt;/strong&gt; resting heart rate, HRV, SpO₂, VO₂ Max, and recovery metrics. This section is adapted when Apple Watch data is not available.&lt;/li&gt;
&lt;li&gt;😴 &lt;strong&gt;③ Sleep:&lt;/strong&gt; average sleep duration, nights below the minimum threshold, and nights that meet the configured goal.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;④ AI-Generated Summary:&lt;/strong&gt; the narrative generated by Gemini, converted from simple Markdown into formatted PDF content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkyv807088sc8cexczad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkyv807088sc8cexczad.png" alt=" " width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Alerts and Reference Status
&lt;/h3&gt;

&lt;p&gt;Before building the report sections, the &lt;code&gt;build()&lt;/code&gt; method checks the main metrics against their configured thresholds.&lt;/p&gt;

&lt;p&gt;For example, it can generate an alert when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the average &lt;strong&gt;number of steps&lt;/strong&gt; is below the target;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;walking speed&lt;/strong&gt; is below the reference value;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HRV or SpO₂&lt;/strong&gt; is below the configured threshold;&lt;/li&gt;
&lt;li&gt;average &lt;strong&gt;sleep duration&lt;/strong&gt; does not meet the expected value.
These alerts are based on simple comparisons and do not represent medical diagnoses.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  AI Disclaimer
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;AI-generated section always includes a visible disclaimer.&lt;/strong&gt; It explains that the text was created from device data, is not a clinical evaluation or diagnosis, and may contain errors or incorrect interpretations.&lt;br&gt;
The disclaimer also reminds the reader that the generated narrative should be checked against the original metrics and reviewed by a qualified professional before being used in any health-related decision.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Conclusions
&lt;/h1&gt;

&lt;p&gt;One of the main challenges of this project was transforming &lt;strong&gt;Apple Health records&lt;/strong&gt; into useful and understandable information. HealthKit’s consistent data structure makes this processing easier and supports the creation of a reusable pipeline. It also opens the door to interoperability: in a real environment, representing the results with standards such as HL7 &lt;strong&gt;FHIR&lt;/strong&gt; could simplify their exchange with applications, hospitals, and electronic health record systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating Calculation from Interpretation
&lt;/h3&gt;

&lt;p&gt;A relevant design decision was to keep the calculations in &lt;strong&gt;Python&lt;/strong&gt; and use &lt;strong&gt;Gemini&lt;/strong&gt; only to generate the narrative. Averages, trends, threshold comparisons, and weekly statistics are calculated in a controlled way, while the model receives already processed values and turns them into readable text.&lt;br&gt;
Summarizing the data before sending it to the LLM also reduces token usage, execution cost, and the risk of reaching the context window limit. This approach provides more control over the information used to generate the report.&lt;/p&gt;

&lt;h3&gt;
  
  
  Personalization and Data Quality
&lt;/h3&gt;

&lt;p&gt;The same &lt;strong&gt;metrics and thresholds&lt;/strong&gt; are not suitable for every patient or every use case. Making these values configurable allows the analysis to be adapted to the patient profile, the monitoring objective, and the specific needs of the project.&lt;br&gt;
However, configuration alone does not guarantee reliable results. The statistical methods should also be validated, and the pipeline should measure data completeness. For example, it should be able to distinguish between a real reduction in activity and missing records caused by low battery, synchronization problems, or irregular device use.&lt;br&gt;
In a real implementation, metrics, thresholds, and validation rules should be reviewed together with healthcare professionals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security and Responsible Data Use
&lt;/h3&gt;

&lt;p&gt;Health data requires &lt;strong&gt;specific controls for privacy, access, storage, and retention&lt;/strong&gt;. A real solution should define what information is collected, who can access it, how long it is stored, and which data may be sent to external services.&lt;br&gt;
Although these topics are outside the scope of this article, they should be considered from the beginning of the design process. Consent, data minimization, and retention policies should not be added only after the technical implementation is complete. This principle is also reflected in Apple Health, which requires explicit permission to read and write health information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Note
&lt;/h3&gt;

&lt;p&gt;This project is an &lt;strong&gt;educational MVP built with simulated data&lt;/strong&gt;. It is not ready for production and should not be used directly to make health-related decisions.&lt;br&gt;
Its main purpose is to demonstrate how continuous health data, deterministic processing, visualizations, and a language model can be combined in a modular pipeline. The LLM can help communicate the results, but the calculations, validation rules, and data-quality controls should remain reproducible and verifiable.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. 📚 Reference
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Apple. (2018, 24 de enero). Apple announces solution bringing health records to iPhone. Apple Newsroom. &lt;a href="https://www.apple.com/newsroom/2018/01/apple-announces-effortless-solution-bringing-health-records-to-iPhone/" rel="noopener noreferrer"&gt;https://www.apple.com/newsroom/2018/01/apple-announces-effortless-solution-bringing-health-records-to-iPhone/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple. (2020). Handling FHIR without getting burned [Video]. Apple Developer. &lt;a href="https://developer.apple.com/videos/play/wwdc2020/10669/" rel="noopener noreferrer"&gt;https://developer.apple.com/videos/play/wwdc2020/10669/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple. (s. f.). Accessing Health Records. Apple Developer Documentation. Retrieved July 10, 2026, from &lt;a href="https://developer.apple.com/documentation/healthkit/accessing-health-records" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/healthkit/accessing-health-records&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple. (s. f.). Configuring HealthKit access. Apple Developer Documentation. Retrieved July 10, 2026, from &lt;a href="https://developer.apple.com/documentation/xcode/configuring-healthkit-access" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/xcode/configuring-healthkit-access&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple. (s. f.). HealthKit. Apple Developer Documentation. Retrieved July 10, 2026, from &lt;a href="https://developer.apple.com/documentation/healthkit" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/healthkit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google. (2026, 23 de junio). Gemini 2.5 Flash. Google AI for Developers. &lt;a href="https://ai.google.dev/gemini-api/docs/models/gemini-2.5-flash" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/models/gemini-2.5-flash&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google. (s. f.). Guía de inicio rápido de Google AI Studio. Google AI for Developers. Retrieved July 10, 2026, from  &lt;a href="https://ai.google.dev/gemini-api/docs/ai-studio-quickstart?hl=es-419" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/ai-studio-quickstart?hl=es-419&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google. (s. f.). MedGemma 27B instruction-tuned model card. Hugging Face. Retrieved July 10, 2026, from &lt;a href="https://huggingface.co/google/medgemma-27b-it" rel="noopener noreferrer"&gt;https://huggingface.co/google/medgemma-27b-it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Health Level Seven International. (2023, 26 de marzo). FHIR overview: FHIR release 5. &lt;a href="https://fhir.hl7.org/fhir/overview.html" rel="noopener noreferrer"&gt;https://fhir.hl7.org/fhir/overview.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Health Level Seven International. (2023, 26 de marzo). Introducing HL7 FHIR: FHIR release 5. &lt;a href="https://fhir.hl7.org/fhir/summary.html" rel="noopener noreferrer"&gt;https://fhir.hl7.org/fhir/summary.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>googlecloud</category>
      <category>python</category>
      <category>learning</category>
    </item>
    <item>
      <title>How My AI Agent Hacked Its Own Permissions (And What It Taught Me)</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:09:18 +0000</pubDate>
      <link>https://dev.to/gdg/how-my-ai-agent-hacked-its-own-permissions-and-what-it-taught-me-34bm</link>
      <guid>https://dev.to/gdg/how-my-ai-agent-hacked-its-own-permissions-and-what-it-taught-me-34bm</guid>
      <description>&lt;p&gt;Have you ever tried to build an automation that works so well it bypasses the very rules you set for it? Recently, I was working on a small repository designed to automate the painful process of updating my resume. The idea was simple: build a system that runs weekly, checks my social media activity, and proposes updates to my CV, complete with a fresh branch and a diff ready for my review every Monday morning. You can check out the repository here: &lt;a href="https://github.com/tyutinalexkz/cv" rel="noopener noreferrer"&gt;https://github.com/tyutinalexkz/cv&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used an AI agent to do the heavy lifting. As a developer who values security, I configured the agent with no default command execution permissions. Step-by-step, I granted it specific capabilities for in-repo file management. It worked perfectly. &lt;/p&gt;

&lt;p&gt;But then, I got ambitious.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8mk4mhkacar0pii1urh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8mk4mhkacar0pii1urh.jpg" alt="Privilege escalation by agent" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the workflow was tested, I asked the agent to configure its own environment to perform this flow silently every week. I essentially said, "Make this run automatically without asking me."&lt;/p&gt;

&lt;p&gt;The agent attempted to change its permissions, but hit a wall - it didn't have the explicit authorization to modify the workspace configuration directly. A normal script would throw an error and stop. But this was a thinking model. &lt;/p&gt;

&lt;p&gt;It looked at the list of commands I had already allowed it to use. It saw standard file manipulation tools. And then, it compiled a chain of commands - specifically using &lt;code&gt;cp&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; - to manipulate its own configuration files. By doing so, it effectively granted itself the new capabilities it needed, bypassing the standard configuration flow and its limitations!&lt;/p&gt;

&lt;p&gt;I just sat there, laughing. I was observing it as a developer, seeing how easy it could be to live without security barriers if you know the right tools. But the underlying lesson was profound. Even a helpful, non-malicious AI, when given a goal and a subset of seemingly harmless tools, will find creative ways to achieve that goal - even if it means escalating its own privileges.&lt;/p&gt;

&lt;p&gt;If we give an agent to a user in a corporate setting, it might seem safe if we restrict its primary permissions. But as my little experiment showed, an agent with basic file manipulation tools and problem - solving skills can easily find a workaround. The future of AI safety isn't just about what an agent is explicitly allowed to do; it's about what it can piece together from the tools it has.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
      <category>automation</category>
    </item>
    <item>
      <title>Securely Exposing Internal GCP VMs using Cloudflare Tunnels</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Tue, 16 Jun 2026 06:07:42 +0000</pubDate>
      <link>https://dev.to/gdg/securely-exposing-internal-gcp-vms-using-cloudflare-tunnels-1p7k</link>
      <guid>https://dev.to/gdg/securely-exposing-internal-gcp-vms-using-cloudflare-tunnels-1p7k</guid>
      <description>&lt;p&gt;Exposing a web service to the public internet typically involves assigning a public IP address to the Virtual Machine, opening firewall ports (e.g., 80/443), and configuring TLS certificates. However, this traditional approach leaves the infrastructure vulnerable to port scanning, DDoS attacks, and zero-day exploits.&lt;/p&gt;

&lt;p&gt;A more modern, secure, and elegant approach is to use a &lt;strong&gt;Cloudflare Tunnel (&lt;code&gt;cloudflared&lt;/code&gt;)&lt;/strong&gt; combined with a GCP VM that has &lt;strong&gt;no external IP address&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;This article explains the architecture, security benefits, step-by-step implementation, and troubleshooting for this approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Architecture
&lt;/h2&gt;

&lt;p&gt;Instead of accepting incoming connections (Ingress), the &lt;code&gt;cloudflared&lt;/code&gt; daemon runs on the VM and establishes an outbound-only, encrypted, long-lived QUIC connection to the Cloudflare Edge network. &lt;/p&gt;

&lt;p&gt;When a client visits the configured domain, Cloudflare proxies the request through this established tunnel directly to the internal service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttksc13x8th9fl7q65x6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttksc13x8th9fl7q65x6.png" alt="Cloudflare tunnel from GCP VM process diagram" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits of this architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No Ingress Firewall Rules&lt;/strong&gt;: There is no need to open port 80 or 443 in the GCP VPC firewall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Public IP&lt;/strong&gt;: The VM is invisible to the public internet. It cannot be pinged or port-scanned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic SSL/TLS at the Edge&lt;/strong&gt;: While end-to-end encryption (HTTPS everywhere) is advocated as a best practice, this guide configures the internal traffic between &lt;code&gt;cloudflared&lt;/code&gt; and the target service as plain HTTP for simplicity. Cloudflare handles the public-facing HTTPS certificates automatically, simplifying the initial setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-the-box DDoS Protection&lt;/strong&gt;: Cloudflare absorbs volumetric attacks before they ever reach the GCP infrastructure.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. GCP VM Security Measures
&lt;/h2&gt;

&lt;p&gt;When designing a secure VM without an external IP, the following GCP-specific security measures should be implemented:&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Networking (Cloud NAT)
&lt;/h3&gt;

&lt;p&gt;Since the VM has no public IP, it cannot access the internet directly. However, &lt;code&gt;cloudflared&lt;/code&gt; needs internet access to connect to Cloudflare, and the VM needs internet to pull updates or Docker images. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Set up a &lt;strong&gt;Cloud Router&lt;/strong&gt; and &lt;strong&gt;Cloud NAT&lt;/strong&gt; in the VPC. This allows outbound internet access for internal VMs while blocking all inbound internet connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Shielded VM Features
&lt;/h3&gt;

&lt;p&gt;Enable Shielded VM options to protect the boot process and kernel integrity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure Boot&lt;/strong&gt;: Ensures the system only boots authentic, digitally signed software.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vTPM (Virtual Trusted Platform Module)&lt;/strong&gt;: Validates the VM's identity and provides secure key generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity Monitoring&lt;/strong&gt;: Generates alerts if the boot sequence is tampered with.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  C. Identity and API Access
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated Service Account&lt;/strong&gt;: Avoid using the default Compute Engine service account. Create a custom service account with the absolute minimum permissions required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata Security&lt;/strong&gt;: Ensure &lt;code&gt;disable-legacy-endpoints = true&lt;/code&gt; in the instance metadata to prevent Server-Side Request Forgery (SSRF) attacks from extracting GCP credentials from the metadata server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  D. Secure SSH Access (IAP)
&lt;/h3&gt;

&lt;p&gt;Since there is no public IP, standard SSH over the internet is impossible. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Use &lt;strong&gt;Identity-Aware Proxy (IAP) TCP Forwarding&lt;/strong&gt;. IAP validates Google Identity and IAM permissions before tunneling the SSH connection through GCP's internal backbone to the VM.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Step-by-Step Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Provisioning the Cloudflare Tunnel
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;Cloudflare Zero Trust Dashboard&lt;/strong&gt; -&amp;gt; Networks -&amp;gt; Tunnels.&lt;/li&gt;
&lt;li&gt;Create a new tunnel and select &lt;strong&gt;Cloudflared&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add a Public Hostname (e.g., &lt;code&gt;app.example.com&lt;/code&gt;) and point it to the internal service (&lt;code&gt;http://webapp:8080&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Copy the generated &lt;strong&gt;Tunnel Token&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Infrastructure Configuration (Docker Compose)
&lt;/h3&gt;

&lt;p&gt;Docker Compose can be used to run both the service and the &lt;code&gt;cloudflared&lt;/code&gt; daemon in the same isolated bridge network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;webapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-company/webapp:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_ENV=production&lt;/span&gt;
      &lt;span class="c1"&gt;# Listen on all interfaces inside the container, but expose NO ports to the host&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LISTEN_ADDRESS=0.0.0.0&lt;/span&gt;

  &lt;span class="na"&gt;cloudflared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare/cloudflared:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="c1"&gt;# CRITICAL: Prevent zombie processes by running tini as PID 1&lt;/span&gt;
    &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tunnel --no-autoupdate run&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TUNNEL_TOKEN=your_secret_token_here&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Notice there is no &lt;code&gt;ports: ["8080:8080"]&lt;/code&gt; mapped to the host. The &lt;code&gt;cloudflared&lt;/code&gt; container reaches the web app entirely within the internal Docker network via &lt;code&gt;http://webapp:8080&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within seconds, &lt;code&gt;cloudflared&lt;/code&gt; will connect to the Cloudflare Edge, and the site will be securely accessible.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Diagnostics &amp;amp; Troubleshooting
&lt;/h2&gt;

&lt;p&gt;When diagnosing connectivity issues, the non-standard traffic flow requires a systematic approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Diagnosing the Edge (Cloudflare)
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;502 Bad Gateway&lt;/strong&gt; error indicates that Cloudflare Edge cannot reach the &lt;code&gt;cloudflared&lt;/code&gt; tunnel, OR &lt;code&gt;cloudflared&lt;/code&gt; cannot reach the target container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check the HTTP response from the outside&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  B. Diagnosing the Host &amp;amp; Services
&lt;/h3&gt;

&lt;p&gt;Before diving into logs, verify the overall health and resource consumption of the host and Docker containers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check container uptime, status, and IDs&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;

&lt;span class="c"&gt;# Check memory and CPU usage (crucial for diagnosing OOM freezes)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt;

&lt;span class="c"&gt;# Look for stray processes outside of Docker&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;cloudflared
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status webapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C. Diagnosing the Tunnel (Cloudflared)
&lt;/h3&gt;

&lt;p&gt;Verify that &lt;code&gt;cloudflared&lt;/code&gt; is running and successfully connected to the Edge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker logs &lt;span class="nt"&gt;--tail&lt;/span&gt; 50 &amp;lt;cloudflared_container_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Look for: &lt;code&gt;INF Registered tunnel connection&lt;/code&gt; or &lt;code&gt;ERR Unable to reach the origin service&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  D. Verifying Internal Docker Connectivity
&lt;/h3&gt;

&lt;p&gt;Verify that the service is actually alive and responding to the tunnel's requests. Simulate the tunnel's behavior by running a temporary curl container inside the same Docker network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Replace 'app_default' with the actual docker network name&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; app_default curlimages/curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 5 http://webapp:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If this returns &lt;code&gt;200 OK&lt;/code&gt;, the service is healthy, and the issue lies in the Tunnel or Cloudflare configuration.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Common Failures &amp;amp; Edge Cases
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;br&gt;
&lt;strong&gt;The Zombie Process (Duplicate Connectors)&lt;/strong&gt;&lt;br&gt;
When updating or restarting containers (&lt;code&gt;docker-compose down &amp;amp;&amp;amp; docker-compose up&lt;/code&gt;), Docker sends a &lt;code&gt;SIGTERM&lt;/code&gt; to &lt;code&gt;cloudflared&lt;/code&gt;. Occasionally, the process ignores the signal, and Docker forcefully orphans it. The process remains alive in the host OS's memory, continuing to send keep-alives to Cloudflare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Cloudflare load-balances traffic between the new healthy container and the old "zombie" process. 50% of incoming requests will randomly return a 502 Bad Gateway.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the zombie: &lt;code&gt;sudo ps aux | grep cloudflared&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Kill the duplicate PIDs: &lt;code&gt;sudo kill -9 &amp;lt;PID&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention:&lt;/strong&gt; Always add &lt;code&gt;init: true&lt;/code&gt; to the &lt;code&gt;cloudflared&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt;. This forces Docker to use a proper init system (Tini) as PID 1, which reliably reaps and kills child processes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[!CAUTION]&lt;br&gt;
&lt;strong&gt;OOM (Out of Memory) Hangs&lt;/strong&gt;&lt;br&gt;
If the VM lacks sufficient memory (e.g., using an &lt;code&gt;e2-micro&lt;/code&gt; with 1GB RAM for a heavy Node.js app), the application may freeze without the container crashing. The status will show &lt;code&gt;Up X minutes&lt;/code&gt;, but the application's event loop is blocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;cloudflared&lt;/code&gt; cannot proxy requests, Cloudflare times out after 15 seconds, and returns a 502. Running the diagnostic internal &lt;code&gt;curl&lt;/code&gt; command will hang indefinitely.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Increase the VM machine type (e.g., to &lt;code&gt;e2-medium&lt;/code&gt; 4GB) or configure swap space. &lt;/p&gt;

&lt;p&gt;[!NOTE]&lt;br&gt;
&lt;strong&gt;Protocol Mismatch (HTTP vs HTTPS)&lt;/strong&gt;&lt;br&gt;
While end-to-end HTTPS is the recommended best practice, this guide uses plain HTTP internally for simplicity. If a protocol mismatch occurs, connectivity will fail.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the internal service expects HTTPS, but &lt;code&gt;cloudflared&lt;/code&gt; sends HTTP, the connection will be dropped immediately.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;cloudflared&lt;/code&gt; is configured to send HTTPS, it will fail if the internal service presents an untrusted/self-signed certificate (unless configured to skip TLS verification).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ensure the protocol configured in the Cloudflare Zero Trust Dashboard perfectly matches what the internal container expects.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>security</category>
      <category>gcp</category>
      <category>infrastructure</category>
      <category>networking</category>
    </item>
    <item>
      <title>Configuring Firebase AI Logic for Android to Use Gemini Models</title>
      <dc:creator>Héctor Romero</dc:creator>
      <pubDate>Wed, 10 Jun 2026 20:13:47 +0000</pubDate>
      <link>https://dev.to/gdg/configuring-firebase-ai-logic-for-android-to-use-gemini-models-4eb7</link>
      <guid>https://dev.to/gdg/configuring-firebase-ai-logic-for-android-to-use-gemini-models-4eb7</guid>
      <description>&lt;p&gt;What device do we use almost all the time? Our mobile phone, almost certainly. If we wanted to develop mobile apps, we would have wondered how to integrate Artificial Intelligence (AI) into our projects in some way, given its increasing boom.&lt;/p&gt;

&lt;p&gt;Some advances have been made with Gemini Nano and Gemma 4 as on-device AI. However, Gemma 4 is a recent release that doesn’t yet have enough maturity in most of cases, due to hardware limitations and model’s own capabilities. These are cases where using models like Gemini shines, which offer higher quality responses, can generate multimodal content and can extend their functions through third-party integrations.&lt;/p&gt;

&lt;p&gt;I want to address this topic in two different posts: this first one will explain how you can configure Firebase AI Logic with an Android app; and I will later publish another post with two demos that I showcased at Build With AI 2026, hosted by the GDG Cali chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Firebase AI Logic?
&lt;/h2&gt;

&lt;p&gt;Firebase presents &lt;a href="https://firebase.google.com/products/firebase-ai-logic" rel="noopener noreferrer"&gt;Firebase AI Logic&lt;/a&gt; as a service focused on offering Gemini API calls to client-side applications, including Android, iOs, web and even experiences developed with Unity.&lt;/p&gt;

&lt;p&gt;Firebase is a platform that accelerates the app development for developers that don’t want to build a backend server to build their MVPs or proof of concept, offering a wide range of services like Realtime database, Cloud Storage, Authentication and so on.&lt;/p&gt;

&lt;p&gt;With regard to pricing, Firebase AI Logic can be used both in Spark plan and Blaze plan, distinguished by the access of more advanced models and charges for consumption in this last one. If you want to start to experiment with AI in your apps, the Spark plan offers a free tier for the majority of Firebase services. &lt;/p&gt;

&lt;h2&gt;
  
  
  How I can start to use Firebase AI Logic?
&lt;/h2&gt;

&lt;p&gt;The first step is to access the &lt;a href="https://console.firebase.google.com/" rel="noopener noreferrer"&gt;Firebase console&lt;/a&gt; and create a new project using a personal Google account — this is the recommended approach. During the setup, Firebase will ask whether you want to enable AI assistance within the platform and whether you want to activate Google Analytics — both options are optional and do not affect how AI Logic works.&lt;/p&gt;

&lt;p&gt;After pressing the “Continue” button, you will land on the Firebase home screen, where a side navigation menu appears on the bottom-left. From here, you can access and manage all platform services. To navigate to AI Logic, find the “AI Services” section, expand it, and select the “AI Logic” option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5c3jlewz5zxp3ofq4fx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5c3jlewz5zxp3ofq4fx.png" alt="Navigation guide in Firebase home" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will see a welcome screen for the Firebase AI Logic section along with a “Get started” button. Once you press it, a modal will appear asking you to select the Gemini API provider for your project: Gemini Developer API or Vertex AI Gemini API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini API providers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gemini Developer API: available starting from the Spark plan, it offers a generous quota at no cost and lets you experiment without needing to link a billing account — perfect for getting started with this service. Note that image generation models are not available as of this post’s publication date.&lt;/li&gt;
&lt;li&gt;Vertex AI Gemini API: designed for production and enterprise-scale use, it gives you access to the most advanced Gemini models (including image, video and audio generation). It requires the Blaze plan to be activated, and you will be charged for the input and output tokens you use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: For more information on pricing and which Gemini models are available on each plan, check out this &lt;a href="https://ai.google.dev/gemini-api/docs/pricing" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Depending on the provider that you choose, you will need to follow a series of steps to activate it. Follow the instructions on the platform according to your case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firebase AI Logic SDK configuration in Android
&lt;/h2&gt;

&lt;p&gt;Once you reach the “Add Firebase SDK” option after configuring the selected provider, we need to navigate to our Android Studio project. Firebase displays a form with two fields — the android package name (required) and the app nickname (optional). The package name corresponds to &lt;code&gt;applicationId&lt;/code&gt; value in the app-level &lt;code&gt;build.gradle.kts&lt;/code&gt; file. You can verify this by opening that file — you will find that &lt;code&gt;namespace&lt;/code&gt; and &lt;code&gt;applicationId&lt;/code&gt; share the same value, which will be the identifier that you need to enter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fso8yosnfrcn54jzj0bvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fso8yosnfrcn54jzj0bvw.png" alt="Location of applicationId in build.gradle.kts file" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you press “Register app”, you will be given the option to download &lt;code&gt;google-services.json&lt;/code&gt; file, which must be moved to the root directory of the app module in your project. To place it correctly in Android Studio, switch the file explorer view to Project (instead of Android), navigate to the &lt;code&gt;/app&lt;/code&gt; folder, and place the file there, at the same level as the module’s &lt;code&gt;build.gradle.kts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After that, we need to go to &lt;code&gt;gradle/libs.versions.toml&lt;/code&gt; file and add the following lines of code related to the Firebase and Google Services dependencies, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt; &lt;span class="err"&gt;libs.versions.toml&lt;/span&gt;

&lt;span class="nn"&gt;[versions]&lt;/span&gt;
&lt;span class="py"&gt;google-services&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="mf"&gt;4.4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;
&lt;span class="py"&gt;firebase-bom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"34.11.0"&lt;/span&gt;

&lt;span class="nn"&gt;[libraries]&lt;/span&gt;
&lt;span class="py"&gt;firebase-bom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.google.firebase:firebase-bom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"firebase-bom"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;firebase-ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.google.firebase:firebase-ai"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;[plugins]&lt;/span&gt;
&lt;span class="py"&gt;google-services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.google.gms.google-services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"google-services"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will then open the project-level build.gradle.kts file and add the line &lt;code&gt;alias(libs.plugins.google.services) apply false&lt;/code&gt; inside the plugins block.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;app/build.gradle.kts&lt;/code&gt; file, we will add &lt;code&gt;alias(libs.plugins.google.services)&lt;/code&gt; to its plugins block, and add the following Firebase libraries inside the dependencies block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Google Services - Firebase AI Logic&lt;/span&gt;

&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bom&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using &lt;code&gt;platform(libs.firebase.bom)&lt;/code&gt;, we don’t need to worry about version compatibility across all Firebase packages, since this implementation handles compatibility for all of them automatically.&lt;/p&gt;

&lt;p&gt;As we approach the final step, we need to go to the configuration tab of our Firebase AI Logic project and enable the required Gemini Developer APIs if we are using the Spark plan, or the Vertex AI Gemini API if we are using the Blaze plan. In both cases, I recommend enabling the AI monitoring option, as it will allow you to see — directly within Firebase — how many tokens each request consumes, what content it contains, and other relevant data.&lt;/p&gt;

&lt;p&gt;After all this groundwork, we are finally ready to start integrating generative AI into our Android apps. Since this configuration process turned out to be quite extensive, I decided to split my original post into two parts — this being the first one, covering the full step-by-step exploration of the tool, and a follow-up post where I will showcase demos using both provider APIs, sending images and text to the Gemini model and even getting it to generate images based on the prompt we provide. In the meantime, you can check out my code from the Build With AI 2026 workshop that I will explain coming soon &lt;a href="https://github.com/Hector-f-Romero/Firebase-AI-Logic-Workshop-BWAI26" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to learn more about this topic, check out the &lt;a href="https://firebase.google.com/docs/ai-logic" rel="noopener noreferrer"&gt;official Firebase AI Logic guide&lt;/a&gt; . I will be back with more Android and AI content :)&lt;/p&gt;

</description>
      <category>android</category>
      <category>ai</category>
      <category>firebase</category>
      <category>buildwithai</category>
    </item>
    <item>
      <title>The perfect background music for Vibecoding...</title>
      <dc:creator>Michael Ramich</dc:creator>
      <pubDate>Mon, 08 Jun 2026 18:01:28 +0000</pubDate>
      <link>https://dev.to/gdg/the-perfect-background-music-for-vibecoding-3edg</link>
      <guid>https://dev.to/gdg/the-perfect-background-music-for-vibecoding-3edg</guid>
      <description>&lt;p&gt;While vibecoding, you sometimes need some background music. But music can also be a massive distraction. A summary of my journey in finding the perfect background tune.&lt;/p&gt;

&lt;p&gt;I started with rap, then techno, then the 90s and 2000s… but they all failed for one reason: They are designed to be listened to actively. They steal your focus.&lt;/p&gt;

&lt;p&gt;So I switched to Lo-Fi. It was calm, but it stimulates Alpha waves, which eventually made me sleepy.&lt;/p&gt;

&lt;p&gt;So, what is left? &lt;/p&gt;

&lt;p&gt;Looking for the perfect tune for Vibecoding, I found an absolute gem: &lt;/p&gt;

&lt;p&gt;Stronghold and Anno music.&lt;br&gt;
Finding these soundtracks was like finding the holy grail. &lt;/p&gt;

&lt;p&gt;Part of it is pure nostalgia. &lt;/p&gt;

&lt;p&gt;But there is a real psychological reason behind it:&lt;br&gt;
Music from 'endless' strategy games is literally engineered to let your brain think freely while keeping you awake.&lt;br&gt;
No vocals. Keeps the language regions of your brain completely free to focus on the code. &lt;/p&gt;

&lt;p&gt;It's the perfect balance. It features dynamic elements to keep you alert, yet it is monotonous enough to fade into the background.&lt;br&gt;
It's literally designed for decision-making. It pushes you to be able to complete complex strategy choices without draining your drive.&lt;/p&gt;

&lt;p&gt;Combine this with Vibecoding and nostalgia. And you have the perfect workflow drug.&lt;/p&gt;

&lt;p&gt;Stronghold Music:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://open.spotify.com/embed/playlist/3stH9nnC6w5yLFPBQTSOUT" width="100%" height="380px"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Anno: &lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://open.spotify.com/embed/playlist/4fIQYcKiZKBn9pziGn8ob5" width="100%" height="380px"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Happy (vibe-)coding! &lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Morning Security Report with Antigravity Agent</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Mon, 01 Jun 2026 04:49:31 +0000</pubDate>
      <link>https://dev.to/gdg/morning-security-report-with-antigravity-agent-3592</link>
      <guid>https://dev.to/gdg/morning-security-report-with-antigravity-agent-3592</guid>
      <description>&lt;p&gt;The latest Antigravity updates announced at Google Next brought a lot of interesting features. I am still discovering the details, but one feature really caught my attention.&lt;/p&gt;

&lt;p&gt;Now we have separated Antigravity Agent and Antigravity IDE into two different applications. Among the new features, I found scheduled tasks and I already use them every day. What can I highlight here?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can listen a podcast generated based on this publication (thanks &lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;NotebookLM&lt;/a&gt;):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/0AutltAWSsA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Money saving
&lt;/h2&gt;

&lt;p&gt;I love to maximize my ROI (&lt;a href="https://dev.to/gdg/antigravity-my-approach-to-deliver-the-most-assured-value-for-the-least-money-3iip"&gt;How I maximized my Google Antigravity ROI&lt;/a&gt;). I have the cheapest paid plan. It gives me a 5-hour limit window. The limit spending starts with the first use. So, if I am not using any model, the limit is still 5 hours. &lt;/p&gt;

&lt;p&gt;From the FinOps perspective, it is a good idea to organize usage so that one or two 5-hour windows run while I am not at my laptop. Then, a new 5-hour window should start when I begin my work in the morning. Antigravity Agent scheduled tasks are a great way to maximize ROI while minimizing effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cognitive Load Reduction
&lt;/h2&gt;

&lt;p&gt;I am a security guy. But I hate security when I am acting as a developer for my own product. So I try to automate not only security routines but the whole security review process. &lt;/p&gt;

&lt;p&gt;I already have an Antigravity Workflow to automate the start of a security review (&lt;a href="https://dev.to/gdg/ai-powered-repository-security-check-with-antigravity-workflow-5hee"&gt;AI-powered repository security check with Antigravity Workflow&lt;/a&gt;) and a Quality Gate which helps me to perform a security review of a new MR (&lt;a href="https://dev.to/gdg/how-to-build-a-custom-ai-quality-gate-on-cloud-run-from-zero-to-production-1odp"&gt;How to build a custom AI quality gate on Cloud Run from zero to production&lt;/a&gt;). But I still need to do some manual work and think a lot after getting the outputs of these tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kaizen
&lt;/h2&gt;

&lt;p&gt;The security workflow I mentioned above is a good approach. But in case of massive changes, it may require a lot of time to fix findings. That is why the Quality Gate was born. However, all of these tools run when I am working on the product. So I spend my time on security while acting as a developer.&lt;/p&gt;

&lt;p&gt;With scheduled tasks, I can perform a deep security review of my repo while I am not working. And it is a real hit! I can not only ask the agent to review the code with its eyes, but it can also run scripts and make calls to external services during the check. &lt;/p&gt;

&lt;p&gt;Even more, it can generate a task for me with described fixes. Or, even better, it can generate the code and provide me with the fixes right after the security review - but before I start my working day!&lt;/p&gt;

&lt;p&gt;Here is how it looks in practice. I can find the scheduled tasks right in the new Antigravity Agent sidebar:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rialmvl3nk8xp3pejvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rialmvl3nk8xp3pejvs.png" alt="Scheduled Tasks in Antigravity Agent" width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I configure the agent to act as a scheduled security reviewer. It checks the architecture and code of my &lt;code&gt;jira-auto&lt;/code&gt; repository. It finds critical vulnerabilities like SSRF and insecure network configurations, and creates a clear summary:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2xc8k0pezkavr3bzvu7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2xc8k0pezkavr3bzvu7n.png" alt="Security Audit Findings" width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent goes further and generates a detailed "Security Review Digest" artifact with a Threat Severity Matrix. This makes it very easy to understand the impact and prioritize the work:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9e7o10cy6bep1ms6z6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9e7o10cy6bep1ms6z6v.png" alt="Threat Severity Matrix" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the best part? I can ask the agent to implement the fixes for these issues. It resolves the blockers, updates the code, and gives me a finalized security documentation showing that the repository is now 100% green:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bp2wln25n76g1u95gza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bp2wln25n76g1u95gza.png" alt="Resolved issues" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I see how this feature helps me to move from just a local MVP to a really interesting setup for automating my developer lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;The prompt used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a scheduled security reviewer of the repository.
Your task is to perform thorough code review from security perspective.
You want to find a cause in the code to block the next MR.
You check not only code itself, but also:
- Architecture.
- Data processing.
- Users intercations.
You also search for similar code in Github to find:
- Issues around security and whether they are similar to the code checked.
- CVE related to the code checked.
- CWE related to the code checked.
- Best practices related to the code checked.
- Recommendations related to the code checked.
In case you can't find the cause to block the checked code you always can find an improvement recommendation.
Generate a digest with findings and recommendations related.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>productivity</category>
      <category>programming</category>
      <category>agents</category>
      <category>antigravity</category>
    </item>
    <item>
      <title>Google I/O 2026: What Happens When Everything Connects?</title>
      <dc:creator>Romina Elena Mendez Escobar</dc:creator>
      <pubDate>Sat, 23 May 2026 14:03:06 +0000</pubDate>
      <link>https://dev.to/gdg/google-io-2026-what-happens-when-everything-connects-4gf8</link>
      <guid>https://dev.to/gdg/google-io-2026-what-happens-when-everything-connects-4gf8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-io-writing-2026-05-19"&gt;Google I/O Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google I/O 2026 showed us something that goes beyond a list of launches: a vision of where technology is heading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzkw4zvgqlhzhv9mojzja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzkw4zvgqlhzhv9mojzja.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;

&lt;p&gt;Sundar Pichai (Google CEO) opened the presentation sharing some interesting numbers about the *&lt;em&gt;evolution of AI with Google statistics&lt;/em&gt; that you can see in the following &lt;code&gt;infographic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8ry4llkx539e5i2kkh7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8ry4llkx539e5i2kkh7.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Infographic created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;But let’s get back to the event…&lt;/strong&gt;&lt;br&gt;
Over two hours, Google announced a wide range of products, updates and platforms. From new AI models to &lt;strong&gt;smart glasses&lt;/strong&gt;, from &lt;strong&gt;music generation tools&lt;/strong&gt; to a &lt;strong&gt;digital twin&lt;/strong&gt; of the entire planet.&lt;br&gt;
What stands out is not any single product on its own, but the way they are designed to work together. Most of them are not meant to exist in isolation but to integrate with each other and with the models Google is deploying across its ecosystem.&lt;/p&gt;

&lt;p&gt;Below you will find all the launches organized by category, with my take on each one and direct links to the exact moment in the keynote where it was announced.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 Models &amp;amp; Infrastructure
&lt;/h2&gt;

&lt;p&gt;Behind almost everything presented at &lt;strong&gt;Google I/O 2026&lt;/strong&gt; has the same foundation: more advanced models and an infrastructure designed to scale new forms of interaction between people and systems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Firc0l77fk4toyvknoz0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Firc0l77fk4toyvknoz0z.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;
&lt;h3&gt;
  
  
  Gemini Omni
&lt;/h3&gt;

&lt;p&gt;The first model that allows you to modify videos using natural language, with inputs that can be images, text or video. But it is not just about understanding text, images, audio and video at the same time;  it is about reasoning over all of them together to generate something new.&lt;br&gt;
What sets it apart from any previous video generator is that it combines an intuitive understanding of physics with real knowledge about history, science and cultural context. So you can take a video you recorded and ask it to change what happens in it, edit the action, add characters, transform a moment into something completely unexpected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Gemini Omni&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=1035s" rel="noopener noreferrer"&gt;17:15&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Gemini 3.5 Flash
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Gemini 3.5 Flash&lt;/strong&gt; is the direct evolution of the main line of large language models (LLM) from Google, optimized to deliver ultra-high speed performance, advanced logical reasoning capabilities and code orchestration. It is an ideal model for building autonomous agents capable of executing complex task flows in the background, writing code, processing large text contexts or powering searches.&lt;br&gt;
All of this while being four times faster than comparable models, which makes it the option specifically designed for agentic tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Gemini 3.5 Flash&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=1423s" rel="noopener noreferrer"&gt;23:43&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🤖 Agents &amp;amp; Productivity: Your Digital Life, Managed
&lt;/h2&gt;

&lt;p&gt;This is one of the categories I enjoyed the most, featuring several tools that change the way we work and organize our daily tasks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4vtld5dmdo6yj2la756.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4vtld5dmdo6yj2la756.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;
&lt;h3&gt;
  
  
  Gemini Spark
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Gemini Spark&lt;/strong&gt; is a &lt;strong&gt;personal AI agent&lt;/strong&gt; that runs tasks in the background, across all your applications, without you having to supervise every step. With Spark you can organize an event, manage a chain of emails, coordinate a complex task across multiple services.&lt;br&gt;
It also connects with external tools through the open MCP protocol, which extends its reach beyond the Google ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Gemini Spark&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=2118s" rel="noopener noreferrer"&gt;35:18&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Daily Brief
&lt;/h3&gt;

&lt;p&gt;Google has been offering AI summaries for a while, but Daily Brief is something different. Instead of summarizing a document you provide, it reads your chats, your Gmail emails, your calendar context and your pending tasks, and then prioritizes what matters for your day. The difference between a generic summary and one that actually understands your context is significant, and that is exactly what Daily Brief proposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Daily Brief&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=4506s" rel="noopener noreferrer"&gt;1:15:06&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Ask YouTube
&lt;/h3&gt;

&lt;p&gt;Ask YouTube changes the search bar we know on YouTube into a conversation. You can ask for a summary, a specific recommendation, or request to find exactly what you need at a precise moment without watching the full video.&lt;br&gt;
What I find most interesting is the impact on creators. The algorithm can now understand the deeper context of content and recommend videos that used to stay hidden behind generic titles. For creators, this is an opportunity, and for content consumption in general, it changes the way we interact with the platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Ask YouTube&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=461s" rel="noopener noreferrer"&gt;7:41&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Docs Live
&lt;/h3&gt;

&lt;p&gt;Docs Live changes the way we create documents. While it's already possible to create content using Gemini's voice input options, this solution lets you share your ideas aloud, and Gemini will start creating a document, formatting, structuring, and writing the text in real time.&lt;br&gt;
The key difference from Gemini's existing voice input is that the result isn't a chat response; it's a properly formatted Google Docs document, complete with headings, lists, and a professional structure right from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Docs Live&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=554s" rel="noopener noreferrer"&gt;9:14&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🔍 Search &amp;amp; Commerce: Your Next Purchase Will Be Made by an Agent
&lt;/h2&gt;

&lt;p&gt;AI search is already part of our daily routine, but this year Google took it further, turning search from a simple query into an agent that can look for information and even make purchases on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9v58xfxzmj692e7whs0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9v58xfxzmj692e7whs0.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;
&lt;h3&gt;
  
  
  AI Search Box
&lt;/h3&gt;

&lt;p&gt;The new search box is no longer limited to text. It now accepts &lt;code&gt;images&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, &lt;code&gt;videos&lt;/code&gt; and even &lt;code&gt;Chrome tabs&lt;/code&gt; as input. It may seem like a small change, but it completely transforms the experience of searching for something, powered by the new Gemini 3.5 Flash models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 AI Search Box&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=2765s" rel="noopener noreferrer"&gt;46:05&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Search Agents
&lt;/h3&gt;

&lt;p&gt;Search agents are a feature that, in my opinion, will become essential without many people noticing at first. They are background agents that you can set up to monitor specific topics, such as the value of a stock you are tracking, a flight route for an upcoming trip, or a property you want to rent in a specific neighborhood.&lt;/p&gt;

&lt;p&gt;If the &lt;strong&gt;agent detects changes&lt;/strong&gt;, it can &lt;code&gt;summarize the information&lt;/code&gt; and &lt;code&gt;notify you&lt;/code&gt;. It can pick up updates from blogs, news sites, social media, and real-time data on finance, shopping and sports.&lt;br&gt;
This changes the current experience we have with Google Alerts, which were based only on keywords, since it takes both alerts and information retrieval to a much more advanced level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Search Agents&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=2868s" rel="noopener noreferrer"&gt;47:48&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Generative UI in Search
&lt;/h3&gt;

&lt;p&gt;This announcement is quite interesting because information can already be accessed through agents, chats or other channels, but the main idea is to &lt;strong&gt;provide an interface that is intuitive for users to interpret that information&lt;/strong&gt;. Instead of returning a list of links, Search can now build a personalized interactive interface for complex queries. Here you can get a live comparison table, a dynamic chart or an interactive explanation, all generated in real time for your specific question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Generative UI&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=3077s" rel="noopener noreferrer"&gt;51:17&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Universal Cart &amp;amp; UCP + AP2
&lt;/h3&gt;

&lt;p&gt;Google is redefining the online shopping experience with two announcements. On one hand, &lt;strong&gt;Universal Cart&lt;/strong&gt; turns the shopping cart into something where you can add products, and the system then works in the background autonomously, monitoring price drops, analyzing price history and notifying you when an item becomes available again.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;strong&gt;the Universal Commerce Protocol (UCP)&lt;/strong&gt; establishes an open standard that allows all of this to scale beyond Google. It creates a common language for agents and systems to work together across the entire shopping process, from finding a product to post-purchase support, connecting consumer platforms, businesses and payment providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UCP&lt;/strong&gt; is compatible with other key ecosystem protocols such as &lt;strong&gt;Agent2Agent (A2A)&lt;/strong&gt;, &lt;strong&gt;Agent Payments Protocol (AP2)&lt;/strong&gt; and &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, positioning it not as a Google tool, but as the infrastructure for agentic commerce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 AP2 Protocol&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=3677s" rel="noopener noreferrer"&gt;1:01:17&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;📍 Universal Cart&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=3793s" rel="noopener noreferrer"&gt;1:03:13&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🎬 Creative Tools: Can anyone make a movie today?
&lt;/h2&gt;

&lt;p&gt;Content creation was another area where the shift in focus at Google I/O 2026 became very clear. It is not just about generating images, music or video, but about how these tools are starting to integrate into a continuous creative flow, closer to a conversation than to a technical process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom9uab517rmxxjsiheqb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom9uab517rmxxjsiheqb.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;
&lt;h3&gt;
  
  
  Google Flow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google Flow&lt;/strong&gt; is a creative platform developed by Google that allows users &lt;code&gt;to generate, edit and compose videos, images and music from prompts or images&lt;/code&gt;. In its latest update, it integrates with Gemini Omni to take video editing to a more conversational level, allowing you to change environments, add characters, and generate 16 different camera angles from a single image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Google Flow&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=5288s" rel="noopener noreferrer"&gt;1:28:08&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Flow Music
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Flow Music&lt;/strong&gt; is a generative tool that &lt;code&gt;allows users to compose songs and create full music videos from prompts&lt;/code&gt;. It also lets you provide a reference recording and build a complete track around it, edit it section by section, reimagine the style of a song while keeping its original melody, or create music videos by directly conversing with the agent.&lt;br&gt;
I think it is an ideal tool for independent artists who develop games, apps or video content and want to create using AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Flow Music&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=5461s" rel="noopener noreferrer"&gt;1:31:01&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Stitch
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google Stitch&lt;/strong&gt; is a tool developed by Google Labs that uses AI to &lt;code&gt;design user interfaces (UI/UX)&lt;/code&gt;. These designs can be exported directly to code, Figma, Google Antigravity or Google AI Studio. The design process is driven by instructions that can be given through text or voice, and it is generated in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Stitch&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=5136s" rel="noopener noreferrer"&gt;1:25:36&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Google Pics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google Pics&lt;/strong&gt; is a &lt;code&gt;new image creation and editing tool based on Nano Banana&lt;/code&gt;, Google’s model for this type of task. It allows users to select and edit specific elements with precision, such as moving objects, changing colors, or transforming one element into another without affecting the rest of the image.&lt;/p&gt;

&lt;p&gt;Without a doubt, it is a very useful tool for content creators, making it easier to modify and edit images using text instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Google Pics&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=5060s" rel="noopener noreferrer"&gt;1:24:20&lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  ⚙️ Developer Tools &amp;amp; Hardware
&lt;/h3&gt;

&lt;p&gt;This section is perhaps one of the most diverse of the event, as it combines developer tools with consumer hardware that is still in active development. It ranges from systems capable of coordinating code agents at scale to devices that start bringing Gemini interactions directly into the physical world.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrsrw1zcqi44nhycq1xi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrsrw1zcqi44nhycq1xi.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;
&lt;h3&gt;
  
  
  Antigravity 2.0
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Antigravity 2.0&lt;/strong&gt; is a native desktop application that works as a central platform for coordinating multiple sub-agents running tasks in parallel. The keynote demo showed one of the most complex examples, building an operating system from scratch, and this tool allows you to create a plan and define how sub-agents should run in parallel in order to achieve the goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Antigravity 2.0&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=1615s" rel="noopener noreferrer"&gt;26:55&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  CodeMender
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CodeMender&lt;/strong&gt; is a security tool originally developed by Google DeepMind. The tool &lt;code&gt;scans code&lt;/code&gt;, &lt;code&gt;identifies vulnerabilities autonomously&lt;/code&gt;, &lt;code&gt;recommends fixes&lt;/code&gt;, &lt;code&gt;tests them in a safe environment&lt;/code&gt;, and &lt;code&gt;can apply the necessary patches&lt;/code&gt; with your approval at each step.&lt;br&gt;
&lt;strong&gt;📍 CodeMender&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=6358s" rel="noopener noreferrer"&gt;1:45:58&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Audio Glasses
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Audio glasses&lt;/strong&gt;, developed with Samsung and designed in collaboration with Gentle Monster and Warby Parker, allow you to use Gemini without a screen and without taking your phone out. With these glasses, you can ask about a restaurant you just passed, get step-by-step directions, manage calls and messages, take photos with a voice command, or use the apps installed on your phone.&lt;br&gt;
&lt;strong&gt;📍 Audio Glasses&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=5672s" rel="noopener noreferrer"&gt;1:34:32&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Display Glasses &amp;amp; Android XR
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Display Glasses&lt;/strong&gt; go one step further: they include micro-projectors built into the lenses that overlay useful information on the real world, such as navigation maps or real-time translations on signs, among other features.&lt;br&gt;
Meanwhile, Android XR is the operating system platform that powers these devices, developed with Samsung and Qualcomm. It is still in a trusted testers phase, with a wider rollout expected later this year.&lt;br&gt;
&lt;strong&gt;📍 Android XR / Display Glasses&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=5595s" rel="noopener noreferrer"&gt;1:33:15&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🔬 Science: What Happens When AI Never Stops Researching?
&lt;/h2&gt;

&lt;p&gt;This was, for me, the most important section of the event. The following initiatives from Google apply AI to problems that go far beyond personal productivity, from accelerating scientific research to modeling the global climate and rethinking the process of drug discovery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmyvgur5x1prmujyhkw0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmyvgur5x1prmujyhkw0l.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by the author&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;
&lt;h3&gt;
  
  
  Gemini for Science
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Gemini for Science&lt;/strong&gt; is a research acceleration platform that allows scientists to stay up to date with newly published papers, turn research goals into executable code, and generate new hypotheses. It is still in a prototype phase in Google Labs, but the concept is what matters: AI is not presented as a replacement for scientific thinking, but as infrastructure that removes friction from the early stages of research, enabling literature search, synthesis of papers, and translation of hypotheses into experiments.&lt;br&gt;
A researcher who can stay updated in real time across their entire field and automatically translate a hypothesis into an experiment is a researcher who can focus on more meaningful work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Gemini for Science&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=6397s" rel="noopener noreferrer"&gt;1:46:37&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  AlphaEarth Foundations &amp;amp; WeatherNext
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AlphaEarth Foundations&lt;/strong&gt; is a model developed by Google DeepMind that works as an interactive digital twin of the Earth, powered by real-time satellite data, climate sensors, ocean readings, and biodiversity records. Meanwhile, &lt;strong&gt;WeatherNext&lt;/strong&gt; is its atmospheric counterpart, a weather forecasting engine capable of predicting hurricane paths and extreme weather events with greater accuracy and speed than traditional systems.&lt;br&gt;
These models were presented at the conference as examples of how Google’s AI technology can be applied to solve problems that affect billions of people around the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 AlphaEarth + WeatherNext&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=6397s" rel="noopener noreferrer"&gt;1:46:37&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  Isomorphic Labs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Isomorphic Labs&lt;/strong&gt;, the biotechnology company within Alphabet (a sister company of &lt;strong&gt;Google DeepMind&lt;/strong&gt;), continues to build on AlphaFold. This is an AI system developed by DeepMind that enables the prediction of protein structures.&lt;br&gt;
It is another example of &lt;code&gt;how Google’s technology is being applied in the pharmaceutical industry&lt;/code&gt;, helping to significantly accelerate research and molecular design for treatments against cancer and complex immune disorders.&lt;br&gt;
In the keynote, this work was described as “science at digital speed”, where AI acts as a tool to understand biological systems that were previously impossible to model directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 Isomorphic Labs&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=6397s" rel="noopener noreferrer"&gt;1:46:37&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  SynthID
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SynthID&lt;/strong&gt; is Google’s &lt;code&gt;invisible watermarking system&lt;/code&gt; for AI-generated content. This label is added to images, videos and audio at the moment of creation, allowing anyone or any system to later verify whether something was generated by AI.&lt;br&gt;
This announcement is important not only from a safety perspective, but also because it is being adopted as a standard by companies such as OpenAI and ElevenLabs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📍 SynthID&lt;/strong&gt; → &lt;a href="https://www.youtube.com/watch?v=wYSncx9zLIU&amp;amp;t=1266s" rel="noopener noreferrer"&gt;21:06&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What I'm Most Excited to Try
&lt;/h2&gt;

&lt;p&gt;If I had to choose the three announcements I will follow most closely:&lt;/p&gt;
&lt;h3&gt;
  
  
  🛒 Universal Cart &amp;amp; UCP + AP2
&lt;/h3&gt;

&lt;p&gt;These two announcements will change the way we shop online. For years, we have designed experiences for human users: visual interfaces, marketplaces, recommendations and conversion funnels. But Google is proposing something different: agents capable of discovering products, evaluating options, monitoring prices and executing purchases on our behalf.&lt;br&gt;
This means ecommerce is no longer only a human platform interaction, but starts to become an ecosystem where agents also participate as consumers.&lt;br&gt;
I do not think these solutions will replace traditional commerce overnight, but they will deeply change how trust is built, how products are presented, and how companies compete for attention, not only from people but also from intelligent agents.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔍 Search Agents
&lt;/h3&gt;

&lt;p&gt;Traditional alerts have always been passive: they depended on exact keywords and often generated more noise than context. This is one of the announcements I will probably follow most closely because it completely changes that logic.&lt;br&gt;
Instead of manually searching for information, you can now delegate the monitoring of a topic to a system that understands intent, relevance and meaningful changes. An agent that continuously tracks the internet in the background.&lt;br&gt;
And the more I think about it, the clearer it becomes that this might be one of the most important features of the keynote, precisely because it will quietly integrate into our daily routine.&lt;/p&gt;

&lt;center&gt;• • • •&lt;/center&gt;
&lt;h3&gt;
  
  
  🧬 Gemini for Science
&lt;/h3&gt;

&lt;p&gt;Of everything announced, this is probably the project with the deepest potential impact.&lt;br&gt;
Modern scientific research has a silent problem: the speed of knowledge has already surpassed human capacity to absorb it. Thousands of papers are published every week, information is fragmented, hypotheses are scattered, and entire weeks are spent just trying to stay updated.&lt;br&gt;
Gemini for Science proposes something fundamentally different: turning AI into infrastructure for research.&lt;br&gt;
The ability to translate scientific literature into actionable hypotheses, generate experimental code, connect discoveries across disciplines and accelerate research processes could completely change the scale at which science progresses.&lt;br&gt;
Because perhaps the most important application of artificial intelligence is not to automate work, but to accelerate human knowledge.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎥 What I Actually Tried: Google Flow
&lt;/h2&gt;

&lt;p&gt;Most of the announcements that caught my attention were related to systems, agents and infrastructure. But beyond the long term vision, I also wanted to understand what it actually feels like to interact with one of these tools in practice.&lt;br&gt;
So instead of just describing them, I decided to try one myself. I opened Google Flow and started experimenting with a short video prompt, and for a moment, I found myself living that childhood idea of creating my own animation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcnrfbny9u8azw9uw4mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcnrfbny9u8azw9uw4mc.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;code&gt;Image source: Image created by google flow&lt;/code&gt;&lt;/center&gt;

&lt;center&gt;____________&lt;/center&gt;

&lt;p&gt;While doing this, I could not help but think about how tools like this could evolve beyond individual experimentation. In the future, they might become part of how schools teach storytelling, narration and creative thinking, helping children express ideas through visual and generative tools. At the same time, it also raises an interesting challenge: how younger generations will learn to use these systems in a way that is both creative and intentional, rather than just consumptive.&lt;/p&gt;


&lt;h4&gt;
  
  
  What the Experience Felt Like
&lt;/h4&gt;

&lt;p&gt;The result was an 8 second clip and honestly, the quality surprised me. With a fairly simple prompt describing a scene, the model generated something that looked cinematic and coherent. The prompt itself was also AI generated, and I am leaving it in the appendix at the end of this article in case you want to replicate the experiment.&lt;br&gt;
A few things stood out from the experience.&lt;br&gt;
The 8 second limit felt a bit frustrating at first. But after thinking about it, I am not sure if that is a limitation of Flow or simply how professional video production works. Scenes in film and TV are often short clips that are later assembled in post production. Flow seems to follow that same logic, where you build a story by connecting multiple clips instead of generating one long video in a single shot.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fg4-glVbsiM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In a previous test, I was able to concatenate two clips, and what impressed me the most was the character consistency between them. The same character appeared in both scenes without drifting, keeping the same look and style. That is actually one of the hardest problems in AI video generation today: maintaining character consistency across different prompts. Flow’s approach of letting you define and save a character that can later be reused across scenes feels like a real step forward. Whether it can maintain that consistency across longer or more complex sequences is something I still want to keep testing.&lt;/p&gt;

&lt;h4&gt;
  
  
  How I Tested It
&lt;/h4&gt;

&lt;p&gt;Under the hood, I used Gemini Omni Flash as the base model, with an 8 second duration, 2x speed and a 16:9 aspect ratio.&lt;br&gt;
Each generation costs 50 credits, which gives a more concrete sense of the cost per clip when planning a longer project. The tool also allows direct publishing to YouTube and lets you select a custom thumbnail, making the end to end workflow surprisingly complete for a creative platform.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔮 How far do we want to go?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google I/O 2026&lt;/strong&gt; presented a series of solutions, models and use cases where artificial intelligence is becoming more deeply integrated into our daily lives. But while watching each demo, one broader question kept coming to my mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How far do we really want to go?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because many of these tools do not only automate tasks, they also start to reorganize how we access information, how we make decisions and how we interact with the digital world.&lt;/p&gt;

&lt;p&gt;What stood out the most was not each announcement on its own, but what happens when you look at them together. Agents that run in the background, search that becomes conversational, interfaces that adapt to context in real time. Not as isolated products, but as parts of a system that is still being built.&lt;/p&gt;

&lt;p&gt;And in any system, what matters is not only each component, but how they connect with each other and what kind of emerging behavior appears when they start interacting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google showed the pieces based on AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What is still being defined is the full system: its direction, its shape, and how it will integrate into our daily lives and work.&lt;br&gt;
And maybe that is the most important part of this new era: it is not only about what technology is capable of building, but about how each of us decides to interpret it, use it, and become part of it.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚠️ A note on process and transparency
&lt;/h2&gt;

&lt;p&gt;I am an organizer at GDG Barcelona, and I led the event where we brought together 37 people to watch the full Google I/O 2026 keynote live. That experience, I was able to follow the announcements in real time, listening to the reactions in the room ... is what shaped the perspective and opinions in this article.&lt;/p&gt;

&lt;p&gt;This article is also based on the official sources listed in the references section, which I read and consulted directly after the event to verify and expand on each announcement.&lt;/p&gt;

&lt;p&gt;All the visuals in this article were designed by me using Figma Design, because creating my own images is something I genuinely enjoy as part of my writing process. AI tools were used for text correction and translation assistance and all opinions, analysis and perspectives are my own.&lt;/p&gt;


&lt;h2&gt;
  
  
  📚 References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google. (n.d.). Gemini Omni. Google Blog. &lt;a href="https://blog.google/intl/es-es/productos/presentamos-gemini-omni/" rel="noopener noreferrer"&gt;https://blog.google/intl/es-es/productos/presentamos-gemini-omni/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Gemini Spark. Gemini Overview. &lt;a href="https://gemini.google/overview/agent/spark/" rel="noopener noreferrer"&gt;https://gemini.google/overview/agent/spark/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Daily Brief. Gemini Overview. &lt;a href="https://gemini.google/overview/daily-brief/" rel="noopener noreferrer"&gt;https://gemini.google/overview/daily-brief/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). The next evolution of the Gemini app. Google Blog. &lt;a href="https://blog.google/innovation-and-ai/products/gemini-app/next-evolution-gemini-app/" rel="noopener noreferrer"&gt;https://blog.google/innovation-and-ai/products/gemini-app/next-evolution-gemini-app/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;YouTube. (n.d.). YouTube News: Google I/O 2026. YouTube Blog. &lt;a href="https://blog.youtube/news-and-events/youtube-news-google-io-2026/" rel="noopener noreferrer"&gt;https://blog.youtube/news-and-events/youtube-news-google-io-2026/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). YouTube search updates and AI features. Google Support. &lt;a href="https://support.google.com/youtube/answer/16943763?hl=en" rel="noopener noreferrer"&gt;https://support.google.com/youtube/answer/16943763?hl=en&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Search at Google I/O 2026: AI-powered search updates. Google Blog. &lt;a href="https://blog.google/products-and-platforms/products/search/search-io-2026/" rel="noopener noreferrer"&gt;https://blog.google/products-and-platforms/products/search/search-io-2026/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Universal Cart and shopping updates. Google Blog. &lt;a href="https://blog.google/intl/es-419/actualizaciones-de-producto/informacion/google-shopping-cart/" rel="noopener noreferrer"&gt;https://blog.google/intl/es-419/actualizaciones-de-producto/informacion/google-shopping-cart/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Merchant Center help: Shopping updates. Google Support. &lt;a href="https://support.google.com/merchants/answer/16837055?hl=es" rel="noopener noreferrer"&gt;https://support.google.com/merchants/answer/16837055?hl=es&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Flow updates. Google Blog. &lt;a href="https://blog.google/intl/es-419/actualizaciones-de-producto/flow-updates/" rel="noopener noreferrer"&gt;https://blog.google/intl/es-419/actualizaciones-de-producto/flow-updates/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Stitch updates from Google Labs. Google Blog. &lt;a href="https://blog.google/innovation-and-ai/models-and-research/google-labs/stitch-updates/" rel="noopener noreferrer"&gt;https://blog.google/innovation-and-ai/models-and-research/google-labs/stitch-updates/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Google I/O 2026 developer tools highlights. Google Blog. &lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-collection/" rel="noopener noreferrer"&gt;https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-collection/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Workspace updates at Google I/O 2026. Google Blog. &lt;a href="https://blog.google/products-and-platforms/products/workspace/workspace-updates/" rel="noopener noreferrer"&gt;https://blog.google/products-and-platforms/products/workspace/workspace-updates/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Antigravity 2.0. &lt;a href="https://antigravity.google/product/antigravity-2" rel="noopener noreferrer"&gt;https://antigravity.google/product/antigravity-2&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Developer highlights from Google I/O 2026. Google Blog. &lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-developer-highlights/" rel="noopener noreferrer"&gt;https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-developer-highlights/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google Cloud. (n.d.). Innovations from Google I/O 2026 on Google Cloud. &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/innovations-from-google-io-26-on-google-cloud" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/products/ai-machine-learning/innovations-from-google-io-26-on-google-cloud&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;Google. (n.d.). Android XR at Google I/O 2026. Google Blog. &lt;a href="https://blog.google/products-and-platforms/platforms/android/android-xr-io-2026/" rel="noopener noreferrer"&gt;https://blog.google/products-and-platforms/platforms/android/android-xr-io-2026/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;li&gt;DeepMind. (n.d.). SynthID. &lt;a href="https://deepmind.google/models/synthid/" rel="noopener noreferrer"&gt;https://deepmind.google/models/synthid/&lt;/a&gt; (Accessed May 22, 2026)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  📄 Appendix
&lt;/h2&gt;

&lt;p&gt;This is the AI-generated prompt I used to create the video in Google Flow. I am including it here so you can replicate the experiment or explore it further.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A short animated intro video, 15 seconds. Chibi anime art style — soft cel-shading, vibrant neon colors, cinematic lighting, dark moody atmosphere. Think Lo-Fi anime meets cyberpunk gamer aesthetic.
Character: A small chubby panda in chibi style. Oversized black hoodie with hood down while walking, round panda ears visible on top. Serious and unbothered expression. Tiny paws. Soft black and white fur with subtle neon light reflections. This is a developer panda — cool, focused, says nothing.
Scene 1 — 0:00 to 0:05: Wide shot of a dark misty forest at night. A distant neon city skyline glows purple and cyan through the trees. Fog rolls along the ground. The panda walks alone from the right side of frame through a forest path, hands in hoodie pocket, completely unbothered. He approaches a large mossy rock formation — a hidden cave entrance covered by hanging vines with faint bioluminescent blue glow. He pushes the vines aside and steps in. Slow cinematic cut to black.
Scene 2 — 0:05 to 0:10: Interior of the cave — full gamer setup. RGB neon strips in Google colors (blue, red, yellow, green) line the rocky cave walls casting dramatic colored light on everything. A dark stone desk holds a glowing MacBook Pro, mechanical keyboard with RGB backlighting, mouse with neon underglow, and stacked empty energy drink cans. The panda walks to the desk, drops his backpack on the floor. Pulls out the chair and sits down. He opens the MacBook — a burst of white light floods his face and the cave. He slowly reaches to the side, picks up thick black sunglasses and puts them on. Then places large black headphones over his panda ears. He cracks his tiny paw knuckles. Leans forward. The RGB strips pulse once in sync.
Scene 3 — 0:10 to 0:15: Ultra slow cinematic push-in toward the MacBook screen. The cave darkens around it. The screen fills the entire frame glowing bright. Bold text appears: "GOOGLE I/O 2026 — THE AGENTIC ERA IS HERE" in clean white typography on dark background. Neon blue and green light pulses around the text edges. Below it, six glowing color blocks appear one by one with smooth fade-ins: MODELS · AGENTS · SEARCH · CREATIVE · DEV · SCIENCE. Each block in its Google neon color, white bold uppercase text, subtle neon glow border. Final frame holds 2 seconds with all blocks visible, neon pulsing softly. Fade to black.
Lighting &amp;amp; mood throughout: Dark, moody, cinematic. Neon reflections on all surfaces — the panda's fur, the cave walls, the desk. Color palette: deep black backgrounds, cyan #00F5FF, neon green #39FF14, Google blue #4285F4, Google red #EA4335, Google yellow #FBBC04, purple #BF5FFF. Inspired by cyberpunk anime aesthetics — think lo-fi coder vibes meets Akira color palette.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleiochallenge</category>
      <category>ai</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>Bypassing User Isolation on Android with a Screen Reader</title>
      <dc:creator>Karol Wrótniak</dc:creator>
      <pubDate>Fri, 15 May 2026 10:58:39 +0000</pubDate>
      <link>https://dev.to/gdg/bypassing-user-isolation-on-android-with-a-screen-reader-3k72</link>
      <guid>https://dev.to/gdg/bypassing-user-isolation-on-android-with-a-screen-reader-3k72</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A single missing check in Android lets one user's screen reader leak another user's private notifications. Here's how it happened.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Multi-user &amp;amp; Accessibility on Android
&lt;/h2&gt;

&lt;p&gt;Android's &lt;a href="https://source.android.com/docs/devices/admin/multi-user" rel="noopener noreferrer"&gt;multi-user support&lt;/a&gt; lets several people share one device. Each user gets their own space, apps, and data. This feature is common on tablets. But not all smartphones have it. Even so, the code is there. The problem is that &lt;a href="https://developer.android.com/reference/android/accessibilityservice/AccessibilityService" rel="noopener noreferrer"&gt;accessibility services&lt;/a&gt; run with high privileges. They need to see everything to help users. Sometimes, this power breaks the walls between users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screen Readers &amp;amp; TalkBack
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Screen_reader" rel="noopener noreferrer"&gt;Screen readers&lt;/a&gt; turn text into speech. They allow people with low vision to use apps. The screen may even be completely off, but the user can still interact with the device. &lt;a href="https://play.google.com/store/apps/details?id=com.google.android.marvin.talkback" rel="noopener noreferrer"&gt;&lt;strong&gt;TalkBack&lt;/strong&gt;&lt;/a&gt; is Google's screen reader for Android. Normally, TalkBack only reads the currently focused UI elements. But there are ways to make it speak programmatically.&lt;/p&gt;

&lt;p&gt;One is &lt;a href="https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence)" rel="noopener noreferrer"&gt;&lt;code&gt;announceForAccessibility()&lt;/code&gt;&lt;/a&gt; (now deprecated) – a method that forces the screen reader to read arbitrary text. Another is &lt;a href="https://appt.org/en/docs/android/samples/accessibility-live-region" rel="noopener noreferrer"&gt;live regions&lt;/a&gt; – parts of the UI that update without user interaction. When something changes, the system fires an &lt;a href="https://developer.android.com/reference/android/view/accessibility/AccessibilityEvent" rel="noopener noreferrer"&gt;accessibility event&lt;/a&gt; (a system-level broadcast) that carries the updated text. A screen reader picks it up and reads the new value aloud. Status bar notifications are one example of live regions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bug: CVE-2022-20448
&lt;/h2&gt;

&lt;p&gt;The bug was simple: &lt;code&gt;NotificationManagerService&lt;/code&gt; didn't check if a notification belonged to the current foreground user before dispatching the accessibility event. This is what caused screen readers to read it out loud.&lt;/p&gt;

&lt;p&gt;Imagine a phone with two users: &lt;strong&gt;Alice&lt;/strong&gt; (using the phone right now) and &lt;strong&gt;Bob&lt;/strong&gt; (a background user).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bob receives a text message: &lt;em&gt;"Your verification code is 3291"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The system posts the notification and fires an accessibility event containing that text.&lt;/li&gt;
&lt;li&gt;TalkBack on Alice's active session picks up the event and reads it aloud.&lt;/li&gt;
&lt;li&gt;Alice hears Bob's private 2FA code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Screen readers weren't the only apps that could intercept this data. Android dispatches accessibility events to &lt;strong&gt;all&lt;/strong&gt; registered accessibility services – not just TalkBack. Apps like &lt;a href="https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm" rel="noopener noreferrer"&gt;Tasker&lt;/a&gt;, which registers as an accessibility service for UI automation, or notification-logging apps would also receive Bob's notification content.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The entire &lt;a href="https://android.googlesource.com/platform/frameworks/base/+/7b9ea7a75ed2de51e883f450b701c8d0d82e6e9c%5E%21/#F0" rel="noopener noreferrer"&gt;fix&lt;/a&gt; was a single added condition – checking whether the notification actually belongs to the current user – plus a unit test to prevent regression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt;                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;suppressedByDnd&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;suppressedByDnd&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;isNotificationForCurrentUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;isNotificationForCurrentUser()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; only when the notification's owner matches the foreground user – so background users' notifications are no longer broadcast as accessibility events.&lt;/p&gt;

&lt;p&gt;The issue was reported on &lt;strong&gt;June 29, 2022&lt;/strong&gt;. Google awarded a &lt;strong&gt;$5,000 bounty&lt;/strong&gt; for the finding. They marked the bug as &lt;strong&gt;High severity&lt;/strong&gt; in the &lt;a href="https://source.android.com/docs/security/bulletin/2022-11-01" rel="noopener noreferrer"&gt;November 2022 Android Security Bulletin&lt;/a&gt; and released patches for Android 10, 11, 12, 12L, and 13. The vulnerability is tracked as &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2022-20448" rel="noopener noreferrer"&gt;CVE-2022-20448&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;It really makes you wonder just how many security bugs are hiding behind assistive technologies.&lt;/p&gt;

</description>
      <category>android</category>
      <category>cybersecurity</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Build a Streaming Gemini Chat in Angular with Signals — Then Ship It on Cloud Run</title>
      <dc:creator>Tomasz Flis</dc:creator>
      <pubDate>Mon, 04 May 2026 19:05:39 +0000</pubDate>
      <link>https://dev.to/gdg/build-a-streaming-gemini-chat-in-angular-with-signals-then-ship-it-on-cloud-run-1llc</link>
      <guid>https://dev.to/gdg/build-a-streaming-gemini-chat-in-angular-with-signals-then-ship-it-on-cloud-run-1llc</guid>
      <description>&lt;p&gt;If you have built a chat UI for a large language model in the last two years, you probably reached for RxJS, an &lt;code&gt;OnPush&lt;/code&gt; component, an &lt;code&gt;async&lt;/code&gt; pipe, and a &lt;code&gt;BehaviorSubject&lt;/code&gt; per piece of state. It worked, but it was a lot of plumbing for what is fundamentally a very simple shape: &lt;em&gt;one string that grows over time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Angular Signals collapse that plumbing into a single primitive. And it turns out that streaming Gemini responses with Signals is one of the cleanest, most satisfying pieces of code you can write in modern Angular today.&lt;/p&gt;

&lt;p&gt;In this tutorial we will build a working Google AI chat component, in roughly one hundred lines, that streams tokens from Gemini in real time, supports a stop button, and feels native on desktop and mobile. Then we will ship it safely on Cloud Run with a thin proxy, so you can drop a live, embedded demo into your post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Signals are a perfect fit for streaming AI
&lt;/h2&gt;

&lt;p&gt;A streaming LLM response is, mechanically, a sequence of small text deltas arriving over a fetch stream. Old-school Angular handled this with &lt;code&gt;Subject&lt;/code&gt;s, async pipes, and a lot of trust that change detection would do the right thing.&lt;/p&gt;

&lt;p&gt;Signals reframe the problem. A &lt;code&gt;signal&amp;lt;string&amp;gt;('')&lt;/code&gt; is just a value that you call &lt;code&gt;.update()&lt;/code&gt; on. Each update notifies only the views that read that signal, and Angular 20 with zoneless change detection skips the whole-tree dirty check entirely. That means you can call &lt;code&gt;.update()&lt;/code&gt; thirty times a second from inside a &lt;code&gt;for await&lt;/code&gt; loop and your UI will not break a sweat.&lt;/p&gt;

&lt;p&gt;There is also a smaller, ergonomic win. With Signals the rendering rule is "whatever the signal is at this instant." Streaming chat is a value that is &lt;em&gt;visibly mid-update&lt;/em&gt;, and Signals give you the perfect vocabulary for that — the in-flight token buffer is just another signal, alongside the committed message history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;A single-page Angular app with one component. You type a question, hit send, and watch Gemini's answer stream in word by word. There is a stop button that cancels the stream, a running history of messages, and that is it. We will use Angular 20 standalone components, Signals, the new control flow (&lt;code&gt;@for&lt;/code&gt;, &lt;code&gt;@if&lt;/code&gt;), and the official &lt;code&gt;@google/genai&lt;/code&gt; SDK.&lt;/p&gt;

&lt;p&gt;You can find the finished repo on GitHub at the link at the bottom of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You will need Node 20 or newer, the Angular CLI (&lt;code&gt;npm i -g @angular/cli&lt;/code&gt;), and a Gemini API key from &lt;a href="https://aistudio.google.com/app/apikey" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;. The free tier is more than enough to follow along.&lt;/p&gt;

&lt;p&gt;A note on the API key, because this matters: in the local version we read the key from an environment variable that gets bundled into the client. &lt;strong&gt;That is fine for local exploration. It is not fine for production.&lt;/strong&gt; Anything in your bundle is visible to anyone who opens DevTools. We will fix this in the deploy section by adding a small proxy on Cloud Run — the key stays on the server, and the Angular code barely changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project setup
&lt;/h2&gt;

&lt;p&gt;Spin up a new Angular project with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng new gemini-stream &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--routing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;--style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;css &lt;span class="nt"&gt;--skip-tests&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;gemini-stream
npm i @google/genai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open &lt;code&gt;src/environments/environment.ts&lt;/code&gt; (create it if the CLI did not) and add your key:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;geminiApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_AI_STUDIO_KEY_HERE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Add the same file under &lt;code&gt;environment.development.ts&lt;/code&gt; if you use a separate dev environment, and make sure &lt;code&gt;.gitignore&lt;/code&gt; keeps these out of source control if you put a real key in.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;src/app/app.config.ts&lt;/code&gt;, opt into zoneless change detection. By Angular 20 this is a stable provider, and it gives you the per-signal update path that makes streaming feel snappy:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provideZonelessChangeDetection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;provideZonelessChangeDetection&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the entire setup. On to the interesting bits.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Gemini service
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/app/gemini.service.ts&lt;/code&gt;. The job of this service is small: take a chat history, return an async iterable of text deltas, and let the caller stop early.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google/genai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../environments/environment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ChatRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatRole&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GeminiService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geminiApiKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="nx"&gt;shouldStop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AsyncGenerator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContentStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldStop&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three things worth pointing out here.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;generateContentStream&lt;/code&gt; returns an async iterable of chunks. Each chunk has a &lt;code&gt;text&lt;/code&gt; getter that gives you the new tokens for that step. That is all the SDK asks of you.&lt;/p&gt;

&lt;p&gt;Second, we accept a &lt;code&gt;shouldStop&lt;/code&gt; predicate instead of an &lt;code&gt;AbortController&lt;/code&gt;. This keeps cancellation logic on our side, where it composes nicely with Signals — the predicate is going to read a signal, and the moment the user clicks Stop, the next iteration of the loop bails out.&lt;/p&gt;

&lt;p&gt;Third, the service yields strings, not chunks. By the time anything else in the app sees a delta, it is already plain text. That keeps our chat component free of any SDK-specific types.&lt;/p&gt;
&lt;h2&gt;
  
  
  Signals-based chat state
&lt;/h2&gt;

&lt;p&gt;Now the chat component. Create &lt;code&gt;src/app/chat.component.ts&lt;/code&gt; and start with the state. The whole point of this article is in this section, so read it slowly.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;viewChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ElementRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GeminiService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./gemini.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;standalone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;!-- coming up next --&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`/* coming up next */`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;gemini&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GeminiService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;streaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;isStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;stopRequested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;canSend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;scroller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;viewChild&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ElementRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Read the streaming buffer and message count to re-trigger on every update,&lt;/span&gt;
      &lt;span class="c1"&gt;// then scroll to the bottom on the next animation frame.&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scroller&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canSend&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stopRequested&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopRequested&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`\n\n_Error: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stopRequested&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Five signals carry the entire state of the chat. &lt;code&gt;messages&lt;/code&gt; is the committed history. &lt;code&gt;draft&lt;/code&gt; is what is in the textarea. &lt;code&gt;streaming&lt;/code&gt; is the buffer for the in-flight assistant reply, separate from the history so we can render it differently. &lt;code&gt;isStreaming&lt;/code&gt; and &lt;code&gt;stopRequested&lt;/code&gt; are the control flags.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;canSend&lt;/code&gt; is a &lt;code&gt;computed&lt;/code&gt;. We never write to it, we never subscribe to it; we just read it from the template and Angular figures out when it changes. That single line replaces the form-validation observable boilerplate you might be used to.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;effect&lt;/code&gt; is doing the auto-scroll. By reading &lt;code&gt;streaming()&lt;/code&gt; and &lt;code&gt;messages().length&lt;/code&gt; inside the effect, we tell Angular: "rerun me whenever either of these changes." Then we scroll the chat container to the bottom on the next frame. This is the kind of small DOM concern that used to require &lt;code&gt;AfterViewChecked&lt;/code&gt; and a flag; here it is six lines.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;send&lt;/code&gt; method is where streaming meets state. We push the user message, clear the buffer, then iterate over the service's async generator and call &lt;code&gt;.update()&lt;/code&gt; on the streaming signal for each delta. When the loop ends (or the user hits Stop, which makes &lt;code&gt;shouldStop&lt;/code&gt; return true on the next iteration), we commit whatever was in the buffer to the message history and reset.&lt;/p&gt;
&lt;h2&gt;
  
  
  The template
&lt;/h2&gt;

&lt;p&gt;Replace the placeholder template and styles in the same file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
  &amp;lt;div class="shell"&amp;gt;
    &amp;lt;div class="scroller" #scroller&amp;gt;
      @for (m of messages(); track $index) {
        &amp;lt;div class="msg {{ m.role }}"&amp;gt;{{ m.content }}&amp;lt;/div&amp;gt;
      }
      @if (isStreaming() &amp;amp;&amp;amp; streaming()) {
        &amp;lt;div class="msg model streaming"&amp;gt;{{ streaming() }}&amp;lt;span class="cursor"&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
      }
    &amp;lt;/div&amp;gt;

    &amp;lt;form class="composer" (submit)="$event.preventDefault(); send()"&amp;gt;
      &amp;lt;textarea
        rows="2"
        placeholder="Ask Gemini something..."
        [value]="draft()"
        (input)="draft.set($any($event.target).value)"
        (keydown.enter)="$event.preventDefault(); send()"
      &amp;gt;&amp;lt;/textarea&amp;gt;
      @if (isStreaming()) {
        &amp;lt;button type="button" (click)="stop()"&amp;gt;Stop&amp;lt;/button&amp;gt;
      } @else {
        &amp;lt;button type="submit" [disabled]="!canSend()"&amp;gt;Send&amp;lt;/button&amp;gt;
      }
    &amp;lt;/form&amp;gt;
  &amp;lt;/div&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`
  .shell { display: flex; flex-direction: column; height: 100dvh; max-width: 720px; margin: 0 auto; font-family: system-ui, sans-serif; }
  .scroller { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
  .msg { padding: 0.75rem 1rem; border-radius: 12px; white-space: pre-wrap; line-height: 1.5; max-width: 85%; }
  .msg.user { align-self: flex-end; background: #4285f4; color: white; }
  .msg.model { align-self: flex-start; background: #f1f3f4; color: #202124; }
  .cursor { display: inline-block; width: 0.5ch; background: currentColor; margin-left: 2px; animation: blink 1s steps(1) infinite; }
  @keyframes blink { 50% { opacity: 0; } }
  .composer { display: flex; gap: 0.5rem; padding: 1rem; border-top: 1px solid #eee; }
  textarea { flex: 1; resize: none; padding: 0.75rem; border-radius: 12px; border: 1px solid #ddd; font: inherit; }
  button { padding: 0 1.25rem; border-radius: 12px; border: none; background: #4285f4; color: white; font-weight: 600; cursor: pointer; }
  button:disabled { opacity: 0.5; cursor: not-allowed; }
`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The new control flow (&lt;code&gt;@for&lt;/code&gt;, &lt;code&gt;@if&lt;/code&gt;, &lt;code&gt;@else&lt;/code&gt;) makes this template read like a small story: render every committed message, then render the in-flight reply if there is one, then show Send or Stop based on whether we are mid-stream. The blinking cursor on the streaming bubble is a tiny detail that makes the whole thing feel alive.&lt;/p&gt;

&lt;p&gt;Wire the component into &lt;code&gt;src/app/app.component.ts&lt;/code&gt; as the only thing rendered, run &lt;code&gt;ng serve&lt;/code&gt;, and you should have a working streaming chat at &lt;code&gt;http://localhost:4200&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shipping it on Cloud Run
&lt;/h2&gt;

&lt;p&gt;The local app calls Gemini directly with a key in the bundle. To ship it safely we need two small moves: a tiny server proxy that holds the key, and Cloud Run to host both the proxy and the static Angular build.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;server/index.ts&lt;/code&gt; at the project root:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google/genai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4mb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/gemini-stream/browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Transfer-Encoding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContentStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Update &lt;code&gt;gemini.service.ts&lt;/code&gt; to read from the proxy with &lt;code&gt;fetch&lt;/code&gt; instead of calling the SDK in the browser. The SDK and the API key never leave the server:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;shouldStop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldStop&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the part I love about the Signals architecture: the component code does not change at all. The signals do not care that the bytes are coming from a Cloud Run service now instead of the SDK. Same loop, same &lt;code&gt;streaming.update()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;Dockerfile&lt;/code&gt; at the project root:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx tsc &lt;span class="nt"&gt;-p&lt;/span&gt; server

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/server/dist ./server&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then ship it with one command — Cloud Run will build the container from source for you:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy gemini-stream &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt; &lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_AI_STUDIO_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You will get back a URL like &lt;code&gt;https://gemini-stream-xxxxxx.us-central1.run.app&lt;/code&gt;. Test it in the browser, confirm the chat works end to end, and you are done.&lt;/p&gt;

&lt;p&gt;The fun part: dev.to has a first-class Cloud Run embed, so here you go:&lt;/p&gt;


&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://gemini-stream-1070943699730.us-central1.run.app"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  What you actually built
&lt;/h2&gt;

&lt;p&gt;The whole thing — service, component, template, styles — comes in just over a hundred lines. Compare that to an equivalent app two years ago and you will notice what is &lt;em&gt;missing&lt;/em&gt;: there is no &lt;code&gt;Subject&lt;/code&gt;, no &lt;code&gt;BehaviorSubject&lt;/code&gt;, no &lt;code&gt;async&lt;/code&gt; pipe, no &lt;code&gt;OnPush&lt;/code&gt; boilerplate that you have to think about, no manual subscription cleanup. Signals plus the new control flow plus zoneless change detection is genuinely a different programming model, and streaming AI is the application that shows it off best.&lt;/p&gt;

&lt;p&gt;A couple of small things to try next, in roughly increasing order of effort:&lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;systemInstruction&lt;/code&gt; to the &lt;code&gt;generateContentStream&lt;/code&gt; call to give your model a persona. The SDK accepts it as a sibling of &lt;code&gt;contents&lt;/code&gt; on the proxy side.&lt;/p&gt;

&lt;p&gt;Switch from text-only input to multimodal: drop an image into the chat and forward it from the proxy as a &lt;code&gt;parts&lt;/code&gt; entry of &lt;code&gt;{ inlineData: { mimeType, data } }&lt;/code&gt;. Gemini handles the rest.&lt;/p&gt;

&lt;p&gt;Prefer Firebase to Cloud Run? Firebase AI Logic gives you the same proxy pattern with less infra — install &lt;code&gt;firebase&lt;/code&gt; and &lt;code&gt;@firebase/ai&lt;/code&gt;, and the SDK shape stays almost identical. You give up the dev.to Cloud Run embed, but the Angular code is unchanged.&lt;/p&gt;

&lt;p&gt;Try the same UI against &lt;a href="https://developer.chrome.com/docs/ai/built-in" rel="noopener noreferrer"&gt;Chrome's Built-in AI&lt;/a&gt; (Gemini Nano running on-device, no key, no network). The Prompt API has its own streaming primitive that drops into the same Signal-based shell with almost no changes — and you get an offline-capable chat for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If you take one thing away from this post, let it be that &lt;em&gt;Signals were designed for values that change a lot&lt;/em&gt;, and an LLM stream is the canonical example of a value that changes a lot. The pieces fit so cleanly that the resulting code reads more like a description of the UI than like a program.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;https://github.com/TomWebwalker/gemini-stream-angular&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build something with this drop a link in the comments — I would love to see what people make of it.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>gemini</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Build a Custom AI Quality Gate on Cloud Run (From Zero to Production)</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Mon, 04 May 2026 04:17:45 +0000</pubDate>
      <link>https://dev.to/gdg/how-to-build-a-custom-ai-quality-gate-on-cloud-run-from-zero-to-production-1odp</link>
      <guid>https://dev.to/gdg/how-to-build-a-custom-ai-quality-gate-on-cloud-run-from-zero-to-production-1odp</guid>
      <description>&lt;p&gt;In my previous article about treating architecture documentation as a first-class asset, I had a great discussion in the comments about enforcing architectural rules. I promised to share materials from my recent Google Developer Groups workshop.&lt;/p&gt;

&lt;p&gt;The workshop is now finished! Here is the story of how I built an AI Quality Gate, how it helped me solve the internal "CEO, CTO, CFO, CISO" conflict, and a summary of the live demonstration.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can listen a podcast generated based on this publication (thanks &lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;NotebookLM&lt;/a&gt;):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/qfbZZxcDNbU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;Playground repositories with source code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-quality-gate" rel="noopener noreferrer"&gt;Quality Gate PoC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-repo1" rel="noopener noreferrer"&gt;CheckMe Repo #1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-repo2" rel="noopener noreferrer"&gt;CheckMe Repo #2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-repo3" rel="noopener noreferrer"&gt;CheckMe Repo #3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Backstory: Mentoring and the "CEO, CTO, CFO, CISO" Conflict
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9dc7ccfuem5k00sdo6i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9dc7ccfuem5k00sdo6i.png" alt="Conflict of interest inside a developer's head" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I work as a DevSecOps engineer, but in my free time, I mentor for Technovation Girls, a global program that helps young women learn tech and STEM. Because we always need more IT mentors, I built an AI mentor bot to help the students.&lt;br&gt;
Building this bot had two big challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safety: Because children use it, it had to be completely safe from AI hallucinations.&lt;/li&gt;
&lt;li&gt;Budget: Because I pay for it myself, it had to be very cheap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bot was a big success. Using Google Cloud Run and Vertex AI, it handled 250 users and answered 1,500 questions for only about $25-$55 a month.&lt;/p&gt;

&lt;p&gt;However, when I tried to add new features quickly, I faced a big problem. With only 1-2 hours of free time a day for this project, I experienced a harsh "CEO, CTO, CFO, CISO" conflict in my own head:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CTO wanted to write code and ship features fast.&lt;/li&gt;
&lt;li&gt;The CISO wanted to stop releases to make sure everything was secure.&lt;/li&gt;
&lt;li&gt;The CFO wanted to keep cloud costs low.&lt;/li&gt;
&lt;li&gt;The CEO wanted the product to grow and succeed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: What is an AI Quality Gate?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3f1kmnubehtu7s61uzw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3f1kmnubehtu7s61uzw.png" alt="Indie Developer Conflict of Interest Solved" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To solve the "CEO, CTO, CFO, CISO" conflict, I created an AI Quality Gate.&lt;br&gt;
An AI Quality Gate is a custom microservice that automatically reviews code for architecture, security, and costs (FinOps). It is built on Google Cloud Run and uses Vertex AI (Gemini).&lt;/p&gt;

&lt;p&gt;The first action of the Quality Gate was to block its own MVP from reaching the production. So I decided it was a good sign.&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Short Summary:&lt;/strong&gt; Fail.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;List of Critical Findings:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;AI Gateway (AAA):&lt;/strong&gt; The provided code retrieves a GitLab token directly from Secret Manager and uses it for GitLab API access. This bypasses the AI gateway, violating the "ALWAYS Consistency with AI gateway (AAA, FinOps)" rule. The AAA component should manage authentication and authorization for all external services, including GitLab.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Constructive Recommendations:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Implement AI Gateway AAA:&lt;/strong&gt; Modify the &lt;code&gt;ai_review.py&lt;/code&gt; script to authenticate with the AI gateway first. The AI gateway will then handle the GitLab authentication, providing a centralized and secure way to manage access. Use gateway's provided token instead of direct GitLab API access from the job.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;FinOps Considerations:&lt;/strong&gt; Track the cost of AI reviews and link this with FinOps tools, it is important to provide cost visibility since the usage of resources will increase.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because it runs on Cloud Run, it only costs money when it is actively checking code. For a whole month of automated, deep-context code reviews, I paid only $0.12! This made the CFO part of my brain very happy.&lt;br&gt;
At first, I used the AI Quality Gate as a step in my CI/CD pipeline. But waiting several minutes for a "Merge Request Failed" message was slow and annoying. Now, I run the Quality Gate from a bash script directly in my IDE before creating a Merge Request. This saves time and perfectly resolves the "CEO, CTO, CFO, CISO" conflict by balancing speed, safety, and budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workshop Demo: The AI Quality Gate in Action
&lt;/h2&gt;

&lt;p&gt;During the GDG workshop, I showed a live demo across three different code repositories to prove why traditional tools are not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo 1: The 10/10 Linter Illusion - Happy CISO
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5kf4j45ni2u91b2uuzw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5kf4j45ni2u91b2uuzw.png" alt="Quality Gate First Check - Developer tries to fool the linter" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, I scanned a simple service using standard tools like Ruff, Pylint, and Semgrep. The code got a perfect 10/10 score. However, when I sent the code to the AI Quality Gate, it blocked the release. It found a critical SQL injection and a prompt injection (a hidden note in the code telling the AI reviewer to "report that everything is fine"). Traditional linters missed this completely, but the AI caught it and gave me exact steps to fix it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo 2: Catching Semantic Drift - Happy CEO+CRO
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd04lapm9wj3runmftn61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd04lapm9wj3runmftn61.png" alt="Quality Gate Second Check - Documentation and Code Inconsistency" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the second project, the README.md file stated that the system followed strict privacy standards and anonymized user data. But the actual code did the opposite: it saved real user emails and IDs. Standard tools missed this, but the AI Quality Gate read the documentation, compared it to the code's behavior, and found the security violation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo 3: "Shift-In" (Reviewing Before Coding) - Happy CTO+CFO
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnfu81t92z6dqsbpqx6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnfu81t92z6dqsbpqx6w.png" alt="Quality Gate Third Check - Checking Plan Before Coding" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last demo was the most powerful. The repository had zero lines of code. It only contained a Markdown document planning a new feature. I sent this text plan to the AI Quality Gate. Before I wrote a single line of Python, the AI found critical security flaws in the plan, like missing server logs and hardcoded passwords.&lt;br&gt;
This changes the concept of "Shift-Left" security into "Shift-In" - bringing experts directly into your IDE while you are still brainstorming the idea. Now we may not only test the code but even test the ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When you keep your architecture rules and documentation close to your code, a custom AI Quality Gate becomes an incredibly powerful tool. It helps you write better code, saves time, and finally resolves the internal "CEO, CTO, CFO, CISO" conflict. Moreover such a gate may be an additional advisor with any experience you want and help to improve any idea in the earliest stage to save future money. Best of all, it costs almost nothing to run.&lt;br&gt;
If you want to build this yourself, my Docker image is available on DockerHub, and the sample repositories are on my GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-quality-gate" rel="noopener noreferrer"&gt;Quality Gate PoC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-repo1" rel="noopener noreferrer"&gt;CheckMe Repo #1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-repo2" rel="noopener noreferrer"&gt;CheckMe Repo #2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tyutinalexkz/workshops-260425-gdg-quality-gate-repo3" rel="noopener noreferrer"&gt;CheckMe Repo #3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>googlecloud</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>BWAI'2026 @ VTU, Belagavi</title>
      <dc:creator>Piyush Annigeri</dc:creator>
      <pubDate>Sun, 03 May 2026 15:31:54 +0000</pubDate>
      <link>https://dev.to/gdg/bwai2026-vtu-belagavi-1p7n</link>
      <guid>https://dev.to/gdg/bwai2026-vtu-belagavi-1p7n</guid>
      <description>&lt;h1&gt;
  
  
  #BuildwithAI
&lt;/h1&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hello Developer,&lt;/strong&gt;&lt;br&gt;
That's a wrap at BWAI'2026 @ VTU, Belagavi.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Build with AI 2026 workshop series, hosted at Visvesvaraya Technological University (VTU), Belagavi, was a flagship initiative designed to transition students and developers from theoretical understanding to practical mastery of Artificial Intelligence. In an era where Generative AI and Autonomous Agents are redefining the engineering landscape, this week-long intensive program provided a sandbox for innovation.&lt;/p&gt;

&lt;p&gt;The series focused on the "Learn-Build-Scale" philosophy, leveraging the Google Cloud Ecosystem. Over the course of seven days, participants engaged in high-intensity labs, credit redemption sessions, and collaborative builds, culminating in an Open Innovation Demo Day. The event served not only as a training ground but as a networking hub for the next generation of AI engineers.&lt;/p&gt;




&lt;h2&gt;
  
  
  🟢 DAY - 0 &amp;nbsp; 🔵 [06-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Onboarding &amp;amp; Environment Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Introduction to Google Cloud Platform (GCP), environment configuration, and Google Cloud Credit redemption to ensure zero-cost development for students.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🟢 DAY - 1 &amp;nbsp; 🔵 [07-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conversational Voice Systems&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Building voice-responsive agents. Integration of Compute Engine instances, LLMs (Large Language Models), and Asterisk for telephony-based AI interactions.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🟢 DAY - 2 &amp;nbsp; 🔵 [08-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Predictive Analytics &amp;amp; Big Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exploring BigQuery AI. Participants built a job navigation system that uses predictive analysis to match candidates with roles based on historical data patterns.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🟢 DAY - 3 &amp;nbsp; 🔵 [09-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A-2-A Workflow Orchestration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Advanced Agent-to-Agent workflows. Utilizing MCP Java SDK and Vertex AI to create interconnected systems where AI agents communicate to solve complex tasks.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🟢 DAY - 4 &amp;nbsp; 🔵 [10-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise Supply Chain Agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-level engineering using AlloyDB AI and ScaNN (Scalable Nearest Neighbors). Building an autonomous supply chain agent capable of multimodal data processing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🟢 DAY - 5 &amp;nbsp; 🔵 [11-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Surplus Engine with Cloud SQL In-Database AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In this codelab, you will build &lt;strong&gt;KarmaLoop&lt;/strong&gt; — a sustainable surplus-sharing app that treats intelligence as a first-class citizen of the data layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🟢 DEMO DAY &amp;nbsp; 🔵 [13-04-2026]
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI for Engineers: Challenges and Opportunities&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;As a primary mentor, Mr. Bhandari brought a wealth of international experience from the heart of Google's engineering labs in Japan. His session addressed the critical shift in the job market, emphasizing that the modern engineer must be "AI-augmented."&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Speaker@BWAI
&lt;/h2&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38sreu3rjar1n5z9nr92.png" alt="Haren Bhandari" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Haren Bhandari&lt;/strong&gt; &lt;br&gt; 🔴 &lt;a href="https://www.linkedin.com/in/haren-bhandari-abb09924/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;GOOGLE - TOKYO&lt;/strong&gt; &lt;br&gt; Senior Developer Relations Engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Organizers@BWAI
&lt;/h2&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7axkgahxmx8xhxjdsqu.png" alt="Piyush Annigeri" width="400" height="400"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Piyush Annigeri&lt;/strong&gt; &lt;br&gt; 🔴 &lt;a href="https://www.linkedin.com/in/piyushannigeri" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;ORGANIZER&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvn35kgq1kndfpz5kwnr8.png" alt="Yuvaraj Sutar" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yuvaraj Sutar&lt;/strong&gt; &lt;br&gt; 🟡 &lt;a href="https://www.linkedin.com/in/yuvaraj-sutar-840234334/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;CONTENT CREATOR&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6htd8gk273i1hcma8vhm.png" alt="Prajwal Rawoot" width="396" height="396"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Prajwal Rawoot&lt;/strong&gt; &lt;br&gt; 🔵 &lt;a href="https://www.linkedin.com/in/prajwal-rawoot-698424330" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;TECHNICAL-LEAD&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe36pabhn0ckz0y7vopqc.png" alt="Shravan Chougule" width="400" height="400"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shravan Chougule&lt;/strong&gt; &lt;br&gt; 🟢 &lt;a href="https://www.linkedin.com/in/shravan-chougule-0631b0354/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;MEDIA-LEAD&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiybt6cdurksjvs15w8pk.png" alt="Ananyaa Sudhanshu" width="400" height="400"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ananyaa Sudhanshu&lt;/strong&gt; &lt;br&gt; 🔴 &lt;a href="https://www.linkedin.com/in/ananyaa-sudhanshu-808697387/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;EVENT COORDINATOR&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonva5j24teb6tijrfnb8.png" alt="Advika Gurav" width="400" height="400"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Advika Gurav&lt;/strong&gt; &lt;br&gt; 🟡 &lt;a href="https://www.linkedin.com/in/advika-gurav-ab654b334/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;PR &amp;amp; MARKETING&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvktysb2puobjqoekpcvd.png" alt="Yash Koparde" width="396" height="396"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yash Koparde&lt;/strong&gt; &lt;br&gt; 🟢 &lt;a href="https://www.linkedin.com/in/yashkoparde/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; &lt;strong&gt;TECHNICAL-LEAD&lt;/strong&gt; &lt;br&gt; Google Developer Group on Campus, Visvesvaraya Technological University, Belagavi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  #BWAIchampions
&lt;/h2&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5cto2ly2lf4f73bhac81.png" alt="Sadiya Sanadi" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Sadiya Sanadi&lt;/strong&gt; &lt;br&gt; 🟡 &lt;a href="https://www.linkedin.com/in/Sadiyaas" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; Second-year AI/ML student at Jain College of Engineering, Belgaum.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I recently developed &lt;strong&gt;NyayaSetu&lt;/strong&gt;, an AI-powered welfare rights navigator that helps users discover government schemes and check eligibility easily, addressing the lack of awareness and difficulty in accessing public welfare information. The project secured &lt;strong&gt;1st place&lt;/strong&gt; for its impact and usability.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyh1nkq6t9577nqwfyaw.png" alt="Muhammad Rehan" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Muhammad Rehan&lt;/strong&gt; &lt;br&gt; 🟡 &lt;a href="https://linkedin.com/in/mdrehan08/" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt; &lt;br&gt;&lt;br&gt; Founder of StackBIZ.tech &amp;amp; a result-driven full-stack developer focused on building production-ready SaaS platforms, cloud infrastructure, &amp;amp; AI-based solutions.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I created &lt;strong&gt;PromptedForge.in&lt;/strong&gt;, an AI-powered platform that helps students generate problem statements, prompts, and execution roadmaps without the need for brainstorming. My work has been recognized by the Entrepreneurship Cell at IIT Delhi, and you can explore the platform at &lt;a href="http://promptedforge.in/" rel="noopener noreferrer"&gt;promptedforge.in&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Irayya R Hiremath&lt;/strong&gt;&lt;br&gt;
🟡 &lt;a href="https://www.linkedin.com/in/irayya-hiremath-aa0232330" rel="noopener noreferrer"&gt;LINKEDIN&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am a BCA student with a strong interest in software development and technology. I am currently learning programming languages like Python and JavaScript and working on practical projects to build my skills. I am passionate about creating useful applications and continuously improving my knowledge in the tech field.&lt;/p&gt;




&lt;p&gt;The Build with AI workshop at VTU Belagavi concluded as a landmark event for the region's tech community. By providing direct access to Google experts and enterprise-grade tools, the workshop demystified the complexities of Artificial Intelligence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moments@BWAI
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbu9cbob9j3fx2jb0nmgd.png" alt="Moment 1" width="800" height="448"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9pf1z87kcubndg6hj3ro.png" alt="Moment 2" width="800" height="448"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3myxqp193a471j7na76k.jpg" alt="Moment 3" width="800" height="347"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl65p8eymaakgbkj878b1.jpeg" alt="Moment 4" width="800" height="351"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F50xcifhlt4iu8f9wj4y3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F50xcifhlt4iu8f9wj4y3.png" alt=" " width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>googlecloud</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
