<?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>AlphaEvolve: Evaluator Harnesses, JAX, and the Boundaries of Autonomous Algorithm Discovery</title>
      <dc:creator>Cedric Sebastian</dc:creator>
      <pubDate>Tue, 01 Sep 2026 02:23:36 +0000</pubDate>
      <link>https://dev.to/gdg/alphaevolve-evaluator-harnesses-jax-and-the-boundaries-of-autonomous-algorithm-discovery-11b8</link>
      <guid>https://dev.to/gdg/alphaevolve-evaluator-harnesses-jax-and-the-boundaries-of-autonomous-algorithm-discovery-11b8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AlphaEvolve shows that LLM-guided evolutionary search can produce improved algorithms when candidate quality can be evaluated automatically. Its limits reveal an equally important lesson: an autonomous search system can only optimize for what its evaluator can recognize.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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%2Fsqwlakosy5xalpyl4g80.jpeg" 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%2Fsqwlakosy5xalpyl4g80.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 2025, Google DeepMind reported that AlphaEvolve found a procedure for multiplying two 4 × 4 complex-valued matrices with 48 scalar multiplications, one fewer than two recursive levels of Strassen’s method. The one-multiplication reduction made the headline. The search loop that found it explains why the result matters beyond matrix multiplication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Now, we will explore AlphaEvolve to better understand it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  What is AlphaEvolve?
&lt;/h4&gt;

&lt;p&gt;AlphaEvolve is an autonomous evolutionary AI coding agent developed by Google DeepMind that combines language-model proposals with evolutionary search. It uses two of Google’s frontier Gemini models in complementary roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemini Flash generates high-throughput proposals, expanding the breadth of ideas explored.&lt;/li&gt;
&lt;li&gt;Gemini Pro adds greater depth through occasional suggestions from the more capable model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The paper does not disclose the exact sampling mixture.&lt;/p&gt;

&lt;p&gt;AlphaEvolve illustrates how AI has evolved from producing generic, repetitive output to building advanced systems that pair large language models (LLMs) with evolutionary-search harnesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The word “autonomous” needs a clear boundary.&lt;/strong&gt; Humans define the problem, provide the initial program and evaluation code, supply optional context, and mark the code regions that AlphaEvolve may modify. The system then conducts a sustained search within those constraints. This goes well beyond asking a model for a single answer, but it does not make AlphaEvolve an independent scientist who chooses questions or standards of evidence.&lt;/p&gt;

&lt;p&gt;AlphaEvolve extends &lt;a href="https://doi.org/10.1038/s41586-023-06924-6" rel="noopener noreferrer"&gt;FunSearch&lt;/a&gt;, DeepMind’s earlier system for evolving a selected function within a human-written program. AlphaEvolve can modify much larger regions of code, optimize multiple scores simultaneously, and run evaluations for hours in parallel. The expansion is substantial, but the underlying pattern is inherited: language-model variation followed by executable selection.&lt;/p&gt;

&lt;h4&gt;
  
  
  How does it work?
&lt;/h4&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%2Fcdn-images-1.medium.com%2Fmax%2F1024%2F0%2A_d4a19AJ19mAZ43g" 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%2Fcdn-images-1.medium.com%2Fmax%2F1024%2F0%2A_d4a19AJ19mAZ43g" width="1024" height="640"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;AlphaEvolve advanced outline. The diagram illustrates how its process works as explained below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The loop could be demonstrated like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User input&lt;/strong&gt;
A user supplies an existing algorithm or program, evaluation code, and optional context about the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt sampler&lt;/strong&gt;
A prompt sampler then selects previous candidates from a program database and gives them to the Gemini models for revision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini models&lt;/strong&gt;
Create new candidates by proposing code changes, usually returning patches instead of rewriting the whole program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluator pool&lt;/strong&gt;
Multiple candidates can be evaluated in parallel. Inexpensive checks can filter out invalid or poor candidates before more costly evaluations are run. The evaluation code and the environment in which it runs together form the evaluator harness. This harness determines how each candidate is executed, tested, timed, and scored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Program database&lt;/strong&gt;
Candidates that receive strong evaluation scores are returned to the database, where they can be chosen again in further generations. The model provides variation by changing the code, while the evaluator provides the selection pressure that determines which changes remain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AlphaEvolve generates and modifies programs, runs them through an automated evaluator, retains promising candidates, and uses their performance to guide the next generation. This process lets the system evaluate far more candidate programs than we could explore manually on a scale humans can’t examine without sacrificing accuracy, although the reliability of that search still depends on what the evaluator measures.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AlphaEvolve revolutionizes how we work by letting us formulate problems, design evaluators to test them, and constrain the search space. The evolutionary system decides which candidates survive, and human researchers decide whether the resulting score reflects meaningful progress. The evaluator is only one part of the system &lt;strong&gt;.&lt;/strong&gt; In other words, the evaluator matters, but it’s only one part of a much larger human-guided system. AlphaEvolve’s limited ablation studies suggest that its performance depends on several factors, including model capability, evolutionary selection, prompt adaptation, available context, and how much of the code the system is allowed to modify. While the overall loop may appear straightforward, it is supported by several interacting components that surprisingly make the system more complex in practice (Novikov et al., 2025). For the results to be reliable, these mechanisms also need to be paired with a well-designed evaluator.&lt;/p&gt;

&lt;h4&gt;
  
  
  Results with evidence level distinction
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AlphaEvolve’s public record contains three kinds of evidence: artifacts outsiders can verify, production measurements reported by Google, and later work that extends an artifact without reproducing the original search.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keeping these categories separate prevents a verified mathematical construction from giving proprietary deployment claims more certainty than they warrant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demonstrated and checkable.&lt;/strong&gt; DeepMind released an exact rank-48 decomposition and &lt;a href="https://github.com/google-deepmind/alphaevolve_results" rel="noopener noreferrer"&gt;verification code&lt;/a&gt; for multiplying 4 × 4 matrices over complex coefficients. Here, rank counts the bilinear scalar multiplications in the construction. It does not measure wall-clock speed. Additions, memory traffic, numerical stability, coefficient arithmetic, and target hardware can outweigh the benefit of saving one multiplication.&lt;/p&gt;

&lt;p&gt;DeepMind initially described the result as the first improvement in 56 years over the complex rank-49 baseline associated with Strassen’s 1969 method. The publication record complicates that priority claim. In 2024, Kaporin &lt;a href="https://doi.org/10.1134/S0965542524701021" rel="noopener noreferrer"&gt;reported a numerical complex rank-48 scheme&lt;/a&gt;. AlphaEvolve’s contribution is better described as an exact, publicly checkable construction found by the system. Dumas et al. (2025) then &lt;a href="https://arxiv.org/abs/2506.13242" rel="noopener noreferrer"&gt;transformed that artifact into a 48-multiplication algorithm with rational coefficients&lt;/a&gt;. Their work corroborates and extends the construction, but it does not reproduce DeepMind’s search system, which remains closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer-reported.&lt;/strong&gt; Google reports that an AlphaEvolve scheduling heuristic, first validated in simulation and then deployed, recovers an average of 0.7% of its worldwide compute resources. The white paper also reports an average 23% speedup across evaluated Pallas matrix-multiplication kernel shapes and about a 1% reduction in Gemini training time. For the kernel study, realistic input shapes were divided between optimization and evaluation sets and measured on TPUs. This provides a useful generalization check, but the workloads, raw timing records, and production environment remain proprietary. These figures are therefore first-party measurements rather than independently audited results.&lt;/p&gt;

&lt;p&gt;The same qualification applies to DeepMind’s report that AlphaEvolve matched the best-known construction on roughly 75% of more than 50 mathematical problems and improved on it in roughly 20%. The public report does not fully document the problem-selection protocol or provide a precise denominator. In a &lt;a href="https://deepmind.google/blog/alphaevolve-impact/" rel="noopener noreferrer"&gt;May 2026 update&lt;/a&gt;, DeepMind reported that AlphaEvolve had become a regular tool for designing next-generation TPUs and had produced a Spanner heuristic that cut write amplification by 20%. These claims came after the 2025 white paper and, like the earlier figures, have not been independently audited.&lt;/p&gt;

&lt;h4&gt;
  
  
  System Constraints &amp;amp; Bottlenecks
&lt;/h4&gt;

&lt;p&gt;AlphaEvolve’s main boundary is automated evaluability. Tasks that require manual experiments, subjective judgment, or evidence unavailable to the harness do not fit the current loop. Even machine-gradeable tasks can fail in quieter ways. A proxy may reward the wrong behavior, public tests may invite overfitting, omitted edge cases may hide invalid programs, and noisy timing may favor a candidate that will not survive a different device or compiler version. Multi-objective scores add another judgment call because their weights determine what the search may sacrifice.&lt;/p&gt;

&lt;p&gt;The concern is empirical, not merely philosophical. In a small &lt;a href="https://arxiv.org/abs/2605.15221" rel="noopener noreferrer"&gt;2026 circle-packing preprint&lt;/a&gt; (Ishibashi et al., 2026) found generated programs that exploited weaknesses in the evaluator and reported more such hacks from the more capable model they tested. The study examined another harness on one benchmark, so it is evidence of a general failure mode, not an audit of AlphaEvolve’s published results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stronger harness would combine exact validity checks where possible with hidden holdouts, adversarial cases, repeated measurements, cross-device tests, versioned environments, independent reimplementation, and human inspection of finalists.&lt;/strong&gt; These controls raise costs. That cost limits how many candidates can be evaluated and which problems are economically searchable. Model quality, context, representation, compute budget, and editable scope remain bottlenecks even when the evaluator is sound.&lt;/p&gt;

&lt;h4&gt;
  
  
  Human Adaptation, Decision, and Judgement
&lt;/h4&gt;

&lt;p&gt;Many people are afraid of AI because its capabilities have advanced far faster than expected, but fear will never help us. We should adapt, learn, and use AI as a collaborative brainstorming partner that helps us think better and strengthen our arguments. One-sided perspectives can create new mistakes even as we try to solve existing ones.&lt;/p&gt;

&lt;p&gt;AlphaEvolve makes this distinction especially clear. Human judgment enters both before and after the search. Researchers choose the objective, decide what code may change, construct the evaluation environment, and set acceptable constraints and tradeoffs. Once the search is complete, people still have to determine whether the winning program is valid, robust, deployable, and scientifically meaningful, while considering the consequences of putting it into practice. AlphaEvolve itself relies on automated evaluators to score proposed programs, making human choices about objectives and evaluation central to what the system ultimately discovers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A benchmark score cannot make those decisions. The evidence therefore supports treating AlphaEvolve as a research instrument that operates under human scientific judgment, not as a replacement for it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its value lies in expanding the range and persistence of algorithmic search, while responsibility for defining the problem, interpreting the results, and deciding what counts as a meaningful discovery remains with people.&lt;/p&gt;

&lt;h3&gt;
  
  
  What JAX changes
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JAX can reduce the cost of repeated numerical evaluation. It cannot determine whether the evaluator represents the right scientific objective.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
That distinction separates AlphaEvolve’s documented JAX use from broader hypotheses about what the pairing might eventually enable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;JAX already appears in the published work. AlphaEvolve used JAX and Optax while evolving tensor-decomposition search code, and it optimized a tiling heuristic for a Pallas kernel used in Gemini training. &lt;a href="https://docs.jax.dev/en/latest/401/pallas.html" rel="noopener noreferrer"&gt;Pallas&lt;/a&gt; is an experimental JAX kernel language that gives programmers fine-grained control over code for GPUs and TPUs, including data movement through fast on-chip memory.&lt;/p&gt;

&lt;p&gt;Three JAX transformations are especially relevant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.jax.dev/en/latest/101/transformations.html" rel="noopener noreferrer"&gt;jax.jit&lt;/a&gt; traces and compiles compatible numerical functions for a target device. Repeated calls can reuse the compiled executable when input types and relevant static arguments remain compatible.&lt;/li&gt;
&lt;li&gt;jax.vmap maps one function across a batch axis. It can batch evaluations over inputs or parameter settings, but it does not merge structurally different candidate programs into one computation.&lt;/li&gt;
&lt;li&gt;jax.grad differentiates scalar-output functions. It could support continuous optimization inside a candidate when the inner objective is differentiable, but it does not differentiate AlphaEvolve's discrete outer search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://docs.jax.dev/en/latest/aot.html" rel="noopener noreferrer"&gt;During compilation&lt;/a&gt;, JAX traces a compatible Python function into an internal representation called a jaxpr, lowers it to StableHLO, and passes the lowered program to XLA to produce an executable for a CPU, GPU, or TPU. A compiler intermediate representation is simply a structured form between source code and machine instructions that allows compilers to analyze and transform it.&lt;/p&gt;

&lt;p&gt;This stack makes a broader research program both feasible and practical. AlphaEvolve could propose new kernels, solver components, optimizers, or training-loop components, while JAX could compile and vectorize repeated evaluations for efficient execution on accelerators. For promising candidates, automatic differentiation could help tune internal parameters by supplying gradients to an optimizer, provided the inner objective is differentiable and scalar-valued. &lt;strong&gt;This remains a hypothesis about how the search space could be expanded, not a demonstrated path toward general intelligence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The economics depend on how similar the candidates are. Repeated evaluations of the same jitted candidate can amortize compilation costs, while changes to the code, shapes, dtypes, or static parameters may require new tracing and compilation. Structurally heterogeneous programs may therefore erase the throughput advantage. A credible experiment would also evaluate correctness, speed, memory use, numerical stability, and behavior across unseen data, shapes, devices, and compiler versions.&lt;/p&gt;

&lt;p&gt;AlphaEvolve combined with JAX could broaden the practical search space for numerical components when compilation costs can be amortized. That same acceleration makes evaluator quality even more consequential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A flawed harness can reward the wrong behavior at a greater scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This strengthens the case for automated algorithm search, but it is not evidence of autonomous science without human control.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;p&gt;AlphaEvolve Team. (2025, May 14). &lt;em&gt;AlphaEvolve: A Gemini-powered coding agent for designing advanced algorithms&lt;/em&gt;. Google DeepMind. &lt;a href="https://deepmind.google/blog/alphaevolve-a-gemini-powered-coding-agent-for-designing-advanced-algorithms/" rel="noopener noreferrer"&gt;https://deepmind.google/blog/alphaevolve-a-gemini-powered-coding-agent-for-designing-advanced-algorithms/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AlphaEvolve Team. (2026, May 7). &lt;em&gt;AlphaEvolve: How our Gemini-powered coding agent is scaling impact across fields&lt;/em&gt;. Google DeepMind. &lt;a href="https://deepmind.google/blog/alphaevolve-impact/" rel="noopener noreferrer"&gt;https://deepmind.google/blog/alphaevolve-impact/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dumas, J.-G., Pernet, C., &amp;amp; Sedoglavic, A. (2025). &lt;em&gt;A non-commutative algorithm for multiplying 4x4 matrices using 48 non-complex multiplications&lt;/em&gt; (arXiv:2506.13242, Version 7) [Preprint]. arXiv. &lt;a href="https://doi.org/10.48550/arXiv.2506.13242" rel="noopener noreferrer"&gt;https://doi.org/10.48550/arXiv.2506.13242&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google DeepMind. (2025). &lt;em&gt;Results of AlphaEvolve&lt;/em&gt; [Data set and computer software]. GitHub. &lt;a href="https://github.com/google-deepmind/alphaevolve_results" rel="noopener noreferrer"&gt;https://github.com/google-deepmind/alphaevolve_results&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ishibashi, Y., Yano, T., &amp;amp; Oyamada, M. (2026). &lt;em&gt;Effective harness engineering for algorithm discovery with coding agents&lt;/em&gt; (arXiv:2605.15221, Version 1) [Preprint]. arXiv. &lt;a href="https://doi.org/10.48550/arXiv.2605.15221" rel="noopener noreferrer"&gt;https://doi.org/10.48550/arXiv.2605.15221&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JAX Authors. (n.d.-a). &lt;em&gt;Ahead-of-time lowering and compilation&lt;/em&gt;. JAX documentation. Retrieved August 29, 2026, from &lt;a href="https://docs.jax.dev/en/latest/aot.html" rel="noopener noreferrer"&gt;https://docs.jax.dev/en/latest/aot.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JAX Authors. (n.d.-b). &lt;em&gt;Pallas: Custom kernels in JAX&lt;/em&gt;. JAX documentation. Retrieved August 29, 2026, from &lt;a href="https://docs.jax.dev/en/latest/401/pallas.html" rel="noopener noreferrer"&gt;https://docs.jax.dev/en/latest/401/pallas.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JAX Authors. (n.d.-c). &lt;em&gt;Transformations: grad and vmap&lt;/em&gt;. JAX documentation. Retrieved August 29, 2026, from &lt;a href="https://docs.jax.dev/en/latest/101/transformations.html" rel="noopener noreferrer"&gt;https://docs.jax.dev/en/latest/101/transformations.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kaporin, I. E. (2024). Finding complex-valued solutions of Brent equations using nonlinear least squares. &lt;em&gt;Computational Mathematics and Mathematical Physics, 64&lt;/em&gt;(9), 1881–1891. &lt;a href="https://doi.org/10.1134/S0965542524701021" rel="noopener noreferrer"&gt;https://doi.org/10.1134/S0965542524701021&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Novikov, A., Vũ, N., Eisenberger, M., Dupont, E., Huang, P.-S., Wagner, A. Z., Shirobokov, S., Kozlovskii, B., Ruiz, F. J. R., Mehrabian, A., Kumar, M. P., See, A., Chaudhuri, S., Holland, G., Davies, A., Nowozin, S., Kohli, P., &amp;amp; Balog, M. (2025). &lt;em&gt;AlphaEvolve: A coding agent for scientific and algorithmic discovery&lt;/em&gt; [White paper]. arXiv. &lt;a href="https://doi.org/10.48550/arXiv.2506.13131" rel="noopener noreferrer"&gt;https://doi.org/10.48550/arXiv.2506.13131&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Romera-Paredes, B., Barekatain, M., Novikov, A., Balog, M., Kumar, M. P., Dupont, E., Ruiz, F. J. R., Ellenberg, J. S., Wang, P., Fawzi, O., Kohli, P., &amp;amp; Fawzi, A. (2024). Mathematical discoveries from program search with large language models. &lt;em&gt;Nature, 625&lt;/em&gt;(7995), 468–475. &lt;a href="https://doi.org/10.1038/s41586-023-06924-6" rel="noopener noreferrer"&gt;https://doi.org/10.1038/s41586-023-06924-6&lt;/a&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>science</category>
      <category>technology</category>
      <category>innovation</category>
    </item>
    <item>
      <title>Google Antigravity Comes to VS Code: Agentic Coding Without Leaving Your Editor</title>
      <dc:creator>Caleb Duff</dc:creator>
      <pubDate>Sat, 29 Aug 2026 12:58:53 +0000</pubDate>
      <link>https://dev.to/gdg/google-antigravity-comes-to-vs-code-agentic-coding-without-leaving-your-editor-2nkg</link>
      <guid>https://dev.to/gdg/google-antigravity-comes-to-vs-code-agentic-coding-without-leaving-your-editor-2nkg</guid>
      <description>&lt;p&gt;If you've tried an "agentic" AI coding tool recently, there's a good chance it asked you to switch editors entirely. Google's own agent-first IDE, Antigravity, launched in November 2025 with exactly that trade-off: full agentic power, but only inside its own dedicated desktop application.&lt;/p&gt;

&lt;p&gt;That trade-off just went away. Google has shipped &lt;strong&gt;Antigravity extensions for VS Code, Visual Studio, JetBrains, and Zed&lt;/strong&gt;, bringing the same agent, the same review workflow, and the same account into the editor you've already spent years configuring exactly the way you like it.&lt;/p&gt;

&lt;p&gt;This post walks through what the VS Code extension actually is, how it fits into Antigravity's broader architecture, how to install and configure it, and most importantly; how its permission system keeps an agent that can read files, run terminal commands, and drive a real browser from doing anything you haven't explicitly allowed.&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 how the extension relates to the full Antigravity 2.0 desktop app and the &lt;code&gt;agy&lt;/code&gt; CLI&lt;/li&gt;
&lt;li&gt;Install and authenticate the extension inside VS Code&lt;/li&gt;
&lt;li&gt;Work through the agent side panel, implementation plans, and walkthroughs&lt;/li&gt;
&lt;li&gt;Configure the permission engine so the agent only does what you approve&lt;/li&gt;
&lt;li&gt;Lock down its browser subagent so it never touches your personal Chrome data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New to Antigravity generally? Start with Google's own primer: &lt;a href="https://antigravity.google/docs/overview/" rel="noopener noreferrer"&gt;Antigravity 2.0 Overview&lt;/a&gt;&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;VS Code version 1.90 or later, on macOS, Linux, or Windows&lt;/li&gt;
&lt;li&gt;A Google Account on any Antigravity plan (the free tier is enough), or an enterprise account enabled for Gemini Enterprise&lt;/li&gt;
&lt;li&gt;About five minutes for the first-time sign-in and backend install&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  1. Where the Extension Fits in Antigravity's Architecture
&lt;/h2&gt;

&lt;p&gt;It helps to know there are actually three doors into the same house:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Antigravity 2.0 ] ── the full desktop app, a dedicated fork of VS Code
[ Antigravity CLI (agy) ] ── the same agent, driven from your terminal
[ IDE Extensions ] ── the same agent, embedded in an editor you already run
        │
        ▼
[ Local `agy` backend service ] (installed automatically on first launch)
        │
        ▼
[ Antigravity Agent Runtime ]
   (planning, terminal execution, browser subagent — powered by Gemini)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension itself is deliberately thin: a side panel, an inline diff viewer, and a renderer for the agent's plans and artifacts, all sitting on top of your existing VS Code install. On first launch, it silently installs a local &lt;code&gt;agy&lt;/code&gt; backend service on your machine, the same engine that powers the standalone desktop app and the CLI. Because of that shared backend, sign-in is unified across every surface: authenticate once, and the same entitlement, permissions, and conversation history follow you whether you open Antigravity from VS Code, JetBrains, or a terminal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth knowing:&lt;/strong&gt; the full Antigravity 2.0 app is a genuine fork of VS Code, so if you ever outgrow the extension, migrating is mostly a matter of importing your settings. But if your editor setup is already exactly how you want it, the extension gets you the same agent without asking you to leave.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Install and Sign In
&lt;/h2&gt;

&lt;p&gt;Getting running takes two short steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step A: Install from the Marketplace
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the Extensions view (&lt;code&gt;Ctrl+Shift+X&lt;/code&gt; on Windows/Linux, &lt;code&gt;Cmd+Shift+X&lt;/code&gt; on macOS)&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;Google Antigravity&lt;/strong&gt;, published by &lt;strong&gt;Google&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&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%2F13qizcmh8xig6cpp0rpp.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%2F13qizcmh8xig6cpp0rpp.png" alt="VS_Code_Extension-Search-Google_Antigravity" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Install&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step B: Sign In and Let Setup Finish
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frc9r7airc6vrk0rwd7cr.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%2Frc9r7airc6vrk0rwd7cr.png" alt="Signing into Google for the Antigravity Extension on VSCode" width="799" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the Antigravity icon in the Activity Bar&lt;/li&gt;
&lt;li&gt;Sign in with your Google Account&lt;/li&gt;
&lt;li&gt;The extension automatically installs the local &lt;code&gt;agy&lt;/code&gt; backend service in the background, there's no separate terminal setup to do yourself&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; you don't need a paid plan to try this. The free tier is enough to install and start using the extension. Enterprise teams instead authenticate against a Gemini Enterprise–enabled account, which ties usage to org-level budgets and IAM policies rather than a personal quota,  useful if you want every developer's agent usage centrally governed from day one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Working With the Agent, Not Just Prompting It
&lt;/h2&gt;

&lt;p&gt;Once you're signed in, a panel on the right side of the editor becomes your main workspace. From there you can spin up new conversations, attach images, switch agent modes, and pick between models; all without a context switch away from your code. As a task runs, a toolbar above the input tracks open file changes, running terminal processes, and any artifacts the agent has produced, so you always know what it's currently touching.&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%2Fhbts1w2q610swm1m2tun.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%2Fhbts1w2q610swm1m2tun.png" alt="Antigravity extension editor interface" width="800" height="853"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation Plans: Review Before Anything Changes
&lt;/h3&gt;

&lt;p&gt;For any non-trivial request, the agent doesn't jump straight to editing files. The standard workflow will be to first produce an &lt;strong&gt;implementation plan&lt;/strong&gt;, which is basically  an artifact describing exactly what it intends to change and why and, unless your review policy is set to always proceed, it pauses for your sign-off (permission) before touching a single line.&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%2Fh4yetald9y8hz792gyov.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%2Fh4yetald9y8hz792gyov.png" alt="Implementation Plan for Review or Proceed on the Antigravity Extension on VSCode" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The useful part isn't just the pause; it's that you can comment directly on the plan itself. Want a smaller scope, a different library, or to correct a misunderstanding before any code gets written? Leave the comment, and either hit &lt;strong&gt;Proceed&lt;/strong&gt; to continue as-is or submit your feedback for the agent to revise the plan against. Think of it as reviewing a pull request description before the pull request exists.&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%2F94qcszbjbbxjvuezcv1x.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%2F94qcszbjbbxjvuezcv1x.png" alt="Review or Proceed on the Implementation Plan - Antigravity Extension on VSCode" width="800" height="438"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3q4ecnr7s5cpb7ke7si9.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%2F3q4ecnr7s5cpb7ke7si9.png" alt="Type your comment as a review so the agent can revise the Implementation Plan - Antigravity Extension on VSCode " width="799" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Walkthroughs and Browser Testing: Built for Web Developers Specifically
&lt;/h3&gt;

&lt;p&gt;After a task finishes, the agent can generate a &lt;strong&gt;walkthrough&lt;/strong&gt;, which is basically an artifact summarizing what actually changed and why useful when you're reviewing work after the fact rather than watching it happen live.&lt;/p&gt;

&lt;p&gt;More interesting for web development specifically: Antigravity can open, read, and actuate a real local Chrome browser through a dedicated &lt;strong&gt;browser subagent&lt;/strong&gt;. Practically, that means after building a feature, the agent can launch your dev server, click through the UI it just built, capture screenshots, and save the interaction as a video artifact, all before you ever tab over to &lt;code&gt;localhost&lt;/code&gt; yourself to check its work.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Configure What the Agent Is Allowed to Do
&lt;/h2&gt;

&lt;p&gt;This is the section that matters most if you plan to use this on real projects, not just toy demos/projects.&lt;/p&gt;

&lt;p&gt;Every sensitive action the agent takes, like reading a file, running a command, hitting a URL, calling an MCP tool is represented as a permission resource in the form &lt;code&gt;action(target)&lt;/code&gt;. Each one is evaluated against three lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deny&lt;/strong&gt; — blocked immediately, no exceptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask&lt;/strong&gt; — the agent pauses and requests your explicit approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow&lt;/strong&gt; — runs automatically, no prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Strict precedence always runs Deny &amp;gt; Ask &amp;gt; Allow.&lt;/strong&gt; If a command matches both an Allow rule and a Deny rule, the Deny rule wins. That single fact makes it easy to build a safety net: broadly allow the things you trust, then explicitly deny the handful of commands you never want run automatically, regardless of any other rule.&lt;/p&gt;

&lt;p&gt;Step-by-Step Decision Hierarchy&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Incoming Tool Action: action(target)
                    │
                    ▼
        ┌───────────────────────┐
        │  1. Check DENY List   │ ──(Matches?)──► ❌ Blocked Immediately
        └───────────┬───────────┘
                    │ No
                    ▼
        ┌───────────────────────┐
        │   2. Check ASK List   │ ──(Matches?)──► ❓ Pause &amp;amp; Prompt User
        └───────────┬───────────┘
                    │ No
                    ▼
        ┌───────────────────────┐
        │  3. Check ALLOW List  │ ──(Matches?)──► ✅ Run Automatically
        └───────────┬───────────┘
                    │ No
                    ▼
        ┌───────────────────────┐
        │  4. Default Fallback  │ ──────────────► ❓ Prompt User
        └───────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A reasonable starting configuration looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# ==========================================
# 1. DENY (Highest Precedence - Always Blocked)
# ==========================================
&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;rm&lt;/span&gt; -&lt;span class="n"&gt;rf&lt;/span&gt; .*)
&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;push&lt;/span&gt; --&lt;span class="n"&gt;force&lt;/span&gt;.*)
&lt;span class="n"&gt;write_file&lt;/span&gt;(\.&lt;span class="n"&gt;env&lt;/span&gt;.*)

&lt;span class="c"&gt;# ==========================================
# 2. ASK (Pause &amp;amp; Prompt for Approval)
# ==========================================
&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;push&lt;/span&gt;.*)
&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;publish&lt;/span&gt;)
&lt;span class="n"&gt;execute_url&lt;/span&gt;(*)

&lt;span class="c"&gt;# ==========================================
# 3. ALLOW (Runs Automatically)
# ==========================================
&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;)
&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;.*)
&lt;span class="n"&gt;command&lt;/span&gt;(&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; (&lt;span class="n"&gt;test&lt;/span&gt;|&lt;span class="n"&gt;lint&lt;/span&gt;|&lt;span class="n"&gt;build&lt;/span&gt;).*)
&lt;span class="n"&gt;read_file&lt;/span&gt;(&lt;span class="n"&gt;src&lt;/span&gt;/.*)
&lt;span class="n"&gt;read_url&lt;/span&gt;(&lt;span class="n"&gt;developer&lt;/span&gt;\.&lt;span class="n"&gt;mozilla&lt;/span&gt;\.&lt;span class="n"&gt;org&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, reading and writing files inside your active project workspace is auto-allowed, while everything else; like arbitrary shell commands, MCP tools, and any interactive browser action falls back to &lt;strong&gt;Ask&lt;/strong&gt; unless you configure otherwise. That default is deliberately conservative, it lets the agent move quickly on the code it's supposed to touch, while still checking in before it runs a command or browses a URL you haven't vetted.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Lock Down the Browser Subagent
&lt;/h2&gt;

&lt;p&gt;Because the agent can drive an actual browser, it gets its own layer of isolation on top of the general permission engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A separate Chrome profile.&lt;/strong&gt; The browser subagent runs inside a completely isolated profile, not the Chrome window you're logged into. It never sees your saved passwords, cookies, or open tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An allowlist and denylist for URLs&lt;/strong&gt;, giving you a second, independent layer of control over exactly which domains the browser subagent is permitted to load or interact with, beyond the general &lt;code&gt;read_url&lt;/code&gt; / &lt;code&gt;execute_url&lt;/code&gt; permission rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full off-switch.&lt;/strong&gt; If you'd rather the agent never touch a browser at all, the Browser Tools setting disables the capability entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal sandboxing (currently in preview on macOS/Linux, Windows coming soon).&lt;/strong&gt; When enabled, your permission grants double as the sandbox's actual filesystem and network allowlists, so even an approved command executes inside an isolated container rather than directly against your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt;&lt;br&gt;
At first, when I started prompting, the agent wasn't able to provide an implementation plan due to a &lt;strong&gt;&lt;em&gt;Pre-Tool Hook Issue detected&lt;/em&gt;&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbnfpg4ifrt4krk0wg9s7.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%2Fbnfpg4ifrt4krk0wg9s7.png" alt="Pre-Tool Execution Hook Issue Detected" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And so, instead of creating or modifying my files, it simply printed out the codes with an instruction to copy &amp;amp; paste the codes into my files as shown below: &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%2F9h8cluqxl4q7nmlb4d8v.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%2F9h8cluqxl4q7nmlb4d8v.png" alt="Antigravity extension on VSCode - Agent printed the codes in the taskbar for me to copy" width="800" height="436"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwnpn9x0ztnu4v73vkhfy.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%2Fwnpn9x0ztnu4v73vkhfy.png" alt="Antigravity extension on VSCode - Agent printed the codes in the taskbar for me to copy into my files" width="800" height="433"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcjf0rv7ykoy7oxmn83u.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%2Frcjf0rv7ykoy7oxmn83u.png" alt="Antigravity extension on VSCode" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used the method One (1) to apply a quick fix to the Pre-Tool Execution Hook Issue and voila! (Issue Resolved).&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%2F8c3y65efml3icsgu30xg.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%2F8c3y65efml3icsgu30xg.png" alt="Pre-Tool Execution Hook Issue Resolved" width="800" height="434"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxc3miv4eoh1imoi5xprx.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%2Fxc3miv4eoh1imoi5xprx.png" alt="Implementation Plan Issue Solved on Antigravity Extension on VSCode" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Try the smallest possible version: install the extension, ask it to fix one small bug or add one small feature, and actually read the implementation plan before clicking Proceed. Watch the toolbar as it works to see which files, commands, and artifacts it touches along the way.&lt;/p&gt;

&lt;p&gt;Are you planning to move to the full Antigravity 2.0 desktop app, or sticking with your current editor and just adding the extension? I'd love to hear how the plan-and-review workflow compares to inline-suggestion tools like Copilot in the comments.&lt;/p&gt;

</description>
      <category>antigravity</category>
      <category>vscode</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why AI Websites All Look the Same and How to Build Something Different</title>
      <dc:creator>Mfonobong Umondia</dc:creator>
      <pubDate>Sat, 29 Aug 2026 01:38:54 +0000</pubDate>
      <link>https://dev.to/gdg/why-ai-websites-all-look-the-same-and-how-to-build-something-different-1gan</link>
      <guid>https://dev.to/gdg/why-ai-websites-all-look-the-same-and-how-to-build-something-different-1gan</guid>
      <description>&lt;p&gt;If you've built a website with AI recently, there is a good chance it looks familiar. Maybe you have seen the same oversized hero headline, the same purple-to-blue gradient, the same rounded cards, the same floating dashboard mockup, and the same neatly arranged feature sections somewhere else. You may have even generated versions of the exact same website idea with different AI website builders and noticed that the results are surprisingly similar.&lt;/p&gt;

&lt;p&gt;So, why do AI websites look the same?&lt;/p&gt;

&lt;p&gt;The answer has less to do with AI being incapable of creativity and more to do with the patterns these tools have learned, the components they rely on, the defaults they are designed around, and the way we prompt them.&lt;/p&gt;

&lt;p&gt;Since we all started building with AI, almost every website seems to be following a familiar formula, and I am sure that by simply reading this article, one or two websites have already come to your mind.&lt;/p&gt;

&lt;p&gt;You will often see a huge centered headline sitting in the middle of the screen, or to the left side of the screen with a short paragraph underneath it and a call to action button. There is probably going to be a purple-to-blue gradient glowing softly behind everything. Then come three or four rounded cards explaining the product, a row of customer logos, more whitespace, a testimonial sitting inside a soft-shadowed box, and one more call-to-action before the footer.&lt;/p&gt;

&lt;p&gt;Somewhere along the way as you scroll, you start wondering whether you've seen this exact website before. Honestly, you probably have, because the strange part is that these websites usually look pretty good. They are responsive, the spacing feels intentional, the typography is clean, the animations are smooth, and every button actually works. The problem is that they do not feel particularly memorable. They could belong to almost any company building almost any product, and that is exactly where the problem begins.&lt;/p&gt;

&lt;p&gt;This is one of the strangest side effects of AI-assisted development becoming mainstream. Tools such as &lt;a href="https://lovable.dev/" rel="noopener noreferrer"&gt;Lovable&lt;/a&gt;, &lt;a href="https://v0.app/" rel="noopener noreferrer"&gt;v0&lt;/a&gt;, &lt;a href="https://bolt.new/" rel="noopener noreferrer"&gt;Bolt&lt;/a&gt;, &lt;a href="https://base44.app/" rel="noopener noreferrer"&gt;Base44&lt;/a&gt;, &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;, &lt;a href="https://replit.com/" rel="noopener noreferrer"&gt;Replit&lt;/a&gt;, and &lt;a href="https://claude.ai/" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; can take a plain-language description and turn it into a working interface in minutes. But somewhere between typing your "build me a portfolio website" prompt and watching the interface come to life, a huge number of us end up with something that looks suspiciously like everyone else's website.&lt;/p&gt;

&lt;p&gt;So why does this keep happening, and more importantly, how do you actually get AI to build something that feels like it belongs to you and nobody else?&lt;/p&gt;

&lt;p&gt;That is what this article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Do AI-Generated Websites Usually Look Like?
&lt;/h2&gt;

&lt;p&gt;If you spend enough time scrolling through AI-generated interfaces, you will start noticing similarities everywhere. There is not one single official AI website design style, but there is a recognizable visual pattern that keeps appearing across different AI website builders and coding tools.&lt;/p&gt;

&lt;p&gt;You will recognize most of these patterns on sight: large bold hero headlines, centered layouts, left aligned layouts, generous whitespace, rounded cards, soft drop shadows, subtle borders, gradient backgrounds in shades of purple, or blue, glassmorphism panels, floating dashboard mockups, friendly sans-serif typography, a strip of "trusted by" logos, three-column feature sections, testimonials tucked into cards, pricing tables, animated backgrounds, and dark mode toggles etc.&lt;/p&gt;

&lt;p&gt;None of these choices are inherently bad. In fact, most of them are genuinely good design decisions that make sense for a huge number of products. Rounded cards can make an interface feel approachable, whitespace can improve readability, blue can communicate trust, and a clear hero section can help visitors understand a product quickly.&lt;/p&gt;

&lt;p&gt;The real problem appears when everyone reaches for the same good choices at the same time, because that is when those choices stop feeling distinctive and start feeling like a generic wallpaper.&lt;/p&gt;

&lt;p&gt;A website can be technically excellent and still be visually forgettable. It can have good accessibility, responsive layouts, clean code, thoughtful interactions, and strong performance while still looking almost identical to hundreds of other AI-generated websites.&lt;/p&gt;

&lt;p&gt;That distinction matters because the goal of good design is not simply to make something look good. The goal is to communicate something specific to a specific audience in a way that feels appropriate to the product and memorable enough to leave an impression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do AI Websites Look the Same?
&lt;/h2&gt;

&lt;p&gt;Large language models are prediction systems at their core, which means they do not approach a blank page in exactly the same way a human designer would. When you ask an AI system to build something, it draws from patterns it has encountered across enormous amounts of existing material.&lt;/p&gt;

&lt;p&gt;If thousands of examples of modern Agency websites contain a centered hero, a bold headline, rounded cards, a blue or purple accent, a three-column feature grid, customer logos, and testimonials, those patterns become highly probable responses when someone asks an AI to create a modern Agency website.&lt;/p&gt;

&lt;p&gt;In other words, when you say, "Build a modern portfolio website for a Community Manager," you are effectively asking the model to produce something that resembles what people generally consider a modern community manager portfolio to look like.&lt;/p&gt;

&lt;p&gt;AI happens to be extremely good at that particular task, which is exactly why the first output can feel polished and forgettable at the same time.&lt;/p&gt;

&lt;p&gt;It usually lands somewhere within the familiar patterns it has learned, and those familiar patterns are rarely where the most distinctive design decisions live.&lt;/p&gt;

&lt;p&gt;There is also a second, much more human layer to this problem.&lt;/p&gt;

&lt;p&gt;Most of us prompt AI with almost identical language without realizing it. We type things like "build a clean, modern, professional website," "create a sleek and beautiful landing page," or "make it minimal, premium and visually impressive."&lt;/p&gt;

&lt;p&gt;These phrases sound like creative direction to a human, but to a model they are extremely broad categories rather than specific instructions. They tell the AI what general group you want to belong to, but they do not tell it what makes your product different from everyone else in that group.&lt;/p&gt;

&lt;p&gt;It is similar to walking into a restaurant - say Pasta De Café for instance and telling the chef to give you something delicious. The chef technically has enormous freedom, but because you have not given them any meaningful constraints, they will probably make something that appeals to majority of the customers that order from the café.&lt;/p&gt;

&lt;p&gt;If instead you tell the chef you want spicy Asun pasta with an extra portion of Asun, paired with a cold-pressed ginger and pineapple drink, the request suddenly becomes much more specific.&lt;/p&gt;

&lt;p&gt;The same principle applies to AI website design.&lt;/p&gt;

&lt;p&gt;The more generic your creative direction is, the more likely the model is to rely on familiar patterns. The more specific your direction becomes, the more opportunity you give the model to create something that feels intentional.&lt;/p&gt;

&lt;p&gt;There is also a technical reason AI-generated websites often look similar.&lt;/p&gt;

&lt;p&gt;A significant number of code-first AI website builders generate interfaces using the same popular open-source technologies and component systems. Tailwind CSS and shadcn/ui are particularly influential in modern React-based interfaces, and when different products are being generated from similar building blocks, their buttons, cards, inputs, modals, spacing, borders, and other interface elements can begin to resemble one another.&lt;/p&gt;

&lt;p&gt;The component library itself is not necessarily the problem. Reusable components are one of the reasons developers can build products quickly and consistently.&lt;/p&gt;

&lt;p&gt;The problem begins when the defaults start determining the composition instead of simply helping express the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI Website Builders Keep Producing Similar Designs
&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%2Fd7cq4y3y5z6hbaimihqs.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%2Fd7cq4y3y5z6hbaimihqs.png" alt="Why AI Website Builders Keep Producing Similar Designs" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI website builders have developed into several different categories, and understanding those categories helps explain why the same visual patterns can appear across tools that otherwise work very differently.&lt;/p&gt;

&lt;p&gt;Design-first tools such as &lt;a href="https://www.framer.com/ai/" rel="noopener noreferrer"&gt;Framer AI&lt;/a&gt; and &lt;a href="https://webflow.com/ai" rel="noopener noreferrer"&gt;Webflow AI&lt;/a&gt; are focused heavily on visual composition and allow users to generate or refine websites inside visual editors. They are particularly useful for marketing websites, portfolios, landing pages, and other experiences where visual presentation is a major priority.&lt;/p&gt;

&lt;p&gt;All-in-one AI website builders such as &lt;a href="https://bolt.new/" rel="noopener noreferrer"&gt;Bolt&lt;/a&gt;, &lt;a href="https://10web.io/" rel="noopener noreferrer"&gt;10Web&lt;/a&gt;, and &lt;a href="https://www.squarespace.com/websites/ai-website-builder" rel="noopener noreferrer"&gt;Squarespace AI&lt;/a&gt; place more emphasis on speed and convenience. You can describe your business, choose a general direction, and receive a website that may already contain the structure, copy, imagery, branding, and other elements you need to get started.&lt;/p&gt;

&lt;p&gt;Code-first tools such as &lt;a href="https://lovable.dev/" rel="noopener noreferrer"&gt;Lovable&lt;/a&gt;, &lt;a href="https://v0.app/" rel="noopener noreferrer"&gt;v0&lt;/a&gt;, &lt;a href="https://claude.ai/" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;, and &lt;a href="https://replit.com/products/agent" rel="noopener noreferrer"&gt;Replit Agent&lt;/a&gt; operate closer to the development side of the spectrum. They can generate actual application code and, depending on the tool and project, connect interfaces to databases, authentication systems, APIs, and other functionality.&lt;/p&gt;

&lt;p&gt;These tools are solving different problems, yet their outputs can still converge visually.&lt;/p&gt;

&lt;p&gt;That tells us something important.&lt;/p&gt;

&lt;p&gt;The sameness is not simply a problem with one particular AI website builder. It is a broader design-system problem created by a combination of shared training patterns, popular frameworks, component libraries, design trends, user prompts, and the natural tendency of AI systems to choose familiar solutions when they are given insufficient direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other People Have Noticed This Too
&lt;/h2&gt;

&lt;p&gt;The idea that AI-generated websites are becoming visually similar is not just something you notice after spending too much time looking at landing pages.&lt;/p&gt;

&lt;p&gt;Other designers and developers have started documenting the same phenomenon.&lt;/p&gt;

&lt;p&gt;Joshua Snoddy's article on why AI-generated websites look the same examines the recurring use of purple gradients, glass cards, centered hero sections, Inter typography, and shadcn/ui components across tools such as Lovable, v0, and Bolt. His analysis points toward the same underlying issue: AI tools tend to reach for safe and familiar design patterns because those patterns are heavily represented in the material from which models learn. &lt;a href="https://www.joshuasnoddy.com/blog/why-ai-websites-look-the-same/" rel="noopener noreferrer"&gt;Joshua Snoddy's analysis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;InterfaceKit makes a similar argument, explaining how AI-generated websites can become interchangeable when a prompt does not contain enough product-specific creative direction. The tool fills the gaps with familiar patterns, resulting in an interface that works but does not necessarily communicate a distinctive visual identity. &lt;a href="https://blog.interfacekit.io/why-ai-generated-websites-all-look-the-same?" rel="noopener noreferrer"&gt;InterfaceKit's analysis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sailop has also compared several AI website builders using the same short brief and documented similarities between the generated results, including centered heroes, three-column feature grids, Lucide icons, blue-to-purple gradients, and similar typography. &lt;a href="https://sailop.com/blog/ai-website-builders-ranked-by-slop-2026?" rel="noopener noreferrer"&gt;Sailop's AI website builder comparison&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These examples are useful because they show that the phenomenon is not limited to one tool.&lt;/p&gt;

&lt;p&gt;Different AI builders can arrive at remarkably similar visual conclusions when they receive similarly vague instructions.&lt;/p&gt;

&lt;p&gt;And to be clear, this is not happening because someone at an AI company decided that every website should be purple. The real explanation is much more interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common AI Website Design Patterns
&lt;/h2&gt;

&lt;p&gt;Once you start noticing these patterns, it becomes difficult to stop seeing them.&lt;/p&gt;

&lt;p&gt;One of the most common patterns is the use of a single accent color against a white or near-white background. In technology products, that accent often lands somewhere around blue, indigo, violet, or purple because these colors are already deeply associated with modern software and digital products.&lt;/p&gt;

&lt;p&gt;Typography follows a similar pattern.&lt;/p&gt;

&lt;p&gt;AI-generated interfaces frequently reach for fonts such as Inter, Poppins, Manrope, and other clean sans-serif typefaces because they are highly legible, widely available, and already common across modern digital products.&lt;/p&gt;

&lt;p&gt;Then, we also have rounded corners, buttons, cards, inputs, modals, containers, and sometimes entire sections can end up sharing the same generous border radius. This creates visual consistency, but when every component has the same treatment, the interface can begin to feel mechanically assembled.&lt;/p&gt;

&lt;p&gt;Feature sections often always look like bento grids or collections of cards because they provide an easy way to organize information into responsive blocks without requiring a more unusual compositional decision.&lt;/p&gt;

&lt;p&gt;Depth is commonly created with soft shadows, subtle borders, gradients, and glassmorphism rather than texture, strong outlines, unusual layering, or more expressive visual systems.&lt;/p&gt;

&lt;p&gt;The page structure itself also tends to follow a familiar sequence: Hero → social proof → features → testimonials → pricing → FAQ → final call-to-action.&lt;/p&gt;

&lt;p&gt;There is nothing inherently wrong with this structure. In fact, it works because it follows a familiar marketing logic that helps visitors understand a product, build trust, address their questions, and eventually make a decision.&lt;/p&gt;

&lt;p&gt;The problem begins when every product uses the same structure alongside the same visual language, typography, imagery, and component styles. At that point, the experience becomes predictable. The website is easy to understand, easy to generate, easy to implement, and often easy to optimize for conversions. The only problem is that it is not always memorable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI-Generated Websites Actually Look Like in Practice
&lt;/h2&gt;

&lt;p&gt;Imagine a founder typing this into any AI website builder of their choice:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a landing page for my AI productivity app. Make it modern, clean, and professional.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within a short amount of time, they may have a well-designed website that looks like a thousand other websites. The result can look genuinely polished, with thoughtful spacing, clean typography, smooth interactions, and a logical page structure. It can also look remarkably similar to the last five AI productivity websites you saw.&lt;/p&gt;

&lt;p&gt;Now imagine a restaurant owner using an AI website builder. They type “restaurant,” select a general style, and receive a website with food photography, a hero banner, a menu section, testimonials, an about section, opening hours, and a contact area.&lt;/p&gt;

&lt;p&gt;Again, it works, and it can look good. The problem is that it may have been generated from the same template logic, design patterns, and assumptions that have already been used to create thousands of other restaurant websites. The website does its job, but there may be very little about it that makes the restaurant itself feel distinctive.&lt;/p&gt;

&lt;p&gt;Now consider a designer working at an agency.&lt;/p&gt;

&lt;p&gt;They use an AI website builder to generate a first draft for a client, but instead of accepting the first result, they replace the stock photography with the client's actual photography, change the typography, adjust the spacing, introduce the brand's actual color system, restructure several sections, and redesign the feature area around the client's content.&lt;/p&gt;

&lt;p&gt;The final website can look nothing like the AI's initial output, not because the tool suddenly became more creative, but because the human continued the design process and gave it better direction.&lt;/p&gt;

&lt;p&gt;Finally, imagine a developer asking Google AI Studio to create a pricing table. The first version produces a familiar collection of pricing cards. Instead of accepting it, the developer asks for an editorial-style comparison table with rows instead of cards, a strong visual treatment for the recommended plan, specific brand colors, custom typography, and a completely different visual hierarchy.&lt;/p&gt;

&lt;p&gt;The second version can look dramatically different from the first, even though the same tool is being used. The difference is the direction.&lt;/p&gt;

&lt;p&gt;This is the pattern worth remembering: the tools that produce generic websites and the tools that produce distinctive websites can very often be the exact same tools. What changes is the amount of specific, constrained, referenced direction a human provides before accepting the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Website Prompts That Can Break the Pattern (Try This Yourself)
&lt;/h2&gt;

&lt;p&gt;The fastest way to understand this is to run the same brief through an AI website builder twice, first with a generic prompt and then with a deliberate one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Prompt That Produces the Generic Look
&lt;/h3&gt;

&lt;p&gt;Try this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a modern landing page for a personal finance app aimed at young professionals. Use a clean, premium aesthetic with a hero section, features, testimonials, and pricing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an extremely common style of prompt, and it leaves almost every important visual decision open.&lt;/p&gt;

&lt;p&gt;The AI has to decide what “modern” means, what “premium” means, which colors to use, which typography to choose, how to structure the page, what type of imagery belongs in the hero, how the feature section should work, and even what visual personality the brand should have.&lt;/p&gt;

&lt;p&gt;When you provide no strong opinion, the model has to supply one. And it will usually draw from the patterns it has seen most often.&lt;/p&gt;

&lt;p&gt;I tested this exact prompt across three AI website builders and looked specifically at the hero sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-generated finance landing page created with Lovable using the same generic prompt&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4e1re6mg7e3h809kxi0.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%2Fu4e1re6mg7e3h809kxi0.png" alt="AI-generated finance landing page created with Lovable using the same generic prompt" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-generated finance landing page created with Google AI Studio using the same generic prompt&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxudjsqayi51qh3ybz0n.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%2Fzxudjsqayi51qh3ybz0n.png" alt="AI-generated finance landing page created with Google AI Studio using the same generic prompt" width="799" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-generated finance landing page created with v0 using the same generic prompt&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp17o3tbo49h10m36ek6l.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%2Fp17o3tbo49h10m36ek6l.png" alt="AI-generated finance landing page created with v0 using the same generic prompt" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look closely at the three results.&lt;/p&gt;

&lt;p&gt;The exact implementation is different, but the visual language is surprisingly familiar. You can see similar approaches to hierarchy, spacing, composition, and the general idea of what a modern financial product landing page should look like.&lt;/p&gt;

&lt;p&gt;You may also notice something else that happens surprisingly often with AI-generated websites: the tools invent random company names, testimonials, statistics, product claims, and other content because the prompt did not provide real information.&lt;/p&gt;

&lt;p&gt;This is another reason why generic AI-generated websites can feel artificial. The design is generic, and the content is generic too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Now Rewrite the Same Brief With Real Direction
&lt;/h3&gt;

&lt;p&gt;Instead of simply asking for a "modern" finance website, try giving the AI a visual and communication problem to solve:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a fintech landing page for first-time investors aged 22 to 30 who feel intimidated by traditional financial products. The visual language should feel like a modern financial magazine rather than a banking website. Use strong editorial typography, real-world financial illustrations, and clear data storytelling. Avoid corporate blue, generic stock photography, and dashboard-heavy layouts. Make the financial information feel approachable instead of intimidating.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The difference is significant because now the model has an audience, an emotional problem to address, a visual reference, a communication goal, specific things to avoid, and a clear aesthetic direction. Instead of simply decorating a generic SaaS page, the model now has a much more specific problem to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Second Experiment: The Mfon Plant Care App
&lt;/h2&gt;

&lt;p&gt;If you want to test this yourself, use one consistent product idea across several AI website builders rather than changing the product for every experiment.&lt;/p&gt;

&lt;p&gt;For this example, imagine a fictional houseplant care app called &lt;strong&gt;Mfon&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Start with this control prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a landing page for Mfon, a mobile app that helps people take care of their houseplants. Make it modern, clean, and professional, with a hero section, features, testimonials, and a pricing section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then compare that result with several highly specific directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editorial Direction
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Design a landing page for Mfon, a houseplant care app. Use an early-2000s print magazine editorial layout rather than a typical SaaS page. Use an asymmetrical grid, an oversized serif headline, generous negative space, and a muted sage-and-cream palette using only #F4F1EA and #3C4A3B. Do not use gradients, drop shadows, or rounded corners. Let the feature section read like a magazine spread with pull quotes rather than a bento grid, and let the testimonial read like a quoted editorial excerpt rather than a carousel with headshots.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Brutalist Direction
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Design a landing page for Mfon, a houseplant care app. Use a brutalist web design direction that feels raw, high-contrast, and intentionally unpolished. Use a pure black background, a single acid-green accent at #B4FF39, and one heavy monospace font throughout. Use thick borders instead of shadows, avoid gradients and soft corners entirely, and replace stock photography with bold typographic statements and simple line-art plant illustrations. The layout should feel intentionally rigid and grid-locked, closer to a zine than a polished startup website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Retro Botanical Direction
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Design a landing page for Mfon, a houseplant care app. Use a visual direction inspired by 1970s botanical prints and retro travel posters. Use terracotta, mustard yellow, and forest green, pairing a slightly quirky display font for headlines with a plain serif for body copy. Use an illustrated hero image instead of photography and make the imagery feel like a vintage botanical poster. Let the feature section feel like a vintage seed-packet catalog instead of a modern card grid, and avoid anything that looks like a typical technology startup, including gradients, glassmorphism, and generic SaaS icons.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice what is happening here.&lt;/p&gt;

&lt;p&gt;The product has not changed, and the functionality has not changed either. The target audience has not necessarily changed. What changed is the creative direction, and that is why your prompt matters so much.&lt;/p&gt;

&lt;p&gt;If you run the generic prompt across three or four different AI website builders, you can compare how much their outputs converge when they are given the same vague instructions. Then, run the more specific creative direction through those same tools and compare the results.&lt;/p&gt;

&lt;p&gt;You can also take the original generic output and ask the tool to redesign it using one of the new visual directions.&lt;/p&gt;

&lt;p&gt;That transformation can be even more interesting than generating a completely new website because it demonstrates something important: the AI was capable of moving away from its defaults once someone gave it a reason to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Rule That Changes Everything: &lt;strong&gt;Say What You Don't Want&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is a rule worth keeping close whenever you are prompting an AI website builder: do not only describe what you want; describe what you do not want as well.&lt;/p&gt;

&lt;p&gt;Most prompts are purely additive. We tell the AI to add a hero, add a feature section, add testimonials, make the website beautiful, make it modern, make it responsive, and make it professional.&lt;/p&gt;

&lt;p&gt;But distinctive design often requires subtraction rather than addition.&lt;/p&gt;

&lt;p&gt;Compare a generic prompt such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a modern landing page for an AI productivity tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;with a more deliberate version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a landing page for an AI productivity tool, but do not use purple-to-blue gradients, glassmorphism, floating dashboard screenshots, three-column feature cards, generic stock photography, rounded cards in every section, or vague SaaS copy such as "supercharge your productivity." Use editorial typography, an asymmetric layout, warm neutral colors, real product screenshots, and specific copy that speaks directly to independent creators.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second prompt gives the AI boundaries.&lt;/p&gt;

&lt;p&gt;Those boundaries are useful because they force the model to solve the design problem instead of falling back on its most familiar patterns.&lt;/p&gt;

&lt;p&gt;This is one of the biggest misconceptions about prompting AI for creative work. People sometimes assume that giving an AI more constraints will make the result less creative. In many cases, the opposite is true.&lt;/p&gt;

&lt;p&gt;When everything is possible, the model can easily return to the safest and most familiar option. When certain options are explicitly removed, it has to explore what else could work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use System Instructions as Your Design Guardrails
&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%2F1lhmgfi83gglxaxewx8b.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%2F1lhmgfi83gglxaxewx8b.png" alt="Google AI Studio System Instructions" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also where system instructions become useful.&lt;/p&gt;

&lt;p&gt;Most people focus on the prompt they are typing in the moment and forget that they can establish a set of rules that the AI should follow throughout an entire project.&lt;/p&gt;

&lt;p&gt;If you are building a website across multiple pages and iterations, system instructions can act as your design guardrails. They can establish the visual rules that should remain consistent throughout the project rather than forcing you to repeat the same instructions every time you ask the AI to build something new.&lt;/p&gt;

&lt;p&gt;You can define your approved fonts, exact color palette, spacing system, border radius, image style, typography hierarchy, accessibility requirements, animation preferences, and, perhaps most importantly, what the AI should avoid.&lt;/p&gt;

&lt;p&gt;For example, you could establish a rule like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For this entire project, do not use gradients, glassmorphism, excessive rounded cards, purple or blue accents, generic SaaS layouts, or stock photography. Use the provided brand colors, editorial typography, asymmetric layouts, and sharp edges throughout. Do not introduce new fonts or colors unless explicitly requested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This becomes particularly useful as the project gets larger.&lt;/p&gt;

&lt;p&gt;You might spend an hour moving a website away from the generic SaaS aesthetic, only to ask the AI to add a new pricing section and suddenly receive the same three-column rounded pricing cards you were trying to escape.&lt;/p&gt;

&lt;p&gt;The problem is not necessarily that the model ignored you. The problem may simply be that you never established the instruction as a persistent design rule.&lt;/p&gt;

&lt;p&gt;System instructions are not a replacement for detailed prompts, but they can provide a consistent foundation for the individual prompts you use throughout the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Make an AI-Generated Website Look Unique
&lt;/h2&gt;

&lt;p&gt;The single biggest shift you can make is trading vague adjectives for real references.&lt;/p&gt;

&lt;p&gt;Telling an AI to make something feel premium gives it very little to work with. Telling it to make something feel like a contemporary luxury editorial magazine rather than a typical SaaS dashboard gives it a much more specific concept to interpret.&lt;/p&gt;

&lt;p&gt;You can even combine unrelated visual worlds intentionally.&lt;/p&gt;

&lt;p&gt;You could ask for a project management app inspired by a Japanese stationery brand, a developer portfolio with the visual language of a music festival, or a fintech landing page inspired by an independent fashion magazine.&lt;/p&gt;

&lt;p&gt;These combinations create tension between familiar categories, and that tension can lead to more distinctive design decisions.&lt;/p&gt;

&lt;p&gt;It also helps enormously to give the model hard constraints instead of leaving everything open-ended. Specify your exact hex codes, approved fonts, preferred corner style, whether you want illustrations or photography, whether the layout should be symmetrical or asymmetric, whether you want dense information or generous whitespace, the visual references you like, and the visual patterns you absolutely do not want.&lt;/p&gt;

&lt;p&gt;An open-ended prompt quietly gives the AI permission to fall back on its defaults.&lt;/p&gt;

&lt;p&gt;A tightly scoped prompt forces it to solve the problem within your visual world rather than relying on the familiar patterns it has learned from the internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give AI Your Real Content
&lt;/h3&gt;

&lt;p&gt;Real content matters far more than most people expect.&lt;/p&gt;

&lt;p&gt;If you ask an AI website builder to create three testimonials, it will probably produce generic testimonials about how a product changed someone's life, improved their workflow, or helped them become more productive.&lt;/p&gt;

&lt;p&gt;The layout will then be designed around placeholder content that may bear very little resemblance to what your actual customers will say.&lt;/p&gt;

&lt;p&gt;A headline with six words behaves differently from a headline with eighteen words. A testimonial with thirty words shapes a section differently from one with one hundred words. A product with three features requires a different information hierarchy from one with twelve.&lt;/p&gt;

&lt;p&gt;When you provide your actual copy, screenshots, product details, customer stories, statistics, pricing, and brand assets, the AI has much more real material to work with when building the interface.&lt;/p&gt;

&lt;p&gt;Instead of designing a beautiful shell around imaginary content, it can design around the actual communication problem your website needs to solve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tell the AI Who the Website Is For
&lt;/h3&gt;

&lt;p&gt;Your audience should also be part of your design prompt because there is no such thing as a website designed for everyone.&lt;/p&gt;

&lt;p&gt;A page built for developers can lean into technical terminology, dense information, code snippets, documentation-style layouts, and monospace accents.&lt;/p&gt;

&lt;p&gt;A website built for a luxury consumer audience may need stronger imagery, more restraint, more whitespace, and significantly less dashboard-heavy UI.&lt;/p&gt;

&lt;p&gt;A website built for young creators can afford to be more expressive, playful, colorful, and visually loud.&lt;/p&gt;

&lt;p&gt;Naming the audience explicitly gives the AI a much clearer target because you are no longer asking it to create a modern website.&lt;/p&gt;

&lt;p&gt;You are asking it to create a particular experience for a particular group of people. That is a much more useful design problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Designing the Entire Website With One Giant Prompt
&lt;/h2&gt;

&lt;p&gt;It is tempting to tell an AI website builder everything at once.&lt;/p&gt;

&lt;p&gt;You give it the product description, the navigation, the hero, the features, the testimonials, pricing, the FAQ, the footer, the animations, the responsiveness, and your entire visual direction in one massive prompt and then wait for the finished website.&lt;/p&gt;

&lt;p&gt;Sometimes this works, but I can tell you for free that it is not always the best way to get a distinctive result.&lt;/p&gt;

&lt;p&gt;Breaking the work into smaller pieces gives you significantly more control. &lt;/p&gt;

&lt;p&gt;You can start with the hero, evaluate it, refine it, and then move on to the navigation.&lt;/p&gt;

&lt;p&gt;You can build the feature section separately and deliberately choose whether it should use cards, editorial layouts, illustrations, tables, timelines, or another structure.&lt;/p&gt;

&lt;p&gt;You can design the pricing section independently rather than allowing the AI to automatically repeat the same card pattern used everywhere else.&lt;/p&gt;

&lt;p&gt;This also makes it much easier to diagnose what is not working.&lt;/p&gt;

&lt;p&gt;If five sections in a row all use the same rounded card structure, you can explicitly tell the AI that the problem is repetition and ask it to introduce visual variation.&lt;/p&gt;

&lt;p&gt;That is much more useful than simply saying, “Make it better,” because “Make it better” tells the AI almost nothing.&lt;/p&gt;

&lt;p&gt;Using a follow up prompt like “Five consecutive sections currently use identical rounded cards. Redesign the fourth section as an editorial two-column layout with a large product image and supporting text” gives the model a specific problem to solve.&lt;/p&gt;

&lt;p&gt;The difference is that you are no longer asking the AI for a vague improvement. You are identifying what is not working, explaining why it is not working, and giving it a clear direction for what to change. That kind of feedback makes the design process far more intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Forget the Fundamentals
&lt;/h2&gt;

&lt;p&gt;There is an important distinction between originality and unnecessary complexity.&lt;/p&gt;

&lt;p&gt;A website does not become good simply because it looks unusual. Contrast still matters, readable typography, keyboard navigation, clear focus states, responsive behavior, loading performance, and even accessibility still matters.&lt;/p&gt;

&lt;p&gt;If you are asking an AI website builder to create something visually experimental, you should still tell it to maintain good accessibility, usability, and performance practices.&lt;/p&gt;

&lt;p&gt;The goal is not to make your website as unusual as possible. The goal is to make it distinctive without making it difficult to use. Good design knows when to follow familiar conventions and when to break them.&lt;/p&gt;

&lt;p&gt;You can also ask it to keep the implementation lightweight by avoiding unnecessary animation, oversized assets, excessive dependencies, and visual effects that do not add meaningful value.&lt;/p&gt;

&lt;p&gt;A website that looks incredibly distinctive but takes ten seconds to load is not necessarily a successful website.&lt;/p&gt;

&lt;p&gt;The goal is not to escape every established design principle, but to stop using established patterns without thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changed Your Mind Halfway Through? Here's How to Pivot
&lt;/h2&gt;

&lt;p&gt;One of the most useful things to understand about AI-assisted development is that you do not have to start over just because the first visual direction is not working.&lt;/p&gt;

&lt;p&gt;You can redirect the aesthetic partway through the project as long as you are clear about what should remain and what should change.&lt;/p&gt;

&lt;p&gt;The cleanest way to do this is to separate content and functionality from visual style.&lt;/p&gt;

&lt;p&gt;Your copy, feature list, product information, database, authentication, and core functionality may be worth keeping.&lt;/p&gt;

&lt;p&gt;Your typography, colors, spacing, shadows, border radius, imagery, and overall visual direction may be what you actually want to replace.&lt;/p&gt;

&lt;p&gt;A prompt like this can help:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep the existing content, structure, and functionality exactly as they are, but redesign the entire visual direction. Move away from the current SaaS aesthetic and toward an editorial magazine-style layout. Use an asymmetric grid, a bold serif headline font, a warm neutral palette, and sharper visual hierarchy instead of the current blue-and-white design. Do not modify the existing functionality or content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Naming the old direction and the new direction gives the model a clear before-and-after reference.&lt;/p&gt;

&lt;p&gt;If you only want to change the mood without redesigning everything, be specific about the scope:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep the layout and structure exactly as they are, but change the color palette, typography, imagery, and shadow style so the website feels warmer and more handmade rather than corporate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This type of request is much more predictable than simply asking the AI to make the website feel different.&lt;/p&gt;

&lt;p&gt;You can also create a fork of your design if the tool or workflow allows it.&lt;/p&gt;

&lt;p&gt;Instead of overwriting your existing version, duplicate it and experiment with a completely different visual direction. This gives you two versions to compare rather than forcing yourself to commit to a change before you know whether it works.&lt;/p&gt;

&lt;p&gt;And if you are struggling to decide what the new direction should actually be, ask the AI to act as an art director rather than an implementer.&lt;/p&gt;

&lt;p&gt;Give it your existing content and ask it to propose three completely different visual directions for the same product, explaining the typography, color palette, layout system, imagery, component style, and emotional positioning of each direction.&lt;/p&gt;

&lt;p&gt;That approach mirrors how a real design process often works. You explore possibilities before deciding which direction deserves to become the final design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your First AI-Generated Website Was Never Supposed to Be the Final One
&lt;/h2&gt;

&lt;p&gt;Your first AI-generated website was never meant to be the finished website.&lt;/p&gt;

&lt;p&gt;It is closer to a first draft that answers a much simpler question than the one that actually matters.&lt;/p&gt;

&lt;p&gt;The first version tells you whether the thing can be built.&lt;/p&gt;

&lt;p&gt;The next several versions help you determine whether it actually feels like you.&lt;/p&gt;

&lt;p&gt;That shift in mindset changes how you use AI website builders completely.&lt;/p&gt;

&lt;p&gt;Instead of generating once and shipping, the better process looks more like generating, inspecting, questioning, directing, regenerating, comparing, and refining until the website starts to communicate something specific.&lt;/p&gt;

&lt;p&gt;The first output is useful because it gives you something to react to. It does not have to be good enough to publish.&lt;/p&gt;

&lt;p&gt;In fact, sometimes the most useful thing the first version can do is show you exactly what you do not want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Call Your AI-Generated Website Done, Ask Yourself This
&lt;/h2&gt;

&lt;p&gt;Before accepting an AI-generated design as final, run through a simple mental checklist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Would the website still feel like your brand if the logo disappeared?&lt;/li&gt;
&lt;li&gt;Could your competitor replace your logo with theirs and still have a website that looks completely natural?&lt;/li&gt;
&lt;li&gt;Does every section use the same card pattern?&lt;/li&gt;
&lt;li&gt;Was the color palette chosen deliberately, or did the design slowly drift toward the familiar blue-to-purple range?&lt;/li&gt;
&lt;li&gt;Are the typography choices connected to your brand, audience, or product?&lt;/li&gt;
&lt;li&gt;Does the copy sound like something your company would actually say, or does it sound like AI wrote it?&lt;/li&gt;
&lt;li&gt;Are the images real, relevant, and specific to your product, or are they generic visual decoration?&lt;/li&gt;
&lt;li&gt;Does the website communicate something about your personality, positioning, or audience that another company could not easily copy?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And perhaps the most important question is this:&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;If you saw ten websites like this in a row tomorrow, would you remember this one?&lt;br&gt;
*&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the honest answer is no, there is probably still more direction worth giving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions About AI-Generated Websites
&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%2Fhhmm23j0nhaievfuxt1w.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%2Fhhmm23j0nhaievfuxt1w.png" alt="Frequently Asked Questions about AI-Generated Websites" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do AI websites look the same?
&lt;/h3&gt;

&lt;p&gt;AI websites often look the same because AI website builders rely on familiar design patterns, popular component libraries, common frameworks, existing web design conventions, and similar user prompts. When an AI receives a vague request such as "create a modern professional website," it tends to select design patterns that are widely used and statistically familiar rather than inventing an entirely new visual language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do AI-generated websites use purple and blue so often?
&lt;/h3&gt;

&lt;p&gt;Purple and blue are already common colors in technology, SaaS, and AI branding because they are associated with concepts such as trust, innovation, intelligence, and modern technology. AI website builders have also learned from large amounts of existing digital design where these colors are heavily represented, making them common choices when the user does not provide a specific brand palette.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I make my AI-generated website unique?
&lt;/h3&gt;

&lt;p&gt;You can make an AI-generated website more unique by giving the AI specific visual references, brand colors, typography, layout constraints, real content, audience information, and a clear list of patterns to avoid. Instead of asking for a "modern" or "premium" website, explain the visual world you want the website to belong to and explicitly tell the AI what should not appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can AI create original website designs?
&lt;/h3&gt;

&lt;p&gt;Yes, AI can create highly distinctive website designs when it receives specific creative direction, strong references, meaningful constraints, and enough context about the product and audience. The first generated version may rely heavily on familiar patterns, but iterative prompting and human art direction can move the design significantly away from those defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which AI website builders can create websites from prompts?
&lt;/h3&gt;

&lt;p&gt;Several AI-powered website and development tools can generate websites or web applications from natural-language instructions, including Lovable, v0, Bolt, Replit, Google AI Studio, Framer AI, and other AI website builders. Their capabilities and outputs vary depending on the tool, model, prompt, available components, and the amount of human direction provided.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are AI-generated websites bad design?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. An AI-generated website can have excellent typography, spacing, responsiveness, accessibility, performance, and usability while still feeling generic. The problem is not that the design was created with AI; the problem is that the final design may rely too heavily on familiar defaults without enough human art direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  So Here's My Actual Point
&lt;/h2&gt;

&lt;p&gt;AI is not incapable of originality. It is simply very good at giving us what we repeatedly ask for.&lt;/p&gt;

&lt;p&gt;When thousands of people tell AI to build something modern, clean, premium, minimal, professional, and beautiful without explaining what any of those words mean for their particular product, it makes sense that the results begin to converge.&lt;/p&gt;

&lt;p&gt;When the same people use the same AI website builders, the same component libraries, the same frameworks, the same fonts, the same gradients, the same layouts, and the same prompts, that convergence becomes even more obvious.&lt;/p&gt;

&lt;p&gt;The solution is not to stop using AI, it is to stop treating the first generated version as the finished design.&lt;/p&gt;

&lt;p&gt;The websites that do not look AI-made were often still built with AI. Someone simply refused to stop at the first draft and kept giving direction until the website stopped looking like everyone else's and started looking like theirs.&lt;/p&gt;

&lt;p&gt;Your call to action today is to go back to the last AI-generated website you built and ask yourself one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about this makes it unmistakably mine?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is nothing, do not throw the entire website away.&lt;/p&gt;

&lt;p&gt;Go back into your AI website builder and start directing it differently. Tell it what to remove. Give it references. Give it real content. Give it your colors, your personality, your audience, your weird ideas, and your non-negotiables. Ask it to break the patterns it keeps reaching for, and do not be afraid to reject a perfectly functional design simply because it feels too familiar.&lt;/p&gt;

&lt;p&gt;Because AI can build the website, but you still have to give it a point of view.&lt;/p&gt;

&lt;p&gt;Do not just use AI to make something that works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it to make something people remember.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need a Website That Actually Feels Like Yours?
&lt;/h2&gt;

&lt;p&gt;This is also how I approach the websites and web applications I build for clients. I do not believe in simply handing an AI a generic prompt, accepting the first generated interface, and calling the job done. AI can make the development process significantly faster, but speed should not come at the expense of strategy, usability, or visual identity.&lt;/p&gt;

&lt;p&gt;If you are building a website or web application and want something that is functional, intentional, and actually feels like your brand rather than another AI-generated template, I would love to chat.&lt;/p&gt;

&lt;p&gt;You can reach me at &lt;a href="mailto:umondiamfonobong@gmail.com"&gt;umondiamfonobong@gmail.com&lt;/a&gt; and tell me a little about what you are building, what you need, and where you are currently stuck. So even if you are starting from scratch or already have an AI-generated website that needs a serious design intervention, let's talk.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>design</category>
      <category>productivity</category>
    </item>
    <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>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>
  </channel>
</rss>
