<?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: Somesh Bhardwaj</title>
    <description>The latest articles on DEV Community by Somesh Bhardwaj (@devsomesh).</description>
    <link>https://dev.to/devsomesh</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%2F3231175%2F21afff20-295d-4021-9d64-00241c858e91.jpeg</url>
      <title>DEV Community: Somesh Bhardwaj</title>
      <link>https://dev.to/devsomesh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devsomesh"/>
    <language>en</language>
    <item>
      <title>Five dashboards nobody was opening</title>
      <dc:creator>Somesh Bhardwaj</dc:creator>
      <pubDate>Wed, 23 Sep 2026 07:46:34 +0000</pubDate>
      <link>https://dev.to/devsomesh/five-dashboards-nobody-was-opening-49k5</link>
      <guid>https://dev.to/devsomesh/five-dashboards-nobody-was-opening-49k5</guid>
      <description>&lt;p&gt;The ask was small. Once a month, post a summary of how the website is doing into Slack, so nobody has to log into five separate tools to find out.&lt;/p&gt;

&lt;p&gt;Five tools, because that is what measuring a website honestly takes now: analytics for traffic, Search Console for how people arrive, a second cookieless analytics tool as a cross-check, the email platform for sign-ups, and the error tracker for whether anything is broken. Each has its own login, its own interface, and its own way of framing the same question. The result was predictable: most months, nobody checked.&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%2F5giwdnvna3eoe41wdelh.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%2F5giwdnvna3eoe41wdelh.png" alt="Diagram" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The report is the easy half
&lt;/h2&gt;

&lt;p&gt;Reading five APIs and formatting a message is an afternoon. What took the time was deciding what each number could honestly be said to mean, and then building something that would keep being true when one of the five had a bad day.&lt;/p&gt;

&lt;p&gt;The message ended up in four sections: traffic, search, sign-ups, and site health. Each leads with the figure that matters, set as a label above a value rather than written out as a sentence. That sounds cosmetic and is not. A paragraph asks to be read; a label and a number can be taken in at a glance, and the whole point was that this be readable in thirty seconds by someone who was never going to open a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every source fails on its own
&lt;/h2&gt;

&lt;p&gt;The naive shape is one function that fetches five things and posts the result. It works until an API is slow, and then the whole run throws, nothing is posted, and nobody notices for a month. A monthly job is the worst possible place for a silent failure, because the feedback loop is thirty days long.&lt;/p&gt;

&lt;p&gt;So each source is fetched independently and wrapped. If one is unavailable, its section says so in one honest line and the other four still post. Partial truth beats silence, and the line saying "search figures unavailable this month" is itself useful information, where an absent message is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failing closed, and failing open, on purpose
&lt;/h2&gt;

&lt;p&gt;The endpoint that triggers the report refuses to run without its shared secret, compared in constant time. The bot check on the site's signup form does the opposite: if the verification service is unreachable, the form submits anyway.&lt;/p&gt;

&lt;p&gt;Those are opposite choices and both are right. Failing open on a bot check costs you a bot check, and blocking real sign-ups during somebody else's outage is the worse outcome. Failing open on a write endpoint hands anyone who finds the path the ability to post into a team channel. The question is never "should this fail open or closed" in the abstract. It is what each failure actually costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pause switch that survives a deploy
&lt;/h2&gt;

&lt;p&gt;The schedule lives in version-controlled config. Turning it off in the hosting dashboard therefore lasts exactly until the next deploy puts it back, which is the kind of thing you discover by watching someone stop it three times and wondering why it keeps returning.&lt;/p&gt;

&lt;p&gt;The durable control is an environment variable, precisely because it is not in git. Anything you can disable from a dashboard but that also exists in the repository will be re-enabled by the repository eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting the right thing
&lt;/h2&gt;

&lt;p&gt;Two small decisions did more for the report's credibility than anything else.&lt;/p&gt;

&lt;p&gt;The error tracker reports a lifetime count per issue, not a count for the period you asked about. Summing those into a monthly report would re-report every historical occurrence every month, so the number could only ever climb, and a rising error count that means nothing is worse than no error count at all. The report counts issues active in the window instead.&lt;/p&gt;

&lt;p&gt;And the errors are classified into ours and not ours with a deliberately tiny allowlist, everything unrecognised defaulting to ours. The temptation is the reverse, because a generous "not our problem" rule makes the report look calmer. It also buries things. A false "needs a look" costs somebody a glance. A false "not our problem" costs weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found on the way
&lt;/h2&gt;

&lt;p&gt;Wiring up the sources turned up a live failure that had nothing to do with reporting: the newsletter form on the homepage had been rejecting most attempts since launch. The bot-check token is minted at page load and expires after a few minutes, and that form sits at the bottom of the longest page on the site, so anyone who actually read on the way down arrived with an expired token. The fix was to mint the token at submission instead of trusting the one from page load.&lt;/p&gt;

&lt;p&gt;The evidence for that was strong, from event data and the error tracker, though the expiry was never reproduced in a live session, and the failure rates come from event tracking rather than reconciled records. So the fix is the durable part and the numbers are not quoted.&lt;/p&gt;

&lt;p&gt;Two monitoring systems had been recording it the whole time. Nobody was reading either of them, which is the same problem the digest exists to solve, arriving from a different direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;A scheduled function, five sources read in parallel in under ten seconds, one Slack message in a workspace the team already uses, and a runbook covering every credential, every failure mode hit while building it, and how to pause and resume it safely. That last part is not documentation for its own sake: an automation only one person can operate is a liability wearing the costume of an asset.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>operations</category>
      <category>slack</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Six agents were running and I could not tell you what any of them did</title>
      <dc:creator>Somesh Bhardwaj</dc:creator>
      <pubDate>Sat, 05 Sep 2026 06:50:08 +0000</pubDate>
      <link>https://dev.to/devsomesh/six-agents-were-running-and-i-could-not-tell-you-what-any-of-them-did-1a47</link>
      <guid>https://dev.to/devsomesh/six-agents-were-running-and-i-could-not-tell-you-what-any-of-them-did-1a47</guid>
      <description>&lt;p&gt;Six coding agents were running. I could not tell you what any of them had done.&lt;/p&gt;

&lt;p&gt;Not roughly. Not approximately. The output was there, the files had changed, and the honest answer to "which one did that" was a shrug. Three questions in particular had no answer: which run burned the tokens, whether they genuinely ran at the same time or merely started together, and whether two of them had quietly edited the same file.&lt;/p&gt;

&lt;p&gt;That last one is the expensive question. An agent working on the wrong file looks exactly like an agent working on the right one, right up until you read the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that was already true
&lt;/h2&gt;

&lt;p&gt;Every one of those runners writes a transcript to disk while it works. Claude Code does. So do Cursor, Codex, Gemini CLI, Copilot CLI and Kiro. The record of what happened was sitting in my home directory the entire time, in six different formats, none of which I had ever looked at.&lt;/p&gt;

&lt;p&gt;So runlanes does not wrap anything. There is no SDK, no instrumentation step, no account, and nothing to start before the run starts. It reads what the runner already wrote.&lt;/p&gt;

&lt;p&gt;The consequence is the part I did not expect to matter as much as it does: &lt;strong&gt;it works on runs that already finished.&lt;/strong&gt; Most tools in this space need you to have decided, in advance, that this particular run was worth watching. This one can answer a question you only thought to ask afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx runlanes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens a console on 127.0.0.1:4180 for whatever project you are standing in. There is no configuration file to write first.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What it actually shows
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt; is every live session, across every runner it found, with what the main conversation spent against what it handed to subagents. On the session that motivated the whole thing, that split was 8.3 million tokens of conversation against 2.1 million delegated, which was not the ratio I would have guessed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhn1r8md6l2mbsbhkegrg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhn1r8md6l2mbsbhkegrg.webp" alt="The Now tab: live sessions, spend, context and parallelism" width="800" height="1483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The parallelism figure is the one I keep coming back to. Peak concurrency was four agents. The share of elapsed time where anything genuinely overlapped was &lt;strong&gt;9%&lt;/strong&gt;. Four agents were running, and for 91% of the wall clock they were politely taking turns. Nobody reports that number, and it changes how you plan a fan-out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graph&lt;/strong&gt; links runs to the files that more than one of them touched.&lt;/p&gt;

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

&lt;p&gt;The distinction that makes this worth having: an edge exists because a run opened that file. Not because a prompt said it would. Those are different claims, and only one of them is evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;History&lt;/strong&gt; is every session found for the project, with spend and agent time. &lt;strong&gt;Docs&lt;/strong&gt; is the plans, skills and agent instructions sitting beside the project, which turn out to be the thing you most want to read when a run has gone sideways.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Refusing to make numbers up
&lt;/h2&gt;

&lt;p&gt;Kiro bills in credits, not tokens. Its transcripts record which files were touched and which documents were read, and no usage at all.&lt;/p&gt;

&lt;p&gt;The console shows a dash.&lt;/p&gt;

&lt;p&gt;It would be trivially easy to show &lt;code&gt;0&lt;/code&gt; there. It would also be a lie of the most ordinary kind, the sort that makes a dashboard feel authoritative and quietly poisons every total on the page. An unmeasured run is not a free run. It is left out of the sums rather than counted as nothing.&lt;/p&gt;

&lt;p&gt;The same instinct runs through the token arithmetic. Tokens counted are input plus output plus cache creation. Cache &lt;em&gt;reads&lt;/em&gt; are deliberately excluded, because they re-report the entire prompt on every single turn, and summing them across a session counts the same context dozens of times and produces a number several times larger than anything that happened. They are still included in cost, because they are still billed. Those are two different questions and the tool answers them separately.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The claims are tests, not sentences
&lt;/h2&gt;

&lt;p&gt;runlanes reads local files, makes no outbound network calls, and binds to 127.0.0.1. Every tool in this category says something like that in its README.&lt;/p&gt;

&lt;p&gt;The difference here is that &lt;strong&gt;CI fails the build if any of it stops being true.&lt;/strong&gt; There is a step that greps the source for &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;axios&lt;/code&gt; and &lt;code&gt;https.request&lt;/code&gt; and fails if it finds one. A step that asserts 127.0.0.1 still appears in the server. A step that fails if a runtime dependency ever appears, which is how the dependency count stays at zero rather than aspirationally low. And one that fails if a vendor name leaks outside the adapter directory, which is what keeps six runners from turning into six special cases scattered through the app.&lt;/p&gt;

&lt;p&gt;A README claim is a sentence somebody wrote once. A CI step is a claim that has to survive every commit. I would rather ship the second kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the package came from
&lt;/h2&gt;

&lt;p&gt;Published from GitHub Actions through OIDC trusted publishing, so npm records the commit, the workflow and the runner that built the tarball. &lt;code&gt;npm audit signatures&lt;/code&gt; reports a verified attestation, and anyone can check that what is on npm matches what is on GitHub. Sigstore signs it with a certificate that lives about ten minutes and files the record in a public transparency log, so there is no long-lived signing key to steal.&lt;/p&gt;

&lt;p&gt;For a tool whose entire argument is "these numbers survive checking", it seemed inconsistent to ask anyone to take the tarball on faith.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo is the product
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;runlanes --export&lt;/code&gt; writes the whole console to a single self-contained HTML file. The &lt;a href="https://dev-somesh.github.io/runlanes/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; is that export, rebuilt by CI whenever the source changes. It is not a screenshot and not a mock, which means a change that breaks the console breaks the demo in CI before it reaches anybody.&lt;/p&gt;

&lt;p&gt;The demo also says in its first line that its runs are staged. A tool arguing that its numbers can be checked should not open with invented data presented as real.&lt;/p&gt;

&lt;h2&gt;
  
  
  It was called something else
&lt;/h2&gt;

&lt;p&gt;This shipped as "agenttrace" for about a fortnight. Then I looked properly, and five published CLIs already install a binary by that name, one of them with a near-copy of my own opening line in its README.&lt;/p&gt;

&lt;p&gt;The registry entry was still free. Taking it would have meant winning the package name while losing the command name, the search result and the repository name, which is losing three arguments to win one.&lt;/p&gt;

&lt;p&gt;"runlanes" is also just a better description. The timeline draws one lane per run, and whether those lanes overlap is the entire question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx runlanes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT, Node 18 and up, zero runtime dependencies, 95 tests running on Node 18, 20 and 22. The &lt;a href="https://github.com/Dev-Somesh/runlanes" rel="noopener noreferrer"&gt;source is on GitHub&lt;/a&gt; and the &lt;a href="https://www.npmjs.com/package/runlanes" rel="noopener noreferrer"&gt;package is on npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run more than one agent at a time, the data is already on your disk. This just reads it.&lt;/p&gt;

</description>
      <category>developertools</category>
      <category>aiagents</category>
      <category>observability</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Routing email into Slack, which is not the same as forwarding it</title>
      <dc:creator>Somesh Bhardwaj</dc:creator>
      <pubDate>Sat, 05 Sep 2026 06:50:05 +0000</pubDate>
      <link>https://dev.to/devsomesh/routing-email-into-slack-which-is-not-the-same-as-forwarding-it-19mk</link>
      <guid>https://dev.to/devsomesh/routing-email-into-slack-which-is-not-the-same-as-forwarding-it-19mk</guid>
      <description>&lt;p&gt;Every organisation that works in Slack still has email arriving somewhere else. Vendor notifications, form submissions, the address a partner replies to. The work is in one place and a meaningful slice of the information about it is in another, which someone checks when they remember.&lt;/p&gt;

&lt;p&gt;The obvious fix is forwarding. Point the mailbox at a Slack channel and let the integration post everything. It takes an afternoon, and it fails in a way that is worth describing precisely, because the failure is not "it did not work". It is that it worked exactly as specified and made things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What forwarding actually produces
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Truncated bodies.&lt;/strong&gt; The integration posts a preview. The part of the email that says what to do is below the fold, so every message becomes a link to the thing you actually needed, and the channel is a table of contents for an inbox people are still opening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reply chains, repeatedly.&lt;/strong&gt; A thread with six replies does not arrive as one conversation. It arrives as six posts, each quoting all the previous ones, so the channel fills with the same text at increasing lengths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-replies.&lt;/strong&gt; Out-of-office, delivery receipts, no-reply confirmations. None of it is work and all of it arrives with the same weight as the message from a partner asking a real question.&lt;/p&gt;

&lt;p&gt;The predictable outcome is that the channel gets muted, and now the information is in two places neither of which anyone is reading.&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%2Fx66vd4inmyq6kslgnnjp.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%2Fx66vd4inmyq6kslgnnjp.png" alt="Diagram" width="799" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing, not forwarding
&lt;/h2&gt;

&lt;p&gt;The distinction that makes this work: a router decides what belongs in Slack and in what shape, rather than moving everything and hoping the reader filters. Concretely that means four things the forwarding integration does not do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deduplication on the RFC Message-ID.&lt;/strong&gt; Every email carries a globally unique &lt;code&gt;Message-ID&lt;/code&gt; header, and replies carry &lt;code&gt;In-Reply-To&lt;/code&gt; and &lt;code&gt;References&lt;/code&gt; pointing back at what they answer. That is the correct identity for a message, rather than a hash of subject and sender, which collides on exactly the automated mail you receive most. The router keeps a seven-day lookback of what it has already posted, which is long enough to cover a thread going quiet over a weekend and short enough that the state stays small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread and reply detection.&lt;/strong&gt; Because the headers say what answers what, a reply can be posted into the Slack thread of the message it answers instead of starting a new one. The conversation in Slack has the shape of the conversation in the mailbox, which is the whole point and is the thing forwarding cannot do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Body extraction rather than preview.&lt;/strong&gt; Multipart MIME, quoted history, signature blocks and tracking pixels are all separable from the sentence somebody actually wrote. Extracting that means the Slack message is the message, and nobody has to open the mail client to find out what was being asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Noise suppression.&lt;/strong&gt; Auto-replies and no-reply senders are identifiable before posting, not after. They are dropped rather than muted, because a muted channel is a solved problem that will unsolve itself the moment the mute is lifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that decides whether people trust it
&lt;/h2&gt;

&lt;p&gt;The router processes in controlled batches and &lt;strong&gt;preserves unread state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That sounds like an implementation footnote and it is the reason the system is used. If reading the mailbox programmatically marks messages read, the router has silently taken over as the only reader, and the person who owns that inbox has lost the ability to work it directly. They will not say this explicitly. They will just stop relying on it, because something is now touching their mail in a way they did not ask for and cannot see.&lt;/p&gt;

&lt;p&gt;An automation that changes state it was not asked to change is not a convenience. Leaving the mailbox exactly as it found it is what makes the router additive rather than a takeover, and it is the difference between a tool people keep and a tool people quietly route around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits now
&lt;/h2&gt;

&lt;p&gt;Operational email is owned in Slack, where the team already works, rather than in a side inbox somebody checks. Threads look like threads. Auto-replies do not arrive. The mailbox still works normally for anyone who wants to open it.&lt;/p&gt;

&lt;p&gt;The general principle, which applies well beyond email: moving information between systems is easy, and almost never the problem. The problem is that the receiving system has a shape, and information that arrives in the wrong shape is worse than information that did not arrive, because it costs attention before it can be ignored.&lt;/p&gt;

</description>
      <category>slack</category>
      <category>gmail</category>
      <category>automation</category>
      <category>operations</category>
    </item>
    <item>
      <title>The message that mentioned finance and tagged nobody</title>
      <dc:creator>Somesh Bhardwaj</dc:creator>
      <pubDate>Sat, 05 Sep 2026 06:50:03 +0000</pubDate>
      <link>https://dev.to/devsomesh/the-message-that-mentioned-finance-and-tagged-nobody-3mpg</link>
      <guid>https://dev.to/devsomesh/the-message-that-mentioned-finance-and-tagged-nobody-3mpg</guid>
      <description>&lt;p&gt;Somebody types "can we loop in finance on this?" in a product channel. Nobody tags the finance channel. The thread moves on. Three weeks later there is a contract nobody in finance has seen.&lt;/p&gt;

&lt;p&gt;That is not a tooling problem in any obvious sense. Slack worked exactly as designed. Search would have found the message if anyone had known to look for it. The failure is that the people who needed to know were never told, and nothing in the workspace was watching for the difference between mentioning a team and involving one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why keyword matching does not solve this
&lt;/h2&gt;

&lt;p&gt;The instinct is to grep for the word "finance" and alert on it. That produces a channel nobody reads inside a week, because "finance" appears in sentences that have nothing to do with governance, and the sentences that do matter often do not contain the word at all.&lt;/p&gt;

&lt;p&gt;What actually carries the signal is structure. A Slack message is not plain text on the wire. When someone references a channel, it arrives looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can we loop in &amp;lt;#C01ABCDEF|finance&amp;gt; before this goes out?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a channel &lt;em&gt;reference&lt;/em&gt;, distinct from a mention that notifies the channel, and it survives in the event payload whether or not anyone was actually alerted. It means the workspace already knows the difference between "I said the word finance" and "I pointed at the finance channel and did not bring anyone in". Nobody was reading it.&lt;/p&gt;

&lt;p&gt;So the bot parses references rather than words. It maps channel IDs to what those channels are for, and it looks for the specific shape of a message that points at a governance channel from outside it. Context beats keywords, and in this case the context was already structured and already being thrown away.&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%2Fjlm48fr54v4d1zi6okvz.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%2Fjlm48fr54v4d1zi6okvz.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two decisions that mattered more than the detection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It joins every public channel by itself.&lt;/strong&gt; The obvious build asks an admin to add the bot wherever it should watch, which means coverage is a function of somebody remembering. Every channel created after launch is a gap, and nobody finds out until something is missed in one. Instead it lists the public channels through the API and joins them, then listens for &lt;code&gt;channel_created&lt;/code&gt; and joins new ones as they appear. Coverage is complete by construction rather than by diligence, across more than a hundred channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs on Socket Mode.&lt;/strong&gt; The usual Slack integration takes an inbound webhook, which means a public HTTPS endpoint that anyone can reach and that has to be secured, monitored and kept alive. Socket Mode reverses the direction: the bot opens an outbound connection and Slack pushes events down it. For an organisation that would otherwise be exposing a new inbound surface to catch its own internal messages, that trade is worth taking on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shaped the deployment
&lt;/h2&gt;

&lt;p&gt;Socket Mode has a consequence that only shows up on deploy. Because the bot never receives inbound HTTP, it never binds a port, and the platform hosting it was watching for exactly that to decide whether the service was alive. A correctly working bot looked, to the host, like a process that had failed to start.&lt;/p&gt;

&lt;p&gt;The fix is a sidecar: a minimal HTTP server inside the same process whose only job is to answer a health check.&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="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="s2"&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="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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bot is running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten lines that do nothing for the product and without which the product does not stay running. It is worth saying plainly, because this is the part that never appears in an architecture diagram and is most of what deployment actually is: the platform has an opinion about what a healthy service looks like, and you either meet it or you do not run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scopes, deliberately narrow
&lt;/h2&gt;

&lt;p&gt;The bot holds &lt;code&gt;channels:join&lt;/code&gt;, &lt;code&gt;channels:history&lt;/code&gt; and &lt;code&gt;chat:write&lt;/code&gt;. Public channels only, no user impersonation, no private conversations, no ability to act as anyone. A tool that reads a workspace to improve governance has to be governable itself, and the smallest scope set that does the job is the one that survives the security review it will eventually get.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it changed
&lt;/h2&gt;

&lt;p&gt;Coverage went from whichever channels someone remembered to whichever channels exist. Finance mentions that previously depended on a person noticing now arrive in one place with the surrounding context attached. The work of watching moved off people and into something that does not get busy or go on leave.&lt;/p&gt;

&lt;p&gt;The wider lesson is about where automation should sit. This one requires no configuration, adapts to new channels on its own, and nobody in the workspace has to change how they write. The best version of a system like this is one the team never thinks about, which is also the version that is hardest to justify building, because the evidence of it working is the absence of an expensive surprise.&lt;/p&gt;

</description>
      <category>slack</category>
      <category>typescript</category>
      <category>integrations</category>
      <category>governance</category>
    </item>
    <item>
      <title>A small event tool, and the day it stopped being small</title>
      <dc:creator>Somesh Bhardwaj</dc:creator>
      <pubDate>Fri, 13 Mar 2026 12:00:33 +0000</pubDate>
      <link>https://dev.to/devsomesh/how-my-simple-side-project-mutated-into-a-full-blown-saas-orchestration-engine-55jo</link>
      <guid>https://dev.to/devsomesh/how-my-simple-side-project-mutated-into-a-full-blown-saas-orchestration-engine-55jo</guid>
      <description>&lt;p&gt;The task was scoped as a small automation: North America only, one kind of event, some registrations and a confirmation email. The kind of thing a spreadsheet and a script would cover.&lt;/p&gt;

&lt;p&gt;It was not that, and the tell came early. The events were multi-session, the attendees were educators across several timezones, and completion had to produce a certification that somebody would later rely on. Each of those is survivable alone. Together they mean state: who registered for which session, who actually attended, what that leaves outstanding, and what has to happen next and when. A spreadsheet holds state right up until two things change at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54qv6z1nl35wcv9iha2g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54qv6z1nl35wcv9iha2g.webp" alt="From a simple event tool to an integration-orchestrated platform" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the complexity actually lives
&lt;/h2&gt;

&lt;p&gt;Almost none of it is in the event model. Events, sessions and registrations are a straightforward schema, and if that were the whole job the original estimate would have been right.&lt;/p&gt;

&lt;p&gt;The complexity is that nearly every meaningful fact arrives from a system you do not control. Attendance lives in the video platform. Whether a reminder was delivered lives in the email platform. Segmentation lives in the marketing tool. Whether the event is on somebody's calendar depends on an ICS file being parsed correctly by a client you have never tested.&lt;/p&gt;

&lt;p&gt;Each of those has its own model of a person, its own idea of time, and its own failure modes. The platform's real work is translation: keeping one coherent account of what happened while four external systems each report a partial and occasionally contradictory version of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q49jcfcj1771r96yrid.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q49jcfcj1771r96yrid.webp" alt="The production architecture, layer by layer" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is why the architecture ends up with an explicit translation boundary rather than external calls scattered through the features. When the video platform changes what it returns, the change lands in one adapter instead of in every place attendance is touched. This is the same instinct as any anti corruption layer: external models do not get to dictate internal ones, because you will be living with your internal model long after the external one has been revised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attendance is not a boolean
&lt;/h2&gt;

&lt;p&gt;Zoom knows who joined. It does not know who they are in your system, and the join records do not reliably line up with your registrations. People join from a second device. They join late from a phone with a display name their parent chose. They attend a session they never registered for because a colleague forwarded the link.&lt;/p&gt;

&lt;p&gt;Automatic matching handles most of it. The decision that mattered was building a manual match path for the rest, rather than either guessing or dropping them. A certification is a claim someone will make about themselves later, possibly to an employer. It should not rest on fuzzy name matching being confident enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reminders as scheduled work, not delayed jobs
&lt;/h2&gt;

&lt;p&gt;Reminders go out at seven days, twenty-four hours and one hour. The naive version schedules three delayed jobs per registration at signup and hopes nothing changes.&lt;/p&gt;

&lt;p&gt;Things change. Sessions move, people cancel, someone registers ninety minutes before a session starts and needs the one-hour reminder immediately or not at all. The reliable shape is a background process that periodically evaluates current state and asks what should be sent now, rather than a queue of promises made under conditions that no longer hold. It costs more to build and it survives a rescheduled event, which the delayed-job version does not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6ie4bovv9imfeynzt8t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6ie4bovv9imfeynzt8t.webp" alt="Background processing and the automation layer" width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifteen days, and the conversation about day sixteen
&lt;/h2&gt;

&lt;p&gt;It shipped in about fifteen days as sole engineer and went into production for real educator cohort trainings, iterated under live testing first: timezone and ICS handling, mobile, and simplification of an admin UI that had grown to match the domain rather than the person using it.&lt;/p&gt;

&lt;p&gt;The part worth writing down is what happened next. The work had clearly outgrown what was contracted, and there were two available responses: absorb it quietly, or say so. I raised it in writing and we agreed a time-boxed stabilisation window instead of an open-ended tail.&lt;/p&gt;

&lt;p&gt;That is not a technical decision but it determined the outcome more than most of the technical ones. Scope creep on a fixed engagement is rarely a single conversation anybody refused to have. It is a series of small unremarked absorptions, each individually reasonable, and the project ends in a place neither side would have chosen deliberately. Naming it early is cheaper for the client than discovering it late, and it is the difference between a platform that gets finished and one that gets abandoned in a good-enough state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell the version of me on day one
&lt;/h2&gt;

&lt;p&gt;The estimate was not wrong about the event tool. It was wrong about how many systems had to agree for the event tool to be true. If a scope mentions attendance, certification and reminders in the same sentence, the build is an integration platform wearing a smaller job's clothes, and the honest estimate is the one that prices the translation layer rather than the schema.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>integrations</category>
      <category>saas</category>
      <category>scope</category>
    </item>
    <item>
      <title>A portfolio chatbot that can drive the page it lives on</title>
      <dc:creator>Somesh Bhardwaj</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:38:12 +0000</pubDate>
      <link>https://dev.to/devsomesh/building-an-ai-powered-portfolio-with-react-19-vite-and-gemini-25-flash-3ij2</link>
      <guid>https://dev.to/devsomesh/building-an-ai-powered-portfolio-with-react-19-vite-and-gemini-25-flash-3ij2</guid>
      <description>&lt;p&gt;Most portfolio chatbots are a search box that answers in paragraphs. You ask what someone has worked on, you get prose, and then you scroll to find it yourself. The model knows the answer and the page it is sitting on stays exactly where it was.&lt;/p&gt;

&lt;p&gt;The one on this site can move the page. Ask to see incident work and it opens the record filtered to incidents. Ask about a section and it scrolls there. That is not a different model, it is the same tool-calling mechanism everyone already has, pointed at the interface rather than at a knowledge base.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key stays on the server, and that decides the architecture
&lt;/h2&gt;

&lt;p&gt;The first real decision is not about the model. If the browser calls the model provider directly, the API key is in the browser, and anybody who opens devtools has your billing. Every "add AI to your site in five minutes" guide that keeps the key client-side is teaching a mistake that only shows up on an invoice.&lt;/p&gt;

&lt;p&gt;So the request goes to a serverless function, which holds the key, attaches the system instruction and the tool declarations, calls the model, and streams the answer back. The client sends the conversation and nothing else. That single constraint determines most of the rest of the design, and it is worth accepting it up front rather than discovering it later.&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%2Fzl9ckhfsf3vgvxflrpq7.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%2Fzl9ckhfsf3vgvxflrpq7.png" alt="Diagram" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It has a second effect that is easy to miss: because the system instruction is assembled server-side, it can be changed without shipping a new bundle, and a visitor cannot read it. The prompt is not secret, but it is also not something to hand out with the JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five tools, and what they are for
&lt;/h2&gt;

&lt;p&gt;Four of them do something to the page: search the work, open the record with an optional category filter, scroll to a section, toggle the theme. One of them, &lt;code&gt;saveLead&lt;/code&gt;, records that a visitor wants to be contacted.&lt;/p&gt;

&lt;p&gt;The declarations are the interesting part, because they are prompt engineering wearing a schema. The description on &lt;code&gt;openWork&lt;/code&gt; does not just say what it does, it says when to reach for it and names the one argument value that will not work. A tool whose description reads "opens the record" gets called at the wrong moments; a tool that explains its own preconditions gets called at the right ones. Most of the behaviour people attribute to the system prompt is actually in the tool descriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval that is deliberately not a vector database
&lt;/h2&gt;

&lt;p&gt;Before the model answers, a retrieval step pulls the entries most relevant to the question and puts them in the prompt. It is a substring matcher over the record. There are no embeddings and no vector store.&lt;/p&gt;

&lt;p&gt;For a few dozen entries with distinctive names, that is the correct engineering. Embeddings would add an index to build, a service to depend on, a rebuild step to forget, and a class of failure where the nearest neighbour is confidently wrong. The substring matcher is inspectable, and when it misses, it misses for a reason you can read.&lt;/p&gt;

&lt;p&gt;What it does badly is fail silently. Rename a record or change the category vocabulary and retrieval quietly returns less, and the bot gets vaguer rather than erroring. Nothing in the response tells you this happened. That is precisely why the retrieval layer has tests: not because the matching is subtle, but because its failure mode is degradation rather than a stack trace, and degradation does not page anyone.&lt;/p&gt;

&lt;p&gt;The same reasoning covers the couplings between the bot and the site. The categories in the tool declarations have to match the categories in the data, and the section names it can scroll to have to match the sections that exist. Both are asserted in tests, because the alternative is a chatbot confidently offering to scroll somewhere that was renamed six commits ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lead flow, and the gate before it
&lt;/h2&gt;

&lt;p&gt;The tempting build fires &lt;code&gt;saveLead&lt;/code&gt; the moment it has five fields. That produces a form wearing a conversation, and it submits while the visitor is still mid-thought.&lt;/p&gt;

&lt;p&gt;Instead the model has to ask one closing question first, confirming there is nothing else to add, and only then calls the tool. The reply that follows carries a request ID, confirms where the transcript was sent, and gives an address to follow up on with that ID, so the visitor leaves with a reference rather than a hope.&lt;/p&gt;

&lt;p&gt;That gate is one paragraph in the instruction and it is the difference between a conversation that captures a lead and an interrogation that happens to be written in sentences.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep
&lt;/h2&gt;

&lt;p&gt;Tool calling for interface control rather than only for retrieval. The key on the server, without exception. The smallest retrieval that works, with tests around it precisely because it fails quietly. And a persona that positions the work honestly: the site argues that AI is one tool inside systems work rather than the identity, and a chatbot that oversold itself would contradict the page it lives on.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>toolcalling</category>
      <category>netlify</category>
      <category>retrieval</category>
    </item>
  </channel>
</rss>
