<?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: sky</title>
    <description>The latest articles on DEV Community by sky (@metrix187).</description>
    <link>https://dev.to/metrix187</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%2Fuser%2Fprofile_image%2F4103165%2F6e1b115f-a516-4485-a948-4d8251aadf6a.png</url>
      <title>DEV Community: sky</title>
      <link>https://dev.to/metrix187</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/metrix187"/>
    <language>en</language>
    <item>
      <title>Seven Silent Failures: building an autonomous email agent on Cloud Run</title>
      <dc:creator>sky</dc:creator>
      <pubDate>Mon, 31 Aug 2026 18:06:48 +0000</pubDate>
      <link>https://dev.to/metrix187/seven-silent-failures-building-an-autonomous-email-agent-on-cloud-run-85a</link>
      <guid>https://dev.to/metrix187/seven-silent-failures-building-an-autonomous-email-agent-on-cloud-run-85a</guid>
      <description>&lt;p&gt;&lt;em&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://quantara.cv/articles/seven-silent-failures.html" rel="noopener noreferrer"&gt;quantara.cv&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  the thing i built
&lt;/h2&gt;

&lt;p&gt;An agent with no chat box. An email arrives, and it wakes up on Cloud Run, reads the thread, checks my actual calendar, writes a proposal document, and prepares a reply — then stops and asks a human the moment it isn't sure. Nobody types anything. There is no prompt. The email &lt;em&gt;is&lt;/em&gt; the prompt.&lt;/p&gt;

&lt;p&gt;Gemini 3.5 Flash does the tool selection through function calling, Google's Agent Development Kit runs the loop, and three Cloud Run services keep ingress separate from execution. Pub/Sub carries events between them and Firestore holds the state. The &lt;a href="https://github.com/Metrix187/quantara-routing-engine" rel="noopener noreferrer"&gt;repo is here&lt;/a&gt; if you want the actual code.&lt;/p&gt;

&lt;p&gt;That's not what this piece is about. This is about the seven things that went wrong, because six of them went wrong &lt;em&gt;quietly&lt;/em&gt;, and that turned out to be the real lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  the one that shouted
&lt;/h2&gt;

&lt;p&gt;Exactly one failure announced itself. Line endings. I wrote the deploy scripts on Windows, git helpfully stored them with CRLF, and anyone cloning the repo would get &lt;code&gt;bad interpreter: no such file or directory&lt;/code&gt;. Ugly, obvious, fixed with a &lt;code&gt;.gitattributes&lt;/code&gt; in about ninety seconds.&lt;/p&gt;

&lt;p&gt;Every other failure was polite about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. the model that exists, but not here
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gemini-3.5-flash&lt;/code&gt; returns a 404 on Vertex AI in &lt;code&gt;us-central1&lt;/code&gt;. The error says &lt;em&gt;publisher model not found&lt;/em&gt;. That sentence sends you hunting for a typo in the model name, which is the wrong place to look — the model is fine, the region isn't. It's only served from the &lt;code&gt;global&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;I found it by brute force, asking three regions in a row. Nothing in the error text points at geography.&lt;/p&gt;

&lt;h2&gt;
  
  
  2 and 6. iam, twice, the same way
&lt;/h2&gt;

&lt;p&gt;Two separate failures, identical shape. A service account gets created, the very next line binds a role to it, and the bind fails with &lt;em&gt;service account does not exist&lt;/em&gt;. It does exist. It just doesn't exist &lt;em&gt;yet&lt;/em&gt; — IAM is eventually consistent and the propagation window is tens of seconds.&lt;/p&gt;

&lt;p&gt;The first time this bit me, the failure was inside a line ending in &lt;code&gt;|| true&lt;/code&gt;. I'd written that to make the script re-runnable. What it actually did was swallow the error, skip creating the Pub/Sub subscription, and leave me with a deploy that printed three green service URLs and a completely disconnected pipeline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every piece of visible config was correct. The thing simply did nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second time was Cloud Scheduler, which needs its own service agent to hold &lt;code&gt;serviceAccountTokenCreator&lt;/code&gt; on the account it impersonates. Without that grant: no scheduler log, no request at the target, no error anywhere. And while the grant propagates, it fails in exactly the same silent way — so I concluded it was broken and wrote it off. It started working four retries later.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. gmail won't tell you what arrived
&lt;/h2&gt;

&lt;p&gt;A Gmail push notification contains &lt;code&gt;{emailAddress, historyId}&lt;/code&gt;. That's it. No message ID, ever. To find out what actually landed you replay &lt;code&gt;users.history.list&lt;/code&gt; from a watermark you stored yourself, and one notification can cover several messages.&lt;/p&gt;

&lt;p&gt;I'd built the obvious version, passing the &lt;code&gt;historyId&lt;/code&gt; through as though it identified something. Every real trigger 404'd.&lt;/p&gt;

&lt;p&gt;The part worth sitting with: every test I'd written before that point passed, because every test supplied the message ID by hand. I had thoroughly tested a code path that production would never take.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. a container has no idea where it is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;datetime.now().astimezone()&lt;/code&gt; resolves to UTC on Cloud Run. So "available 9 to 5" quietly became 9–5 UTC, and my agent started offering a prospective client &lt;strong&gt;5 a.m.&lt;/strong&gt; meetings.&lt;/p&gt;

&lt;p&gt;It labelled them UTC correctly, too. The output looked right. It was just useless.&lt;/p&gt;

&lt;p&gt;This is the one I'd most likely have shipped, because it passes every test written by someone sitting in the same timezone as their server — which is to say, every test I would have thought to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. the guardrail worked; the output didn't
&lt;/h2&gt;

&lt;p&gt;The agent signed a draft &lt;code&gt;[Operator]&lt;/code&gt;. A literal placeholder, in an email a human was one click from sending.&lt;/p&gt;

&lt;p&gt;Nothing was broken. The escalation logic was working perfectly the entire time. The tool returned &lt;code&gt;ok=True&lt;/code&gt;, because the API call genuinely succeeded. A tool reporting success tells you the call went through, not that what came back was any good — and nobody had read the output yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. i proved it, wrote it down, and shipped it anyway
&lt;/h2&gt;

&lt;p&gt;This is the one I'd lead with.&lt;/p&gt;

&lt;p&gt;Pub/Sub delivers at least once, so every event eventually arrives twice. For an agent with side effects that's the difference between one draft and two. I handled it properly: before doing anything, the router performs an atomic Firestore &lt;code&gt;create()&lt;/code&gt; on a key derived from the event. The create fails if the key exists, and the failure is atomic, so there's no read-then-write race. I was pleased enough with this that I put the formula in the write-up.&lt;/p&gt;

&lt;p&gt;Then I sent two emails at once and the agent wrote two proposal documents for one of them.&lt;/p&gt;

&lt;p&gt;Gmail sends one notification per arriving message. Two emails, two notifications — and both replay the same history window, so both surface &lt;em&gt;both&lt;/em&gt; messages. I'd keyed the claim on &lt;code&gt;historyId + messageId&lt;/code&gt;. Two views of one message, two different keys, no collision, two drafts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The lock worked exactly as designed. It was guarding the wrong door.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lesson isn't "handle at-least-once delivery." I &lt;em&gt;had&lt;/em&gt; handled it, and I had a proof. It's that a deduplication key has to name &lt;strong&gt;the thing with the side effect&lt;/strong&gt;, not the thing that told you about it. I'd tested Pub/Sub redelivery, which is the easy case and the one I'd thought of. The case that broke it only appears when two messages arrive close enough together to overlap their history windows — which is exactly what a demo does, and exactly what a quiet test inbox never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  the pattern
&lt;/h2&gt;

&lt;p&gt;Six of seven failures produced no error. Not a caught exception, not a warning, not a log line. Several produced output that looked &lt;em&gt;correct&lt;/em&gt;: meeting times labelled with the right timezone, a deploy printing three healthy service URLs, a cron job sitting in the console marked &lt;code&gt;ENABLED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The common thread is that distributed systems fail by omission far more than by exception. A missing IAM grant and a still-propagating IAM grant are indistinguishable. A created cron job and a working cron job look identical until the day you need it. A tool returning &lt;code&gt;ok&lt;/code&gt; and a tool returning something useful are different claims.&lt;/p&gt;

&lt;p&gt;Which means the only thing that actually found these was running the real thing against real data and reading the output. Not tests — my tests passed throughout. Watching it work, end to end, and being suspicious of success.&lt;/p&gt;

&lt;h2&gt;
  
  
  what i'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never write &lt;code&gt;|| true&lt;/code&gt; without logging what it swallowed.&lt;/strong&gt; That one line cost me a disconnected pipeline that looked deployed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume IAM lags.&lt;/strong&gt; Poll for the resource before binding to it, instead of assuming that created means usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test in the environment's timezone, not yours.&lt;/strong&gt; Or better: refuse to have a default. Business hours without a named zone are meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the agent's actual output.&lt;/strong&gt; Every run. &lt;code&gt;ok=True&lt;/code&gt; is not a quality signal, and the placeholder signature proved it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name your idempotency key after the side effect.&lt;/strong&gt; Not after the notification, not after the request. After the thing you'd hate to do twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent works now. It reads a real inbox, does real multi-step work on real APIs, and stops to ask a human when it isn't sure. That last behaviour is the one I'm most pleased with, and it's the only one that never broke.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>backend</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
