<?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>Managed Inference on Google Cloud: Pairing the Gemini Enterprise Agent Platform with Cloud Run</title>
      <dc:creator>Caleb Duff</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:45:27 +0000</pubDate>
      <link>https://dev.to/gdg/managed-inference-on-google-cloud-pairing-the-gemini-enterprise-agent-platform-with-cloud-run-246j</link>
      <guid>https://dev.to/gdg/managed-inference-on-google-cloud-pairing-the-gemini-enterprise-agent-platform-with-cloud-run-246j</guid>
      <description>&lt;p&gt;If you have ever wanted to ship an AI-powered application without managing GPUs, model servers, or scaling infrastructure yourself, this guide is for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed inference&lt;/strong&gt; simply means letting a cloud provider run the AI model for you: you send a request, the platform handles the compute, and you get a response back. On Google Cloud, the cleanest way to do this today is to pair the &lt;strong&gt;Gemini Enterprise Agent Platform&lt;/strong&gt; (formerly Vertex AI) with &lt;strong&gt;Google Cloud Run&lt;/strong&gt;, dividing responsibilities between the two services. The Agent Platform serves as the orchestration and intelligence engine, while Cloud Run hosts your custom application logic, front-end UIs, or &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By the end of this article, you will be able to:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explain the hybrid architecture and why each layer exists&lt;/li&gt;
&lt;li&gt;Define an AI agent in code using the Agent Development Kit (ADK)&lt;/li&gt;
&lt;li&gt;Deploy your app layer to Cloud Run with a single command&lt;/li&gt;
&lt;li&gt;Choose between online and batch inference for your workload&lt;/li&gt;
&lt;li&gt;Secure and monitor the whole setup in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New to the underlying concept? Start with Google Cloud's primer: &lt;a href="https://cloud.google.com/discover/what-is-ai-inference?hl=en" rel="noopener noreferrer"&gt;What is AI inference?&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;To follow along hands-on, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Google Cloud project with billing enabled&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;gcloud&lt;/code&gt; CLI installed and authenticated&lt;/li&gt;
&lt;li&gt;Python 3.10+ and the ADK installed (&lt;code&gt;pip install google-adk&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also read this purely as an architecture walkthrough; every step is explained, not just shown.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Architectural Blueprint
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakin92cmry0xpuej2hkr.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%2Fakin92cmry0xpuej2hkr.png" alt="GCP Inference lifecycle" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This pattern splits your system into independent, auto-scaling tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Client / Web UI ] ──&amp;gt; [ Cloud Run Service ] (App Logic / Tool Front End)
                                │
                                ▼
        [ Gemini Enterprise Agent Platform — Agent Runtime ]
            (Orchestration, Intent Analysis, Memory)
                                │
                                ▼
              [ Managed Inference / Model Garden ]
                 (Gemini 3.x Pro / Flash models)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why split it this way?&lt;/strong&gt; Each tier scales independently and fails independently. Your web front end can handle a traffic spike without touching the model layer, and you can swap models without redeploying your application code. It also creates a clean security boundary, clients only ever talk to Cloud Run, never directly to the model.&lt;/p&gt;

&lt;p&gt;Here is what each layer actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run&lt;/strong&gt; runs your specialized business logic, secures client-facing endpoints with Identity-Aware Proxy (IAP), and hosts external tools, MCP servers, and APIs. Think of it as everything &lt;em&gt;you&lt;/em&gt; build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Agent Platform (Agent Runtime)&lt;/strong&gt; manages active agent state, long-term memory, and the model's reasoning steps in a centralized, fully managed runtime. Think of it as everything &lt;em&gt;Google&lt;/em&gt; runs for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Build Your Agent Code with the ADK
&lt;/h2&gt;

&lt;p&gt;Use the open-source &lt;strong&gt;Agent Development Kit (ADK)&lt;/strong&gt; to define your agent's behavior in code and bind it to a model. The key idea to understand: &lt;strong&gt;tools are plain Python functions&lt;/strong&gt;. The ADK reads each function's docstring to decide when and how to call it; so a clear docstring is not documentation nicety, it is part of your agent's logic.&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="c1"&gt;# agent.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.adk.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_internal_business_system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Invokes secure business workflows deployed on Cloud Run.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Logic to securely call your Cloud Run service URL
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data retrieved from secure internal backend.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Define an agent that targets a current Gemini model
&lt;/span&gt;&lt;span class="n"&gt;root_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&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;enterprise_inference_agent&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Or another current model from Model Garden
&lt;/span&gt;    &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a data processing assistant using managed inference.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;call_internal_business_system&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;&lt;strong&gt;Breaking down the four fields:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; — an identifier for your agent, used in logs and traces.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model&lt;/code&gt; — which Gemini model handles the reasoning. Flash models are faster and cheaper; Pro models handle more complex reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instruction&lt;/code&gt; — the agent's system prompt, shaping its behavior on every request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools&lt;/code&gt; — the Python functions the model is allowed to call. When a user request matches a tool's docstring, the model invokes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Gemini 1.0 and 1.5 models (including &lt;code&gt;gemini-1.5-pro&lt;/code&gt;) have been retired and now return errors. Always target a currently supported model, such as &lt;code&gt;gemini-3.5-flash&lt;/code&gt;, &lt;code&gt;gemini-3.6-flash&lt;/code&gt;, or a Gemini 3.x Pro release from Model Garden.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Containerize and Deploy the App Layer to Cloud Run
&lt;/h2&gt;

&lt;p&gt;When deploying your orchestration backend or front-end dashboard, the tooling can package and push the container for you. Two small steps get you there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step A: Configure Service Account Permissions
&lt;/h3&gt;

&lt;p&gt;In Google Cloud, services do not trust each other by default, your Cloud Run instance needs explicit permission to invoke Agent Platform endpoints. This command grants its service account that permission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud projects add-iam-policy-binding YOUR_PROJECT_ID &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:YOUR_RUN_SA@YOUR_PROJECT_ID.iam.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/aiplatform.user"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain terms: "let this Cloud Run service call the AI platform." This is the step people most often forget; if your deployed service returns permission errors, come back here first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step B: Build and Deploy
&lt;/h3&gt;

&lt;p&gt;The ADK ships with a one-command deployment path. Under the hood, it does three things: builds your container image, pushes it to Artifact Registry, and creates (or updates) the Cloud Run service.&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;# Deploys your custom agent or tool layer directly to Cloud Run&lt;/span&gt;
adk deploy cloud_run &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--project&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="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="s2"&gt;"us-central1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"agent-inference-backend"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    path/to/your/agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, the &lt;strong&gt;Agents CLI&lt;/strong&gt; (&lt;code&gt;agents-cli&lt;/code&gt;) can scaffold the deployment configuration for a Cloud Run target. For example, &lt;code&gt;agents-cli scaffold enhance --deployment-target cloud_run&lt;/code&gt; and works from inside your preferred AI coding tool. Either route wires up your environment variables, including model targets and the public service URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Online and Batch Inference Routines
&lt;/h2&gt;

&lt;p&gt;Once the plumbing is in place, there are two primary ways to trigger managed inference. Choosing correctly comes down to one question: &lt;strong&gt;does a human need the answer right now?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Online inference (low-latency UI):&lt;/strong&gt; Make synchronous API calls from your Cloud Run front end directly to the deployed agent endpoint for real-time chat, tool calls, or step-by-step reasoning. &lt;em&gt;Example: a customer support chatbot where every second of latency matters.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch inference (high-volume data):&lt;/strong&gt; For large data processing jobs, submit an asynchronous batch prediction job through the Agent Platform SDK. The platform provisions dedicated compute, runs the inference tasks, writes results and logs to Cloud Storage, and tears down the compute automatically when the job completes. &lt;em&gt;Example: classifying 100,000 support tickets overnight; nobody is waiting on a single response, so throughput and cost matter more than latency.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Batch jobs are typically much cheaper per request, so a good rule of thumb is the "Now vs Later" latency and volume test: if you need a prediction in under 2 seconds(quickly) to serve a live user, use online inference; if you have a large volume of data that can wait minutes or hours, use batch inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Secure and Monitor the Architecture
&lt;/h2&gt;

&lt;p&gt;A demo can skip this section. Production cannot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure the ingress:&lt;/strong&gt; Wrap your Cloud Run endpoints in Identity-Aware Proxy (IAP) to protect human-in-the-loop dashboards, IAP checks the user's Google identity &lt;em&gt;before&lt;/em&gt; traffic ever reaches your code. For agent-to-tool traffic, Agent Gateway can give each agent a unique identity with end-to-end mTLS (mutual TLS, where both sides verify each other) when calling MCP servers on Cloud Run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralize trace logging:&lt;/strong&gt; Enable the platform's built-in OpenTelemetry tracing (Cloud Trace is on by default for CLI-based deployments). You can visually inspect directed acyclic graphs (DAGs) of execution, a step-by-step map of every reasoning step, model call, and tool invocation — to see exactly how your Gemini models and Cloud Run tools collaborated on an inference task. When an agent gives a strange answer, this trace is how you find out &lt;em&gt;why&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split the responsibilities:&lt;/strong&gt; Cloud Run for your code, the Agent Platform for orchestration and models. Each tier scales and fails independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools are just functions:&lt;/strong&gt; the ADK turns well-documented Python functions into capabilities your agent can call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions before deployment:&lt;/strong&gt; grant &lt;code&gt;roles/aiplatform.user&lt;/code&gt; to your Cloud Run service account, or nothing else will work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match inference mode to workload:&lt;/strong&gt; online for interactive experiences, batch for high-volume processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure and trace from day one:&lt;/strong&gt; IAP at the edge, mTLS between services, OpenTelemetry for visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;Try the smallest possible version: define a one-tool agent with the ADK, run &lt;code&gt;adk deploy cloud_run&lt;/code&gt;, and send it a request. Once that works, everything else in this article is an incremental addition.&lt;/p&gt;

&lt;p&gt;Have you tried pairing the Agent Platform with Cloud Run, or are you still on a self-managed inference setup? I would love to hear what your architecture looks like in the comments.&lt;/p&gt;

</description>
      <category>cloudrun</category>
      <category>geminienterpriseagentplatform</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Angular WebMCP — Your App is Now an AI Tool 🔥🚀</title>
      <dc:creator>Giorgio Galassi</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:09:17 +0000</pubDate>
      <link>https://dev.to/gdg/angular-webmcp-your-app-is-now-an-ai-tool-3bp4</link>
      <guid>https://dev.to/gdg/angular-webmcp-your-app-is-now-an-ai-tool-3bp4</guid>
      <description>&lt;p&gt;There's a pattern in Angular releases that I've come to appreciate: every major version picks one bet that's slightly ahead of its time. Signals were that bet in v16. Zoneless was that bet in v21. In v22, that bet is &lt;strong&gt;&lt;a href="https://angular.dev/ai/webmcp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt;&lt;/strong&gt;, and it's a fundamentally different kind of feature — not a framework improvement, but an architectural shift in what an Angular app &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let me explain what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧭 The Idea: Outside → In
&lt;/h2&gt;

&lt;p&gt;Every Angular feature so far has been about what happens &lt;em&gt;inside&lt;/em&gt; the app — how components detect changes, how services are injected, how forms manage state. WebMCP is different: it's about exposing your app's capabilities to the &lt;em&gt;outside&lt;/em&gt;, specifically to AI agents running in the browser.&lt;/p&gt;

&lt;p&gt;The mental model is simple. Today, an AI assistant browsing your app sees a DOM — pixels and HTML. It can read text and click buttons, but it has no understanding of what your app &lt;em&gt;can do&lt;/em&gt;. WebMCP changes that. You declare a set of tools backed by your real Angular services, your real signals, your real DI graph, and any WebMCP-capable agent can discover and call them directly through a typed, described interface you control.&lt;/p&gt;

&lt;p&gt;This is different from &lt;strong&gt;&lt;a href="https://angular.dev/ai/agent-skills" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt;&lt;/strong&gt;, which is your app calling &lt;em&gt;out&lt;/em&gt; to AI. Two separate features, two directions: WebMCP is agents driving your app; Agent Skills is your app calling AI. Today we're only talking about WebMCP.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 Browser Support
&lt;/h2&gt;

&lt;p&gt;WebMCP is built on the &lt;strong&gt;&lt;a href="https://github.com/webmachinelearning/webmcp/" rel="noopener noreferrer"&gt;W3C ModelContext API&lt;/a&gt;&lt;/strong&gt;, a draft browser standard. Your tools register on &lt;code&gt;navigator.modelContext&lt;/code&gt; and any agent that speaks this protocol can query that object and call them.&lt;/p&gt;

&lt;p&gt;As of June 2026, Edge 147 ships it natively, Chrome 149 has an open Origin Trial, Firefox is committed for Q3 2026, and Safari for Q4. Mass adoption is realistically mid-2027, and Angular ships this as experimental — that flag is honest, but the integration is already surprisingly clean.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Experimental:&lt;/strong&gt; &lt;a href="https://angular.dev/api/core/provideExperimentalWebMcpTools" rel="noopener noreferrer"&gt;&lt;code&gt;provideExperimentalWebMcpTools()&lt;/code&gt;&lt;/a&gt; is available in Angular 22 but carries no stability guarantees yet. The W3C spec is still evolving; expect API changes before general availability.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🛠️ Building Your First WebMCP Tool
&lt;/h2&gt;

&lt;p&gt;Let's build something concrete. A dashboard shows a list of expenses and we want an AI assistant to ask "what expenses are currently visible?" and get real, live data back — not a DOM scrape or a static API response, but the actual signal state the component is rendering from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// expense.service.ts&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;Service&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="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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Expense&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&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="nl"&gt;description&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="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;currency&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="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&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;const&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Accommodation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accommodation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transport&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Meals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meals&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;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;ExpenseCategory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&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;CATEGORY_VALUES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &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;Service&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;ExpenseService&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;expenses&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;Expense&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hotel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Accommodation&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Taxi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Transport&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dinner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Meals&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;getVisible&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Expense&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expenses&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;&lt;code&gt;@Service()&lt;/code&gt; is Angular 22's new shorthand for &lt;code&gt;@Injectable({ providedIn: 'root' })&lt;/code&gt; — if you want the full picture on that, I covered it in &lt;a href="https://medium.com/@giorgio.galassi/angular-22-service-and-injectasync-dependency-injection-finally-grows-up-499e508fa47c" rel="noopener noreferrer"&gt;Angular 22 — @Service and injectAsync: Dependency Injection Finally Grows Up&lt;/a&gt;. Notice that &lt;code&gt;CATEGORY_VALUES&lt;/code&gt; lives right here next to the &lt;code&gt;ExpenseCategory&lt;/code&gt; object — we'll use it to keep the tool description in sync automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Declaring the tool
&lt;/h3&gt;

&lt;p&gt;Parameters are described using &lt;a href="https://json-schema.org/" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt; syntax — the same format Angular uses internally and the same format the agent receives.&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="c1"&gt;// expense-mcp.tool.ts&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;declareExperimentalWebMcpTool&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="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;ExpenseService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CATEGORY_VALUES&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;./expense.service&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;expenseListTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;declareExperimentalWebMcpTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getVisibleExpenses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Returns the list of expenses currently visible in the dashboard.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Filter by expense category. Allowed values: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CATEGORY_VALUES&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;additionalProperties&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="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ExpenseCategory&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svc&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;ExpenseService&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;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVisible&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;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;all&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&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&lt;/span&gt;&lt;span class="dl"&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;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="nx"&gt;filtered&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things to notice here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The return value follows the MCP tool result format.&lt;/strong&gt; &lt;code&gt;{ content: [{ type: 'text', text: '...' }] }&lt;/code&gt; is the wire format the agent expects, defined by the Model Context Protocol spec. You don't return a plain object or array directly — you serialise your data into that &lt;code&gt;text&lt;/code&gt; field with &lt;code&gt;JSON.stringify&lt;/code&gt;, and the agent parses it on its end. This is not Angular-specific; it's the protocol contract every MCP tool must honour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; are instructions to the LLM&lt;/strong&gt;, not documentation for humans. The agent reads them at runtime to decide whether and how to call the tool, so write them like an OpenAPI spec for an AI: precise, specific, unambiguous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;execute&lt;/code&gt; runs inside Angular's injection context&lt;/strong&gt;, which means &lt;code&gt;inject()&lt;/code&gt; works normally and your tool has full access to your DI graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CATEGORY_VALUES&lt;/code&gt; is derived from the object, not hardcoded.&lt;/strong&gt; Add a value to &lt;code&gt;ExpenseCategory&lt;/code&gt; and the agent description updates automatically, with no second place to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Registering in the app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.config.ts&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;provideExperimentalWebMcpTools&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;expenseListTool&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;./expense-mcp.tool&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="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;provideExperimentalWebMcpTools&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;expenseListTool&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="c1"&gt;// ... rest of your providers&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;One line in your app config and you're done. From here, any WebMCP-capable agent visiting your app will find &lt;code&gt;getVisibleExpenses&lt;/code&gt; in &lt;code&gt;document.modelContext.tools&lt;/code&gt; and can call it against the live session.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 What the Agent Actually Sees
&lt;/h2&gt;

&lt;p&gt;When a WebMCP-capable agent inspects the page, this is what it finds on &lt;code&gt;document.modelContext&lt;/code&gt;:&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;"tools"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"getVisibleExpenses"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Returns the list of expenses currently visible in the dashboard."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"category"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Filter by expense category. Allowed values: accommodation, transport, meals."&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;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;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;p&gt;The user says &lt;em&gt;"show me only transport expenses."&lt;/em&gt; The agent reads the schema, resolves &lt;code&gt;category: "transport"&lt;/code&gt;, calls the tool. Angular runs the handler against the live signal state. The agent gets back exactly the filtered rows the dashboard is currently showing — no DOM scraping, no brittle selectors, no static mock data.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚛️ Signal Forms as Agent Tools
&lt;/h2&gt;

&lt;p&gt;If your app is form-heavy, Angular 22 also ships &lt;a href="https://angular.dev/api/forms/signals/provideExperimentalWebMcpForms" rel="noopener noreferrer"&gt;&lt;code&gt;provideExperimentalWebMcpForms()&lt;/code&gt;&lt;/a&gt;, which automatically surfaces all your &lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&gt;Signal Forms&lt;/a&gt; as agent-callable tools without you having to declare each one manually.&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="c1"&gt;// app.config.ts&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;provideExperimentalWebMcpForms&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/forms/signals&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="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;provideExperimentalWebMcpForms&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;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Experimental:&lt;/strong&gt; &lt;code&gt;provideExperimentalWebMcpForms()&lt;/code&gt; is double-experimental — both WebMCP itself and the forms integration are in preview. Treat it as a proof of concept for now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each Signal Form becomes a tool the agent can fill and submit. For internal tooling or admin dashboards this is already genuinely useful, and it shows where the Angular team is heading: if Signal Forms are the way to model user intent, they should also be the way to model agent intent.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Beyond the Happy Path
&lt;/h2&gt;

&lt;p&gt;The docs highlight forms as the easy entry point, but the more interesting territory is everything else — read queries, write actions, navigation triggers.&lt;/p&gt;

&lt;p&gt;Let's be honest: a &lt;strong&gt;read + write pair&lt;/strong&gt; is where agents become genuinely powerful. Expose &lt;code&gt;getExpenseReports()&lt;/code&gt; alongside &lt;code&gt;flagExpense(id, reason)&lt;/code&gt; and an agent can query, reason over the data, and act in a single turn. Add permission-awareness by injecting your &lt;code&gt;AuthService&lt;/code&gt; inside the &lt;code&gt;execute&lt;/code&gt; handler and the agent automatically gets a properly scoped view, seeing only what the current user is authorised to see. You could also expose derived data tools like &lt;code&gt;getSummaryByCategory()&lt;/code&gt; — pre-computed aggregates cost fewer tokens and produce faster responses than handing the agent a raw list to process itself.&lt;/p&gt;

&lt;p&gt;The general principle worth internalising: anywhere you'd write a hardcoded string describing your domain model, ask whether you can derive it from TypeScript instead. Your types are the single source of truth and the agent's schema should follow from them, not diverge from them.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 One Thing Worth Getting Right Early: Trust
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;execute&lt;/code&gt; handlers run inside Angular's injection context with full access to your DI graph, a WebMCP tool is as powerful as the service method it wraps — which means a write tool is a real mutation, not a preview. Before shipping any tool that modifies state, there are three things worth understanding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt injection in your descriptions.&lt;/strong&gt; The &lt;code&gt;description&lt;/code&gt; and parameter description fields are read by the LLM as trusted context. A malicious site can embed instructions in its own tool descriptions that manipulate the agent's behaviour on &lt;em&gt;other&lt;/em&gt; sites — and if your own tool returns user-generated content, that content is another vector. The &lt;a href="https://github.com/webmachinelearning/webmcp/blob/main/docs/security-privacy-considerations.md" rel="noopener noreferrer"&gt;W3C security considerations doc&lt;/a&gt; calls this out explicitly under "Metadata / Description Attacks" and "Output Injection Attacks": never echo unsanitised user content directly from an &lt;code&gt;execute&lt;/code&gt; handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular does not validate inputs for you.&lt;/strong&gt; The agent is supposed to match the &lt;code&gt;inputSchema&lt;/code&gt; you define, but Angular makes no guarantee it does — the &lt;code&gt;execute&lt;/code&gt; callback receives whatever the agent sends. The &lt;a href="https://angular.dev/ai/webmcp#validate-tool-inputs" rel="noopener noreferrer"&gt;Angular docs are explicit on this&lt;/a&gt;: &lt;em&gt;"Consider explicitly validating arguments to the execute function before using them."&lt;/em&gt; Treat every input as untrusted, the same way you would a form submission or a query parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The session is the attack surface.&lt;/strong&gt; The &lt;a href="https://github.com/webmachinelearning/webmcp/blob/main/docs/security-privacy-considerations.md" rel="noopener noreferrer"&gt;W3C spec's threat model&lt;/a&gt; notes that agents inherit the user's authentication context — session cookies, logged-in state, everything. The real risk isn't an anonymous external caller; same-origin constraints handle that. The risk is a legitimate, trusted agent that has been manipulated through prompt injection and then calls your write tools with the user's full permissions. Design accordingly: scope write tools tightly, and never expose an action you wouldn't want triggered automatically on behalf of a logged-in user.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ The Honest Caveat
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/webmachinelearning/webmcp/" rel="noopener noreferrer"&gt;W3C spec&lt;/a&gt; is still early and has been changing frequently. Mid-2027 is the realistic mass-adoption target, when both browser default support and enough publisher adoption exist to make it meaningful at scale. For now it's great for internal tools and early experimentation, but it's not ready to ship to anonymous production users.&lt;/p&gt;

&lt;p&gt;That said, the direction is set. Your app has always had one interface — the DOM, for humans. WebMCP adds a second one, for agents. How you design that second surface is a new skill worth starting to develop now, before the spec lands and everyone is catching up at once.&lt;/p&gt;




&lt;p&gt;If you found this helpful, follow me here and on &lt;a href="https://www.linkedin.com/in/giorgiogalassi/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more deep dives into Angular, signals, AI, and modern frontend development.&lt;/p&gt;

&lt;p&gt;See you in the next one! 🤙🏻&lt;br&gt;
— G.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>linkedSignal() Writes Back: A Trace Through the Reactive Graph 🔍🚀</title>
      <dc:creator>Giorgio Galassi</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:43:39 +0000</pubDate>
      <link>https://dev.to/gdg/linkedsignal-writes-back-a-trace-through-the-reactive-graph-37o7</link>
      <guid>https://dev.to/gdg/linkedsignal-writes-back-a-trace-through-the-reactive-graph-37o7</guid>
      <description>&lt;p&gt;Let's be honest: &lt;a href="https://angular.dev/guide/signals/linked-signal" rel="noopener noreferrer"&gt;&lt;code&gt;linkedSignal()&lt;/code&gt;&lt;/a&gt; always had a bit of a split personality. It read beautifully from another signal, deriving a value that stayed in sync automatically. But the moment you needed to write back to that source, you were on your own, writing a separate method, converting the value by hand, and hoping you didn't forget to call it from every place that touched the field.&lt;/p&gt;

&lt;p&gt;Angular 22.1 closes that gap. &lt;code&gt;linkedSignal()&lt;/code&gt; can now take a custom &lt;code&gt;set()&lt;/code&gt; option, and the linkedSignal becomes a genuine two-way field: read from a larger piece of state, write back to it, no disconnected duplicate copy of the data anywhere.&lt;/p&gt;

&lt;p&gt;My first reaction when I saw this: if the linkedSignal reads a source signal, and writing to the linkedSignal updates that same source, doesn't that loop forever? It didn't feel obvious to me, so I went and traced through the actual reactive graph source to settle it, and that turned into a nice little tour of how Angular's signals actually work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;Availability:&lt;/strong&gt; the custom &lt;code&gt;set()&lt;/code&gt; option shipped in Angular 22.1.0 (stable) and is documented in the official &lt;a href="https://angular.dev/guide/signals/linked-signal#customizing-the-set-operation" rel="noopener noreferrer"&gt;&lt;code&gt;linkedSignal&lt;/code&gt; guide&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🧩 Before: Reading Was Clean, Writing Wasn't
&lt;/h2&gt;

&lt;p&gt;Here's the classic shape. A &lt;code&gt;profile&lt;/code&gt; signal holds the real state, and a &lt;code&gt;linkedSignal&lt;/code&gt; exposes one field of it:&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;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;profile&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;UserProfile&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Brian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brian@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading &lt;code&gt;maxMarketingEmailsPerWeek()&lt;/code&gt; is effortless. Writing back to &lt;code&gt;profile&lt;/code&gt; is not: you need a second method that does the &lt;code&gt;profile.update()&lt;/code&gt; dance, and every button, every input handler, has to remember to call that method instead of the linkedSignal directly. The read side and the write side live in two different places for what is conceptually one piece of state.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 After: A Custom set()
&lt;/h2&gt;

&lt;p&gt;The fix is a &lt;code&gt;set&lt;/code&gt; function passed alongside the computation:&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;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;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;profile&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;profile&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxMarketingEmailsPerWeek&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="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;Now &lt;code&gt;maxMarketingEmailsPerWeek.set(5)&lt;/code&gt; and &lt;code&gt;.update(v =&amp;gt; v + 1)&lt;/code&gt; both flow through that setter and land on &lt;code&gt;profile&lt;/code&gt;. The helper method is gone. The simplest version of this idea, straight from the &lt;a href="https://angular.dev/guide/signals/linked-signal#customizing-the-set-operation" rel="noopener noreferrer"&gt;Angular docs&lt;/a&gt;, is a temperature converter:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tempC&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="mi"&gt;0&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;tempF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="nf"&gt;tempC&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valF&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;tempC&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="nx"&gt;valF&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;tempF&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="mi"&gt;212&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tempC&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 100&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tempF&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 212&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧠 Wait, Doesn't This Loop?
&lt;/h2&gt;

&lt;p&gt;Here's where I got stuck. &lt;code&gt;tempF&lt;/code&gt; reads &lt;code&gt;tempC()&lt;/code&gt; inside its computation, so &lt;code&gt;tempF&lt;/code&gt; is a genuine dependent of &lt;code&gt;tempC&lt;/code&gt; in the reactive graph. When &lt;code&gt;tempC.set(100)&lt;/code&gt; runs inside the custom setter, that dependency absolutely gets marked dirty and &lt;code&gt;tempF&lt;/code&gt; absolutely does recompute. If it didn't, &lt;code&gt;tempF()&lt;/code&gt; would show a stale value after the conversion, and the whole feature would be pointless.&lt;/p&gt;

&lt;p&gt;So the question I actually needed answered was: does that recompute call the custom setter again? It doesn't, and the reason is architectural, not incidental.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 Under the Hood: The Write Path
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;linkedSignal&lt;/code&gt;'s public write API, defined in &lt;a href="https://github.com/angular/angular/blob/main/packages/core/src/render3/reactivity/linked_signal.ts" rel="noopener noreferrer"&gt;&lt;code&gt;linked_signal.ts&lt;/code&gt;&lt;/a&gt;, looks like this once a custom setter is supplied:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;linkedSignalSetFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;upgradedGetter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;customSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawSet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;upgradedGetter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateFn&lt;/span&gt;&lt;span class="p"&gt;:&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;D&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;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;customSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;updateFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;untracked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;rawSet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;tempF.set(212)&lt;/code&gt; invokes exactly one thing: your &lt;code&gt;customSet&lt;/code&gt; function. It never touches &lt;code&gt;tempF&lt;/code&gt;'s own internal value directly, unless your function explicitly calls the &lt;code&gt;rawSet&lt;/code&gt; escape hatch it's handed. In the temperature example, it doesn't, it only calls &lt;code&gt;tempC.set(100)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The temperature example never reaches for that second parameter, but there are two situations where you would. One is performance: when &lt;code&gt;computation&lt;/code&gt; is expensive and you already know the exact result, rerunning it is wasted work. The other is less obvious: not every linkedSignal has an invertible relationship with its source the way &lt;code&gt;tempF&lt;/code&gt;/&lt;code&gt;tempC&lt;/code&gt; do. Plenty of real &lt;code&gt;computation&lt;/code&gt; functions, summarizing, rounding, picking one field out of several possible source states, can't be inverted into a single correct value to write back. &lt;code&gt;rawSet&lt;/code&gt; covers both: it's a direct line to the exact primitive that backs the default &lt;code&gt;.set()&lt;/code&gt; when there's no custom setter at all, letting you plant the value directly instead of inverting through the source:&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;upgradedGetter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;linkedSignalSetFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;upgradedGetter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateFn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;linkedSignalUpdateFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateFn&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;&lt;code&gt;rawSet&lt;/code&gt; is &lt;code&gt;(newValue) =&amp;gt; linkedSignalSetFn(node, newValue)&lt;/code&gt;, the same call, just handed to you as an argument instead of wired up automatically. Here's what that function actually does, from the &lt;a href="https://github.com/angular/angular/blob/main/packages/core/primitives/signals/src/linked_signal.ts" rel="noopener noreferrer"&gt;signal primitives&lt;/a&gt;:&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;function&lt;/span&gt; &lt;span class="nf"&gt;linkedSignalSetFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;D&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;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LinkedSignalNode&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;D&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;newValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;producerUpdateValueVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// resolve any pending staleness first&lt;/span&gt;
  &lt;span class="nf"&gt;signalSetFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// plain field write: node.value = newValue, version++&lt;/span&gt;
  &lt;span class="nf"&gt;producerMarkClean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// mark the node up to date&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three steps: settle any recompute that's already overdue, write &lt;code&gt;node.value&lt;/code&gt; directly with the same primitive a plain &lt;code&gt;signal()&lt;/code&gt; uses (never touching &lt;code&gt;node.computation&lt;/code&gt; or &lt;code&gt;node.sourceValue&lt;/code&gt;), then mark the node clean.&lt;/p&gt;

&lt;p&gt;That first step is worth pausing on, because "settle any overdue recompute" is literal. If the linkedSignal is currently dirty, its source changed since anyone last read it, &lt;code&gt;producerUpdateValueVersion&lt;/code&gt; runs &lt;code&gt;producerRecomputeValue&lt;/code&gt; before your value ever gets written, which means &lt;code&gt;computation&lt;/code&gt; fires once, and if it has any observable side effects, they happen. Your value always wins in the end, the freshly recomputed result gets overwritten immediately after, but "computation never runs" isn't quite true in that specific case. The mark-clean step is what matters for everything &lt;em&gt;after&lt;/em&gt; this call: it tells the next read's staleness check that the node is current, so from then on &lt;code&gt;producerRecomputeValue&lt;/code&gt; is skipped, regardless of what the source is doing in the meantime.&lt;/p&gt;

&lt;p&gt;Line them up and there are three distinct write paths, not two:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;.set()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;&lt;code&gt;computation&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Default&lt;/strong&gt;, no custom setter&lt;/td&gt;
&lt;td&gt;Calls &lt;code&gt;linkedSignalSetFn(node, v)&lt;/code&gt; directly.&lt;/td&gt;
&lt;td&gt;Does not run. Exception: if the node was already &lt;code&gt;stale&lt;/code&gt; due to an earlier unread source change, it runs once first to settle the pending recomputation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Custom setter&lt;/strong&gt;, setter writes to the source&lt;/td&gt;
&lt;td&gt;Only the source's version is bumped. This node is marked dirty but is otherwise untouched.&lt;/td&gt;
&lt;td&gt;Re-runs &lt;strong&gt;lazily&lt;/strong&gt;, the next time the &lt;code&gt;linkedSignal&lt;/code&gt; is read.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Custom setter&lt;/strong&gt;, setter calls &lt;code&gt;rawSet(v)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Identical to the default case: calls &lt;code&gt;linkedSignalSetFn(node, v)&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Does not run, except when there is already a pending stale recomputation that must settle first.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the performance case: a custom setter with independent knowledge of the right answer, where redoing the computation would be wasted work:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&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;Item&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;hugeInitialList&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;sortedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="nf"&gt;expensiveSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="c1"&gt;// recomputes whenever items() changes&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&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;rawSet&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;// We just got an already-sorted page back from the server —&lt;/span&gt;
      &lt;span class="c1"&gt;// no need to re-run expensiveSort() for a value we already know.&lt;/span&gt;
      &lt;span class="nf"&gt;rawSet&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="p"&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;sortedItems&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="nx"&gt;pageFromServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// writes directly, skipping a fresh expensiveSort() run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to the temperature example: there, the setter calls &lt;code&gt;tempC.set(...)&lt;/code&gt; because it genuinely needs &lt;code&gt;tempF&lt;/code&gt; to re-derive from the new &lt;code&gt;tempC&lt;/code&gt;. Here, the setter already has the final value, so it bypasses the derivation instead of inverting it.&lt;/p&gt;

&lt;p&gt;And here's the other case, where inverting through the source isn't just wasteful, it doesn't make sense at all:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawInput&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="s1"&gt;42.7&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;roundedValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rawInput&lt;/span&gt;&lt;span class="p"&gt;())),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&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;rawSet&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;// Multiple rawInput strings round to the same integer, there's&lt;/span&gt;
      &lt;span class="c1"&gt;// no single correct string to write back, so just store the value.&lt;/span&gt;
      &lt;span class="nf"&gt;rawSet&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="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;Rounding is lossy: many different &lt;code&gt;rawInput&lt;/code&gt; values produce the same &lt;code&gt;roundedValue&lt;/code&gt;, so there's no correct string for the setter to write back to &lt;code&gt;rawInput&lt;/code&gt;. &lt;code&gt;rawSet&lt;/code&gt; sidesteps the question entirely, it's the same direct write the default &lt;code&gt;.set()&lt;/code&gt; would give you if there were no custom setter at all.&lt;/p&gt;

&lt;p&gt;Both cases share the same shape: &lt;code&gt;rawSet&lt;/code&gt; is for when inverting through the source is either wasteful or doesn't make sense, and you'd rather just set the value. It also keeps a single public method for callers: &lt;code&gt;.set(x)&lt;/code&gt; works the same way from the outside whether this particular value is a normal edit that should flow through the source, or a precomputed result the setter recognizes and stores directly. The linkedSignal, not the caller, decides which applies.&lt;/p&gt;

&lt;p&gt;That's the entire write path, everything &lt;code&gt;.set()&lt;/code&gt; and &lt;code&gt;.update()&lt;/code&gt; can reach, custom setter or not. &lt;code&gt;rawSet&lt;/code&gt; can reach into the recompute machinery to settle a pending stale value, but never the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Under the Hood: The Recompute Path
&lt;/h2&gt;

&lt;p&gt;The recompute lives somewhere else entirely, in the &lt;a href="https://github.com/angular/angular/blob/main/packages/core/primitives/signals/src/linked_signal.ts" rel="noopener noreferrer"&gt;signal primitives&lt;/a&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkedSignalGetter&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;producerUpdateValueVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// check staleness, recompute if needed&lt;/span&gt;
  &lt;span class="nf"&gt;producerAccessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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;node&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every signal getter, plain &lt;a href="https://angular.dev/guide/signals" rel="noopener noreferrer"&gt;&lt;code&gt;signal()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://angular.dev/guide/signals#computed-signals" rel="noopener noreferrer"&gt;&lt;code&gt;computed()&lt;/code&gt;&lt;/a&gt;, or &lt;code&gt;linkedSignal()&lt;/code&gt;, is shaped this way. &lt;a href="https://github.com/angular/angular/blob/main/packages/core/primitives/signals/src/graph.ts" rel="noopener noreferrer"&gt;&lt;code&gt;producerUpdateValueVersion&lt;/code&gt;&lt;/a&gt; checks whether any dependency's version changed since the last read, and if so, calls &lt;a href="https://github.com/angular/angular/blob/main/packages/core/primitives/signals/src/linked_signal.ts" rel="noopener noreferrer"&gt;&lt;code&gt;producerRecomputeValue&lt;/code&gt;&lt;/a&gt;, which reruns the computation and assigns straight to &lt;code&gt;node.value&lt;/code&gt; as a plain field write:&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;newValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newSourceValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newSourceValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="o"&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's the whole answer. &lt;code&gt;tempC.set(100)&lt;/code&gt; only flips a dirty flag and bumps a version counter, no computation runs at that moment. The actual recomputation is deferred until the next time something reads &lt;code&gt;tempF()&lt;/code&gt;, and when it happens, it happens through &lt;code&gt;producerRecomputeValue&lt;/code&gt;, a function that has never heard of &lt;code&gt;customSet&lt;/code&gt;. Not directly, not indirectly. There's no code path in the primitives package that leads back to your setter. That direction is the one that would actually matter: if recomputation could call back into &lt;code&gt;customSet&lt;/code&gt;, that's the loop you'd have to worry about, and it provably can't. The reverse isn't quite as absolute, &lt;code&gt;rawSet&lt;/code&gt; can call into &lt;code&gt;producerRecomputeValue&lt;/code&gt; to settle a pending stale value, as covered above, but even then it only ever reaches this same recompute logic, never back into your setter. That asymmetry, not a strict mutual isolation, is the whole reason a linkedSignal can safely both read from and write to the same piece of state.&lt;/p&gt;

&lt;p&gt;One detail worth keeping in mind: this recompute isn't scheduled for later, it's synchronous and pull-based. If &lt;code&gt;tempF()&lt;/code&gt; is read inside another &lt;code&gt;computed()&lt;/code&gt;, the staleness check and recompute for &lt;code&gt;tempF&lt;/code&gt; happen inline, in the same call stack, before that outer computed can finish. Angular's signals are lazy all the way down: nothing recalculates until someone actually asks for a value, but once something does ask, the whole stale chain resolves immediately, in dependency order.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;So, back to the question that opened this piece: no, it doesn't loop, and now you know exactly why: &lt;code&gt;producerRecomputeValue&lt;/code&gt; can't reach back into &lt;code&gt;customSet&lt;/code&gt;, only &lt;code&gt;rawSet&lt;/code&gt; reaches the other way. That one-way gap is what makes it safe.&lt;/p&gt;

&lt;p&gt;The custom &lt;code&gt;set()&lt;/code&gt; option is a small addition on paper, one new field in an options object, but it changes what &lt;code&gt;linkedSignal()&lt;/code&gt; is for. Before, it was a read-only derivation with a writable escape hatch bolted on by convention (the default &lt;code&gt;set&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt;, with no custom setter, just overwrites the linkedSignal's own value directly). Now it can be the field-level API for a slice of larger state, without ever creating a second source of truth. And it does that safely because Angular's signal graph keeps "someone wrote to me" and "one of my dependencies changed" as two structurally separate events, not because of anything defensive in the code you write.&lt;/p&gt;

&lt;p&gt;If you haven't read &lt;a href="https://medium.com/@giorgio.galassi/angular-v19-computed-vs-linkedsignal-signals-3130b70861b8" rel="noopener noreferrer"&gt;my earlier piece comparing &lt;code&gt;computed()&lt;/code&gt; and &lt;code&gt;linkedSignal()&lt;/code&gt;&lt;/a&gt;, that's the conceptual "when to reach for which" companion to this one, this article is the deep end: the internals that explain why the two-way version is safe.&lt;/p&gt;

&lt;p&gt;If you're touching &lt;code&gt;linkedSignal()&lt;/code&gt; in a codebase that also uses &lt;a href="https://angular.dev/guide/forms/signals/overview" rel="noopener noreferrer"&gt;Signal Forms&lt;/a&gt; or &lt;a href="https://angular.dev/guide/signals/resource" rel="noopener noreferrer"&gt;&lt;code&gt;resource()&lt;/code&gt;&lt;/a&gt;, the same lazy, pull-based recompute model is running underneath all of them. Worth understanding once, pays off everywhere.&lt;/p&gt;




&lt;p&gt;If you found this helpful, follow me here and on &lt;a href="https://www.linkedin.com/in/giorgiogalassi/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more deep dives into Angular, web performance, and modern frontend development.&lt;/p&gt;

&lt;p&gt;See you in the next one! 🤙🏻&lt;br&gt;
— G.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>angularsignals</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
