<?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: Alex Shev</title>
    <description>The latest articles on DEV Community by Alex Shev (@alexshev).</description>
    <link>https://dev.to/alexshev</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%2F3812101%2F0946c1f5-f8a5-464c-bf18-10850a434ef9.png</url>
      <title>DEV Community: Alex Shev</title>
      <link>https://dev.to/alexshev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexshev"/>
    <language>en</language>
    <item>
      <title>How to Build a Local-Service Site That Can Answer ‘Can You Fix My RV Today?’</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Sat, 22 Aug 2026 15:16:54 +0000</pubDate>
      <link>https://dev.to/alexshev/how-to-build-a-local-service-site-that-can-answer-can-you-fix-my-rv-today-5ad3</link>
      <guid>https://dev.to/alexshev/how-to-build-a-local-service-site-that-can-answer-can-you-fix-my-rv-today-5ad3</guid>
      <description>&lt;p&gt;An RV repair business does not lose a service call because a visitor failed to read a clever headline.&lt;/p&gt;

&lt;p&gt;It loses the call when a person with a broken slide-out, roof leak, or electrical issue cannot answer four basic questions quickly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do you handle this exact problem?&lt;/li&gt;
&lt;li&gt;Do you serve where I am?&lt;/li&gt;
&lt;li&gt;Are you available and credible?&lt;/li&gt;
&lt;li&gt;What do I do next?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds like marketing. It is mostly a systems-design problem.&lt;/p&gt;

&lt;p&gt;The implementation goal is not “make more city pages.” It is to make the business's real-world facts available, consistent, crawlable, and usable across the website, Google Business Profile, analytics, and the conversion flow.&lt;/p&gt;

&lt;p&gt;This post turns SEOG’s RV repair checklist into an implementation pattern a developer can apply to any local-service site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model: one source of truth, many decision surfaces
&lt;/h2&gt;

&lt;p&gt;Local customers do not encounter a business in one place. They may see a Google result, a Maps profile, a service page, a review, or a call button before they ever submit a form.&lt;/p&gt;

&lt;p&gt;Treat the site as one consumer of a small, canonical business data model rather than a collection of independently written pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;business facts ─┬─&amp;gt; server-rendered service pages
                ├─&amp;gt; JSON-LD
                ├─&amp;gt; XML sitemap + canonical URLs
                ├─&amp;gt; GBP sync/review queue (with human approval)
                ├─&amp;gt; call/form events
                └─&amp;gt; audit and change history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the left side. If a mobile RV technician's phone number, service coverage, repair categories, and hours live in five unrelated CMS fields, a mismatch is inevitable.&lt;/p&gt;

&lt;p&gt;Start with an explicit domain object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BusinessLocation&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;legalName&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;publicName&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;phoneE164&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;website&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;address&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;streetAddress&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;addressLocality&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;addressRegion&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;postalCode&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;addressCountry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US&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="nl"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&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;longitude&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;serviceAreas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;state&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;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dayOfWeek&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;opens&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;closes&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slug&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;name&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;customerProblem&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;evidence&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;emergency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="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;proof&lt;/code&gt; and &lt;code&gt;evidence&lt;/code&gt; are deliberate fields. They prevent the familiar anti-pattern where a generator creates a page merely because a city name and a service name exist in a spreadsheet. A service-area claim should be backed by something real: a dispatch region, technician coverage, case work, a physical facility, or a documented operating policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build pages from real combinations, not every possible combination
&lt;/h2&gt;

&lt;p&gt;A useful URL is not a matrix cell.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/rv-roof-repair/mesa-az&lt;/code&gt; should exist only if the business actually serves Mesa for roof repair and the page can provide information a customer cannot get from a generic page.&lt;/p&gt;

&lt;p&gt;For a Next.js application, make the eligibility rule executable instead of leaving it to editorial memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canPublishServiceAreaPage&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;BusinessLocation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;services&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BusinessLocation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceAreas&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBusinessLocation&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;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&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="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceAreas&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;area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;canPublishServiceAreaPage&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;area&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;service&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;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;slugify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;This is a product guardrail, not an SEO trick. Google’s spam policies explicitly call out substantially similar regional pages that funnel users toward one destination as doorway abuse. A page needs its own user value, not just a different token in the title. &lt;a href="https://developers.google.com/search/docs/essentials/spam-policies#doorway-abuse" rel="noopener noreferrer"&gt;Google’s policy&lt;/a&gt; is worth reading before automating local landing pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render the decision-critical facts in the initial HTML
&lt;/h2&gt;

&lt;p&gt;Do not make a potential customer — or a crawler — wait for a client-side API call to learn whether you repair RV electrical systems or whether emergency dispatch is available.&lt;/p&gt;

&lt;p&gt;Use server rendering or static generation for the core page content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[service]/[area]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ServiceAreaPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;business&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;area&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveServiceArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; in &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;customerProblem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"coverage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"coverage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Coverage and dispatch&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proof&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="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"proof"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"proof"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;What we repair&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`tel:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phoneE164&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data-event&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"service_call_click"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Call for &lt;span class="si"&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;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestServiceForm&lt;/span&gt; &lt;span class="na"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;areaId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Google can render JavaScript, but it processes JavaScript in crawling, rendering, and indexing stages. Server-rendering the essential content improves the experience for people and for crawlers, and helps other bots that do not execute the full application. See Google’s &lt;a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics" rel="noopener noreferrer"&gt;JavaScript SEO guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The page should answer the real query, not manufacture confidence. If dispatch availability is unknown, say how the customer gets an answer. Never render “same-day service” because a marketing field was left enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate structured data from the same object
&lt;/h2&gt;

&lt;p&gt;Schema should be a projection of visible, maintained facts — never a second source of truth.&lt;/p&gt;

&lt;p&gt;For a fixed physical location, use the most specific appropriate &lt;code&gt;LocalBusiness&lt;/code&gt; subtype. For a mobile-only service, do not invent a customer-facing address. Keep the markup aligned with the content a person can see on the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;localBusinessJsonLd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BusinessLocation&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="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AutoRepair&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LocalBusiness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@id&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#location-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;telephone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phoneE164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PostalAddress&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;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GeoCoordinates&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;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;openingHoursSpecification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;hours&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OpeningHoursSpecification&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;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;areaServed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceAreas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;area&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AdministrativeArea&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&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="na"&gt;hasOfferCatalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OfferCatalog&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;RV repair services&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;service&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;itemOffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Service&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;
  &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&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="nf"&gt;localBusinessJsonLd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;business&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate the final URL with Google’s Rich Results Test and URL Inspection — not only the JSON object in a unit test. Google recommends JSON-LD, but valid markup is an eligibility signal, not a promise of a rich result. Its &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/local-business" rel="noopener noreferrer"&gt;LocalBusiness documentation&lt;/a&gt; is precise about both the implementation and that distinction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make canonicalization and discovery part of the build
&lt;/h2&gt;

&lt;p&gt;The system should publish only the URLs it wants search engines to treat as canonical.&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/sitemap.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPublishedServiceAreaPages&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;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://example.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;area&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="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&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;Each route also needs a self-referencing canonical in its metadata. Do not put filtered, preview, parameterized, or unpublished pages in the sitemap.&lt;/p&gt;

&lt;p&gt;Google recommends listing the canonical URLs you prefer in a sitemap; for larger sites, generate that sitemap from the same data store that controls publishing. See its &lt;a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="noopener noreferrer"&gt;sitemap guide&lt;/a&gt; and &lt;a href="https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;canonicalization guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrument the conversion path, not only page views
&lt;/h2&gt;

&lt;p&gt;If the business says calls are down, “organic sessions are flat” is not enough diagnosis.&lt;/p&gt;

&lt;p&gt;Record the transition from discovery to action without collecting more personal data than the business needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LocalIntentEvent&lt;/span&gt; &lt;span class="o"&gt;=&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="s2"&gt;service_call_click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quote_form_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quote_form_submitted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;service&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;area&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;pageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service_area&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;referrerClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;organic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;direct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trackLocalIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LocalIntentEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&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;Track the call click, form start, form submission, and any booking completion that the business can measure. Send only the fields needed for analysis; do not put phone numbers, issue descriptions, or other sensitive request content into generic analytics events.&lt;/p&gt;

&lt;p&gt;Then join those events with a small operational audit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service + area + page version
  -&amp;gt; rendered page checks
  -&amp;gt; profile/review freshness check
  -&amp;gt; call/form events
  -&amp;gt; prioritized human-approved change
  -&amp;gt; re-check window
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible to distinguish “we need more demand” from “people are reaching us but the service, coverage, trust, or contact path is unclear.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Add tests where local sites usually drift
&lt;/h2&gt;

&lt;p&gt;The highest-value tests are boring:&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does not publish a service-area route without service and coverage proof&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canPublishServiceAreaPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;noEvidenceService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;coveredArea&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canPublishServiceAreaPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;realService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;noProofArea&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keeps the phone number consistent across page and JSON-LD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rv-electrical-repair&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mesa-az&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+14805550199&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extractJsonLd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;telephone&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+14805550199&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also run an integration check in CI for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; responses for published pages and &lt;code&gt;404&lt;/code&gt; for ineligible combinations;&lt;/li&gt;
&lt;li&gt;canonical URL matches the route;&lt;/li&gt;
&lt;li&gt;sitemap contains only published canonical URLs;&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;noindex&lt;/code&gt; leaks from preview/staging configuration;&lt;/li&gt;
&lt;li&gt;JSON-LD reflects visible service, hours, phone, and location data;&lt;/li&gt;
&lt;li&gt;call/form controls exist and are keyboard accessible;&lt;/li&gt;
&lt;li&gt;content changes have a reviewed audit record.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The operating rule: suggest changes, do not silently mutate trust surfaces
&lt;/h2&gt;

&lt;p&gt;A developer can automate discovery, validation, and draft generation. That does not mean an integration should silently rewrite a Google Business Profile, alter service areas, or publish review responses.&lt;/p&gt;

&lt;p&gt;Use an approval queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observed fact -&amp;gt; proposed correction -&amp;gt; evidence -&amp;gt; risk -&amp;gt; human approval -&amp;gt; apply -&amp;gt; verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps automation useful without treating a business identity as an unattended configuration file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The durable local-service implementation is not a pile of keywords and location templates.&lt;/p&gt;

&lt;p&gt;It is a small, tested facts system that makes the same answer available everywhere a customer needs it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We fix this problem. We serve this area. Here is the proof. Here is what to do next.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a better foundation for search visibility, Maps presence, AI-assisted discovery, and — most importantly — a service call that actually reaches the business.&lt;/p&gt;

&lt;p&gt;Original business-facing guide: &lt;a href="https://seog.ai/blog/rv-repair-seo-checklist-service-calls" rel="noopener noreferrer"&gt;RV Repair SEO Checklist: What to Fix When Service Calls Slow Down&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the audit, or start with the evidence
&lt;/h2&gt;

&lt;p&gt;If you are building this workflow for a local-service team, &lt;a href="https://seog.ai" rel="noopener noreferrer"&gt;SEOG&lt;/a&gt; turns the same inputs — Google Business Profile completeness, map visibility, competitor context, reviews, local pages, and conversion gaps — into a prioritized, human-approved action plan.&lt;/p&gt;

&lt;p&gt;Explore the platform: &lt;a href="https://seog.ai" rel="noopener noreferrer"&gt;seog.ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>seo</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Stop Treating Local SEO Expansion Like a City-Page Generator</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Mon, 10 Aug 2026 20:00:18 +0000</pubDate>
      <link>https://dev.to/alexshev/stop-treating-local-seo-expansion-like-a-city-page-generator-114h</link>
      <guid>https://dev.to/alexshev/stop-treating-local-seo-expansion-like-a-city-page-generator-114h</guid>
      <description>&lt;p&gt;Most local SEO expansion plans start too late in the workflow.&lt;/p&gt;

&lt;p&gt;The client says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We want more jobs in this suburb.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The team answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a city page.
Add the city to the service area.
Track a few keywords.
Maybe buy more leads.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can work sometimes.&lt;/p&gt;

&lt;p&gt;But it is also how teams create doorway pages, make risky Google Business Profile changes, and spend money in markets where the actual blocker was not content.&lt;/p&gt;

&lt;p&gt;The better question is not:&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 target this city?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The better question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What local signals say this market is worth pursuing, and what should be fixed before we expand?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is where location intelligence becomes useful for local SEO.&lt;/p&gt;

&lt;p&gt;Not as a fancy map demo.&lt;/p&gt;

&lt;p&gt;As a decision system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with city-level thinking
&lt;/h2&gt;

&lt;p&gt;Local search is geographic, but many local SEO workflows still behave as if geography is just a keyword modifier.&lt;/p&gt;

&lt;p&gt;They treat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plumber dallas
plumber plano
plumber frisco
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as three content tasks.&lt;/p&gt;

&lt;p&gt;But a local market is not only a keyword.&lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;competitor density&lt;/li&gt;
&lt;li&gt;review expectations&lt;/li&gt;
&lt;li&gt;map visibility patterns&lt;/li&gt;
&lt;li&gt;category fit&lt;/li&gt;
&lt;li&gt;service-area reality&lt;/li&gt;
&lt;li&gt;local proof&lt;/li&gt;
&lt;li&gt;customer trust signals&lt;/li&gt;
&lt;li&gt;citation and entity consistency&lt;/li&gt;
&lt;li&gt;website support for the actual service and area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A business can be strong near its office and invisible ten miles away.&lt;/p&gt;

&lt;p&gt;A service-area business can have a wide radius on paper and still look weak in a specific neighborhood.&lt;/p&gt;

&lt;p&gt;A website can mention a suburb and still fail to prove that the business actually serves it.&lt;/p&gt;

&lt;p&gt;That is why a single rank check from the office is not enough.&lt;/p&gt;

&lt;p&gt;It tells you one viewpoint.&lt;/p&gt;

&lt;p&gt;It does not tell you the market.&lt;/p&gt;




&lt;h2&gt;
  
  
  Location intelligence, in practical terms
&lt;/h2&gt;

&lt;p&gt;For local SEO, I would define location intelligence 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;Turning geographic signals into a prioritized action plan.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds simple, but it changes the order of work.&lt;/p&gt;

&lt;p&gt;Instead of starting with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What page should we create?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where is the business already credible?
Where is it visible but not trusted?
Where is it trusted but not visible?
Where are competitors stronger?
Where does the website fail to support the local promise?
Which fix is likely to matter first?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much better operating loop.&lt;/p&gt;

&lt;p&gt;It also makes the final recommendation safer.&lt;/p&gt;

&lt;p&gt;Sometimes the answer is still a new local page.&lt;/p&gt;

&lt;p&gt;Sometimes the first fix is review velocity.&lt;/p&gt;

&lt;p&gt;Sometimes it is a Google Business Profile category or service mismatch.&lt;/p&gt;

&lt;p&gt;Sometimes it is citation cleanup.&lt;/p&gt;

&lt;p&gt;Sometimes it is simply: do not expand here yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  A market-area scorecard beats an unordered audit
&lt;/h2&gt;

&lt;p&gt;The most useful version of this workflow is not a giant report.&lt;/p&gt;

&lt;p&gt;It is a market-area scorecard.&lt;/p&gt;

&lt;p&gt;For each target area, compare a small set of signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market area: North suburb
Primary service: emergency HVAC repair

Profile readiness:
- GBP categories match the service?
- services listed clearly?
- photos and hours current?
- call / booking path visible?

Map visibility:
- visible near the office?
- visible near the target suburb?
- missing for money terms?

Trust:
- review count vs competitors?
- review recency?
- negative reviews unanswered?
- service-specific proof?

Competitors:
- who dominates the local pack?
- are they stronger because of reviews, proximity, category fit, website support, or all of it?

Website support:
- useful service page?
- useful location / area proof?
- internal links?
- conversion path?

Entity consistency:
- name, phone, site, address or service-area signals aligned?
- important directories clean enough?

Recommendation:
- first three fixes
- what not to do yet
- re-check date
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not complicated.&lt;/p&gt;

&lt;p&gt;That is the point.&lt;/p&gt;

&lt;p&gt;A client can understand it.&lt;/p&gt;

&lt;p&gt;An agency can repeat it.&lt;/p&gt;

&lt;p&gt;An operator can re-check it after the fixes.&lt;/p&gt;

&lt;p&gt;And an AI agent can run parts of it if the inputs are structured enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The dangerous fixes are usually the premature ones
&lt;/h2&gt;

&lt;p&gt;The biggest value of this approach is not that it finds more tasks.&lt;/p&gt;

&lt;p&gt;It stops bad tasks.&lt;/p&gt;

&lt;p&gt;For example, do not start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changing a Google Business Profile address or service area without risk review&lt;/li&gt;
&lt;li&gt;creating dozens of thin city pages because competitors have them&lt;/li&gt;
&lt;li&gt;adding keywords to the business name&lt;/li&gt;
&lt;li&gt;buying citations before core entity data is clean&lt;/li&gt;
&lt;li&gt;increasing ad spend before fixing profile, review, and conversion gaps&lt;/li&gt;
&lt;li&gt;assuming one office-location rank check represents the whole market&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is especially common.&lt;/p&gt;

&lt;p&gt;The map is not one result.&lt;/p&gt;

&lt;p&gt;The map is a local surface that changes with the searcher, query, proximity, category fit, competitor strength, and trust signals.&lt;/p&gt;

&lt;p&gt;If you are making expansion decisions, you need area-level evidence.&lt;/p&gt;

&lt;p&gt;Not vibes.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple priority order
&lt;/h2&gt;

&lt;p&gt;When a client asks, "Should we expand into this area?", I like this order:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Entity mismatch
&lt;/h3&gt;

&lt;p&gt;If the business name, phone, website, address, or service-area signals are inconsistent, fix that before scaling.&lt;/p&gt;

&lt;p&gt;Messy entity data makes everything else less trustworthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Profile mismatch
&lt;/h3&gt;

&lt;p&gt;If the Google Business Profile does not clearly support the target service, fix categories, services, hours, photos, and conversion links before writing more pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Trust gap
&lt;/h3&gt;

&lt;p&gt;If local competitors have fresher reviews, better photos, clearer services, or stronger ratings, the first fix may be trust, not content.&lt;/p&gt;

&lt;p&gt;Customers often choose from a small local set.&lt;/p&gt;

&lt;p&gt;Small trust differences compound fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Coverage gap
&lt;/h3&gt;

&lt;p&gt;If the site does not support the service-area promise, build or improve the relevant page.&lt;/p&gt;

&lt;p&gt;But make it useful.&lt;/p&gt;

&lt;p&gt;A local page should help a customer choose you, not just repeat a city name.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Competitor gap
&lt;/h3&gt;

&lt;p&gt;If competitors dominate because their profile, reviews, categories, and pages are aligned, you need a market-specific improvement plan.&lt;/p&gt;

&lt;p&gt;Not a generic "local SEO package."&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Measurement gap
&lt;/h3&gt;

&lt;p&gt;If you cannot re-check the same area after changes, the report becomes opinion.&lt;/p&gt;

&lt;p&gt;Track the target area, the first fixes, and the re-check window.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where software should help
&lt;/h2&gt;

&lt;p&gt;This is where I like the SEOG framing.&lt;/p&gt;

&lt;p&gt;The product is not interesting because it promises a magic ranking button.&lt;/p&gt;

&lt;p&gt;That would be the wrong promise.&lt;/p&gt;

&lt;p&gt;The useful promise is more operational:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run a local visibility preview.
Compare the market-area signals.
Turn the gaps into a prioritized action plan.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the job most local SEO teams actually need help with.&lt;/p&gt;

&lt;p&gt;Not another dashboard full of disconnected metrics.&lt;/p&gt;

&lt;p&gt;Not another spreadsheet where every row has the same urgency.&lt;/p&gt;

&lt;p&gt;A workflow that says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This market is worth pursuing.
These competitors are the pressure.
These signals are weak.
Fix these three things first.
Do not touch these risky things yet.
Re-check this area after the changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a stronger client conversation.&lt;/p&gt;

&lt;p&gt;It is also a better internal workflow for agencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters for developers and automation teams
&lt;/h2&gt;

&lt;p&gt;If you are building internal tools for marketing teams, this is a good example of a broader product lesson:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not only expose data.
Expose decisions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local SEO tool can show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rankings&lt;/li&gt;
&lt;li&gt;reviews&lt;/li&gt;
&lt;li&gt;competitors&lt;/li&gt;
&lt;li&gt;profile fields&lt;/li&gt;
&lt;li&gt;citations&lt;/li&gt;
&lt;li&gt;pages&lt;/li&gt;
&lt;li&gt;traffic&lt;/li&gt;
&lt;li&gt;calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the operator still needs to know what to do first.&lt;/p&gt;

&lt;p&gt;The more useful system connects those signals into a decision model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input signals -&amp;gt; market comparison -&amp;gt; risk checks -&amp;gt; priority order -&amp;gt; action plan -&amp;gt; re-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between a dashboard and an operating workflow.&lt;/p&gt;

&lt;p&gt;Dashboards are good for visibility.&lt;/p&gt;

&lt;p&gt;Workflows are good for action.&lt;/p&gt;

&lt;p&gt;Local SEO expansion needs both, but the workflow is usually the missing piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical template
&lt;/h2&gt;

&lt;p&gt;Here is the template I would use before recommending expansion into a new local market:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Target area:
Primary service:
Current local visibility:
Strongest three competitors:
Profile gaps:
Review / trust gaps:
Website support gaps:
Citation / entity gaps:
Conversion path gaps:
Risky changes to avoid:
First three fixes:
Re-check date:
Decision:
  - expand now
  - fix first, then expand
  - hold / not worth the effort yet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line matters.&lt;/p&gt;

&lt;p&gt;Not every market deserves action right now.&lt;/p&gt;

&lt;p&gt;Sometimes the best SEO recommendation is sequencing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Local SEO expansion should not begin as a city-page generator.&lt;/p&gt;

&lt;p&gt;It should begin as a market decision.&lt;/p&gt;

&lt;p&gt;If the evidence says the business is visible, trusted, and supported by the site, then expansion work can make sense.&lt;/p&gt;

&lt;p&gt;If the evidence says the profile is mismatched, reviews are stale, competitors are stronger, and the website does not support the area, more pages may only create noise.&lt;/p&gt;

&lt;p&gt;The better workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compare the market -&amp;gt; find the gap -&amp;gt; fix the blocker -&amp;gt; re-check -&amp;gt; expand with evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the angle I like in SEOG's location-intelligence checklist.&lt;/p&gt;

&lt;p&gt;It turns local SEO from an unordered pile of tasks into a decision system.&lt;/p&gt;

&lt;p&gt;Original SEOG guide:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://seog.ai/blog/location-intelligence-local-seo-checklist-market-expansion" rel="noopener noreferrer"&gt;https://seog.ai/blog/location-intelligence-local-seo-checklist-market-expansion&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>data</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Midnight Borscht: CSS Comfort Food Art</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Tue, 04 Aug 2026 05:09:56 +0000</pubDate>
      <link>https://dev.to/alexshev/midnight-borscht-css-comfort-food-art-1in2</link>
      <guid>https://dev.to/alexshev/midnight-borscht-css-comfort-food-art-1in2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Midnight Borscht is inspired by the kind of comfort food that feels quiet instead of flashy: a deep beet soup, a spoon on the side, rye bread nearby, sour cream melting into the surface, dill scattered on top, and an embroidered tablecloth underneath.&lt;/p&gt;

&lt;p&gt;I wanted the art to feel like a small kitchen after dark. The bowl is warm, the wall tiles are dim, and the tablecloth pattern gives the scene a handmade feeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://desciple88.github.io/midnight-borscht-css-art-devchallenge-2026/" rel="noopener noreferrer"&gt;https://desciple88.github.io/midnight-borscht-css-art-devchallenge-2026/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/desciple88/midnight-borscht-css-art-devchallenge-2026" rel="noopener noreferrer"&gt;https://github.com/desciple88/midnight-borscht-css-art-devchallenge-2026&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;The constraint for this piece was simple: no images, no SVG, and no JavaScript. Everything is HTML and CSS.&lt;/p&gt;

&lt;p&gt;The bowl, soup surface, sour cream, dill, beet pieces, potato, spoon, bread, wall tiles, and woven tablecloth are all built from layered &lt;code&gt;div&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt; elements with gradients, border radii, shadows, repeated backgrounds, transforms, and pseudo-depth.&lt;/p&gt;

&lt;p&gt;The hardest part was making it read as food at both desktop and mobile sizes. The desktop version has a composed poster feel, while the mobile version keeps the bowl large enough to show the toppings without letting the scene become a random crop.&lt;/p&gt;

&lt;p&gt;CSS details I cared about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The borscht surface uses stacked radial gradients to keep the soup from looking flat.&lt;/li&gt;
&lt;li&gt;The sour cream and dill sit above the beet base to create a clear focal point.&lt;/li&gt;
&lt;li&gt;The tablecloth is made with repeating linear gradients and stitched bands instead of an image.&lt;/li&gt;
&lt;li&gt;The wall tiles use rotated CSS squares so the background still feels handcrafted.&lt;/li&gt;
&lt;li&gt;Steam is animated, but &lt;code&gt;prefers-reduced-motion&lt;/code&gt; disables the motion for users who request it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I self-checked the final piece with local desktop and mobile screenshots, verified that the page contains no image, SVG, or script tags, and deployed it as a public GitHub Pages demo.&lt;/p&gt;

&lt;p&gt;License: MIT.&lt;/p&gt;

</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
    </item>
    <item>
      <title>Leftover Ladle: Comfort Food from What You Already Have</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Tue, 04 Aug 2026 04:42:21 +0000</pubDate>
      <link>https://dev.to/alexshev/leftover-ladle-comfort-food-from-what-you-already-have-4h7l</link>
      <guid>https://dev.to/alexshev/leftover-ladle-comfort-food-from-what-you-already-have-4h7l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Leftover Ladle is a landing page for a fictional comfort-food service that turns fridge leftovers into a warm dinner plan.&lt;/p&gt;

&lt;p&gt;The theme is built around one very human comfort-food moment: it is late, it is raining, you do not want another delivery app, and the best meal is probably already hiding in your kitchen.&lt;/p&gt;

&lt;p&gt;I focused the page around a complete landing experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A first-viewport hero with a CSS kitchen scene, simmering pot, window, rain, jars, and steam.&lt;/li&gt;
&lt;li&gt;A clear “how it works” section that explains the product without a wall of copy.&lt;/li&gt;
&lt;li&gt;An interactive bowl builder where users choose a base, comfort mood, heat level, and allergy adjustments.&lt;/li&gt;
&lt;li&gt;A concise menu section with three comfort-food directions.&lt;/li&gt;
&lt;li&gt;A reservation form with accessible status feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://desciple88.github.io/leftover-ladle-devchallenge-2026/" rel="noopener noreferrer"&gt;https://desciple88.github.io/leftover-ladle-devchallenge-2026/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/desciple88/leftover-ladle-devchallenge-2026" rel="noopener noreferrer"&gt;https://github.com/desciple88/leftover-ladle-devchallenge-2026&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;I chose the Perfect Landing prompt because it rewards more than decoration. A good food landing page has to make the visitor hungry, but it also has to feel easy to use.&lt;/p&gt;

&lt;p&gt;The main design choice was to avoid a generic restaurant page. Instead of selling one dish, Leftover Ladle sells a useful comfort-food ritual: pick what is already in your pantry, choose the mood, and get a bowl plan.&lt;/p&gt;

&lt;p&gt;Accessibility and resilience were part of the build from the beginning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semantic &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;form&lt;/code&gt;, &lt;code&gt;fieldset&lt;/code&gt;, and &lt;code&gt;footer&lt;/code&gt; structure.&lt;/li&gt;
&lt;li&gt;A skip link for keyboard users.&lt;/li&gt;
&lt;li&gt;Real labels for controls and inputs.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;aria-live&lt;/code&gt; bowl preview so updates are announced.&lt;/li&gt;
&lt;li&gt;High-contrast buttons and focus states.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; handling for the rain and steam animation.&lt;/li&gt;
&lt;li&gt;No runtime dependencies, so the page stays fast and easy to inspect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I am happiest with is the CSS kitchen scene. It gives the page its own visual identity without relying on a stock food photo, and it still supports the landing page rather than becoming a separate art experiment.&lt;/p&gt;

&lt;p&gt;I also self-checked the project with desktop and mobile screenshots, static HTML checks for duplicate IDs and label targets, and a JavaScript syntax check before submitting.&lt;/p&gt;

&lt;p&gt;License: MIT.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I Debugged a Broken Vertex AI Image Pipeline and Finally Made It Reliable</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:47:18 +0000</pubDate>
      <link>https://dev.to/alexshev/how-i-debugged-a-broken-vertex-ai-image-pipeline-and-finally-made-it-reliable-5557</link>
      <guid>https://dev.to/alexshev/how-i-debugged-a-broken-vertex-ai-image-pipeline-and-finally-made-it-reliable-5557</guid>
      <description>&lt;p&gt;Last week, I lost more time to AI infrastructure than to the actual creative work.&lt;/p&gt;

&lt;p&gt;The task sounded simple: generate images through Vertex AI for a content pipeline.&lt;/p&gt;

&lt;p&gt;What actually happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one key worked yesterday and failed today&lt;/li&gt;
&lt;li&gt;one project returned &lt;code&gt;invalid_grant&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;another returned &lt;code&gt;permission denied&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;free tier worked sometimes, then hit quota walls&lt;/li&gt;
&lt;li&gt;the code looked fine, but the system still didn’t produce images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the part nobody tells you about AI workflows:&lt;br&gt;
&lt;strong&gt;most failures are not model failures. They’re credential, project, and policy failures.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s how I finally debugged the whole thing and got the image route working again.&lt;/p&gt;


&lt;h2&gt;
  
  
  The symptoms
&lt;/h2&gt;

&lt;p&gt;At first, the failures looked unrelated.&lt;/p&gt;

&lt;p&gt;I saw three different classes of errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invalid_grant: account not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;403 Permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 RESOURCE_EXHAUSTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That usually means one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;your system is broken in multiple places&lt;/li&gt;
&lt;li&gt;your system is pointing at multiple environments and you don’t know which one is real&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my case, it was the second one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Stop guessing which path is canonical
&lt;/h2&gt;

&lt;p&gt;The first useful move was brutally simple:&lt;br&gt;
find the one script that the team actually trusts.&lt;/p&gt;

&lt;p&gt;For us, that was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/clawd/ops/production/scripts/generate_panels.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That became the source of truth.&lt;/p&gt;

&lt;p&gt;Not old snippets.&lt;br&gt;
Not half-working notebooks.&lt;br&gt;
Not memory.&lt;/p&gt;

&lt;p&gt;Once I checked the actual script, I immediately found one hidden problem:&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;PROJECT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old-project-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline was still hardcoded to an old project.&lt;/p&gt;

&lt;p&gt;So even when I updated the credentials, the requests were still going to the wrong place.&lt;/p&gt;

&lt;p&gt;That alone explained a lot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Separate free-tier failure from paid-route failure
&lt;/h2&gt;

&lt;p&gt;We had two different routes mixed together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gemini API / AI Studio free tier&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vertex AI paid route&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds harmless, but it creates terrible debugging conditions.&lt;/p&gt;

&lt;p&gt;Because the failure modes are different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;free tier dies with quota errors&lt;/li&gt;
&lt;li&gt;Vertex dies with IAM / service account / project errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you mix them, you start solving the wrong problem.&lt;/p&gt;

&lt;p&gt;For example, this looked like a model problem at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 RESOURCE_EXHAUSTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it turned out to be just a burned free-tier key.&lt;/p&gt;

&lt;p&gt;Meanwhile the paid route was failing for a completely different reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; treat free and paid as separate systems, even if they use the same model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Verify the service account before touching the code
&lt;/h2&gt;

&lt;p&gt;Once I had the new Vertex JSON, I didn’t start by generating an image.&lt;br&gt;
I started by checking whether the credential could even mint a token.&lt;/p&gt;

&lt;p&gt;That kind of test saves time because it tells you whether the problem is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auth&lt;/li&gt;
&lt;li&gt;project permissions&lt;/li&gt;
&lt;li&gt;or model invocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Python, the logic is basically:&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;google.oauth2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.auth.transport.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;

&lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_service_account_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vertex_ai_key.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;scopes&lt;/span&gt;&lt;span class="o"&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;https://www.googleapis.com/auth/cloud-platform&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;creds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&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="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this step fails, don’t touch your prompts.&lt;br&gt;
Don’t touch the model.&lt;br&gt;
Don’t touch the rendering code.&lt;/p&gt;

&lt;p&gt;You don’t have an image problem yet.&lt;br&gt;
You have an auth problem.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4: Watch for organization policies
&lt;/h2&gt;

&lt;p&gt;This one burned the most time.&lt;/p&gt;

&lt;p&gt;I created a new service account, everything looked correct, and then Google Cloud refused to create a JSON key.&lt;/p&gt;

&lt;p&gt;The error turned out to be caused by this policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iam.disableServiceAccountKeyCreation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That wasn’t obvious from the first screen.&lt;br&gt;
The UI showed one policy as “not enforced,” while a legacy constraint was still active somewhere above it.&lt;/p&gt;

&lt;p&gt;That kind of mismatch is why cloud debugging feels cursed.&lt;/p&gt;

&lt;p&gt;The practical fix was not to keep fighting the same project.&lt;br&gt;
The practical fix was to create a clean personal project without inherited org-policy baggage.&lt;/p&gt;

&lt;p&gt;That ended up being faster than trying to untangle admin policy state.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5: Create one clean project and move on
&lt;/h2&gt;

&lt;p&gt;The final working setup looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new clean Vertex project&lt;/li&gt;
&lt;li&gt;a new service account&lt;/li&gt;
&lt;li&gt;a fresh JSON key&lt;/li&gt;
&lt;li&gt;the canonical script updated to the new project id&lt;/li&gt;
&lt;li&gt;one successful test generation to prove the whole path worked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after that did I consider the route fixed.&lt;/p&gt;

&lt;p&gt;Not when the key existed.&lt;br&gt;
Not when the policy screen looked green.&lt;br&gt;
Not when the script stopped crashing.&lt;/p&gt;

&lt;p&gt;Only when this produced an actual file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outputs/nanobanana_vertex_test.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the only result that mattered.&lt;/p&gt;




&lt;h2&gt;
  
  
  The working mental model
&lt;/h2&gt;

&lt;p&gt;When an AI image pipeline breaks, I now check things in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Which script is canonical?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which project is the request actually hitting?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can the credential mint a token?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is this free-tier quota or Vertex IAM?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Are org policies blocking service-account keys?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did I generate one real image successfully?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That order is much faster than randomly changing keys and re-running prompts.&lt;/p&gt;




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

&lt;p&gt;For us, the final fix was not “better prompting.”&lt;br&gt;
It was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stopping reliance on stale project ids&lt;/li&gt;
&lt;li&gt;replacing broken credentials&lt;/li&gt;
&lt;li&gt;separating free-tier and paid routes&lt;/li&gt;
&lt;li&gt;avoiding inherited org-policy traps&lt;/li&gt;
&lt;li&gt;testing the pipeline end-to-end with a real output file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not glamorous.&lt;br&gt;
But it’s the difference between a pipeline you trust and a pipeline that only works when you’re lucky.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;A lot of AI tooling discourse is still obsessed with models.&lt;/p&gt;

&lt;p&gt;But once you work with these systems in production, the real bottleneck is often much more boring:&lt;br&gt;
&lt;strong&gt;identity, permissions, quotas, and project hygiene.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model can be state of the art.&lt;br&gt;
If your project graph is a mess, you still won’t ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Terminal Skills fits
&lt;/h2&gt;

&lt;p&gt;This is also exactly the kind of workflow I want to turn into a Terminal Skill.&lt;/p&gt;

&lt;p&gt;Not because a skill should hide the cloud setup behind magic, but because the debug order should not live only in someone’s memory.&lt;/p&gt;

&lt;p&gt;A useful &lt;code&gt;vertex-ai-image-pipeline&lt;/code&gt; skill would give an agent a repeatable checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identify the canonical script&lt;/li&gt;
&lt;li&gt;verify the configured project id&lt;/li&gt;
&lt;li&gt;test whether the service account can mint a token&lt;/li&gt;
&lt;li&gt;separate free-tier quota failures from Vertex IAM failures&lt;/li&gt;
&lt;li&gt;check for organization-policy blockers&lt;/li&gt;
&lt;li&gt;run one minimal generation test before touching prompts&lt;/li&gt;
&lt;li&gt;report the exact failing layer instead of guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the broader idea behind &lt;a href="https://terminalskills.io" rel="noopener noreferrer"&gt;Terminal Skills&lt;/a&gt;: turn messy, real operational workflows into reusable agent skills.&lt;/p&gt;

&lt;p&gt;I will probably translate this article into a proper Terminal Skills use case next, because this is the kind of boring production workflow agents need more than another prompt template.&lt;/p&gt;

&lt;p&gt;If you’ve had to debug a broken AI pipeline recently, I’d genuinely love to hear what failed first for you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>googlecloud</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Testing APIs by Hand. Give Your AI Agent a Bruno Workflow.</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Wed, 29 Jul 2026 23:25:57 +0000</pubDate>
      <link>https://dev.to/alexshev/stop-testing-apis-by-hand-give-your-ai-agent-a-bruno-workflow-3gk2</link>
      <guid>https://dev.to/alexshev/stop-testing-apis-by-hand-give-your-ai-agent-a-bruno-workflow-3gk2</guid>
      <description>&lt;p&gt;Most API testing workflows break in a boring way.&lt;/p&gt;

&lt;p&gt;Not because the team does not care about testing.&lt;/p&gt;

&lt;p&gt;Not because nobody knows how to send a request.&lt;/p&gt;

&lt;p&gt;Because the API workflow lives in too many places at once.&lt;/p&gt;

&lt;p&gt;One developer has a Postman collection.&lt;br&gt;
Another has a few curl commands in shell history.&lt;br&gt;
The backend repo has environment variables in &lt;code&gt;.env.example&lt;/code&gt;.&lt;br&gt;
CI has a smoke test that only checks one endpoint.&lt;br&gt;
The docs say one thing.&lt;br&gt;
The staging API behaves slightly differently.&lt;/p&gt;

&lt;p&gt;Then an AI coding agent enters the repo and gets asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can you check this API flow?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can read code.&lt;br&gt;
It can write tests.&lt;br&gt;
It can generate curl commands.&lt;br&gt;
It can explain HTTP status codes.&lt;/p&gt;

&lt;p&gt;But it still has the same problem a new human has.&lt;/p&gt;

&lt;p&gt;Where is the API workflow?&lt;/p&gt;

&lt;p&gt;If the answer is "somewhere in a GUI tool, someone's local history, and a few scattered docs," the agent has to reconstruct the whole thing from scratch.&lt;/p&gt;

&lt;p&gt;That is not a tool problem.&lt;/p&gt;

&lt;p&gt;That is a workflow packaging problem.&lt;/p&gt;


&lt;h2&gt;
  
  
  API collections should live near the code
&lt;/h2&gt;

&lt;p&gt;This is why I like Bruno.&lt;/p&gt;

&lt;p&gt;Bruno is an open-source API client that stores collections as plain text files in your Git repository.&lt;/p&gt;

&lt;p&gt;That detail matters more than the UI.&lt;/p&gt;

&lt;p&gt;If your API collection lives next to the application code, it becomes part of the engineering system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request changes show up in pull requests&lt;/li&gt;
&lt;li&gt;environment files can be reviewed&lt;/li&gt;
&lt;li&gt;auth flows can be documented as runnable requests&lt;/li&gt;
&lt;li&gt;test assertions can live with the request that needs them&lt;/li&gt;
&lt;li&gt;CI can run the same collection humans use locally&lt;/li&gt;
&lt;li&gt;new team members can learn the API by running the collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The collection is not a private artifact hidden in a synced workspace.&lt;/p&gt;

&lt;p&gt;It is a repo artifact.&lt;/p&gt;

&lt;p&gt;That changes what an AI agent can do with it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The real unit is not a request
&lt;/h2&gt;

&lt;p&gt;A single API request is easy.&lt;/p&gt;

&lt;p&gt;The useful workflow is usually a sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Login.&lt;/li&gt;
&lt;li&gt;Save the token.&lt;/li&gt;
&lt;li&gt;Create a resource.&lt;/li&gt;
&lt;li&gt;Read it back.&lt;/li&gt;
&lt;li&gt;Update it.&lt;/li&gt;
&lt;li&gt;Verify the response shape.&lt;/li&gt;
&lt;li&gt;Clean up or test the failure path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When developers test this manually, they often remember the sequence in their head.&lt;/p&gt;

&lt;p&gt;That works until the API changes.&lt;/p&gt;

&lt;p&gt;It works until staging has different data.&lt;/p&gt;

&lt;p&gt;It works until someone else needs to debug the same flow.&lt;/p&gt;

&lt;p&gt;It works until an agent has to run it and cannot rely on tribal memory.&lt;/p&gt;

&lt;p&gt;The important thing to encode is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send a POST request.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is the auth flow.
These variables move from one request to the next.
These assertions prove the contract still works.
This command runs the flow in CI.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is where Bruno's file-based model becomes useful.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;.bru&lt;/code&gt; request can contain the request metadata, headers, body, scripts, and tests in a human-readable format.&lt;/p&gt;

&lt;p&gt;An environment file can define &lt;code&gt;baseUrl&lt;/code&gt;, test users, and secret placeholders.&lt;/p&gt;

&lt;p&gt;Post-response scripts can save values for later requests.&lt;/p&gt;

&lt;p&gt;The CLI can run the collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bru run &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or a folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bru run auth/ &lt;span class="nt"&gt;--env&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or output CI-friendly results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bru run &lt;span class="nt"&gt;--env&lt;/span&gt; dev &lt;span class="nt"&gt;--output&lt;/span&gt; results.xml &lt;span class="nt"&gt;--format&lt;/span&gt; junit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much better artifact for an agent than a vague instruction like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test the API.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What an AI agent should not improvise
&lt;/h2&gt;

&lt;p&gt;I do not want an agent inventing an API testing workflow every time.&lt;/p&gt;

&lt;p&gt;That is how you get impressive-looking activity and weak verification.&lt;/p&gt;

&lt;p&gt;The agent might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hit the happy path only&lt;/li&gt;
&lt;li&gt;test against the wrong environment&lt;/li&gt;
&lt;li&gt;forget auth state&lt;/li&gt;
&lt;li&gt;use fake data that bypasses the real bug&lt;/li&gt;
&lt;li&gt;skip assertions and only report that the request returned something&lt;/li&gt;
&lt;li&gt;paste a curl command that nobody ever runs again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those failures look dramatic.&lt;/p&gt;

&lt;p&gt;They look like work.&lt;/p&gt;

&lt;p&gt;The agent says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I tested the endpoint.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the team has no durable artifact, no diff, no reusable flow, and no CI hook.&lt;/p&gt;

&lt;p&gt;That is the wrong end state.&lt;/p&gt;

&lt;p&gt;For API work, I want the agent to leave behind something the repo can keep.&lt;/p&gt;




&lt;h2&gt;
  
  
  A better Bruno workflow for agents
&lt;/h2&gt;

&lt;p&gt;The workflow I would want from an AI coding agent looks like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Discover the API surface
&lt;/h3&gt;

&lt;p&gt;The agent should inspect routes, controllers, OpenAPI specs if they exist, existing tests, and any current API docs.&lt;/p&gt;

&lt;p&gt;The output should be a short map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auth endpoints&lt;/li&gt;
&lt;li&gt;core resources&lt;/li&gt;
&lt;li&gt;destructive actions&lt;/li&gt;
&lt;li&gt;environment assumptions&lt;/li&gt;
&lt;li&gt;test data requirements&lt;/li&gt;
&lt;li&gt;flows that need chained state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No collection changes yet.&lt;/p&gt;

&lt;p&gt;First understand the shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Git-friendly collection structure
&lt;/h3&gt;

&lt;p&gt;The collection should mirror the API, not the agent's thought process.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api-collection/
  bruno.json
  environments/
    dev.bru
    staging.bru
  auth/
    login.bru
    refresh-token.bru
  users/
    list-users.bru
    get-user.bru
    create-user.bru
  orders/
    create-order.bru
    process-refund.bru
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure is boring on purpose.&lt;/p&gt;

&lt;p&gt;People can navigate it.&lt;/p&gt;

&lt;p&gt;Agents can navigate it.&lt;/p&gt;

&lt;p&gt;Reviewers can diff it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Encode variables and secrets correctly
&lt;/h3&gt;

&lt;p&gt;Environment files should carry normal config like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;baseUrl: http://localhost:3000
testEmail: test@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secrets should be marked as secrets and not committed as plain values.&lt;/p&gt;

&lt;p&gt;This is one of the places where an agent needs a rule, not vibes.&lt;/p&gt;

&lt;p&gt;If a tool can run requests, it can also leak credentials by accident.&lt;/p&gt;

&lt;p&gt;So the workflow has to define where config lives, which values can be committed, and what must stay local or injected by CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Chain requests deliberately
&lt;/h3&gt;

&lt;p&gt;Many real API tests are stateful.&lt;/p&gt;

&lt;p&gt;Login returns a token.&lt;/p&gt;

&lt;p&gt;Create user returns an ID.&lt;/p&gt;

&lt;p&gt;Create order depends on the user.&lt;/p&gt;

&lt;p&gt;Refund depends on the order.&lt;/p&gt;

&lt;p&gt;The agent should not fake those IDs when it can capture them from the real response.&lt;/p&gt;

&lt;p&gt;That is where post-response scripts are useful.&lt;/p&gt;

&lt;p&gt;The point is not clever scripting.&lt;/p&gt;

&lt;p&gt;The point is making the API flow executable instead of explanatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Put assertions next to the request
&lt;/h3&gt;

&lt;p&gt;A request without assertions is only a smoke signal.&lt;/p&gt;

&lt;p&gt;The agent should add tests for the contract that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status code&lt;/li&gt;
&lt;li&gt;required fields&lt;/li&gt;
&lt;li&gt;expected types&lt;/li&gt;
&lt;li&gt;auth behavior&lt;/li&gt;
&lt;li&gt;error shape&lt;/li&gt;
&lt;li&gt;permission boundaries&lt;/li&gt;
&lt;li&gt;idempotency or retry behavior when relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test should answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What would make this endpoint unsafe to ship?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did the server return any JSON?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Add a CI command
&lt;/h3&gt;

&lt;p&gt;The workflow is not done until there is a command the team can run again.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bru run &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bru run auth/ &lt;span class="nb"&gt;users&lt;/span&gt;/ &lt;span class="nt"&gt;--env&lt;/span&gt; ci &lt;span class="nt"&gt;--output&lt;/span&gt; results.xml &lt;span class="nt"&gt;--format&lt;/span&gt; junit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact command depends on the project.&lt;/p&gt;

&lt;p&gt;But the artifact should be clear enough that a developer, CI job, or future agent can repeat the same check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Terminal Skills fits
&lt;/h2&gt;

&lt;p&gt;This is exactly the kind of workflow that belongs in a skill.&lt;/p&gt;

&lt;p&gt;The Terminal Skills catalog has a &lt;code&gt;bruno&lt;/code&gt; skill for AI agents:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://terminalskills.io/skills/bruno" rel="noopener noreferrer"&gt;Bruno on Terminal Skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The useful part is not "the agent has heard of Bruno."&lt;/p&gt;

&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;The useful part is giving the agent an operating procedure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use Bruno as a Git-first API testing tool&lt;/li&gt;
&lt;li&gt;store collections as reviewable repo files&lt;/li&gt;
&lt;li&gt;organize requests by API structure&lt;/li&gt;
&lt;li&gt;use environment files for config&lt;/li&gt;
&lt;li&gt;keep secrets out of committed values&lt;/li&gt;
&lt;li&gt;write assertions in requests&lt;/li&gt;
&lt;li&gt;chain state between calls intentionally&lt;/li&gt;
&lt;li&gt;run collections through the Bruno CLI&lt;/li&gt;
&lt;li&gt;treat the collection as living API documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install for Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;bruno &lt;span class="nt"&gt;--agent&lt;/span&gt; codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install for Gemini CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;bruno &lt;span class="nt"&gt;--agent&lt;/span&gt; gemini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install for Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;bruno
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broader point is simple:&lt;/p&gt;

&lt;p&gt;If an agent is going to touch your API workflow, do not ask it to improvise testing.&lt;/p&gt;

&lt;p&gt;Give it a repeatable workflow it can follow, edit, run, and leave behind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters for API teams
&lt;/h2&gt;

&lt;p&gt;API testing is one of those areas where AI can create false confidence quickly.&lt;/p&gt;

&lt;p&gt;An agent can generate a convincing explanation of an endpoint.&lt;/p&gt;

&lt;p&gt;It can generate sample requests.&lt;/p&gt;

&lt;p&gt;It can say the test passed.&lt;/p&gt;

&lt;p&gt;But unless the workflow leaves a durable artifact, the value disappears after the chat.&lt;/p&gt;

&lt;p&gt;The better outcome is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Bruno collection was updated&lt;/li&gt;
&lt;li&gt;the change is visible in Git&lt;/li&gt;
&lt;li&gt;the request has assertions&lt;/li&gt;
&lt;li&gt;the environment is explicit&lt;/li&gt;
&lt;li&gt;the command can run locally&lt;/li&gt;
&lt;li&gt;CI can run it later&lt;/li&gt;
&lt;li&gt;the next agent does not need to rediscover the flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between assistance and operational memory.&lt;/p&gt;

&lt;p&gt;The agent did not just test the API.&lt;/p&gt;

&lt;p&gt;It improved how the team tests the API.&lt;/p&gt;




&lt;h2&gt;
  
  
  My practical checklist
&lt;/h2&gt;

&lt;p&gt;If I were reviewing an agent-generated Bruno workflow, I would ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the collection live in the repo?&lt;/li&gt;
&lt;li&gt;Does the folder structure match the API shape?&lt;/li&gt;
&lt;li&gt;Are environments separated cleanly?&lt;/li&gt;
&lt;li&gt;Are secrets marked and excluded from committed values?&lt;/li&gt;
&lt;li&gt;Do important requests include assertions?&lt;/li&gt;
&lt;li&gt;Are auth tokens and IDs passed through real response state?&lt;/li&gt;
&lt;li&gt;Can the collection run from the CLI?&lt;/li&gt;
&lt;li&gt;Is there a CI-ready command?&lt;/li&gt;
&lt;li&gt;Would a new teammate understand the API faster after reading and running it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is yes, the agent left behind something useful.&lt;/p&gt;

&lt;p&gt;If the answer is no, it probably just tested by hand with extra steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;AI agents are good at producing one-off work.&lt;/p&gt;

&lt;p&gt;Engineering teams need repeatable work.&lt;/p&gt;

&lt;p&gt;That is the gap skills are meant to close.&lt;/p&gt;

&lt;p&gt;For API testing, the difference is especially visible.&lt;/p&gt;

&lt;p&gt;Do you want the agent to send a request once?&lt;/p&gt;

&lt;p&gt;Or do you want it to build the API workflow your team can keep using?&lt;/p&gt;

&lt;p&gt;That is why Bruno is a good fit for Terminal Skills.&lt;/p&gt;

&lt;p&gt;It turns API testing into files, diffs, commands, and checks.&lt;/p&gt;

&lt;p&gt;And those are exactly the kinds of artifacts agents can use without asking everyone to trust a chat transcript.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: AI assistance was used to draft and edit this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>testing</category>
      <category>devtools</category>
      <category>ai</category>
    </item>
    <item>
      <title>I fixed an unbounded audio queue in a live translation server before it could take the process down</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Wed, 15 Jul 2026 01:51:04 +0000</pubDate>
      <link>https://dev.to/alexshev/i-fixed-an-unbounded-audio-queue-in-a-live-translation-server-before-it-could-take-the-process-down-i6m</link>
      <guid>https://dev.to/alexshev/i-fixed-an-unbounded-audio-queue-in-a-live-translation-server-before-it-could-take-the-process-down-i6m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;The project is a live speech translation app.&lt;/p&gt;

&lt;p&gt;A browser client captures microphone audio, converts it to 16 kHz PCM, and streams it to a NestJS server over Socket.IO. The server opens one WebSocket session to a local ML service, forwards the audio frames, and relays translated segments back to listeners.&lt;/p&gt;

&lt;p&gt;The important part for this bug: audio starts arriving from the browser before the ML WebSocket is guaranteed to be open.&lt;/p&gt;

&lt;p&gt;That is normal for realtime systems. The dangerous part was what happened when the ML service was unavailable, slow to handshake, or stuck behind a bad connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;MlSession&lt;/code&gt; class buffered audio frames while waiting for the ML WebSocket to open:&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="nf"&gt;sendAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&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="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks harmless until you remember what the data is: raw PCM audio.&lt;/p&gt;

&lt;p&gt;At 16 kHz, mono, 16-bit PCM, the stream is roughly 32 KB per second per active speaker. If the ML WebSocket never opens, every &lt;code&gt;audio&lt;/code&gt; event keeps appending another &lt;code&gt;Buffer&lt;/code&gt; to &lt;code&gt;queue&lt;/code&gt;. A stalled ML service could quietly turn one active audio source into an unbounded memory growth path.&lt;/p&gt;

&lt;p&gt;The server did not have to be under heavy traffic for this to matter. One broadcaster with a stuck ML backend was enough to create an ever-growing in-memory queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;I added an explicit cap for queued audio before the ML connection opens, tracked queued bytes, reset the counter once the connection opens, and released queued buffers on close/error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_QUEUED_AUDIO_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&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;ML_MAX_QUEUED_AUDIO_BYTES&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&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;The send path now fails fast instead of buffering forever:&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="nf"&gt;sendAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&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="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queuedBytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_QUEUED_AUDIO_BYTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ML websocket audio queue overflow before connection opened&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queuedBytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&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;When the socket opens, queued audio is flushed and the byte counter goes back to zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config&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;config&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&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="k"&gt;for &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;buf&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&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="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queuedBytes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Close/error paths also clear the queue:&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="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queuedBytes&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queuedBytes&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The fix changes the failure mode.&lt;/p&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ML service unavailable or stuck&lt;/li&gt;
&lt;li&gt;browser keeps streaming audio&lt;/li&gt;
&lt;li&gt;server queues every PCM frame&lt;/li&gt;
&lt;li&gt;memory grows until the process is restarted or killed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ML service unavailable or stuck&lt;/li&gt;
&lt;li&gt;server accepts only a bounded amount of pending audio&lt;/li&gt;
&lt;li&gt;session closes with a specific overflow error&lt;/li&gt;
&lt;li&gt;existing gateway cleanup runs and listeners get the normal ended signal&lt;/li&gt;
&lt;li&gt;memory is released immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I kept the default cap at &lt;code&gt;512 KB&lt;/code&gt;, which is around 16 seconds of 16 kHz mono PCM audio. That is enough for normal connection delay but small enough to prevent a single stalled session from becoming a memory leak.&lt;/p&gt;

&lt;p&gt;The value is configurable:&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="nv"&gt;ML_MAX_QUEUED_AUDIO_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1048576
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lets production tune the buffer based on expected network conditions without changing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The normal server build still passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; live-translation-server@0.1.0 build
&amp;gt; tsc -p tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also ran a smoke test that accepts the TCP connection but intentionally never completes the WebSocket handshake. Then I sent audio chunks into &lt;code&gt;MlSession&lt;/code&gt; with a small queue limit.&lt;/p&gt;

&lt;p&gt;Expected result: the session should close with the overflow error instead of keeping the buffers forever.&lt;/p&gt;

&lt;p&gt;Observed result:&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="nl"&gt;"closed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"errMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ML websocket audio queue overflow before connection opened"&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;That is the behavior I wanted: when the ML backend is not actually ready to receive audio, the realtime server fails the session explicitly instead of becoming a quiet memory sink.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Realtime systems make small buffers feel safe.&lt;/p&gt;

&lt;p&gt;This was not a dramatic bug in the happy path. The app worked when the ML WebSocket opened quickly. The bug lived in the waiting room between "the browser is already sending audio" and "the ML service is ready."&lt;/p&gt;

&lt;p&gt;That is exactly where production bugs like to hide: not in the main algorithm, but in the glue code that assumes the next component will be ready soon.&lt;/p&gt;

&lt;p&gt;The fix was not a bigger abstraction. It was a boundary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;audio may wait briefly, but it may not wait forever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one rule turns an unbounded failure into a controlled one.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Your AI App Does Not Need Ten Model Clients. It Needs a Gateway Workflow.</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Tue, 14 Jul 2026 23:44:12 +0000</pubDate>
      <link>https://dev.to/alexshev/your-ai-app-does-not-need-ten-model-clients-it-needs-a-gateway-workflow-15il</link>
      <guid>https://dev.to/alexshev/your-ai-app-does-not-need-ten-model-clients-it-needs-a-gateway-workflow-15il</guid>
      <description>&lt;p&gt;Most AI apps start with one provider.&lt;/p&gt;

&lt;p&gt;That is usually fine.&lt;/p&gt;

&lt;p&gt;You add an OpenAI SDK call, pick a model, ship the feature, and move on.&lt;/p&gt;

&lt;p&gt;Then the product grows.&lt;/p&gt;

&lt;p&gt;One workflow needs a cheaper model. Another needs a stronger reasoning model. Another needs image or video generation. A customer wants data routed through a specific provider. A fallback model becomes necessary because the primary one is rate-limited during a launch.&lt;/p&gt;

&lt;p&gt;Suddenly, "just call the model" is not really the architecture anymore.&lt;/p&gt;

&lt;p&gt;The real problem is routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: provider logic spread across the app
&lt;/h2&gt;

&lt;p&gt;The messy version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support_summary&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deep_research&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;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&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;imageProvider&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works for a while.&lt;/p&gt;

&lt;p&gt;But as soon as the team needs fallbacks, model swaps, cost controls, or media endpoints, provider decisions start leaking into product code.&lt;/p&gt;

&lt;p&gt;Now the app has two problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product logic&lt;/li&gt;
&lt;li&gt;model routing logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those should not be the same layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gateway is boring in the best way
&lt;/h2&gt;

&lt;p&gt;This is why I like the &lt;code&gt;routerbase-gateway&lt;/code&gt; pattern on Terminal Skills.&lt;/p&gt;

&lt;p&gt;The useful idea is not "one more AI provider."&lt;/p&gt;

&lt;p&gt;The useful idea is putting OpenAI-compatible calls, model IDs, fallback choices, streaming behavior, tool calling, JSON mode, and media generation behind one gateway layer.&lt;/p&gt;

&lt;p&gt;Instead of teaching every part of the app how to talk to every model provider, the application talks to one interface.&lt;/p&gt;

&lt;p&gt;The routing layer decides what happens behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&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;openai&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROUTERBASE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.routerbase.com/v1&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fast-support-summary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarize the customer issue clearly.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transcript&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;The application code stays boring.&lt;/p&gt;

&lt;p&gt;The routing policy can change without rewriting every feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for agents
&lt;/h2&gt;

&lt;p&gt;An AI coding agent can wire up raw API calls.&lt;/p&gt;

&lt;p&gt;That is not the hard part.&lt;/p&gt;

&lt;p&gt;The hard part is getting the agent to make the same architectural decision the team would make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep provider-specific logic out of product code&lt;/li&gt;
&lt;li&gt;use environment variables consistently&lt;/li&gt;
&lt;li&gt;keep model names configurable&lt;/li&gt;
&lt;li&gt;support fallbacks without scattering conditionals everywhere&lt;/li&gt;
&lt;li&gt;preserve streaming and JSON-mode behavior where the feature needs it&lt;/li&gt;
&lt;li&gt;avoid logging sensitive prompts or keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly the kind of workflow that belongs in a skill.&lt;/p&gt;

&lt;p&gt;Not because the commands are complicated.&lt;/p&gt;

&lt;p&gt;Because the judgment should be repeatable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skill version of the task
&lt;/h2&gt;

&lt;p&gt;Without a skill, the prompt tends to be vague:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrate this app to use RouterBase.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That leaves too many decisions open.&lt;/p&gt;

&lt;p&gt;With a workflow-oriented skill, the agent has a narrower job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;routerbase-gateway &lt;span class="nt"&gt;--agent&lt;/span&gt; codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the agent can follow a safer pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find existing OpenAI-compatible client calls.&lt;/li&gt;
&lt;li&gt;Move provider configuration into environment variables.&lt;/li&gt;
&lt;li&gt;Replace direct provider clients with the gateway base URL.&lt;/li&gt;
&lt;li&gt;Keep model IDs outside hard-coded feature logic where possible.&lt;/li&gt;
&lt;li&gt;Preserve streaming, tool calling, and structured-output behavior.&lt;/li&gt;
&lt;li&gt;Add a small verification path before claiming the migration worked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is much closer to production work than "switch the API key."&lt;/p&gt;

&lt;h2&gt;
  
  
  The real win is change management
&lt;/h2&gt;

&lt;p&gt;Model routing is not just an infra preference.&lt;/p&gt;

&lt;p&gt;It changes how product teams can operate.&lt;/p&gt;

&lt;p&gt;When the routing layer is explicit, the team can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;test cheaper models on low-risk tasks&lt;/li&gt;
&lt;li&gt;keep stronger models for high-value workflows&lt;/li&gt;
&lt;li&gt;add a fallback route for outages&lt;/li&gt;
&lt;li&gt;move media generation behind the same operational pattern&lt;/li&gt;
&lt;li&gt;compare providers without rewriting the feature&lt;/li&gt;
&lt;li&gt;keep provider churn away from product code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application becomes less attached to one vendor.&lt;/p&gt;

&lt;p&gt;The team becomes more explicit about which model is used for which job.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is where skills are useful
&lt;/h2&gt;

&lt;p&gt;Most teams do not need an agent that merely knows a gateway exists.&lt;/p&gt;

&lt;p&gt;They need an agent that knows how to migrate safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect the codebase first&lt;/li&gt;
&lt;li&gt;identify provider-specific assumptions&lt;/li&gt;
&lt;li&gt;make the smallest useful change&lt;/li&gt;
&lt;li&gt;avoid leaking secrets&lt;/li&gt;
&lt;li&gt;preserve existing behavior&lt;/li&gt;
&lt;li&gt;verify the path after the edit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between a tool and a workflow.&lt;/p&gt;

&lt;p&gt;The tool is the gateway.&lt;/p&gt;

&lt;p&gt;The workflow is how you introduce it without making the codebase harder to reason about.&lt;/p&gt;

&lt;p&gt;Source workflow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://terminalskills.io/use-cases/route-ai-model-requests-through-routerbase" rel="noopener noreferrer"&gt;https://terminalskills.io/use-cases/route-ai-model-requests-through-routerbase&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related skill:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://terminalskills.io/skills/routerbase-gateway" rel="noopener noreferrer"&gt;https://terminalskills.io/skills/routerbase-gateway&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;p&gt;If your AI app is starting to juggle providers, models, fallbacks, and media endpoints, do not scatter that logic through the product.&lt;/p&gt;

&lt;p&gt;Put it behind a gateway.&lt;/p&gt;

&lt;p&gt;Then teach the agent the migration workflow.&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>Your AI Coding Agent Does Not Need a Refactor Prompt. It Needs an Architecture Workflow.</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Sat, 11 Jul 2026 17:30:59 +0000</pubDate>
      <link>https://dev.to/alexshev/your-ai-coding-agent-does-not-need-a-refactor-prompt-it-needs-an-architecture-workflow-oof</link>
      <guid>https://dev.to/alexshev/your-ai-coding-agent-does-not-need-a-refactor-prompt-it-needs-an-architecture-workflow-oof</guid>
      <description>&lt;p&gt;Most AI refactoring advice starts in the wrong place.&lt;/p&gt;

&lt;p&gt;It assumes the hard part is telling the model what to change.&lt;/p&gt;

&lt;p&gt;So the prompt gets longer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Act as a senior architect.
Review this codebase.
Find refactoring opportunities.
Improve maintainability.
Make it more testable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds reasonable.&lt;/p&gt;

&lt;p&gt;But in a real repo, that prompt is too vague to be useful.&lt;/p&gt;

&lt;p&gt;The hard part is not asking for better architecture.&lt;/p&gt;

&lt;p&gt;The hard part is finding the parts of the codebase where architecture is actually hurting the work.&lt;/p&gt;

&lt;p&gt;Not every ugly file needs a refactor.&lt;br&gt;
Not every repeated function needs extraction.&lt;br&gt;
Not every small module is a good module.&lt;br&gt;
Not every "clean" interface hides useful complexity.&lt;/p&gt;

&lt;p&gt;Sometimes the code that looks messy is doing the real work.&lt;br&gt;
Sometimes the code that looks clean is only clean because the complexity was pushed into every caller.&lt;/p&gt;

&lt;p&gt;That is the part an AI coding agent needs help with.&lt;/p&gt;

&lt;p&gt;It needs an architecture workflow.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem with generic refactoring agents
&lt;/h2&gt;

&lt;p&gt;When I let an agent inspect a codebase with only a broad refactor instruction, it tends to do one of three things.&lt;/p&gt;

&lt;p&gt;First, it finds obvious style issues.&lt;/p&gt;

&lt;p&gt;Naming.&lt;br&gt;
Duplication.&lt;br&gt;
Large files.&lt;br&gt;
Missing comments.&lt;br&gt;
Inconsistent patterns.&lt;/p&gt;

&lt;p&gt;Those can matter, but they are rarely the architectural bottleneck.&lt;/p&gt;

&lt;p&gt;Second, it proposes interface extraction too early.&lt;/p&gt;

&lt;p&gt;The agent sees coupling, so it creates a service.&lt;br&gt;
It sees repeated logic, so it creates a helper.&lt;br&gt;
It sees conditionals, so it creates strategies.&lt;/p&gt;

&lt;p&gt;The code looks more "designed" afterward, but the system may become harder to understand because the same concept is now split across more files.&lt;/p&gt;

&lt;p&gt;Third, it treats testability as an extraction problem.&lt;/p&gt;

&lt;p&gt;If something is hard to test, the agent may pull small pure functions out of the flow until the tests become easy.&lt;/p&gt;

&lt;p&gt;That can make unit tests pleasant while hiding the real bugs in orchestration, boundaries, state, and integration behavior.&lt;/p&gt;

&lt;p&gt;The result is a codebase with more files, more seams, more mock points, and not necessarily better architecture.&lt;/p&gt;

&lt;p&gt;I do not want an agent to refactor like that.&lt;/p&gt;

&lt;p&gt;I want the agent to slow down and ask a better question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where did understanding this system become expensive?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Architectural friction is a better signal than file size
&lt;/h2&gt;

&lt;p&gt;A useful architecture pass should start by exploring the codebase the way a new senior engineer would.&lt;/p&gt;

&lt;p&gt;Not by counting lines.&lt;br&gt;
Not by ranking files by complexity score.&lt;br&gt;
Not by searching for "TODO".&lt;/p&gt;

&lt;p&gt;By following concepts.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where does one business concept require bouncing through five shallow modules?&lt;/li&gt;
&lt;li&gt;Where is the public interface almost as complicated as the implementation?&lt;/li&gt;
&lt;li&gt;Where do callers need to know too much about internal sequencing?&lt;/li&gt;
&lt;li&gt;Where are tests mostly checking implementation details?&lt;/li&gt;
&lt;li&gt;Where do changes require touching several files that should probably move together?&lt;/li&gt;
&lt;li&gt;Where does the agent keep losing the thread while trying to understand one behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is underrated.&lt;/p&gt;

&lt;p&gt;If an agent gets lost while navigating the repo, that is not only a model limitation.&lt;/p&gt;

&lt;p&gt;It may be an architecture signal.&lt;/p&gt;

&lt;p&gt;Humans feel this too. We just describe it differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every change here takes longer than it should.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You have to know too much context before touching this module.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The tests pass, but I do not trust this area.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the layer worth investigating.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deep module idea maps surprisingly well to agents
&lt;/h2&gt;

&lt;p&gt;John Ousterhout's deep module idea is still one of the best ways to think about this.&lt;/p&gt;

&lt;p&gt;A deep module has a small interface that hides meaningful complexity.&lt;/p&gt;

&lt;p&gt;That matters for humans because a good interface reduces the amount of system knowledge a developer needs to hold in their head.&lt;/p&gt;

&lt;p&gt;It matters for AI agents for the same reason.&lt;/p&gt;

&lt;p&gt;Agents are good at following local instructions.&lt;/p&gt;

&lt;p&gt;They are worse when every small change requires reconstructing a hidden social map of the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This helper is called here.
But only after this validation.
Unless this flag is set.
And this type is technically shared.
But this caller mutates it.
And the test mock does not match production behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the implementation is scattered but the concept is unified, the agent has to infer the module boundary every time.&lt;/p&gt;

&lt;p&gt;That is waste.&lt;/p&gt;

&lt;p&gt;Good architecture gives both humans and agents a smaller surface to reason about.&lt;/p&gt;

&lt;p&gt;The goal is not fewer files.&lt;/p&gt;

&lt;p&gt;The goal is better boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  A better workflow for agent-assisted architecture review
&lt;/h2&gt;

&lt;p&gt;The workflow I prefer has seven stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Explore before proposing
&lt;/h3&gt;

&lt;p&gt;The agent should walk the codebase and record friction.&lt;/p&gt;

&lt;p&gt;No refactor proposals yet.&lt;br&gt;
No new interfaces yet.&lt;br&gt;
No patches yet.&lt;/p&gt;

&lt;p&gt;Just exploration.&lt;/p&gt;

&lt;p&gt;The output should be a list of architectural friction points, not a list of code smells.&lt;/p&gt;

&lt;p&gt;For each candidate, I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the module or concept cluster involved&lt;/li&gt;
&lt;li&gt;why those files are coupled&lt;/li&gt;
&lt;li&gt;what state or behavior is spread across them&lt;/li&gt;
&lt;li&gt;what tests are hard to write or trust&lt;/li&gt;
&lt;li&gt;what kind of change becomes expensive there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the step most refactor prompts skip.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Present candidates, not solutions
&lt;/h3&gt;

&lt;p&gt;The agent should show the candidates and ask which one to explore.&lt;/p&gt;

&lt;p&gt;That sounds small, but it prevents a common failure mode:&lt;/p&gt;

&lt;p&gt;The agent picks an area that looks bad but is not important.&lt;/p&gt;

&lt;p&gt;Architecture work should follow business and maintenance pressure.&lt;/p&gt;

&lt;p&gt;If an area is ugly but stable, it may not be the next refactor.&lt;/p&gt;

&lt;p&gt;If an area changes every week and every change creates review anxiety, that is a better target.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Frame the problem space
&lt;/h3&gt;

&lt;p&gt;Before designing anything, the agent should describe the constraints.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This cluster handles customer eligibility.
It currently depends on plan state, billing status, account flags, and region rules.
The callers mostly need one decision: can this customer use this feature?
The current implementation exposes too many intermediate checks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This framing is useful because it separates the problem from the proposed interface.&lt;/p&gt;

&lt;p&gt;The team can disagree with the framing before arguing about code.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Generate multiple interface designs
&lt;/h3&gt;

&lt;p&gt;One proposal is not enough.&lt;/p&gt;

&lt;p&gt;Architecture design benefits from contrast.&lt;/p&gt;

&lt;p&gt;I like asking separate agents, or separate passes, to design different interfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one minimal interface&lt;/li&gt;
&lt;li&gt;one flexible interface&lt;/li&gt;
&lt;li&gt;one optimized for the most common caller&lt;/li&gt;
&lt;li&gt;one ports-and-adapters version if external dependencies are involved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to blindly choose the most elegant design.&lt;/p&gt;

&lt;p&gt;The point is to expose trade-offs.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Compare the designs in prose
&lt;/h3&gt;

&lt;p&gt;This is where the main agent should be opinionated.&lt;/p&gt;

&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here are three options. Which do you prefer?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Option A is easiest to adopt but leaks dependency order.
Option B hides the right complexity but may be too abstract for current callers.
Option C is the strongest default because most callers only need one decision.
I would use C, with the error reporting shape from B.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the kind of help I actually want from an agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Turn the decision into an RFC
&lt;/h3&gt;

&lt;p&gt;Once the direction is chosen, do not jump straight to a giant PR.&lt;/p&gt;

&lt;p&gt;Create a refactor RFC issue.&lt;/p&gt;

&lt;p&gt;It should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the current friction&lt;/li&gt;
&lt;li&gt;the chosen boundary&lt;/li&gt;
&lt;li&gt;the interface sketch&lt;/li&gt;
&lt;li&gt;migration plan&lt;/li&gt;
&lt;li&gt;test strategy&lt;/li&gt;
&lt;li&gt;risks&lt;/li&gt;
&lt;li&gt;what not to change yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives the team a reviewable architecture artifact before implementation begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Only then edit code
&lt;/h3&gt;

&lt;p&gt;The implementation should be the last step, not the first.&lt;/p&gt;

&lt;p&gt;If the agent starts by editing, it will often optimize the local patch instead of the architecture.&lt;/p&gt;

&lt;p&gt;The workflow has to force the design conversation first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Terminal Skills fits
&lt;/h2&gt;

&lt;p&gt;This is the kind of workflow that belongs in a skill, not in a one-off prompt.&lt;/p&gt;

&lt;p&gt;The Terminal Skills catalog has an &lt;code&gt;improve-codebase-architecture&lt;/code&gt; skill that packages this exact style of process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://terminalskills.io/skills/improve-codebase-architecture" rel="noopener noreferrer"&gt;Improve Codebase Architecture on Terminal Skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not a magic refactoring button.&lt;/p&gt;

&lt;p&gt;That is the point.&lt;/p&gt;

&lt;p&gt;It teaches the agent to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explore the codebase organically&lt;/li&gt;
&lt;li&gt;treat navigation friction as a signal&lt;/li&gt;
&lt;li&gt;identify shallow modules and coupling clusters&lt;/li&gt;
&lt;li&gt;present candidates before proposing interfaces&lt;/li&gt;
&lt;li&gt;design multiple alternative boundaries&lt;/li&gt;
&lt;li&gt;compare trade-offs&lt;/li&gt;
&lt;li&gt;create a GitHub issue RFC instead of silently rewriting the system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install command for Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;improve-codebase-architecture &lt;span class="nt"&gt;--agent&lt;/span&gt; codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;improve-codebase-architecture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native ad here is simple:&lt;/p&gt;

&lt;p&gt;If your agent keeps making refactors that look clean but do not make the codebase easier to change, do not give it a bigger prompt.&lt;/p&gt;

&lt;p&gt;Give it a repeatable architecture workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters more now
&lt;/h2&gt;

&lt;p&gt;Agent skills are becoming a serious part of terminal-agent work.&lt;/p&gt;

&lt;p&gt;The ecosystem is moving in the same direction: reusable procedures are becoming a way to encode what to accomplish, when to apply it, and how to execute terminal tasks. The uncomfortable side is that skill retrieval, skill quality, and reusable automation can become fragile if the procedure is vague or unsafe.&lt;/p&gt;

&lt;p&gt;That matches what I see in practice.&lt;/p&gt;

&lt;p&gt;Skills are powerful when they are specific, procedural, and verifiable.&lt;/p&gt;

&lt;p&gt;They are weak when they are just long advice files.&lt;/p&gt;

&lt;p&gt;An architecture skill is a good test case because it cannot succeed by memorizing commands.&lt;/p&gt;

&lt;p&gt;It has to guide judgment.&lt;/p&gt;

&lt;p&gt;It has to slow the agent down.&lt;/p&gt;

&lt;p&gt;It has to preserve human decision points.&lt;/p&gt;

&lt;p&gt;It has to make the final artifact reviewable.&lt;/p&gt;

&lt;p&gt;That is what separates an agent workflow from an agent vibe.&lt;/p&gt;




&lt;h2&gt;
  
  
  My practical checklist
&lt;/h2&gt;

&lt;p&gt;If I were evaluating an architecture skill for a coding agent, I would ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it force exploration before edits?&lt;/li&gt;
&lt;li&gt;Does it identify concept clusters, not just files?&lt;/li&gt;
&lt;li&gt;Does it distinguish coupling from duplication?&lt;/li&gt;
&lt;li&gt;Does it ask for human selection before deep design?&lt;/li&gt;
&lt;li&gt;Does it generate multiple interface options?&lt;/li&gt;
&lt;li&gt;Does it compare trade-offs in plain language?&lt;/li&gt;
&lt;li&gt;Does it produce an RFC before implementation?&lt;/li&gt;
&lt;li&gt;Does it define what should not be changed?&lt;/li&gt;
&lt;li&gt;Does it improve test boundaries instead of just adding mocks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, it is probably not an architecture workflow.&lt;/p&gt;

&lt;p&gt;It is just a refactor prompt wearing a better title.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;AI agents are getting better at writing code.&lt;/p&gt;

&lt;p&gt;That makes architecture more important, not less.&lt;/p&gt;

&lt;p&gt;When the agent can produce a thousand-line refactor in minutes, the bottleneck shifts from typing to judgment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Was this the right boundary?
Did we hide the right complexity?
Did we make future changes easier?
Can the team review the decision?
Can the agent explain why it chose this shape?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those questions do not belong at the end of the PR.&lt;/p&gt;

&lt;p&gt;They belong in the workflow.&lt;/p&gt;

&lt;p&gt;That is why I think the next useful layer for AI coding agents is not more autonomous editing.&lt;/p&gt;

&lt;p&gt;It is better operating procedures.&lt;/p&gt;

&lt;p&gt;Skills are one way to package those procedures.&lt;/p&gt;

&lt;p&gt;And architecture review is one of the places where that packaging starts to matter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: AI assistance was used to draft and edit this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>devtools</category>
      <category>refactoring</category>
    </item>
    <item>
      <title>The Local SEO Report Should Be an Agent Run, Not a Dashboard Screenshot</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Fri, 03 Jul 2026 16:21:25 +0000</pubDate>
      <link>https://dev.to/alexshev/the-local-seo-report-should-be-an-agent-run-not-a-dashboard-screenshot-469i</link>
      <guid>https://dev.to/alexshev/the-local-seo-report-should-be-an-agent-run-not-a-dashboard-screenshot-469i</guid>
      <description>&lt;p&gt;Every local SEO agency has some version of the Monday reporting ritual.&lt;/p&gt;

&lt;p&gt;Someone opens a spreadsheet.&lt;/p&gt;

&lt;p&gt;Someone opens Google Business Profiles.&lt;/p&gt;

&lt;p&gt;Someone checks rankings for a few money keywords.&lt;/p&gt;

&lt;p&gt;Someone looks at reviews.&lt;/p&gt;

&lt;p&gt;Someone peeks at competitors.&lt;/p&gt;

&lt;p&gt;Then the team turns all of that into a client-friendly update that says what changed, what matters, and what needs approval.&lt;/p&gt;

&lt;p&gt;The work is valuable.&lt;/p&gt;

&lt;p&gt;The ritual is awful.&lt;/p&gt;

&lt;p&gt;It is repetitive, stateful, and full of tiny judgment calls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did the business drop out of the map pack?&lt;/li&gt;
&lt;li&gt;did a negative review arrive over the weekend?&lt;/li&gt;
&lt;li&gt;does the reply need owner approval?&lt;/li&gt;
&lt;li&gt;did a nearby competitor suddenly gain reviews?&lt;/li&gt;
&lt;li&gt;is this a real issue, or just noise?&lt;/li&gt;
&lt;li&gt;should the client act this week, or just keep watching?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly the kind of workflow where I do not want an AI agent to "write an SEO report" from scratch.&lt;/p&gt;

&lt;p&gt;I want the agent to run the operating loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard is not the workflow
&lt;/h2&gt;

&lt;p&gt;Dashboards are useful for humans.&lt;/p&gt;

&lt;p&gt;They are less useful as the primary interface for agents.&lt;/p&gt;

&lt;p&gt;A dashboard usually answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What can I see if I click around?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent workflow needs a different interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What state exists?
What changed?
What should be checked next?
What requires approval?
What should never be done automatically?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;Local SEO reporting is not just data extraction. It is a recurring workflow with memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;businesses already onboarded&lt;/li&gt;
&lt;li&gt;keywords already tracked&lt;/li&gt;
&lt;li&gt;ranking history over time&lt;/li&gt;
&lt;li&gt;reviews already answered or still pending&lt;/li&gt;
&lt;li&gt;competitors already on a watchlist&lt;/li&gt;
&lt;li&gt;locations that need neighborhood-specific checks&lt;/li&gt;
&lt;li&gt;client-specific rules around tone, compliance, and approval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the agent only gets a screenshot or CSV export, it has to infer too much.&lt;/p&gt;

&lt;p&gt;If the agent gets a structured tool layer, it can operate much closer to the real job.&lt;/p&gt;

&lt;p&gt;That is why the SEOG skill on Terminal Skills is interesting to me.&lt;/p&gt;

&lt;p&gt;It turns local SEO monitoring into a terminal-native MCP workflow instead of another browser tab to babysit.&lt;/p&gt;

&lt;p&gt;Source skill: &lt;a href="https://terminalskills.io/skills/seog" rel="noopener noreferrer"&gt;SEOG on Terminal Skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use case: &lt;a href="https://terminalskills.io/use-cases/automate-local-seo-agency-reporting" rel="noopener noreferrer"&gt;Automate Local SEO Monitoring for Client Businesses&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the skill actually gives the agent
&lt;/h2&gt;

&lt;p&gt;The SEOG skill connects an AI coding agent to the SEOG MCP server.&lt;/p&gt;

&lt;p&gt;The platform is built around local businesses that live or die on Google Maps visibility: Google Business Profile data, map-pack rankings, reviews, and nearby competitors.&lt;/p&gt;

&lt;p&gt;The MCP server exposes those operations as tools.&lt;/p&gt;

&lt;p&gt;That is the important part.&lt;/p&gt;

&lt;p&gt;Instead of asking the agent to scrape pages or reason from a dashboard screenshot, the agent can call tools for the core entities in the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;businesses&lt;/li&gt;
&lt;li&gt;keywords and rankings&lt;/li&gt;
&lt;li&gt;Google reviews&lt;/li&gt;
&lt;li&gt;competitors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skill describes a streamable HTTP MCP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http seog https://api.seog.ai/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;your-seog-mcp-token&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the agent can work through a structured local SEO loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;list_businesses
check_keyword
keyword_history
sync_reviews
list_reviews
draft_review_response
discover_competitors
snapshot_competitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much better contract than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open my dashboard and tell me how things look.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The useful report is an impact queue
&lt;/h2&gt;

&lt;p&gt;A weak AI-generated SEO report sounds 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;Your local visibility is important. Reviews matter. Keep optimizing your profile.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically true.&lt;/p&gt;

&lt;p&gt;Completely useless.&lt;/p&gt;

&lt;p&gt;A useful local SEO report is closer to an impact queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. You dropped from #2 to #5 for "emergency dentist near me" in Hyde Park.
2. Two new negative reviews need owner-approved replies.
3. A competitor within 1km added 43 reviews this month.
4. Your service page still does not match the GBP category you are trying to rank for.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the reporting shape agencies actually need.&lt;/p&gt;

&lt;p&gt;The agent should not just summarize.&lt;/p&gt;

&lt;p&gt;It should triage.&lt;/p&gt;

&lt;p&gt;The Terminal Skills use case frames this well: the agent checks rankings, syncs reviews, snapshots watched competitors, and writes a weekly digest with movement, unanswered negative reviews, drafted replies, and competitor alerts.&lt;/p&gt;

&lt;p&gt;The detail I like most is prioritization.&lt;/p&gt;

&lt;p&gt;A client dropping out of the 3-pack for a money keyword matters more than a 4-star review that has not been answered yet.&lt;/p&gt;

&lt;p&gt;That sounds obvious to an operator.&lt;/p&gt;

&lt;p&gt;It is not obvious to a generic writing model unless the workflow teaches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Location context is not optional
&lt;/h2&gt;

&lt;p&gt;One trap in local SEO reporting is pretending that rankings are a single global number.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;For local businesses, the search location changes the answer.&lt;/p&gt;

&lt;p&gt;Checking from the agency office is often the wrong measurement.&lt;/p&gt;

&lt;p&gt;A coffee shop, dentist, med spa, or plumber may care about one neighborhood more than another. The useful question is not only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where do we rank?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where do we rank from the places customers actually search?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why I like seeing &lt;code&gt;locationLabel&lt;/code&gt; and coordinates as part of the keyword tracking flow.&lt;/p&gt;

&lt;p&gt;The agent can track a keyword from "Hyde Park" and "South Congress" instead of flattening the whole city into one report.&lt;/p&gt;

&lt;p&gt;That makes the weekly digest more honest.&lt;/p&gt;

&lt;p&gt;It also changes the recommendations.&lt;/p&gt;

&lt;p&gt;A business might be fine near its physical address but weak in the service area it wants to expand into. That is a different action item than "rankings are down."&lt;/p&gt;

&lt;h2&gt;
  
  
  Review replies should be draft-first
&lt;/h2&gt;

&lt;p&gt;This is the line I would not cross with automation:&lt;/p&gt;

&lt;p&gt;An agent can draft review replies.&lt;/p&gt;

&lt;p&gt;It should not silently publish them.&lt;/p&gt;

&lt;p&gt;The SEOG skill makes that boundary explicit: &lt;code&gt;draft_review_response&lt;/code&gt; saves a draft, but the owner approves in-app.&lt;/p&gt;

&lt;p&gt;That is the right design.&lt;/p&gt;

&lt;p&gt;Review replies are public. They touch customer trust. In medical, legal, home services, and other sensitive categories, a careless reply can create real problems.&lt;/p&gt;

&lt;p&gt;For regulated businesses, even a friendly response can go wrong if it confirms that someone was a customer or patient.&lt;/p&gt;

&lt;p&gt;So the automation boundary should look 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;Agent:
- finds reviews that need attention
- groups negative or risky reviews first
- drafts safe replies
- explains why each reply needs review

Human:
- approves
- edits
- publishes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a limitation.&lt;/p&gt;

&lt;p&gt;That is the workflow becoming safer.&lt;/p&gt;

&lt;p&gt;Good agent systems are not the ones that automate every click.&lt;/p&gt;

&lt;p&gt;They are the ones that know which clicks require approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Competitor monitoring is where the report gets interesting
&lt;/h2&gt;

&lt;p&gt;Most agencies already track their own client.&lt;/p&gt;

&lt;p&gt;The better reports also watch the local battlefield.&lt;/p&gt;

&lt;p&gt;If a nearby competitor suddenly gains review velocity, changes categories, adds services, or starts outranking the client for a money keyword, the client should hear about it before they notice it themselves.&lt;/p&gt;

&lt;p&gt;The SEOG workflow includes competitor discovery and snapshots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover_competitors
add_competitor
set_competitor_watchlist
snapshot_competitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the agent a useful job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not tell me everything.
Tell me what changed around this business.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between reporting and monitoring.&lt;/p&gt;

&lt;p&gt;Reporting describes what happened.&lt;/p&gt;

&lt;p&gt;Monitoring notices what is starting to happen.&lt;/p&gt;

&lt;p&gt;For local SEO, that can be the difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your rankings dropped last month.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The competitor two blocks away is about to pass your review count. Start a compliant review request push now.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one is much more valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent still needs guardrails
&lt;/h2&gt;

&lt;p&gt;This kind of workflow is also a good reminder that MCP tools are not enough by themselves.&lt;/p&gt;

&lt;p&gt;The skill has to teach operational boundaries.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deleting a business is irreversible and should require confirmation&lt;/li&gt;
&lt;li&gt;live rank checks consume quota and should not run in tight loops&lt;/li&gt;
&lt;li&gt;review replies are drafts, not published responses&lt;/li&gt;
&lt;li&gt;API tokens are credentials and should never be logged or committed&lt;/li&gt;
&lt;li&gt;a 401 probably means the token was revoked and needs reissue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those details are not decoration.&lt;/p&gt;

&lt;p&gt;They are the difference between a useful agent and a risky one.&lt;/p&gt;

&lt;p&gt;A generic model can understand "local SEO reporting."&lt;/p&gt;

&lt;p&gt;A skill can tell it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not delete this portfolio by accident.
Do not burn quota in a loop.
Do not claim a review reply was published.
Do not leak the token.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly why I keep coming back to skills as the right abstraction for agent work.&lt;/p&gt;

&lt;p&gt;Prompts describe intent.&lt;/p&gt;

&lt;p&gt;Skills describe operating procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A weekly agency run should look boring
&lt;/h2&gt;

&lt;p&gt;The best version of this workflow is not flashy.&lt;/p&gt;

&lt;p&gt;It is a scheduled run.&lt;/p&gt;

&lt;p&gt;Every Monday morning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each client:
1. sync the business state
2. check active keywords by tracked location
3. compare rank movement against the last 7 days
4. sync reviews
5. draft replies for negative or unanswered reviews
6. snapshot watchlisted competitors
7. produce an impact-ranked digest
8. stop before anything public is published
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is boring in the best way.&lt;/p&gt;

&lt;p&gt;It gives the agency leverage without turning the account into an autopilot risk.&lt;/p&gt;

&lt;p&gt;The human still owns strategy, client communication, and approvals.&lt;/p&gt;

&lt;p&gt;The agent owns the repetitive monitoring loop.&lt;/p&gt;

&lt;p&gt;That is the split I trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger lesson
&lt;/h2&gt;

&lt;p&gt;I do not think the future of local SEO work is "AI writes reports."&lt;/p&gt;

&lt;p&gt;That is too shallow.&lt;/p&gt;

&lt;p&gt;The more useful shift is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI agents run recurring evidence-gathering workflows,
then hand humans a prioritized approval queue.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a local SEO agency, that queue might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rankings that moved&lt;/li&gt;
&lt;li&gt;reviews that need attention&lt;/li&gt;
&lt;li&gt;competitors that changed&lt;/li&gt;
&lt;li&gt;pages that need technical cleanup&lt;/li&gt;
&lt;li&gt;schema gaps&lt;/li&gt;
&lt;li&gt;business facts that no longer match across the web&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The report becomes a byproduct of the operating loop.&lt;/p&gt;

&lt;p&gt;That is the part worth building.&lt;/p&gt;

&lt;p&gt;If an agent can run the same checks every week, preserve state, respect approval boundaries, and explain what changed, it is no longer just generating content.&lt;/p&gt;

&lt;p&gt;It is doing operational work.&lt;/p&gt;

&lt;p&gt;And that is where agent skills start to feel less like prompt engineering and more like actual software.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devtools</category>
      <category>seo</category>
    </item>
    <item>
      <title>The Blender Skill That Makes AI Agents Prove the Render Exists</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Tue, 30 Jun 2026 21:36:15 +0000</pubDate>
      <link>https://dev.to/alexshev/the-blender-skill-that-makes-ai-agents-prove-the-render-exists-2e7o</link>
      <guid>https://dev.to/alexshev/the-blender-skill-that-makes-ai-agents-prove-the-render-exists-2e7o</guid>
      <description>&lt;p&gt;Most AI + Blender demos stop one step too early.&lt;/p&gt;

&lt;p&gt;The agent writes a script.&lt;/p&gt;

&lt;p&gt;The script looks plausible.&lt;/p&gt;

&lt;p&gt;The explanation sounds confident.&lt;/p&gt;

&lt;p&gt;But the real question is much simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did Blender actually render the thing?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the line between a demo and a workflow.&lt;/p&gt;

&lt;p&gt;If an AI agent is helping with 3D production, the output cannot just be a paragraph of advice or a Python snippet that might work. At some point, the agent has to cross into the production layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configure the render engine&lt;/li&gt;
&lt;li&gt;set resolution and file format&lt;/li&gt;
&lt;li&gt;create or aim the camera&lt;/li&gt;
&lt;li&gt;set up lights&lt;/li&gt;
&lt;li&gt;apply materials&lt;/li&gt;
&lt;li&gt;render a frame or sequence&lt;/li&gt;
&lt;li&gt;verify that the output file exists&lt;/li&gt;
&lt;li&gt;return something the human can inspect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I like the idea behind the &lt;code&gt;blender-render-automation&lt;/code&gt; skill on Terminal Skills.&lt;/p&gt;

&lt;p&gt;It is not trying to make Blender “magic.”&lt;/p&gt;

&lt;p&gt;It is trying to make Blender boring enough for an agent to operate.&lt;/p&gt;

&lt;p&gt;And boring is where real automation starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with AI-generated Blender scripts
&lt;/h2&gt;

&lt;p&gt;Blender has a powerful Python API.&lt;/p&gt;

&lt;p&gt;That makes it tempting to treat Blender automation as a code-generation problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ask the agent for a bpy script.
Paste it into Blender.
Hope it works.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes it does.&lt;/p&gt;

&lt;p&gt;Sometimes the script uses the wrong render engine name.&lt;/p&gt;

&lt;p&gt;Sometimes the camera points nowhere useful.&lt;/p&gt;

&lt;p&gt;Sometimes the material code assumes a node that is not there.&lt;/p&gt;

&lt;p&gt;Sometimes the render settings are incomplete.&lt;/p&gt;

&lt;p&gt;Sometimes the agent says “done” even though no image was written to disk.&lt;/p&gt;

&lt;p&gt;The annoying part is that these failures are not always dramatic. They are small production misses. The script almost works. The scene almost renders. The output almost matches what you asked for.&lt;/p&gt;

&lt;p&gt;But “almost” is expensive in 3D work.&lt;/p&gt;

&lt;p&gt;If you are creating product shots, thumbnails, animation previews, turntables, or review renders, the workflow needs a stronger contract than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here is some code you can try.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here is the render output. Here is the path. Here is what was configured. Here is what still needs review.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A render skill is a definition of done
&lt;/h2&gt;

&lt;p&gt;The useful thing about a Blender render skill is not only that it knows &lt;code&gt;bpy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The useful thing is that it teaches the agent what “done” means.&lt;/p&gt;

&lt;p&gt;For a render workflow, “done” should not mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the agent wrote a plausible answer&lt;/li&gt;
&lt;li&gt;the agent described Cycles vs EEVEE&lt;/li&gt;
&lt;li&gt;the agent generated a script and stopped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blender was run headlessly or through a known script path&lt;/li&gt;
&lt;li&gt;the render engine was explicitly configured&lt;/li&gt;
&lt;li&gt;output resolution and format were set&lt;/li&gt;
&lt;li&gt;cameras and lights were created or selected&lt;/li&gt;
&lt;li&gt;materials were applied with clear names&lt;/li&gt;
&lt;li&gt;a still frame or animation sequence was rendered&lt;/li&gt;
&lt;li&gt;the expected file appeared at the expected path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a much better interface for AI agents.&lt;/p&gt;

&lt;p&gt;It turns the task from “make a nice Blender thing” into a workflow with observable artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why headless Blender matters
&lt;/h2&gt;

&lt;p&gt;Blender is a visual tool, but it can also run from the command line.&lt;/p&gt;

&lt;p&gt;That changes what an agent can do.&lt;/p&gt;

&lt;p&gt;Instead of manually clicking through the UI, the agent can operate through a repeatable loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blender scene.blend &lt;span class="nt"&gt;--background&lt;/span&gt; &lt;span class="nt"&gt;--render-output&lt;/span&gt; /tmp/frame_ &lt;span class="nt"&gt;--render-frame&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blender scene.blend &lt;span class="nt"&gt;--background&lt;/span&gt; &lt;span class="nt"&gt;--frame-start&lt;/span&gt; 1 &lt;span class="nt"&gt;--frame-end&lt;/span&gt; 100 &lt;span class="nt"&gt;--render-anim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That loop is important because agents are much better when they can run something, inspect the result, and continue.&lt;/p&gt;

&lt;p&gt;A GUI-only workflow often leaves the agent guessing.&lt;/p&gt;

&lt;p&gt;A terminal workflow gives it evidence.&lt;/p&gt;

&lt;p&gt;The agent can check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did the command exit successfully?&lt;/li&gt;
&lt;li&gt;did the PNG sequence appear?&lt;/li&gt;
&lt;li&gt;did the MP4 get created?&lt;/li&gt;
&lt;li&gt;did the output directory contain the expected frames?&lt;/li&gt;
&lt;li&gt;did the render crash halfway through?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those checks are not glamorous.&lt;/p&gt;

&lt;p&gt;They are exactly what make automation useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cycles, EEVEE, and choosing the right failure mode
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;blender-render-automation&lt;/code&gt; skill also points at an important production distinction: not every render needs the same engine.&lt;/p&gt;

&lt;p&gt;Cycles is physically accurate and better for final quality.&lt;/p&gt;

&lt;p&gt;EEVEE is fast and better for previews.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it matters for agents.&lt;/p&gt;

&lt;p&gt;If an agent always chooses the slowest path, it wastes time.&lt;/p&gt;

&lt;p&gt;If it always chooses the fastest path, it may return a preview that is not good enough for review.&lt;/p&gt;

&lt;p&gt;A good render workflow should make that choice explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use EEVEE for fast preview and layout checks&lt;/li&gt;
&lt;li&gt;use Cycles for final stills or higher-quality review frames&lt;/li&gt;
&lt;li&gt;enable denoising when samples are low&lt;/li&gt;
&lt;li&gt;use GPU rendering when the environment supports it&lt;/li&gt;
&lt;li&gt;avoid pretending a preview render is a final render&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That kind of operational judgment is exactly what belongs in a skill.&lt;/p&gt;

&lt;p&gt;The model can still reason about the task.&lt;/p&gt;

&lt;p&gt;But the workflow gives it defaults that are safer than fresh improvisation every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cameras and lights are not decoration
&lt;/h2&gt;

&lt;p&gt;A lot of failed AI-Blender attempts are not really modeling failures.&lt;/p&gt;

&lt;p&gt;They are camera and lighting failures.&lt;/p&gt;

&lt;p&gt;The object exists.&lt;/p&gt;

&lt;p&gt;The scene exists.&lt;/p&gt;

&lt;p&gt;But the camera misses the subject, the focal length is wrong, the light is too weak, or the result is technically rendered but useless.&lt;/p&gt;

&lt;p&gt;A render automation skill can push the agent to treat cameras and lights as part of the deliverable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a camera at a known location&lt;/li&gt;
&lt;li&gt;aim it at the subject&lt;/li&gt;
&lt;li&gt;use a reasonable focal length&lt;/li&gt;
&lt;li&gt;add area lights or environment lighting&lt;/li&gt;
&lt;li&gt;name cameras clearly&lt;/li&gt;
&lt;li&gt;batch render multiple camera views when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially useful for product work.&lt;/p&gt;

&lt;p&gt;One render is rarely enough.&lt;/p&gt;

&lt;p&gt;A human reviewer may need a hero angle, front view, side view, top view, and a transparent-background version.&lt;/p&gt;

&lt;p&gt;That should not require five separate improvisations.&lt;/p&gt;

&lt;p&gt;It should be a repeatable operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render animations as image sequences first
&lt;/h2&gt;

&lt;p&gt;One of the best small rules in render automation is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Render animations as image sequences before assembling video.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds like old-school pipeline advice because it is.&lt;/p&gt;

&lt;p&gt;It is also exactly the kind of rule AI agents need.&lt;/p&gt;

&lt;p&gt;If a direct video render fails at frame 87, you may lose the whole output or have a painful recovery path.&lt;/p&gt;

&lt;p&gt;If an image sequence fails at frame 87, frames 1-86 still exist.&lt;/p&gt;

&lt;p&gt;The agent can inspect what completed, rerun the missing range, and then assemble the sequence with FFmpeg.&lt;/p&gt;

&lt;p&gt;That is a production workflow.&lt;/p&gt;

&lt;p&gt;It is not just a Blender trick.&lt;/p&gt;

&lt;p&gt;It is a reliability pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prefer recoverable intermediate artifacts over one fragile final artifact.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pattern applies far beyond Blender.&lt;/p&gt;

&lt;p&gt;But Blender makes it very visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Materials need conventions, not vibes
&lt;/h2&gt;

&lt;p&gt;When a human says “make it premium,” an agent can generate endless material ideas.&lt;/p&gt;

&lt;p&gt;That is not always helpful.&lt;/p&gt;

&lt;p&gt;Production scenes need conventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;named materials&lt;/li&gt;
&lt;li&gt;predictable PBR settings&lt;/li&gt;
&lt;li&gt;sane roughness and metallic values&lt;/li&gt;
&lt;li&gt;glass separated from plastic and metal&lt;/li&gt;
&lt;li&gt;transparent-background settings when needed&lt;/li&gt;
&lt;li&gt;output formats chosen for the downstream use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A render skill does not remove taste from the process.&lt;/p&gt;

&lt;p&gt;It keeps the mechanical layer consistent so the human can spend attention on the creative layer.&lt;/p&gt;

&lt;p&gt;That is the right division of labor.&lt;/p&gt;

&lt;p&gt;The agent handles setup, naming, rendering, and verification.&lt;/p&gt;

&lt;p&gt;The human judges whether the result is good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real product is the workflow boundary
&lt;/h2&gt;

&lt;p&gt;When people talk about AI agents and creative tools, the conversation often jumps to autonomy.&lt;/p&gt;

&lt;p&gt;Can the agent make a complete scene by itself?&lt;/p&gt;

&lt;p&gt;Can it replace a 3D artist?&lt;/p&gt;

&lt;p&gt;Can it generate a finished animation from one sentence?&lt;/p&gt;

&lt;p&gt;I think that is the wrong starting point.&lt;/p&gt;

&lt;p&gt;The more useful question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the agent own a narrow, repeatable production step?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Blender rendering, that step might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set up a studio render scene&lt;/li&gt;
&lt;li&gt;render a transparent PNG&lt;/li&gt;
&lt;li&gt;create a 36-frame turntable&lt;/li&gt;
&lt;li&gt;generate a contact sheet from multiple cameras&lt;/li&gt;
&lt;li&gt;render an animation sequence&lt;/li&gt;
&lt;li&gt;produce preview and final variants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a realistic agent workflow.&lt;/p&gt;

&lt;p&gt;It does not need to replace the artist.&lt;/p&gt;

&lt;p&gt;It needs to remove the repetitive setup work around the artist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for agent skills
&lt;/h2&gt;

&lt;p&gt;The bigger lesson is not only about Blender.&lt;/p&gt;

&lt;p&gt;It is about how we package work for AI agents.&lt;/p&gt;

&lt;p&gt;A model can know a lot and still fail at the last mile.&lt;/p&gt;

&lt;p&gt;Skills help close that gap by giving the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a trigger for when the workflow applies&lt;/li&gt;
&lt;li&gt;operational steps&lt;/li&gt;
&lt;li&gt;command patterns&lt;/li&gt;
&lt;li&gt;quality checks&lt;/li&gt;
&lt;li&gt;failure modes&lt;/li&gt;
&lt;li&gt;a definition of done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I keep coming back to Terminal Skills as a useful layer.&lt;/p&gt;

&lt;p&gt;It treats skills less like prompt decorations and more like small workflow contracts.&lt;/p&gt;

&lt;p&gt;For Blender, that contract is easy to understand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not just describe the render. Produce it, verify it, and tell me where it is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the part agents need.&lt;/p&gt;

&lt;p&gt;Not more confidence.&lt;/p&gt;

&lt;p&gt;More proof.&lt;/p&gt;




&lt;p&gt;The skill I am talking about is &lt;code&gt;blender-render-automation&lt;/code&gt; on Terminal Skills:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://terminalskills.io/skills/blender-render-automation" rel="noopener noreferrer"&gt;https://terminalskills.io/skills/blender-render-automation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are experimenting with AI agents and Blender, I would start with one narrow workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a clean product scene, render one preview, verify the image exists, and return the output path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not flashy.&lt;/p&gt;

&lt;p&gt;But it is the right kind of boring.&lt;/p&gt;

&lt;p&gt;And once that works, you can build a real pipeline on top of it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>blender</category>
      <category>automation</category>
    </item>
    <item>
      <title>ComfyUI Is Becoming the Workflow Layer for AI Image Agents</title>
      <dc:creator>Alex Shev</dc:creator>
      <pubDate>Sat, 27 Jun 2026 17:46:10 +0000</pubDate>
      <link>https://dev.to/alexshev/comfyui-is-becoming-the-workflow-layer-for-ai-image-agents-8jo</link>
      <guid>https://dev.to/alexshev/comfyui-is-becoming-the-workflow-layer-for-ai-image-agents-8jo</guid>
      <description>&lt;p&gt;Most image generation tutorials still treat ComfyUI like a visual playground.&lt;/p&gt;

&lt;p&gt;Open the UI.&lt;br&gt;
Drag a few nodes together.&lt;br&gt;
Load a checkpoint.&lt;br&gt;
Generate an image.&lt;/p&gt;

&lt;p&gt;That is useful, but it undersells why ComfyUI keeps mattering.&lt;/p&gt;

&lt;p&gt;The more interesting shift is this:&lt;/p&gt;

&lt;p&gt;ComfyUI is not just a UI for image generation anymore. It is becoming a workflow layer.&lt;/p&gt;

&lt;p&gt;That matters a lot for AI agents.&lt;/p&gt;

&lt;p&gt;An agent does not only need to write a prompt. It needs to run a repeatable process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;choose the right model&lt;/li&gt;
&lt;li&gt;place files in the right folders&lt;/li&gt;
&lt;li&gt;load the workflow&lt;/li&gt;
&lt;li&gt;queue the job&lt;/li&gt;
&lt;li&gt;wait for completion&lt;/li&gt;
&lt;li&gt;retrieve the output&lt;/li&gt;
&lt;li&gt;verify that the output exists&lt;/li&gt;
&lt;li&gt;adapt the graph for the next run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A plain prompt does not give you that.&lt;/p&gt;

&lt;p&gt;A graph does.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the graph matters
&lt;/h2&gt;

&lt;p&gt;The big advantage of ComfyUI is that the workflow is explicit.&lt;/p&gt;

&lt;p&gt;Instead of hiding the image pipeline behind one text box, ComfyUI exposes the steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkpoint loading&lt;/li&gt;
&lt;li&gt;prompt encoding&lt;/li&gt;
&lt;li&gt;latent image creation&lt;/li&gt;
&lt;li&gt;sampling&lt;/li&gt;
&lt;li&gt;VAE decoding&lt;/li&gt;
&lt;li&gt;image saving&lt;/li&gt;
&lt;li&gt;ControlNet inputs&lt;/li&gt;
&lt;li&gt;LoRA loading&lt;/li&gt;
&lt;li&gt;upscaling&lt;/li&gt;
&lt;li&gt;custom nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can look intimidating at first, but it is exactly what makes ComfyUI useful for serious automation.&lt;/p&gt;

&lt;p&gt;If an image workflow is just a prompt, an agent has to guess what happened.&lt;/p&gt;

&lt;p&gt;If an image workflow is a graph, an agent can inspect the moving parts.&lt;/p&gt;

&lt;p&gt;It can reason about the graph. It can reuse it. It can change one piece without rewriting the whole pipeline.&lt;/p&gt;

&lt;p&gt;That is the difference between "generate something like this" and "run this visual production workflow again with different inputs."&lt;/p&gt;
&lt;h2&gt;
  
  
  Agents need workflows, not vibes
&lt;/h2&gt;

&lt;p&gt;For casual generation, a prompt box is fine.&lt;/p&gt;

&lt;p&gt;For production work, the weak point is rarely the first image.&lt;/p&gt;

&lt;p&gt;The weak point is consistency.&lt;/p&gt;

&lt;p&gt;Can you run the same style again?&lt;br&gt;
Can you swap the input image?&lt;br&gt;
Can you keep the ControlNet guide but change the subject?&lt;br&gt;
Can you send the result into an upscale pass?&lt;br&gt;
Can you use the same workflow from a script instead of clicking through the UI?&lt;/p&gt;

&lt;p&gt;That is where ComfyUI starts to feel less like an art tool and more like infrastructure.&lt;/p&gt;

&lt;p&gt;The workflow JSON becomes the contract.&lt;/p&gt;

&lt;p&gt;The agent does not need to remember every step from scratch. It can submit the workflow, poll for the result, download the output, then report what happened.&lt;/p&gt;
&lt;h2&gt;
  
  
  The API is the underrated part
&lt;/h2&gt;

&lt;p&gt;The visual graph is what people notice first.&lt;/p&gt;

&lt;p&gt;The API is what makes it automation-friendly.&lt;/p&gt;

&lt;p&gt;A normal agent path looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start ComfyUI locally or on a GPU box.&lt;/li&gt;
&lt;li&gt;Load or generate a workflow JSON.&lt;/li&gt;
&lt;li&gt;Submit that workflow to the &lt;code&gt;/prompt&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;li&gt;Poll &lt;code&gt;/history/{prompt_id}&lt;/code&gt; until the job completes.&lt;/li&gt;
&lt;li&gt;Fetch the generated image through &lt;code&gt;/view&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save the output into a known folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a much better fit for agents than browser-only image generation.&lt;/p&gt;

&lt;p&gt;The agent can run the same workflow from code. It can log prompt IDs. It can keep output paths stable. It can fail cleanly when the server is down or a model file is missing.&lt;/p&gt;

&lt;p&gt;That is not glamorous, but it is what makes the workflow usable.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup details are the real trap
&lt;/h2&gt;

&lt;p&gt;The hard part is not only "how do I use ComfyUI?"&lt;/p&gt;

&lt;p&gt;The hard part is all the small operational details around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which Python version is expected&lt;/li&gt;
&lt;li&gt;which CUDA or ROCm path is being used&lt;/li&gt;
&lt;li&gt;where checkpoint files belong&lt;/li&gt;
&lt;li&gt;where LoRAs belong&lt;/li&gt;
&lt;li&gt;where ControlNet models belong&lt;/li&gt;
&lt;li&gt;how custom nodes are installed&lt;/li&gt;
&lt;li&gt;how to run ComfyUI in Docker with GPU access&lt;/li&gt;
&lt;li&gt;how to avoid losing outputs in random folders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These details are boring, but agents break on boring details.&lt;/p&gt;

&lt;p&gt;That is why I like packaging this kind of workflow as a skill instead of leaving it as a loose note.&lt;/p&gt;

&lt;p&gt;For example, I keep a ComfyUI Terminal Skill here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://terminalskills.io/skills/comfyui" rel="noopener noreferrer"&gt;https://terminalskills.io/skills/comfyui&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The point is not "here is another page to read."&lt;/p&gt;

&lt;p&gt;The point is that an agent needs a repeatable operating path. The skill gives it the ComfyUI install shape, model folder conventions, API queue example, result polling, custom node setup, ControlNet pattern, and Docker deployment notes in one place.&lt;/p&gt;

&lt;p&gt;So when the task is "use ComfyUI for this workflow," the agent is not starting from search results. It has a known path through the tool.&lt;/p&gt;

&lt;p&gt;You can also install it directly for an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx terminal-skills &lt;span class="nb"&gt;install &lt;/span&gt;comfyui &lt;span class="nt"&gt;--agent&lt;/span&gt; codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the useful version of documentation for agent work: not a brochure, not a generic tutorial, but a compact workflow the agent can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical ComfyUI agent workflow
&lt;/h2&gt;

&lt;p&gt;If I were building an agent-controlled ComfyUI flow, I would keep it simple at first.&lt;/p&gt;

&lt;p&gt;Start with one known working workflow.&lt;/p&gt;

&lt;p&gt;Do not begin with ten custom node packs and a giant experimental graph.&lt;/p&gt;

&lt;p&gt;Use a small txt2img or img2img workflow and make the agent prove it can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start or reach the ComfyUI server&lt;/li&gt;
&lt;li&gt;submit one workflow&lt;/li&gt;
&lt;li&gt;capture the prompt ID&lt;/li&gt;
&lt;li&gt;wait for completion&lt;/li&gt;
&lt;li&gt;download the generated file&lt;/li&gt;
&lt;li&gt;verify the file exists&lt;/li&gt;
&lt;li&gt;report the exact output path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after that would I add more complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ControlNet&lt;/li&gt;
&lt;li&gt;LoRA variants&lt;/li&gt;
&lt;li&gt;upscale passes&lt;/li&gt;
&lt;li&gt;image prompt adapters&lt;/li&gt;
&lt;li&gt;animation nodes&lt;/li&gt;
&lt;li&gt;batch generation&lt;/li&gt;
&lt;li&gt;remote GPU deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first milestone is not a beautiful image.&lt;/p&gt;

&lt;p&gt;The first milestone is a reliable loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for creative automation
&lt;/h2&gt;

&lt;p&gt;ComfyUI is one of the places where AI image work becomes more like software engineering.&lt;/p&gt;

&lt;p&gt;You do not just write a prompt.&lt;/p&gt;

&lt;p&gt;You build a pipeline.&lt;/p&gt;

&lt;p&gt;That pipeline can be versioned, reused, inspected, debugged, and called from code.&lt;/p&gt;

&lt;p&gt;For human artists, that gives more control.&lt;/p&gt;

&lt;p&gt;For AI agents, it gives something even more important: structure.&lt;/p&gt;

&lt;p&gt;Agents are much more useful when the workflow has shape.&lt;/p&gt;

&lt;p&gt;ComfyUI gives image generation that shape.&lt;/p&gt;

&lt;p&gt;And once the workflow is explicit, the agent can stop guessing and start operating.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
