<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Jahvon Dockery</title>
    <link>https://jahvon.dev/</link>
    <description>Recent content on Jahvon Dockery</description>
    <image>
      <title>Jahvon Dockery</title>
      <url>https://jahvon.dev/images/og-default.png</url>
      <link>https://jahvon.dev/images/og-default.png</link>
    </image>
    <generator>Hugo -- 0.153.4</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://jahvon.dev/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Discovery</title>
      <link>https://jahvon.dev/architecture/mochi/discovery/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/mochi/discovery/</guid>
      <description>How Mochi finds what you can run, and why discovered executables are generated at read time instead of written into your repo.</description>
      <content:encoded><![CDATA[<h2 id="discovery">Discovery</h2>
<p>Discovery is the &ldquo;find&rdquo; half, and it&rsquo;s the piece I&rsquo;m happiest with architecturally.</p>
<p>A provider is about as small as an interface gets. It answers one question, given a directory:
which files here are importable? It also has to be a cheap check that cannot fail.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">DiscoveryProvider</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">Name</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">Imports</span><span class="p">(</span><span class="nx">dir</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">[]</span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>It returns file paths, not executables. Turning a file into executables is Flow&rsquo;s job (or, for
the formats Flow doesn&rsquo;t know about, a parser on Mochi&rsquo;s side). Nine providers ship today:
Makefile, <code>package.json</code>, docker-compose, loose shell scripts, Justfile, Taskfile, GitHub
Actions, Dockerfile, and Cargo. The first four are parsed by Flow itself; the rest are Mochi&rsquo;s.
Registration order matters. The first provider to claim a file wins, which is how conflicts get
resolved without a merge policy.</p>
<h3 id="nothing-is-written-to-disk">Nothing is written to disk</h3>
<p>The important design decision: discovered executables are generated <strong>at read time</strong>, not
imported into your repo.</p>
<p>Mochi decorates Flow&rsquo;s executable cache. Discovery persists only a selection of importable files
per workspace, and the decorator regenerates their executables on every read, so they appear
identically in the desktop, in <code>mochi run</code>, and in <code>flow browse</code>, while your repo stays exactly
as it was. Generation happens against a virtual flow file that only ever exists in memory; its
directory is the only thing that matters, because that&rsquo;s what imports resolve relative to.</p>
<p>Failures degrade to nothing rather than propagating, so a bad provider can never break the cache
it&rsquo;s wrapping. Repeat scans are cheap: state stores file modification times as a fingerprint and
short-circuits when nothing has changed.</p>
<p>Attribution has a constraint worth mentioning, because it shaped the design. Flow doesn&rsquo;t inherit
flow-file annotations into generated executables, and a slashed namespace would break its
reference parser. Discovered executables get a single namespace plus a per-executable
annotation naming the provider that found them.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How flow Runs the Cluster</title>
      <link>https://jahvon.dev/architecture/homelab/flow-workflows/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/homelab/flow-workflows/</guid>
      <description>There is no GitOps controller. Every deploy is a composed executable, secrets come from the vault, and the diagnostics half exists because it is 11pm.</description>
      <content:encoded><![CDATA[<h2 id="how-flow-is-actually-used">How flow Is Actually Used</h2>
<p>This is the part worth writing down.</p>
<p>The repo <em>is</em> a flow workspace. All of its automation lives in one <code>.execs/</code> directory, split by
concern rather than by application: utilities, cluster operations, apps, infrastructure,
networking, storage, platform, labs, and aggregates. It&rsquo;s a couple thousand lines of flow YAML,
which sounds like a lot until you consider it replaced a pile of shell scripts and a much larger
pile of things I used to keep in my head.</p>
<h3 id="everything-composes-from-a-shared-library">Everything composes from a shared library</h3>
<p>The single most useful thing I did was pull the repeated parts into a <code>utils</code> namespace and
then never write them again. A deploy is a serial composition of references:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">verb</span><span class="p">:</span><span class="w"> </span><span class="l">deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jellyfin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">media]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy the Jellyfin media server to Kubernetes using homelab chart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serial</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">failFast</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dir</span><span class="p">:</span><span class="w"> </span><span class="l">//apps/media/jellyfin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">execs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">install utils:helm-base</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jellyfin, jellyfin]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">verify utils:deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jellyfin, jellyfin]</span><span class="w">
</span></span></span></code></pre></div><p>Every app looks like that. Adding a new one is a values file, a chart reference, and about six
lines of flow. The namespace creation, the repo add, the <code>helm upgrade --install</code>, the rollout
wait, the verification. All of it lives in <code>utils</code> and is written once.</p>
<h3 id="secrets-come-from-the-vault-not-the-repo">Secrets come from the vault, not the repo</h3>
<p>Credentials are declared as parameters and resolved at run time. Nothing is committed, and
nothing sits in my shell history:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">secretRef</span><span class="p">:</span><span class="w"> </span><span class="l">bring-username</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">BRING_USERNAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">ADDITIONAL_ARGS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">text</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      --set controllers.main.containers.bring-api.env.BRING_USERNAME=$BRING_USERNAME</span><span class="w">
</span></span></span></code></pre></div><p>A shared <code>create utils:secret</code> executable turns those into Kubernetes secrets during the deploy.
There&rsquo;s no sealed-secrets controller, no SOPS, no external secrets operator. For a single
operator, flow&rsquo;s vault is the whole secret management story.</p>
<h3 id="aggregates-for-the-things-i-do-together">Aggregates for the things I do together</h3>
<p>I rarely want to deploy one media app. <code>flow deploy media-stack</code> runs the six of them in order
and then prints the URLs; <code>flow verify media-stack</code> checks all six in parallel. The aggregate is
just another executable that references the others.</p>
<h3 id="operations-not-just-deploys">Operations, not just deploys</h3>
<p>The half of the workspace I didn&rsquo;t expect to write is the diagnostic half: <code>check health</code>,
<code>check issues</code>, <code>show overview</code>, <code>show inventory</code>, <code>debug pod</code>, <code>debug service</code>, and
<code>export diagnostics</code>. These exist because at 11pm I do not want to remember the right
<code>kubectl get</code> incantation across four namespaces. <code>flow check health</code> tells me whether anything
is wrong, and that&rsquo;s the whole point.</p>
<p>There&rsquo;s also a small <code>labs</code> namespace with httpbin and netshoot behind <code>test dns</code> / <code>test http</code>
/ <code>exec tcpdump</code> executables, for when something is broken in a way that needs poking at from
inside the cluster.</p>
<h3 id="templates-for-new-services">Templates for new services</h3>
<p>New apps get scaffolded from flow templates with a form that asks for the app name, namespace,
chart repo, and whether it needs ingress, secrets, or monitoring. It emits the values file and
the executable stubs. It&rsquo;s the difference between adding a service being a ten-minute job and a
this-weekend job.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Organization and References</title>
      <link>https://jahvon.dev/architecture/flow/organization/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/flow/organization/</guid>
      <description>Workspaces, namespaces, and the URI-like reference system. How flow finds the right workspace, and why registration is an optimization rather than a requirement.</description>
      <content:encoded><![CDATA[<h2 id="organizational-model">Organizational Model</h2>
<p>Flow&rsquo;s organizational system creates a hierarchical structure that scales from individual projects to complex multi-project ecosystems. The system balances discoverability with isolation, enabling both focused work within projects and cross-project composition.</p>
<h3 id="hierarchy-structure">Hierarchy Structure</h3>
<p><strong>Workspaces</strong> serve as the top-level organizational unit, typically mapping to Git repositories or major project boundaries. Each workspace contains its own configuration, executable discovery rules, and isolated namespace hierarchy.</p>
<p><strong>Namespaces</strong> provide logical grouping within workspaces, similar to packages in programming languages. They enable organizational flexibility. A single workspace might have namespaces for <code>frontend</code>, <code>backend</code>, <code>deploy</code>, or <code>tools</code>. Namespaces are optional but recommended for workspaces with many executables.</p>
<p><strong>Executables</strong> are the atomic units of automation, uniquely identified within their namespace by their name and verb combination. This allows multiple executables with the same name but different purposes (<code>build api</code> vs <code>deploy api</code>).</p>
<h3 id="reference-system">Reference System</h3>
<p>Flow uses a URI-like reference system for executable identification:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">workspace/namespace:name
</span></span><span class="line"><span class="cl">    │         │       │
</span></span><span class="line"><span class="cl">    │         │       └─ Executable name (Optional but unique within verb group + namespace)
</span></span><span class="line"><span class="cl">    │         └───────── Optional namespace grouping
</span></span><span class="line"><span class="cl">    └─────────────────── Workspace boundary
</span></span></code></pre></div><p><strong>Reference Resolution Rules:</strong></p>
<ul>
<li><code>my-task</code> → Current workspace, current namespace, name=&ldquo;my-task&rdquo;</li>
<li><code>backend:api</code> → Current workspace, namespace=&ldquo;backend&rdquo;, name=&ldquo;api&rdquo;</li>
<li><code>project/deploy:prod</code> → workspace=&ldquo;project&rdquo;, namespace=&ldquo;deploy&rdquo;, name=&ldquo;prod&rdquo;</li>
<li><code>project/</code> → workspace=&ldquo;project&rdquo;, no namespace, nameless executable</li>
</ul>
<p><strong>Reference Format Trade-offs:</strong></p>
<ul>
<li><strong>Chosen:</strong> Slightly more verbose for simple cases</li>
<li><strong>Avoided:</strong> Naming collisions, poor tooling support, brittle file/directory coupling</li>
</ul>
<h3 id="verb-system">Verb System</h3>
<p>Verbs describe the action an executable performs while enabling natural language interaction. Verbs can be organized into semantic groups with aliases:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Executable definition</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">verb</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">verbAliases</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">compile, package, bundle]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># With the above, all of these commands are equivalent:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="l">flow build my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="l">flow compile my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="l">flow package my-app</span><span class="w">
</span></span></span></code></pre></div><p>This system allows developers to use whichever verb feels most natural while maintaining executable uniqueness through the <code>[verb group + name]</code> constraint.</p>
<p>I&rsquo;ve significantly reduced the number of default verb groups to focus on the most common actions with the most semantic clarity. See the <a href="https://flowexec.io/types/flowfile#executableverb">flow documentation</a> for the latest default list.</p>
<p><img src="https://jahvon.dev/images/flow-ws-tree.png" srcset="https://jahvon.dev/images/flow-ws-tree_hu_52af242d3fa7b2b1.png 691w, https://jahvon.dev/images/flow-ws-tree.png 1383w" sizes="(min-width: 768px) 720px, 100vw" width="1383" height="425"
     alt="Flow Workspace Tree Example"
     loading="lazy" decoding="async">
</p>
<h3 id="context-awareness">Context Awareness</h3>
<p>Flow maintains context awareness to reduce typing and improve ergonomics:</p>
<p><strong>Current Workspace Resolution:</strong></p>
<ul>
<li><strong>Dynamic Mode</strong>: Automatically detects workspace based on current directory</li>
<li><strong>Fixed Mode</strong>: Uses explicitly set workspace regardless of location</li>
</ul>
<p><strong>Namespace Scoping:</strong></p>
<ul>
<li>Commands inherit current namespace setting</li>
<li>Explicit namespace references override current context</li>
</ul>
<p><em>Note to self: Explicit command overrides of workspace / namespace may become an emerging need with the Desktop UI and MCP server usage.</em></p>
<h3 id="cross-project-composition">Cross-Project Composition</h3>
<p>The reference system enables powerful cross-project workflows:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">executables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">verb</span><span class="p">:</span><span class="w"> </span><span class="l">deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">full-stack</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">serial</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">execs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;build frontend/&#34;</span><span class="w">     </span><span class="c"># Different workspace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;build backend:api&#34;</span><span class="w">   </span><span class="c"># Different namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;deploy&#34;</span><span class="w">              </span><span class="c"># Current context</span><span class="w">
</span></span></span></code></pre></div><h3 id="finding-the-workspace">Finding the Workspace</h3>
<p>Registration is an optimization, not a prerequisite. In dynamic mode flow finds its workspace by
walking up from the current directory to the nearest <code>flow.yaml</code>, the same way <code>make</code> and
<code>bazel</code> find their root. Clone a repo and its executables work immediately.</p>
<p>An unregistered workspace is named after its directory, runs normally, and is never written
anywhere. Not to the config, not to the shared executable cache. What you give up is the ability
to <code>flow workspace switch</code> to it, and other workspaces cannot reference its executables by name.</p>
<p>Resolution runs in this order:</p>
<ol>
<li><code>--workspace</code> or <code>$FLOW_WORKSPACE</code>, which accepts a registered name or a path</li>
<li>The nearest <code>flow.yaml</code> at or above the working directory (dynamic mode only)</li>
<li>A registered workspace whose directory contains the working directory</li>
<li>Whatever <code>flow workspace switch</code> last set</li>
</ol>
<p>A directory containing its own <code>flow.yaml</code> is a boundary. The closest one wins, and a parent
workspace does not scan into it. Discovery also walks past <code>vendor/</code>, <code>node_modules/</code>,
<code>third_party/</code>, <code>external/</code>, <code>.git/</code> and <code>.claude/</code>, because a <code>flow.yaml</code> in there belongs to
that copy rather than to your project. The honest caveat is that there is no stopping point above
your home directory, so a <code>flow.yaml</code> in <code>~</code> makes your entire home directory a workspace.</p>
<h3 id="git-workspaces">Git Workspaces</h3>
<p>A workspace can be a git remote rather than a local path. Clones are cached under
<code>~/.cache/flow/git-workspaces/</code>, following Go module conventions, and can be pinned to a branch
or tag. <code>flow sync --git</code> refreshes them. This is what lets a workspace of shared team
executables be consumed the same way a dependency is.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Platform Integrations</title>
      <link>https://jahvon.dev/architecture/tbox/integrations/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/tbox/integrations/</guid>
      <description>The hubs and services tbox talks to, and the abstract device model that keeps the rest of the system from caring which is which.</description>
      <content:encoded><![CDATA[<h2 id="current-platform-integrations">Current Platform Integrations</h2>
<p><strong>Hubitat Hub Integration</strong></p>
<ul>
<li>Protocol: HTTP REST API using Makers API</li>
<li>Authentication: Access token with App ID</li>
<li>Communication: Local network for minimal latency</li>
<li>Event Handling: Webhook-based real-time device updates</li>
</ul>
<p><strong>Flair HVAC Integration</strong></p>
<ul>
<li>Protocol: OAuth 2.0 REST API with automatic token refresh</li>
<li>Components: Structures, rooms, HVAC units, sensor bridges</li>
<li>Capabilities: Mini-split control, room temperature monitoring</li>
</ul>
<p><strong>Weather Service Integration</strong> (currently disabled)</p>
<ul>
<li>Provider: WeatherAPI.com with API key authentication</li>
<li>Data Points: Temperature, humidity, conditions, feels-like temperature</li>
<li>Caching: Local cache with 30-minute refresh intervals</li>
</ul>
<h3 id="device-management-system">Device Management System</h3>
<p>Most of my devices are registered in the Hubitat platform, which provides a local API for device management.
I have also been evaluating Home Assistant as a potential alternative for future integrations but have not yet migrated.
The core requirements for the device management system include:</p>
<ul>
<li><strong>Unified Device Model</strong>: Abstract representation of devices across platforms</li>
<li><strong>Capability-Based Architecture</strong>: Devices expose capabilities like switches, sensors, and thermostats</li>
<li><strong>Room Organization</strong>: Devices are grouped by rooms with hierarchical structure</li>
</ul>
<p>Device states are synchronized into local cache storage, providing fast API responses while maintaining eventual consistency with upstream platforms.</p>
<p><strong>Abstract Device Interface</strong></p>
<p>All devices implement a common interface to ensure consistent interaction across platforms:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Device</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">ID</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Name</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Room</span><span class="p">()</span><span class="w"> </span><span class="nx">room</span><span class="p">.</span><span class="nx">Room</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">MatchesName</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Capabilities</span><span class="p">()</span><span class="w"> </span><span class="p">[]</span><span class="nx">CapabilityName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">As</span><span class="p">(</span><span class="nx">CapabilityName</span><span class="p">)</span><span class="w"> </span><span class="nx">Capability</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>This interface is implemented for each platform / device type once and reused across the system.
It allows for flexible device management and interaction without needing to know the underlying platform details.</p>
<h4 id="capability-based-controls">Capability-Based Controls</h4>
<p>Devices expose capabilities that define their functionality, allowing for flexible control and automation. For instance:</p>
<ul>
<li>Switch capabilities for on/off control</li>
<li>Sensor capabilities for environmental data</li>
<li>Thermostat capabilities for HVAC control</li>
<li>Button capabilities for trigger events</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Capability</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Name</span><span class="p">()</span><span class="w"> </span><span class="nx">CapabilityName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">IsStateful</span><span class="p">()</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">CurrentState</span><span class="p">()</span><span class="w"> </span><span class="nx">CapabilityState</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Merge</span><span class="p">(</span><span class="nx">Capability</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">SendCommand</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h4 id="room-organization">Room Organization</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Room</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Name</span><span class="w">    </span><span class="kt">string</span><span class="w">   </span><span class="s">`json:&#34;name,omitempty&#34;`</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Desktop and CLI</title>
      <link>https://jahvon.dev/architecture/mochi/desktop/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/mochi/desktop/</guid>
      <description>One binary, two surfaces. The Tauri sidecar boundary, the JSON contract between Rust and Go, and how types stay honest across three languages.</description>
      <content:encoded><![CDATA[<h2 id="desktop-and-cli">Desktop and CLI</h2>
<p>The desktop app holds no business logic. Every operation is a Tauri command that shells out to
the bundled <code>mochi</code> binary and parses its JSON output. Same binary, two surfaces.</p>
<p>A few details that turned out to matter more than expected:</p>
<p><strong>It goes through a shell.</strong> A GUI app on macOS doesn&rsquo;t inherit a login shell&rsquo;s environment, so
invocations source the user&rsquo;s profile first. Arguments are POSIX-quoted, which matters because
executable references contain a space. <code>validate flow/ns:name</code> has to survive the round trip as
one token instead of being split into two.</p>
<p><strong>Runs are tagged with their origin.</strong> Every spawned process gets <code>FLOW_RUN_SOURCE=desktop</code>.
Without it, a run started by clicking Execute is indistinguishable from one typed into a
terminal. Both would record <code>cli</code>, because that&rsquo;s what Flow assumes when nothing says otherwise.
The desktop is its own origin, and history should say so.</p>
<p><strong>Secrets reach providers through the environment, never argv.</strong> Arguments are visible to every
process on the machine; a child process&rsquo;s environment is not.</p>
<h2 id="type-safety-across-three-languages">Type Safety Across Three Languages</h2>
<p>Mochi is Go, TypeScript, and Rust in one repo. JSON Schemas are the contract, vendored from Flow,
which owns them. TypeScript types and the raw schema modules are generated for the
frontend; Rust types are generated for the Tauri backend; Go gets the same types by importing
Flow as a library rather than by generating them.</p>
<p>That&rsquo;s a better arrangement than it sounds like: the schema is what keeps the Rust and
TypeScript mirrors honest against the Go types they&rsquo;re shadowing. Generation is orchestrated by
Flow executables (of course), and CI fails if generated code is out of date.</p>
<p><img src="https://jahvon.dev/images/flow-gen.png" srcset="https://jahvon.dev/images/flow-gen_hu_c5bd86eebb9a6853.png 380w, https://jahvon.dev/images/flow-gen.png 761w" sizes="(min-width: 768px) 720px, 100vw" width="761" height="328"
     alt="Docs and code generation"
     loading="lazy" decoding="async">
</p>
]]></content:encoded>
    </item>
    <item>
      <title>Events and Rules</title>
      <link>https://jahvon.dev/architecture/tbox/events/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/tbox/events/</guid>
      <description>The event pipeline, how state is enriched on the way through, and the YAML rules engine that acts on it.</description>
      <content:encoded><![CDATA[<pre><code>Aliases []string `json:&quot;aliases,omitempty&quot;`
Groups  []Group  `json:&quot;groups,omitempty&quot;`
Rank    uint     `json:&quot;rank,omitempty&quot;`
</code></pre>
<p>}</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Hierarchical room structure with groups and aliases
</span></span><span class="line"><span class="cl">- Device-to-room mapping for contextual automation
</span></span><span class="line"><span class="cl">- Room-based filtering and bulk operations functionality
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Automation Engine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">#### Event Processing
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The gateway now runs a fully async pipeline. HTTP ingestion returns immediately so device platforms are never blocked waiting on rule evaluation:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. HTTP ingestion returns 202 immediately (under 50ms)
</span></span><span class="line"><span class="cl">2. Deduplication window (5 seconds) prevents duplicate event processing
</span></span><span class="line"><span class="cl">3. Events are persisted to SQLite before processing, so no events are lost on failure
</span></span><span class="line"><span class="cl">4. Worker pool processes events in parallel (currently 10 workers)
</span></span><span class="line"><span class="cl">5. Enrichment stage attaches device metadata, room context, and previous state
</span></span><span class="line"><span class="cl">6. Rule evaluation and action execution across relevant platforms
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">#### Rules Engine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules started as Go handlers and are moving toward declarative YAML definitions. Both coexist in the current system, with the Go approach still in place for backward compatibility.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The Go handler interface:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```go
</span></span><span class="line"><span class="cl">type Rule struct {
</span></span><span class="line"><span class="cl">	Name       string
</span></span><span class="line"><span class="cl">	Aliases    []string
</span></span><span class="line"><span class="cl">	HandleFunc Handler
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">type Handler func(Event, room.List, device.List) (handled bool, err error)
</span></span></code></pre></div><p>At the moment, all events flow through all rule handlers so they must handle their own filtering. The goal is smarter routing by device or event type, along with hot-reload so rules can be updated without a deployment.</p>
<p>YAML rule definitions are now available for basic conditions and actions, with validation enforced at load time.</p>
<h4 id="scene-management">Scene Management</h4>
<p>Scenes follow a similar structure to rules but are predefined automation scenarios that coordinate multiple devices across platforms. Currently they&rsquo;re defined in Go:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">pbChillHandler</span><span class="p">(</span><span class="nx">_</span><span class="w"> </span><span class="nx">room</span><span class="p">.</span><span class="nx">List</span><span class="p">,</span><span class="w"> </span><span class="nx">curDevices</span><span class="w"> </span><span class="nx">device</span><span class="p">.</span><span class="nx">List</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">bool</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">tableLamp</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">curDevices</span><span class="p">.</span><span class="nf">GetDevice</span><span class="p">(</span><span class="nx">registry</span><span class="p">.</span><span class="nx">PrimaryBedroomTableLamp</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ceilingLight</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">curDevices</span><span class="p">.</span><span class="nf">GetDevice</span><span class="p">(</span><span class="nx">registry</span><span class="p">.</span><span class="nx">PrimaryBedroomCeilingLight</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">switch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">case</span><span class="w"> </span><span class="nx">timeframe</span><span class="p">.</span><span class="nf">CurrentDay</span><span class="p">().</span><span class="nf">IsWeekday</span><span class="p">()</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">timeframe</span><span class="p">.</span><span class="nf">Night</span><span class="p">().</span><span class="nf">CurTimeInFrame</span><span class="p">():</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">tableLamp</span><span class="p">.</span><span class="nf">As</span><span class="p">(</span><span class="nx">device</span><span class="p">.</span><span class="nx">SwitchName</span><span class="p">).</span><span class="nf">SendCommand</span><span class="p">(</span><span class="nx">device</span><span class="p">.</span><span class="nx">OnCommand</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ceilingLight</span><span class="p">.</span><span class="nf">As</span><span class="p">(</span><span class="nx">device</span><span class="p">.</span><span class="nx">SwitchName</span><span class="p">).</span><span class="nf">SendCommand</span><span class="p">(</span><span class="nx">device</span><span class="p">.</span><span class="nx">OffCommand</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>This works but is a bit clunky. The goal is to get to declarative YAML definitions that can be created and edited without touching Go code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">scenes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">PBRChill</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">room</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;primary_bedroom&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;switch&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;dim_to_30&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">room</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;primary_bedroom&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;hvac&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;cool_to_68&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">window</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;weekday night&#34;</span><span class="w">
</span></span></span></code></pre></div><p>YAML scene definitions are planned alongside the dashboard UI.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Execution Engine</title>
      <link>https://jahvon.dev/architecture/flow/execution/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/flow/execution/</guid>
      <description>The five executable types, how parameters and arguments reach a process, running a step inside a container, and where state lives between steps.</description>
      <content:encoded><![CDATA[<h2 id="execution-engine">Execution Engine</h2>
<p>The execution engine is the core of Flow, responsible for running executables defined in YAML files.</p>
<h3 id="runner-interface">Runner Interface</h3>
<p>The execution system uses a runner interface pattern where each executable type implements:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Runner</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Name</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Exec</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">exec</span><span class="w"> </span><span class="o">*</span><span class="nx">executable</span><span class="p">.</span><span class="nx">Executable</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">eng</span><span class="w"> </span><span class="nx">engine</span><span class="p">.</span><span class="nx">Engine</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">inputEnv</span><span class="w"> </span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">inputArgs</span><span class="w"> </span><span class="p">[]</span><span class="kt">string</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">IsCompatible</span><span class="p">(</span><span class="nx">executable</span><span class="w"> </span><span class="o">*</span><span class="nx">executable</span><span class="p">.</span><span class="nx">Executable</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Current runner implementations include:</p>
<ul>
<li><strong>Exec Runner</strong>: Shell command execution</li>
<li><strong>Request Runner</strong>: HTTP request handling</li>
<li><strong>Launch Runner</strong>: Application/URI launching</li>
<li><strong>Render Runner</strong>: Markdown rendering</li>
<li><strong>Serial Runner</strong>: Sequential execution of multiple executables</li>
<li><strong>Parallel Runner</strong>: Concurrent execution with resource limits</li>
</ul>
<h3 id="workflows-serial-and-parallel">Workflows (Serial and Parallel)</h3>
<p>The serial and parallel runners allow for composing complex workflows from simpler executables. Steps are defined with a <code>RefConfig</code> that supports inline commands or references to other executables:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">SerialRefConfig</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Cmd</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Ref</span><span class="w"> </span><span class="nx">Ref</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Args</span><span class="w"> </span><span class="p">[]</span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">If</span><span class="w"> </span><span class="kt">string</span><span class="w">          </span><span class="c1">// Expression to conditionally skip the step</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Retries</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">ReviewRequired</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="c1">// Prompts the user before continuing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Execution and result handling is managed by the internal <code>engine.Engine</code> interface. The <a href="https://github.com/flowexec/flow/tree/main/internal/runner/engine">current implementation</a> includes retry logic, error handling, and result aggregation.</p>
<h3 id="execution-environment-and-state">Execution Environment and State</h3>
<p><strong>Environment Inheritance Hierarchy:</strong></p>
<p>Environment variables are provided to the running executable in the following order:</p>
<ol>
<li>System environment variables (lowest priority)</li>
<li>Dotenv files (<code>.env</code>, workspace-specific)</li>
<li>Flow context variables (<code>FLOW_WORKSPACE_PATH</code>, <code>FLOW_NAMESPACE</code>, etc.)</li>
<li>Executable <code>params</code> (secrets, prompts, static values)</li>
<li>Executable <code>args</code> (command-line arguments)</li>
<li>CLI <code>--param</code> overrides (highest priority)</li>
</ol>
<p><strong>State Management</strong></p>
<p>There are two ways state can be managed when composing workflows:</p>
<ul>
<li>Cache Store: Key-value persistence across executions with scoped lifetime. Values set outside executables persist globally; values set within executables are cleaned up on completion. Uses <a href="https://go.etcd.io/bbolt">bbolt</a> for cross-process storage.</li>
<li>Temporary Directories: Isolated scratch space (<code>f:tmp</code>) with automatic cleanup and shared access across serial/parallel workflow steps.</li>
</ul>
<p><strong>File System Access</strong></p>
<p>By default, the working directory is the directory containing the flow file that defines the executable. This can be configured using special prefixes: <code>//</code> (workspace root), <code>~/</code> (user home), <code>f:tmp</code> (temporary).</p>
<p>There is no automatic sandboxing. Executables inherit full user permissions. <em>Flow assumes users understand their workflows&rsquo; scope and potential for system modification, prioritizing automation flexibility over execution isolation.</em> Containerized execution is a planned future improvement.</p>
<p>See the <a href="https://flowexec.io/guides/executables">executable guide</a> and <a href="https://flowexec.io/guides/advanced#managing-state">state management</a> for usage details.</p>
<h2 id="performance-and-caching">Performance and Caching</h2>
<p>Flow uses eager discovery with multi-level caching to keep response times fast. Workspace scanning runs up front and is cached to disk, with in-memory caching layered on top for quick lookups. The cache is invalidated and refreshed via <code>flow sync</code> or the <code>--sync</code> flag.</p>
<p><em>Note to self: Some performance testing needed to validate sub-100ms discovery targets across large workspace trees.</em></p>
<p>For implementation details, see the <a href="https://deepwiki.com/flowexec/flow">DeepWiki reference</a>.</p>
<h2 id="getting-values-into-a-process">Getting Values Into a Process</h2>
<p>Everything reaches an executable as an environment variable. There are four sources:</p>
<table>
  <thead>
      <tr>
          <th>Source</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>secretRef</code></td>
          <td>Reads from the vault, including <code>vault/name</code> to cross vaults</td>
      </tr>
      <tr>
          <td><code>prompt</code></td>
          <td>Asks interactively at run time</td>
      </tr>
      <tr>
          <td><code>text</code></td>
          <td>A static value written into the definition</td>
      </tr>
      <tr>
          <td><code>envFile</code></td>
          <td>A <code>key=value</code> file</td>
      </tr>
  </tbody>
</table>
<p>Each can write to <code>envKey</code> or, when something needs a real file on disk, to <code>outputFile</code>, which
is cleaned up after the run.</p>
<p>Arguments are separate from parameters and come from the command line, either positionally
(<code>pos: 1</code>) or as flags (<code>flag: name</code>), with a type and an optional default:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">flow build container -- v1.2.3 --publish<span class="o">=</span><span class="nb">true</span>
</span></span></code></pre></div><p>Resolution runs highest to lowest: a <code>--param</code> override, then the executable&rsquo;s <code>params</code>, then its
<code>args</code>, then the surrounding shell environment. Parent values propagate into children in serial
and parallel workflows.</p>
<p>Paths get their own small vocabulary, which keeps definitions portable: <code>//</code> is the workspace
root, <code>~/</code> is home, <code>./</code> is relative to the flowfile, <code>$VAR</code> expands from the environment, and
<code>f:tmp</code> is a temp directory created once per run and cleaned up after.</p>
<h2 id="containers">Containers</h2>
<p>A step can declare an image and run there instead of on the host:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="l">pytest -q</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">container</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">python:3.13-alpine</span><span class="w">
</span></span></span></code></pre></div><p>The runtime is Docker or Podman, auto-detected unless pinned. The workspace mounts at
<code>/workspace</code> by default, additional volumes use the same path prefixes as everything else, and
the <code>FLOW_*</code> variables come along automatically. Secrets go in through a temporary
<code>--env-file</code> rather than the command line, so they never appear in the container&rsquo;s argv.</p>
<h2 id="conditions-and-state">Conditions and State</h2>
<p>Steps can be skipped with an <code>if</code> expression evaluated against <code>os</code>, <code>arch</code>, <code>env</code>, <code>store</code>, and
a <code>ctx</code> object carrying the current workspace, namespace and flowfile paths. Conditions are the
one place where a <code>$(&quot;command&quot;)</code> shell escape is available.</p>
<p>The <code>store</code> is a small key-value cache with two lifetimes, and the distinction matters more than
it looks:</p>
<ul>
<li><strong>Global</strong>, set outside a run with <code>flow cache set</code>, persists until cleared.</li>
<li><strong>Execution</strong>, set from inside an executable, is cleared automatically when the parent
finishes. A serial workflow can pass state between its own steps without leaking it.</li>
</ul>
<p>Two more things exist at the step level because workflows meet reality: <code>retries: N</code>, and
<code>reviewRequired: true</code>, which pauses for a human before continuing.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Run History</title>
      <link>https://jahvon.dev/architecture/mochi/history/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/mochi/history/</guid>
      <description>What Mochi remembers about every run, where it is stored, and how the dashboard turns it into something readable.</description>
      <content:encoded><![CDATA[<h2 id="run-history">Run History</h2>
<p>This is the &ldquo;remember&rdquo; pillar, and it&rsquo;s the reason Mochi exists at all. Agents run a lot of
commands on your behalf. The transcript is ephemeral and unstructured, there one moment and gone
the next. Mochi keeps the record: what ran, how long it took, whether it failed, and why.</p>
<p>Almost none of that storage is Mochi&rsquo;s. Flow already records every execution to a shared
embedded datastore, with a record that carries the reference, timing, status, exit code, process
ID, log archive, and the useful part, <a href="https://flowexec.io/guides/run-provenance">provenance</a>:
<code>source</code> (cli, desktop, or mcp), <code>clientName</code> (claude-code, cursor), <code>sessionId</code>, and
<code>workingDir</code>. Records are lifecycle-aware: they appear as running the moment a run starts and
update in place when it finishes.</p>
<p>Two of those fields have justifications I like. <code>workingDir</code> exists because the workspace is
already recoverable from the reference but the path is not, and it&rsquo;s the only thing separating
two checkouts of the same repo. <code>sessionId</code> exists so one assistant&rsquo;s related runs stay grouped.</p>
<p>Mochi&rsquo;s contribution is aggregation. A single pass over history rolls executions up per
executable and per workspace, keyed by executable ID rather than by reference so that verb
aliases like <code>run</code>, <code>exec</code>, and <code>start</code> of the same thing all collapse into one bucket. That same
rollup feeds both search ranking and the dashboard, so history gets swept once per invocation
rather than once per feature.</p>
<p><img src="https://jahvon.dev/images/mochi-dashboard_hu_e3b29148d92f8bd4.png" srcset="https://jahvon.dev/images/mochi-dashboard_hu_3e56f64abc0a2da6.png 700w, https://jahvon.dev/images/mochi-dashboard_hu_e3b29148d92f8bd4.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/mochi-dashboard.945fc006d28c9346c4f337b5bfca0a8d040d3f504c20472a5ac2b9b8230d6469.png" width="1400" height="900"
     alt="The Mochi dashboard"
     loading="lazy" decoding="async">
</p>
<p>The dashboard turns it into four purpose-built views rather than one generic screen: Welcome for
first-run setup, Pulse for health and what&rsquo;s happening now, Launch for getting back to work, and
Insights for activity over time. Insights surfaces the busiest, least reliable, and slowest
workflows, plus recommendations. Those are plain heuristics today, a reliability rule and a
duration rule, with stable IDs so that dismissing one survives regeneration. They are
deliberately built as the seam that model-generated insights will later plug into.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Secrets and the Vault</title>
      <link>https://jahvon.dev/architecture/flow/secrets/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/flow/secrets/</guid>
      <description>Five vault backends, how secrets reach a process without touching the command line, and why external vaults store links rather than copies.</description>
      <content:encoded><![CDATA[<h2 id="vault-system">Vault System</h2>
<p>The vault system provides secure storage, management, and retrieval of secrets across workspaces and executables. It extends the executable environment with multiple encryption backends.</p>
<p><strong>Implementation</strong>: <a href="https://github.com/flowexec/vault">github.com/flowexec/vault</a></p>
<h3 id="provider-architecture">Provider Architecture</h3>
<p>The vault system supports multiple storage backends through a common <code>Provider</code> interface:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Provider</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">ID</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">GetSecret</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">Secret</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">SetSecret</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="nx">Secret</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">DeleteSecret</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">ListSecrets</span><span class="p">()</span><span class="w"> </span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">HasSecret</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">bool</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">Metadata</span><span class="p">()</span><span class="w"> </span><span class="nx">Metadata</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nf">Close</span><span class="p">()</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h4 id="current-providers">Current Providers</h4>
<ul>
<li><strong>Unencrypted Provider</strong>: Simple key-value store for development and testing</li>
<li><strong>AES Provider</strong>: Symmetric file encryption using AES-256-GCM (single key management)</li>
<li><strong>Age Provider</strong>: Asymmetric file encryption using the <a href="https://github.com/FiloSottile/age">Age</a> specification (supports multiple recipients)</li>
<li><strong>Keyring Provider</strong>: Uses system keyring (macOS Keychain, Linux Secret Service)</li>
<li><strong>External Provider</strong>: Integration with external CLI tools (1Password, Bitwarden) via command execution</li>
</ul>
<h3 id="vault-switching">Vault Switching</h3>
<p>Vaults can be switched using a context-based system:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">flow vault switch development
</span></span><span class="line"><span class="cl">flow secret <span class="nb">set</span> api-key <span class="s2">&#34;dev-value&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">flow vault switch production
</span></span><span class="line"><span class="cl">flow secret <span class="nb">set</span> api-key <span class="s2">&#34;prod-value&#34;</span>
</span></span></code></pre></div><p>Secret references support both current vault context (<code>secretRef: &quot;api-key&quot;</code>) and explicit vault specification (<code>secretRef: &quot;production/api-key&quot;</code>).</p>
<h2 id="backends">Backends</h2>
<table>
  <thead>
      <tr>
          <th>Type</th>
          <th>Encryption</th>
          <th>Where the key lives</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>aes256</code> (default)</td>
          <td>Symmetric, generated 32-byte key</td>
          <td><code>FLOW_VAULT_KEY</code></td>
      </tr>
      <tr>
          <td><code>age</code></td>
          <td>Asymmetric, recipient keys</td>
          <td><code>FLOW_VAULT_IDENTITY</code></td>
      </tr>
      <tr>
          <td><code>keyring</code></td>
          <td>Delegated to the OS keyring</td>
          <td>OS-managed</td>
      </tr>
      <tr>
          <td><code>external</code></td>
          <td>None of flow&rsquo;s business</td>
          <td>The provider authenticates</td>
      </tr>
      <tr>
          <td><code>unencrypted</code></td>
          <td>Plaintext JSON</td>
          <td>n/a</td>
      </tr>
  </tbody>
</table>
<p>Key storage is configurable per vault, and an existing valid key in the target variable is
reused rather than regenerated, which is how one key ends up shared across several vaults.</p>
<h2 id="external-vaults">External Vaults</h2>
<p>This is the design I am happiest with. An external vault holds <strong>links, not secrets</strong>. Each link
pairs a name you choose with a reference the provider understands. Reading the name resolves the
reference and reads through. Nothing is copied into flow and nothing is ever written back, so
pointing a vault at a store you already use cannot damage it.</p>
<p>The configuration carries a <code>get</code> command, an optional <code>metadata</code> command, and two patterns that
turn out to matter a lot:</p>
<ul>
<li><code>reference_pattern</code> describes what a reference for this provider looks like, so a typo is
caught when you link it rather than weeks later when you read it.</li>
<li><code>not_found_pattern</code> separates &ldquo;this link is broken&rdquo; from &ldquo;the provider is unreachable&rdquo;.
Without it, an expired session is indistinguishable from a deleted secret.</li>
</ul>
<p>Because it is read-through, <code>flow secret set</code> fails against an external vault and
<code>flow secret remove</code> removes the link rather than the secret.</p>
<h2 id="injection">Injection</h2>
<p>Secrets never appear in a command line. They are resolved at run time and handed to the process
as environment variables:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">secretRef</span><span class="p">:</span><span class="w"> </span><span class="l">api-key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">API_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">secretRef</span><span class="p">:</span><span class="w"> </span><span class="l">production/db-password  </span><span class="w"> </span><span class="c"># a different vault</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">DB_PASSWORD</span><span class="w">
</span></span></span></code></pre></div><p>When something genuinely needs a file, <code>outputFile</code> writes one and deletes it afterwards. In
container runs the same values go through a temporary <code>--env-file</code>, for the same reason.</p>
]]></content:encoded>
    </item>
    <item>
      <title>AI Enrichment</title>
      <link>https://jahvon.dev/architecture/mochi/ai/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/mochi/ai/</guid>
      <description>A provider-agnostic AI layer, bring your own key, and why the secret never lands in Mochi configuration.</description>
      <content:encoded><![CDATA[<h2 id="ai-enrichment">AI Enrichment</h2>
<p>The AI layer is provider-agnostic behind a two-method interface. Features depend on that
interface rather than on a concrete client, which keeps <em>where the tokens come from</em> a
resolution-time decision.</p>
<p>You bring your own key: OpenAI, Anthropic, Gemini, Ollama, or any OpenAI-compatible endpoint.
Keys are never stored in Mochi&rsquo;s config. The config file holds only a pointer to a vault entry,
and the secret itself lives in the Flow vault. Each provider gets its own slot, so you can hold
an OpenAI key and an Anthropic key at once and flip between them without re-entering anything.</p>
<p>Reaching the vault involved a small workaround. Mochi&rsquo;s AI package can&rsquo;t
import Flow&rsquo;s vault resolution, since it&rsquo;s internal to Flow and off-limits the same way it is to
the Rust layer. Instead it shells out to <em>its own binary&rsquo;s</em> inherited <code>secret</code> command, exactly
as the desktop does. Vault access always goes through Flow&rsquo;s real implementation rather than a
reimplementation of it.</p>
<p>The agent loop is bounded rather than trusted to stop on its own: a round budget and a wall-clock
timeout, sized for analyzing a whole workspace while still stopping well short of a runaway.
Tools come from Mochi&rsquo;s own MCP server behind a three-tier permission policy, and every call is
recorded as an audit entry. Confirmation is designed to cross a process boundary. If no
confirmation handler is set, the loop returns a pending request rather than blocking, so the
desktop can ask the user and resume.</p>
<p>Usage is logged locally as one JSON line per generation, with age and size retention, so you can
see what your own key is being spent on.</p>
]]></content:encoded>
    </item>
    <item>
      <title>flow as an Agent Runtime</title>
      <link>https://jahvon.dev/architecture/flow/ai/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/flow/ai/</guid>
      <description>The MCP server, why running work through flow beats a raw shell tool, and the Python interpreter that is currently in flight.</description>
      <content:encoded><![CDATA[<p>Most of what an assistant does on your behalf is run commands. The usual way it does that is a
generic shell tool: it composes a string, something executes it, the output comes back, and the
whole thing evaporates when the conversation scrolls. That works, and it is also the reason you
cannot answer &ldquo;what did it actually do&rdquo; an hour later.</p>
<p>flow already had the pieces to do better. It knows your workspace, it holds your secrets, it
captures logs, and it records every run. Exposing that over MCP turns it from a task runner into
somewhere an agent can work.</p>
<h2 id="the-scope-boundary">The Scope Boundary</h2>
<p>Worth stating up front, because it shapes everything else:</p>
<blockquote>
<p>flow is an AI <strong>tool provider</strong>, not an AI <strong>consumer</strong>.</p>
</blockquote>
<p>The core exposes deterministic capabilities: an MCP server, published JSON schemas, an
<code>llms.txt</code>. It does not make model calls. No LLM parsing of natural-language commands, no
generation inside the CLI. That would put vendor keys, per-call cost, and non-determinism in the
critical path of a task runner. Anything applying a model to flow does so from outside, through
the MCP surface. <a href="https://jahvon.dev/architecture/mochi/">Mochi</a> is exactly that: a consumer built on top.</p>
<h2 id="the-ladder">The Ladder</h2>
<p>The run tools are deliberately ordered, closest fit first:</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>For</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>execute</code></td>
          <td>A task you have already named. Runs the project&rsquo;s real <code>test</code> or <code>deploy</code>.</td>
      </tr>
      <tr>
          <td><code>run_command</code></td>
          <td>A one-off shell command.</td>
      </tr>
      <tr>
          <td><code>run_python</code></td>
          <td>The one-off, when it is Python rather than shell.</td>
      </tr>
      <tr>
          <td><code>run_executable</code></td>
          <td>Something richer than a single command.</td>
      </tr>
  </tbody>
</table>
<p>The reason <code>run_python</code> is its own tool rather than a flag on <code>run_command</code> is small and
practical: agents select tools by name, and a tool called &ldquo;run_command&rdquo; is not what gets reached
for when the task is Python.</p>
<p>Around those sit discovery and inspection tools (<code>list_executables</code>, <code>get_executable</code>,
<code>list_workspaces</code>, <code>get_workspace</code>, <code>switch_workspace</code>, <code>get_info</code>), history (<code>get_execution_logs</code>),
and authoring (<code>write_flowfile</code>, which validates against the schema server-side before writing).
There are MCP resources for workspaces, executables, flowfiles and logs, and prompts for
generating and debugging executables.</p>
<p>The server is built on <a href="https://mcp-go.dev/">mcp-go</a>, and exposes Tools, Prompts and
<a href="https://modelcontextprotocol.io/specification/2026-07-28/server/resources">Resources</a>. I discovered that client support for Resources is still thin but I have them for clients that do support them.</p>
<p>The boundary is stated honestly in the server instructions: fall back to a raw shell for things
that genuinely should not be recorded, or that flow is not suited to, like anything needing a TTY.</p>
<h2 id="what-running-through-flow-buys-you">What Running Through flow Buys You</h2>
<p>Compared to a generic execute tool:</p>
<ul>
<li><strong>Named work first.</strong> Discovery means the agent runs your actual <code>test</code> executable rather than
its own approximation of one.</li>
<li><strong>Workspace resolution from a directory.</strong> Pass a path and flow walks up to the nearest
<code>flow.yaml</code>. Works in a fresh clone or a git worktree with nothing registered.</li>
<li><strong>Secrets from the vault</strong>, injected as environment, never in the argv.</li>
<li><strong>Provenance on every run.</strong> <code>source</code>, <code>clientName</code>, <code>sessionId</code>, <code>workingDir</code>.</li>
<li><strong>Lifecycle-aware history.</strong> Written as <code>running</code> at start and upserted on completion, so a log
can be read while the run is still going.</li>
<li><strong>Approval gates in the workflow</strong>, via <code>reviewRequired</code> on a step, rather than depending on the
client to ask.</li>
<li><strong>Byte-capped structured output</strong>, so a runaway log cannot eat the context window.</li>
</ul>
<h3 id="provenance-has-opinions">Provenance Has Opinions</h3>
<p>Three environment variables carry it: <code>FLOW_RUN_SOURCE</code>, <code>FLOW_RUN_CLIENT</code>, <code>FLOW_RUN_SESSION</code>.
Two decisions behind that are worth repeating.</p>
<p>There is <strong>no client registry</strong>. flow does not sniff for <code>CLAUDE_CODE_SESSION_ID</code> or any other
vendor&rsquo;s variables. Those are undocumented internals that get renamed, and detection built on
them fails silently, so history quietly stops grouping and nobody notices. Each tool maps its own
variables onto the contract instead.</p>
<p>And identity is <strong>exported, not passed</strong>. Environment beats a flag the assistant has to remember
on every call: a model can silently omit an argument, but it cannot omit a variable it never
sees. Parameters are for intent, which is the only thing the model actually knows.</p>
<h2 id="the-python-interpreter">The Python Interpreter</h2>
<p>You can run <code>python</code> alongside the built-in POSIX shell:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">executables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">verb</span><span class="p">:</span><span class="w"> </span><span class="l">run</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">report</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interpreter</span><span class="p">:</span><span class="w"> </span><span class="l">python</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        import json, sys
</span></span></span><span class="line"><span class="cl"><span class="sd">        print(json.dumps({&#34;python&#34;: sys.version_info[:2]}))</span><span class="w">
</span></span></span></code></pre></div><p>A <code>.py</code> file needs no <code>interpreter</code> field at all, since the extension implies it. The same field
works on serial and parallel steps, and inside containers, where the entrypoint follows the
interpreter rather than being hardcoded to a shell.</p>
<p>Nothing is embedded. There is no bundled CPython, no Starlark, no WebAssembly. flow resolves a
real interpreter on the host, preferring a project&rsquo;s virtualenv over bare system Python, so an
agent running Python inside a repo gets that repo&rsquo;s dependencies. The search order is
<code>FLOW_PYTHON_BIN</code>, then <code>$VIRTUAL_ENV</code>, then the workspace&rsquo;s <code>.venv</code>, then <code>python3</code> on the path.
An override that does not resolve fails rather than quietly falling back.</p>
<p>Two details I liked:</p>
<p><strong>Inline code runs from a temporary file, never <code>python -c</code>.</strong> That keeps user code, which may
have interpolated secrets, out of the process table; it produces tracebacks with real line
numbers; and it sidesteps shell quoting for multi-line scripts.</p>
<p><strong><code>PYTHONUNBUFFERED</code> is set by default</strong>, because flow pipes stdout to a log writer rather than a
terminal, and CPython block-buffers to a pipe. Without it a long run emits nothing until it
exits, which looks hung to anyone watching, human or otherwise.</p>
<p>The MCP side is the reason the rest exists. <code>run_python</code> gives an assistant a Python runtime with
the same workspace environment and secrets, the same captured logs, and the same attributable
history entry it already gets for shell. It is the difference between an agent writing a scratch
file and an agent doing work you can audit afterwards.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Generation and Integrations</title>
      <link>https://jahvon.dev/architecture/flow/generation/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/flow/generation/</guid>
      <description>Templates for scaffolding new projects, importing executables from files you already have, and where flow plugs into CI and other tools.</description>
      <content:encoded><![CDATA[<h2 id="template-system">Template System</h2>
<p>Flow includes a templating system for generating executables and workspaces from reusable templates, built on Go&rsquo;s <code>text/template</code> and the <a href="https://expr-lang.org/">Expr</a> expression language. See the <a href="https://flowexec.io">documentation</a> for usage details and examples.</p>
<h2 id="where-flow-plugs-in">Where flow Plugs In</h2>
<p>Two integration surfaces have their own pages, because both turned out to be more than a
paragraph:</p>
<ul>
<li><a href="https://jahvon.dev/architecture/flow/github-action/">The GitHub Action</a> runs the same executables in CI that you run locally.</li>
<li><a href="https://jahvon.dev/architecture/flow/ai/">flow as an agent runtime</a> covers the MCP server and the tools it exposes.</li>
</ul>
<p>There is also a Docker image at <code>ghcr.io/flowexec/flow</code> for other CI systems, though it has not
been exercised nearly as hard as the Action has.</p>
<h2 id="schemas">Schemas</h2>
<p>The flowfile, workspace, template and config formats are published as JSON Schema (see the
<a href="https://flowexec.io/types/">configuration reference</a>), which is what gives editors completion and
validation, and what lets an assistant author a valid flowfile without guessing. There is an
<code>llms.txt</code> alongside them. The Go types are generated from those same schemas, so the contract
has one source.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The GitHub Action</title>
      <link>https://jahvon.dev/architecture/flow/github-action/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/flow/github-action/</guid>
      <description>Running the same executables in CI that you run locally. A composite action, an ephemeral vault, and the parts of &amp;ldquo;just run it on a runner&amp;rdquo; that turned out not to be simple.</description>
      <content:encoded><![CDATA[<p>Running the same thing locally and in CI has been a goal from early on. If a project&rsquo;s build
is a flow executable, then CI should run <em>that</em>, not a hand-copied approximation of it that
drifts the first time someone changes a flag.</p>
<p><a href="https://github.com/flowexec/action"><code>flowexec/action</code></a> is how. It publishes to the Marketplace
as <strong>flow-execute</strong>, and the smallest useful thing you can write with it is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">flowexec/action@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">executable</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;build app&#39;</span><span class="w">
</span></span></span></code></pre></div><p>That is the whole point of it. The executable named there is the same one you run with
<code>flow build app</code> at your desk. Every repository in the flowexec organization uses this on itself.</p>
<h2 id="what-it-actually-is">What It Actually Is</h2>
<p>A composite action, not a container or a JavaScript action. It is a handful of bash steps in a
trench coat, which is the right shape for something whose job is to install a binary and run it:</p>
<ol>
<li>Resolve where flow should be installed, then restore it from the runner cache.</li>
<li>Install the CLI if the cache missed.</li>
<li>Register workspaces, cloning any that are git remotes.</li>
<li>Create a vault and load secrets into it, but only if secrets were passed.</li>
<li>Run the executable.</li>
<li>Upload logs as an artifact, but only on failure, and only if asked.</li>
</ol>
<p>Keeping it composite means each step shows up separately in the workflow log, so a failure
points at the thing that failed rather than at one opaque action.</p>
<h2 id="workspaces-including-ones-that-are-not-there-yet">Workspaces, Including Ones That Are Not There Yet</h2>
<p>The interesting input is <code>workspaces</code>. A workspace can be a local path, but it can also be a git
URL, which the action clones and registers before running anything:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">flowexec/action@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">executable</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;deploy staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">workspaces</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      backend: ./backend
</span></span></span><span class="line"><span class="cl"><span class="sd">      frontend: https://github.com/user/frontend-repo.git
</span></span></span><span class="line"><span class="cl"><span class="sd">      shared:
</span></span></span><span class="line"><span class="cl"><span class="sd">        repo: https://github.com/myorg/shared-flows.git
</span></span></span><span class="line"><span class="cl"><span class="sd">        ref: v1.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">clone-token</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.GITHUB_TOKEN }}</span><span class="w">
</span></span></span></code></pre></div><p>This is the CI expression of flow&rsquo;s cross-project composition. A workflow can pull in a shared
workspace of common executables, pin it to a tag, and reference its executables the same way it
would locally. Clone depth defaults to 1, because CI almost never needs the history.</p>
<h2 id="secrets-and-the-ephemeral-vault">Secrets and the Ephemeral Vault</h2>
<p>Secrets were the part that needed real thought. flow reads secrets from a vault, and a CI runner
has no vault, so the action makes one and throws it away.</p>
<p>When secrets are passed, it creates a vault named <code>github-actions</code> keyed to an environment
variable, loads each secret in, and switches to it. The generated key is immediately masked in
the log with <code>::add-mask::</code> and exposed as an output.</p>
<p>That output exists for one reason, and it is the nicest bit of the design: <strong>a vault can outlive
a job</strong>. Emit the key from one job, pass it to the next, and the second job decrypts the same
vault rather than re-loading every secret from GitHub:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">setup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">outputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">vault-key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ steps.init.outputs.vault-key }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">flowexec/action@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">init</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">executable</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;validate&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">secrets</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            SHARED_SECRET=${{ secrets.SHARED_SECRET }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">setup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">flowexec/action@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">executable</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;deploy production&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">vault-key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ needs.setup.outputs.vault-key }}</span><span class="w">
</span></span></span></code></pre></div><p>The vault step is skipped entirely when there are no secrets and no key, so a plain build job
does not pay for machinery it is not using.</p>
<h2 id="failing-usefully">Failing Usefully</h2>
<p>A CI action that only tells you &ldquo;exit code 1&rdquo; is not much better than running the command
yourself. This one parses flow&rsquo;s structured JSON error output and surfaces the code:</p>
<table>
  <thead>
      <tr>
          <th>Output</th>
          <th>What it carries</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>exit-code</code></td>
          <td>The executable&rsquo;s exit code</td>
      </tr>
      <tr>
          <td><code>error-code</code></td>
          <td>A machine-readable code such as <code>EXECUTION_FAILED</code>, <code>TIMEOUT</code>, <code>NOT_FOUND</code></td>
      </tr>
      <tr>
          <td><code>output</code></td>
          <td>Captured stdout, when <code>upload</code> is on</td>
      </tr>
      <tr>
          <td><code>vault-key</code></td>
          <td>The generated key, when secrets were configured without one</td>
      </tr>
  </tbody>
</table>
<p>Which means a workflow can branch on <em>why</em> something failed rather than just that it did:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Handle failure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.migrate.outputs.exit-code != &#39;0&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    if [ &#34;${{ steps.migrate.outputs.error-code }}&#34; = &#34;TIMEOUT&#34; ]; then
</span></span></span><span class="line"><span class="cl"><span class="sd">      echo &#34;Consider increasing the timeout&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    fi</span><span class="w">
</span></span></span></code></pre></div><p>Captured output is truncated at 65,000 bytes, because that is GitHub&rsquo;s limit on a step output,
and the full log is available as an artifact instead.</p>
<h2 id="the-unglamorous-parts">The Unglamorous Parts</h2>
<p>Most of the commit history is Windows and shell edge cases, which is what this kind of tool is
actually made of:</p>
<ul>
<li>Windows runners need <code>$HOME/bin</code> pushed onto <code>GITHUB_PATH</code>, and workspace paths in native form
rather than the POSIX form the rest of the script assumes.</li>
<li><code>TERM=dumb</code> on Windows, because flow&rsquo;s TUI would otherwise try to render into something that
is not a terminal and hang the job.</li>
<li>The vault key is extracted from structured JSON output, with a fallback to scraping the plain
text message for older CLI versions.</li>
<li>The binary is cached between runs, keyed on the resolved version, so a workflow that runs the
action several times installs flow once.</li>
</ul>
<p>None of that is interesting to write about, and all of it is the difference between an action
that works on your machine and one that works on someone else&rsquo;s.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/flowexec/action">flowexec/action</a></li>
<li><a href="https://github.com/marketplace/actions/flow-execute">flow-execute on the Marketplace</a></li>
<li><a href="https://github.com/flowexec/flow/blob/main/.github/workflows/ci.yaml">flow&rsquo;s own CI workflow</a>, which uses it</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Nowhere to Put the Canary: Argo Rollouts &#43; vLLM</title>
      <link>https://jahvon.dev/notes/rollout-vllm/</link>
      <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/rollout-vllm/</guid>
      <description>Running vLLM on a single GPU with room for two pods, and what that does to progressive delivery when there&amp;rsquo;s no capacity to spare for a canary.</description>
      <content:encoded><![CDATA[<p>A few years ago I was tasked with bringing <a href="https://argoproj.github.io/rollouts/">Argo Rollouts</a> to CarGurus. The hardest part wasn&rsquo;t the mechanics of canary analysis. It was helping teams figure out which metrics were worth gating on and defining global gates to apply across the organization. Spinning up an extra pod to shift traffic onto was <em>free</em>, or close enough to it that nobody thought about it. That assumption doesn&rsquo;t survive contact with a GPU. When I started running vLLM on a single VM, I had room for exactly two pods, and both were already serving live traffic.</p>
<p>Capacity wasn&rsquo;t the only assumption that broke. A vLLM pod can take minutes to start serving, and live traffic doesn&rsquo;t drain off as quickly as you would see with microservices. Every instinct I built came from workloads that start fast and drain fast, so this sounded like an interesting space to explore.</p>
<p>I decided to forget about surging canary rollouts with weighted traffic and focus solely on using Argo Rollouts for analysis across a couple of different deployment scenarios. Argo still calls the new revision the canary even with no traffic split, so that&rsquo;s the word I use for it throughout. This setup had the added benefit of controlling my GPU costs instead of wrestling with how to minimize waste as I temporarily spun up canaries. I decided to swap out a live pod for analysis, which I hypothesized would make this more of a networking and rollout configuration problem than a resource problem.</p>
<p>I want to give a disclaimer up front that this is a new area for me. These notes are my rough understanding of this process and my journey to learning and experimenting within this space. I&rsquo;m not exploring this through a production setup angle and I&rsquo;ll be explicit about the lines that I drew along my journey.</p>
<h2 id="the-architecture">The Architecture</h2>
<p>For this project, running a Kubernetes cluster with a GPU node was a clear starting place. To simplify the setup, I decided to keep it as a single node that I can spin up and down as needed. In my homelab, I use <a href="https://k3s.io/">k3s</a> as my Kubernetes distribution and since I didn&rsquo;t need all of the features that come with a managed cluster, I decided to spin up a Google Cloud Platform VM with k3s installed as my foundation.</p>
<p>I had separately landed on using vLLM as my inference engine so GPU compute was the next clear requirement. It was then pretty clear that I needed to run an accelerator-optimized VM, and I landed on the G2 series. The next big constraint that drove a lot of my architecture design was minimizing costs. I didn&rsquo;t want to be surprised by my cloud spend so keeping my experimentation cheap was important. That meant that I needed to use a small model that would fit on a small machine. This wasn&rsquo;t a big deal for me because the model wasn&rsquo;t what I was testing.</p>
<p>Given the nature of my experiment, I knew I needed at least 2 inference workers so that a rollout wouldn&rsquo;t kill traffic entirely. I eventually realized that I landed on a machine that didn&rsquo;t have native support for partitioning (MIG) so time-slicing had to be my path for sharing GPU resources. I configured the device plugin on my <code>g2-standard-8</code> instance to advertise 4 slices and assumed that meant 4 replicas. That was incorrect - more on that later.</p>
<p>Here&rsquo;s a summary of all of the components I deployed to my cluster:</p>
<p><img src="https://jahvon.dev/images/vllm-setup_hu_3081ff756f64ac9.png" srcset="https://jahvon.dev/images/vllm-setup_hu_581644ab2a9ce8c3.png 700w, https://jahvon.dev/images/vllm-setup_hu_3081ff756f64ac9.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/vllm-setup.bbd4b9f2b3226ae50cb2295fe1b092dd7934ac5d700ea912f93638916ccad024.png" width="1400" height="814"
     alt="Cluster Architecture"
     loading="lazy" decoding="async">
</p>
<h3 id="inference-workload">Inference Workload</h3>
<p><strong>vLLM serving <a href="https://huggingface.co/Qwen/Qwen3-0.6B">Qwen3-0.6B</a></strong></p>
<p>Deployed as a <a href="https://argo-rollouts.readthedocs.io/en/stable/features/specification/">Rollout</a> without traffic routing features. With no <code>trafficRouting</code> block, Argo has no connection to my networking layer. The weight instead sets the replication ratio (how many pods run the new revision). With <code>maxSurge=0</code> that ratio is satisfied by converting an existing pod rather than adding one.</p>
<p>The Qwen model was small enough to fit without causing OOM errors for replicas sharing resources. I had to install the <a href="https://github.com/nvidia/k8s-device-plugin">NVIDIA device plugin</a> so that the node would advertise the GPU as a schedulable resource. Nothing can request one without it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">maxSurge</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">maxUnavailable</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">setWeight</span><span class="p">:</span><span class="w"> </span><span class="m">50</span><span class="w">
</span></span></span></code></pre></div><h3 id="networking">Networking</h3>
<p><strong><a href="https://www.envoyproxy.io/">Envoy Proxy</a> Deployment with a static config sitting in front of headless vLLM services</strong></p>
<p>I needed a way to talk to the inference engine. Based on my earlier hypothesis, I went in expecting to have to tune this a bit. I could have used <a href="https://github.com/vllm-project/router">vLLM router</a>, <a href="https://github.com/llm-d/llm-d-router">llm-d router</a>, or a standard k8s controller that I&rsquo;ve used in the past (like nginx ingress or an API gateway) but I didn&rsquo;t want something that would hide too much, too early.</p>
<p>My Envoy config uses the <code>LEAST_REQUEST</code> load balancing policy, which scores endpoints on in-flight request count and knows nothing about what&rsquo;s happening inside vLLM. A cache-aware router would have been the better production choice, which is exactly why I didn&rsquo;t use one. One of the things I wanted to see was what a naive policy does to a pod that just came up cold. In some ways I&rsquo;ll describe later, this decision caused some setup headaches but provided a lot of great learnings.</p>
<h3 id="observability">Observability</h3>
<p><strong>Prometheus &amp; Grafana</strong></p>
<p>Easiest decision I made since vLLM and Envoy metrics could be easily scraped by Prometheus. I also deployed the <a href="https://github.com/nvidia/dcgm-exporter">dcgm-exporter</a> so that I could see GPU device metrics.</p>
<h2 id="orchestration--tools">Orchestration &amp; Tools</h2>
<p>I also needed a way to interact with the Kubernetes control plane and the inference workloads. Since I was deploying into GCP I considered <a href="https://docs.cloud.google.com/iap/docs/concepts-overview">Identity-Aware Proxy (IAP)</a> or <a href="https://tailscale.com/">Tailscale</a> (I already use it for my homelab so was familiar with that setup). I decided to take a simpler path: SSH + port-forwarding.</p>
<p>One of my favorite parts about scaffolding this project was how I ended up orchestrating everything. Some of my early design choices when building my <a href="https://jahvon.dev/architecture/flow/">Flow CLI</a> project were influenced by how I would experiment in similar ways in the past and how I wanted a better tool to orchestrate those things. It was a no-brainer for me to call on it here.</p>
<p>Under the hood, I used a lot of standard tools like Terraform for the infra, Makefile and shell files for scripting, but Flow wrapped all that up in a nice package that provides some nice ergonomics for working on this project. The full cluster setup is one command (<code>flow provision cluster</code>) and includes preflight checks via the serial runner type. Managing the lifecycle of the VM and workloads is done with easy to remember commands (<code>flow start cluster</code>, <code>flow deploy workloads</code>, <code>flow show status</code>, etc.). Running and viewing the report for experiments is standardized (<code>flow run experiment &lt;X&gt;</code>, <code>flow analyze experiments</code>). All those things are documented and very easy to recall within the Flow CLI terminal UI and the <a href="https://jahvon.dev/architecture/mochi/">Mochi Desktop</a> that I&rsquo;ve been building around Flow.</p>
<p>I also leaned on Claude a lot in this project. I didn&rsquo;t want to have to spend a ton of time looking at Envoy documentation to understand how to configure outlier detection or how to turn on access logs, or NVIDIA documentation to figure out how to set up the vLLM workers so that they&rsquo;re time sharing on the node. Claude also gave me clear answers on the many things that were new to me, like inference benchmarking. It was important for me to set up some clear rules, though. I still wanted to learn and experiment myself so my <code>CLAUDE.md</code> anchored the coding agents to take a slower pace at implementing and to share many more details than they would have without my guidance. This was also a great test of some run provenance features I have been building into Flow &amp; Mochi. I now have a cleaner structure for following along and understanding what&rsquo;s running/ran and which of my agents ran it.</p>
<h2 id="analysis-metrics">Analysis Metrics</h2>
<p>A core aspect of this project was understanding which metrics were best suited for monitoring the state of LLM inference workload deployments. Coming into this project, I honestly didn&rsquo;t know too much about what would be important here. I used a variety of resources, like <a href="https://gradientupdate.substack.com/p/llm-inference-metrics-reference">this post</a>, to ground myself in the common inference metrics like time to first token (TTFT), time per output token (TPOT), goodput, and GPU utilization.</p>
<p>The initial analysis gating metrics that I landed on were the p95 TTFT, p95 TPOT, and the request error ratio. Those didn&rsquo;t give me the full story, though. With the help of Claude, I ended up scaffolding a bunch of different SLIs that I could monitor throughout this project, spanning the whole stack: the inference engine, Envoy, networking, and NVIDIA GPU usage. It was very interesting watching how tuning my traffic within the cluster changed the shape of metrics and how one metric alone didn&rsquo;t give the full story.</p>
<p>To generate enough data for the metrics to be meaningful I needed a traffic simulator. I initially started with <a href="https://docs.vllm.ai/en/stable/cli/bench/serve/">vllm bench serve</a>, which worked pretty well, but I struggled with getting a variety of output shapes over a longer period of time without more complexity. After doing some more research, I discovered the <a href="https://github.com/kubernetes-sigs/inference-perf">inference-perf</a> project. What was very interesting was that switching to this from <code>bench serve</code> showed pretty close to the same benchmark measurements, which I read as a good signal. I have two traffic generation paths:</p>
<ol>
<li>Benchmark runner: used to help me define some of the base metrics that I use within my AnalysisTemplate.</li>
<li>Sustained load runner: used to run continuous and varied load that includes short prompts and long prompts to simulate the batch and interactive user types while running my experiments.</li>
</ol>
<h3 id="where-i-landed">Where I landed</h3>
<p>I originally figured I could just point steady traffic at the cluster and not think too hard about the level, since I wasn&rsquo;t optimizing for capacity or speed. That was wrong. My first baseline sat at about 12% of my TTFT threshold with nothing ever queuing, so a rollout could do almost anything and the gate would still read green. My experiments were passing for the wrong reason.</p>
<p>So I cranked the batch tenant up until it failed, holding interactive (short) prompts steady at 3 req/s with 128 in / 64 out:</p>
<table>
  <thead>
      <tr>
          <th>batch tenant</th>
          <th>req/s</th>
          <th>TTFT p95</th>
          <th>TPOT p95</th>
          <th>output tok/s</th>
          <th>KV cache</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>3 x 512</td>
          <td>6.18</td>
          <td>0.080s</td>
          <td>0.024s</td>
          <td>~1000</td>
          <td>15%</td>
      </tr>
      <tr>
          <td>6 x 512</td>
          <td>6.27</td>
          <td>0.091s</td>
          <td>0.024s</td>
          <td>1011</td>
          <td>13%</td>
      </tr>
      <tr>
          <td><strong>6 x 1024</strong></td>
          <td><strong>5.58</strong></td>
          <td><strong>0.222s</strong></td>
          <td><strong>0.047s</strong></td>
          <td><strong>827</strong></td>
          <td><strong>26%</strong></td>
      </tr>
      <tr>
          <td>9 x 1024</td>
          <td>5.09</td>
          <td>0.222s</td>
          <td>0.047s</td>
          <td>818</td>
          <td>23%</td>
      </tr>
  </tbody>
</table>
<p>My target sat between 6 x 512 and 6 x 1024, where TTFT climbed 2.4x while output tokens per second fell. The 9 x 1024 row confirms it by delivering less for 50% more offered load. Having a real operating point meant I could set thresholds off measurement instead of estimated numbers. I pinned my generator load at 6 req/s &amp; 1024 input.</p>
<h2 id="experiment-harness">Experiment Harness</h2>
<p>Before I jump into talking about my results, I want to give a quick overview of the experiment harness that I had. As I mentioned above, Flow was my orchestrator for a lot of this. The last time I ran similar experiments I ended up using kustomize to apply variations on top of the cluster resources. That worked super well and inspired this setup, but with different tooling. I am using raw Kubernetes manifests and <code>envsubst</code> to drop in environment variables from a resolved envfile within those templates. I decided to go with this approach because the envfile configuration works natively with Flow executables and it also works well as a drop-in to my various scripts. I can define my configurations in one file and have that single source of truth be used across the orchestration stack. I considered Helm here as well, but that also would have required a translation layer from the values file to script / Flow inputs.</p>
<p>When it comes to experiments, I can just override some of the configurations that I had defined within that configuration env and run a script that deploys that change to my workloads. It starts the load generator and triggers the rollouts by incrementing a nonce that I have defined on the Rollout pod specs. This forces the analysis process and pod replacement (even without real workload changes).</p>
<p>At the end of this process, I can either jump right into Grafana to see what metrics report or I can run a Flow executable that will gather all the run data and render a standard markdown template with the results.</p>
<blockquote>
<p><em>You can see my entire repo setup, including my experiments, my Flow executables, my Kubernetes manifests, Terraform scripts, etc., all here: <strong><a href="https://github.com/jahvon/inference-cluster-ops">inference-cluster-ops</a></strong>.</em></p>
</blockquote>
<h2 id="experiments">Experiments</h2>
<p><img src="https://jahvon.dev/images/vllm-dashboard_hu_d0cbf2c34de9630d.png" srcset="https://jahvon.dev/images/vllm-dashboard_hu_ce44db0f39b2eca7.png 700w, https://jahvon.dev/images/vllm-dashboard_hu_d0cbf2c34de9630d.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/vllm-dashboard.4c81907fb277c800d428460389d7524571509c4915532e316fcbddc3e8689f2a.png" width="1400" height="878"
     alt="Experiments Dashboard"
     loading="lazy" decoding="async">
</p>
<h3 id="0-cluster-baseline">0. Cluster Baseline</h3>
<p>I didn&rsquo;t want to run into a case where I had 100% failures during a rollout so I knew that I needed at least 2 replicas. But I also didn&rsquo;t want my replicas to grow. With <code>maxSurge=0</code> and <code>maxUnavailable=1</code>, a stable pod is terminated to make room for the new revision rather than a new pod being added, so the cluster serves at N-1 for the entire pod-startup window.</p>
<p>This ended up turning from a policy preference to a requirement as I began tuning. I missed that time-slicing splits compute, not memory. The 4 slices I configured meant 4 workloads taking turns on the same device, but each one still needs its own full copy of everything resident in VRAM. Nothing gets shared. Time-slicing also means one bad neighbor can impact the rest, which is its own problem.</p>
<p>That&rsquo;s when I had to get a better sense of what these workloads were actually using. I admittedly still don&rsquo;t fully understand all the various components of the arithmetic behind this, but at a high level I found that I needed to account for the size of the model weights, the KV cache size (which I was able to configure upfront), and some framework-specific costs, like the CUDA context. I started to go a little bit too far into the weeds here. This was where I gave Claude more rein in terms of just running some tests within the cluster and finding the right setup. I landed on just the 2 pods after doing some calculations and after running some load against them and reviewing metrics, including the device metrics.</p>
<p>Based on my configurations, I came up with this math:</p>
<blockquote>
<p>1137 weights + 1679 runtime + 5120 cache = 7936 MiB per pod<br>
Budget: 23034 card − 472 driver = 22562 MiB usable.</p>
</blockquote>
<p>This leaves just <code>6690 MiB</code> free and a third pod needs 7936, so the 4 replicas I originally planned for were out at this size.</p>
<p>Note: only 2816 MiB of that per-pod number is fixed cost. The 5120 MiB of KV cache is what I picked. I wanted to start with a generous number on purpose so that cache pressure wouldn&rsquo;t be the thing shaping my results. Trimming the cache to around 4700 MiB would have fit a third but making that change while establishing a baseline would have also changed my batching behavior.</p>
<h4 id="tuning-analysis--networking">Tuning Analysis &amp; Networking</h4>
<p>While figuring this out, I ran into even more issues. I mistakenly defined a rollout that had an analysis template before validating that the pods would start up. While this slowed me down, it did give me some more useful insights. I was reminded that an analysis run needs traffic to evaluate against. This was a flaw in my Prometheus queries. They came back empty and the analysis run transitioned to an error state instead of passing. I ended up having to tune my template so that the lack of traffic resolved to a success for non-experiment spec changes.</p>
<p>Then I noticed many failed requests, which pointed to my first networking problem. One of the first things I had to do was disable the request timeout since these requests would use streaming and I didn&rsquo;t want to prematurely kill them before they were done. Then I saw that during a rollout, Envoy was still sending requests to the pod that had been destroyed! This, bundled with the load balancing policy that I had set for Envoy, made the whole routing situation a lot worse than I wanted to settle for at baseline.</p>
<p>I ended up having to reduce the DNS refresh interval, add request retries, and configure outlier detection so that requests wouldn&rsquo;t be stuck going to the missing pod for too long. The retries only cover requests that get sent to a pod that is already gone; Envoy doesn&rsquo;t retry once it starts streaming. That distinction turns out to matter a lot in the second experiment. This was the first sign that my hypothesis was right: the hard part here was configuration, not resources.</p>
<p><em>Snippet of the Envoy config on the cluster</em>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">outlier_detection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">consecutive_5xx</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">base_ejection_time</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">dns_refresh_rate</span><span class="p">:</span><span class="w"> </span><span class="l">2s</span><span class="w">
</span></span></span></code></pre></div><p><em>And on the route</em>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">retry_policy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">retry_on</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;5xx,reset,connect-failure,refused-stream&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">num_retries</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># without this, a retry can land on the same dead pod it failed against</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">host_selection_retry_max_attempts</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span></code></pre></div><p>Finally, I had a good enough base state that allowed me to inject a couple of problem scenarios and see how Argo Rollouts captured and handled them.</p>
<h3 id="1-cold-pod-problem">1. Cold Pod Problem</h3>
<h4 id="what-i-injected">What I injected</h4>
<p>I ran 2 tests:</p>
<ol>
<li>First I updated the vLLM mount path so that, on rollout, the model&rsquo;s weights would have to be downloaded as if this was its first rollout.</li>
<li>Then I artificially increased the startup time for the pod by adding a 180s sleep before starting the vLLM process.</li>
</ol>
<h4 id="what-the-metrics-showed">What the metrics showed</h4>
<table>
  <thead>
      <tr>
          <th>run</th>
          <th>duration</th>
          <th>canary Ready</th>
          <th>gate</th>
          <th>capacity mean</th>
          <th>below full</th>
          <th>incidents</th>
          <th>breached by</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>good-revision</td>
          <td>432s</td>
          <td>138s</td>
          <td>passed</td>
          <td>0.710</td>
          <td>270s</td>
          <td>2</td>
          <td>truncation</td>
      </tr>
      <tr>
          <td>cold-rollout</td>
          <td>445s</td>
          <td>157s</td>
          <td>passed</td>
          <td>0.688</td>
          <td>300s</td>
          <td>2</td>
          <td>truncation</td>
      </tr>
      <tr>
          <td>cold-rollout / slow (180s)</td>
          <td>788s</td>
          <td>323s</td>
          <td>passed</td>
          <td>0.609</td>
          <td>645s</td>
          <td>1</td>
          <td>truncation</td>
      </tr>
  </tbody>
</table>
<ul>
<li>canary ready: how long the new revision took to start serving</li>
<li>capacity ratio: replicas_available / replicas_desired, read from Argo&rsquo;s controller metrics. With 2 replicas it&rsquo;s 1.0 when both pods are serving, 0.5 when one is.</li>
<li>capacity mean: the mean of that ratio across every 15s sample in the run window. 0.688 means that averaged over the whole rollout, 68.8% of desired capacity was actually available.</li>
<li>below full: the amount of time where capacity ratio was under 1</li>
<li>incidents: continuous stretches where the overall health gate failed. That gate is a composite of the TTFT p95, TPOT p95, error ratio and truncation ratio, each measured against its own objective. <code>breached by</code> records the measurement that caused the incident.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>run</th>
          <th>peak TTFT p95</th>
          <th>peak TPOT p95</th>
          <th>peak error ratio</th>
          <th>peak truncation ratio</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>good-revision / stock</td>
          <td>0.244</td>
          <td>0.047</td>
          <td>0.000</td>
          <td><strong>0.0402</strong></td>
      </tr>
      <tr>
          <td>cold-rollout / stock</td>
          <td>0.241</td>
          <td>0.049</td>
          <td>0.000</td>
          <td><strong>0.0397</strong></td>
      </tr>
      <tr>
          <td>cold-rollout / slow (180s)</td>
          <td>0.246</td>
          <td>0.049</td>
          <td>0.000</td>
          <td><strong>0.0310</strong></td>
      </tr>
  </tbody>
</table>
<p>Doubling the startup time barely moved the peak numbers. The damage was the longer stretch of time that the cluster sat below full capacity.</p>
<h4 id="did-the-gate-catch-it">Did the gate catch it</h4>
<p>No, and the reason is my configuration rather than Argo itself. The important metric that I thought I needed to watch here was the time until the canary was ready. Given the small model size, I only saw a 19s difference in my first test, which is what convinced me to try artificially increasing the startup with a sleep.</p>
<p>I was running analysis as a canary step and a step doesn&rsquo;t start until the new pod has been transitioned to the <code>Ready</code> state. The entire startup window happens before the gate is ever evaluated, which is exactly the window I was injecting into. I should have used background analysis instead, analyzing across the whole rollout rather than at a single step.</p>
<p>Moving the gate wouldn&rsquo;t have been enough on its own, though. The metrics I gated on were tuned for canary monitoring. TTFT p95, TPOT p95, and the error ratio all answer the same question: how well is the new pod serving now that it&rsquo;s up? That&rsquo;s the right question at a step and the wrong one across a rollout. The tables above show why. Doubling the startup time barely moved any of those peaks because the slow pod wasn&rsquo;t serving badly, it just wasn&rsquo;t there yet. The damage only showed up in the cluster-shaped measurements: capacity mean fell from 0.710 to 0.609 and the time below full capacity went from 270s to 645s. Those were numbers I collected for the report, not numbers I gated on. Running analysis in the background means gating on the health of the fleet through the transition rather than the performance of a single revision after it lands.</p>
<p>The clear issue that I was able to draw from the data was the batch request truncation across all of the tests I ran.</p>
<h3 id="2-truncated-batch-requests">2. Truncated Batch Requests</h3>
<h4 id="what-i-injected-1">What I injected</h4>
<p>I ended up running several more tests to understand how to fix the truncation issue:</p>
<ol>
<li>Increased the batch output size so that there are always multi-second streams in flight when the pod goes away. This was an attempt to zoom into the truncation issues that I saw with the last experiment.</li>
<li>Then I ran a test with the same batch output size, but an increase in the pod&rsquo;s <code>terminationGracePeriodSeconds</code>.</li>
<li>I further optimized #2 by setting up a naive <code>preStop</code> hook that waits long enough for the requests to drain before killing the pod:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">lifecycle</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">preStop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/bin/sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;sleep ${PRESTOP_SLEEP_SECONDS}&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="l">${TERMINATION_GRACE_SECONDS}</span><span class="w">
</span></span></span></code></pre></div><h4 id="what-the-metrics-showed-1">What the metrics showed</h4>
<table>
  <thead>
      <tr>
          <th>grace</th>
          <th>preStop</th>
          <th>duration</th>
          <th>requests</th>
          <th>truncated</th>
          <th>incidents</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>30s (default)</td>
          <td>none</td>
          <td>438s</td>
          <td>1666</td>
          <td>15</td>
          <td>1</td>
      </tr>
      <tr>
          <td>90s</td>
          <td>none</td>
          <td>435s</td>
          <td>1749</td>
          <td>23</td>
          <td>2</td>
      </tr>
      <tr>
          <td>90s</td>
          <td><strong>60s</strong></td>
          <td>433s</td>
          <td>1670</td>
          <td><strong>0</strong></td>
          <td><strong>0</strong></td>
      </tr>
  </tbody>
</table>
<p>The grace period includes the preStop time so both of them needed to be set in the last test. This additional configuration didn&rsquo;t cost me any more time because the teardown runs while the replacement pod is starting up and startup is much longer.</p>
<table>
  <thead>
      <tr>
          <th>grace</th>
          <th>preStop</th>
          <th>stream killed at</th>
          <th>delivered before dying</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>30s</td>
          <td>none</td>
          <td>+27s after drain</td>
          <td>31% of a full response</td>
      </tr>
      <tr>
          <td>90s</td>
          <td>none</td>
          <td>+87s after drain</td>
          <td>38% of a full response</td>
      </tr>
      <tr>
          <td>90s</td>
          <td>60s</td>
          <td>—</td>
          <td>none truncated</td>
      </tr>
  </tbody>
</table>
<h4 id="did-the-fix-work">Did the fix work</h4>
<p>Eventually. At first, I thought the grace period would provide enough time for the requests to drain completely. I saw in the Envoy access logs that requests weren&rsquo;t being sent to the draining pod after I started the rollout process but the grace period configuration didn&rsquo;t solve the truncation issue. It seemed to make it slightly worse. I ran another pair of tests and saw 15 and 22 as my truncation values, which confirmed that this was just due to run variance.</p>
<p>The timings provided a more complete picture. The vLLM workloads stopped producing tokens when they received SIGTERM. This left the remaining streams in a zombie state until they died at 27s with a 30s grace and 87s with a 90s grace - when the pod received SIGKILL at the end of the grace period. Adding the <code>preStop</code> hook was the actual fix. The hook runs before the SIGTERM is sent, so it holds off the signal that stops generation instead of extending the window after it. The grace period still has to be long enough to cover the hook but on its own it was never going to help.</p>
<h3 id="3-aborting-a-bad-revision">3. Aborting a Bad Revision</h3>
<h4 id="what-i-injected-2">What I injected</h4>
<p>My focus here was to see how Argo&rsquo;s automatic rollback prevented long-running incidents. I held the changes from experiment 2 and then did the following test:</p>
<ol>
<li>Collapsed vLLM batching so the pod serves one sequence at a time and everything else queues. This triggered higher latency that I knew would trip the analysis gates</li>
<li>Same test as #1 but without the rollout gating</li>
</ol>
<h4 id="what-the-metrics-showed-2">What the metrics showed</h4>
<table>
  <thead>
      <tr>
          <th>metric</th>
          <th>gated (rolled back)</th>
          <th>ungated</th>
          <th>ratio</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>interactive e2e p95</td>
          <td>2.40s</td>
          <td>30.11s</td>
          <td>12.5x</td>
      </tr>
      <tr>
          <td>batch e2e p95</td>
          <td>8.47s</td>
          <td>31.33s</td>
          <td>3.7x</td>
      </tr>
      <tr>
          <td>degraded window throughput</td>
          <td>1.96 req/s</td>
          <td>0.64 req/s</td>
          <td>3.1x</td>
      </tr>
      <tr>
          <td>rollout phase</td>
          <td>Degraded (auto rollback)</td>
          <td>Healthy (no rollback)</td>
          <td>—</td>
      </tr>
      <tr>
          <td>pods on bad revision</td>
          <td>1 of 2 pods</td>
          <td>2 of 2 pods</td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<h4 id="did-the-gate-catch-it-1">Did the gate catch it</h4>
<p>Yes, as expected. The p95 TTFT on the canary failed on 3/4 samples and aborted the rollout after 260s. On the test without the gate, the same revision was rolled out to all pods, leaving the overall experience in a degraded state until I restored baseline. My gated test didn&rsquo;t prevent the damage. Requests still hit the low performing pod once it was ready. However, it reduced the duration and impact of the bad change automatically.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>One of my biggest takeaways here was that the Envoy routing is what provided the most connection resiliency against rollouts, but still left a gap at the stream level. I came in thinking that using Envoy for networking would allow me to just see rollouts in action, and it did, but the retries and outlier detection absorbed enough of the disruption that the gates had very little left to detect at my baseline. It leaves me wondering how another iteration of this experiment with Argo handling traffic routing alongside the metrics analysis would play out.</p>
<p>I was able to validate the experience of Argo as an instrument during these scenarios; however in some cases it didn&rsquo;t give me as much of a signal as I would have expected. As I reflect on this, I think that has a lot to do with how I configured my load generation and metric thresholds. I had the advantage of knowing exactly what the inputs were and tuning things to get the state into what I hypothesize would happen. It was really cool seeing Argo Rollouts applied to this type of workload and seeing that understanding the system&rsquo;s behavior and core signals is key to using Argo.</p>
<p>If I were to take this exploration a step further, I would bring back the canary. The highest KV usage I saw was at around 70%. That peak didn&rsquo;t come at my saturation point, it came during a drain. The reduced capacity during the rollout added a lot more pressure that could have been mitigated if I could have trimmed the KV size to fit a 3rd temporary pod. That introduces a new challenge, though: what do you do with all that wasted space when you&rsquo;re not running a rollout?</p>
]]></content:encoded>
    </item>
    <item>
      <title>AI as a Development Partner</title>
      <link>https://jahvon.dev/notes/ai-development-partner/</link>
      <pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/ai-development-partner/</guid>
      <description>Reflections on how I&amp;rsquo;ve been using AI as a development partner - what I delegate, what I don&amp;rsquo;t, and what it&amp;rsquo;s produced.</description>
      <content:encoded><![CDATA[<p>Last fall I wrote about <a href="https://jahvon.dev/notes/ai-creative-partner/">using AI as a creative partner</a> after helping with a HGSE module on vibe coding. The conclusion I landed on was careful: AI works best as a scaffold, not a substitute. Use it consciously, review everything, keep your judgment in the loop. I still believe that. But I&rsquo;ve spent the last few months testing what that actually looks like when the output has to live somewhere.</p>
<p>Creative work you experience once. Development work you live in. That difference changes what you need from a partner.</p>
<h2 id="the-bench">The Bench</h2>
<p>When I was at <a href="https://jahvon.dev/tags/recurse/">Recurse Center</a> last summer, I started integrating AI more intentionally into how I build. Not for speed, for learning. I wanted to experiment with architectures I wouldn&rsquo;t normally try, undo decisions cheaply, and see what held up. <a href="https://jahvon.dev/tags/flow/">Flow</a> was the natural workbench. It&rsquo;s my own tool and I know every corner of it.</p>
<p>Over the last few months I&rsquo;ve been building Flow Desktop and refactoring pieces of the core CLI with AI doing a lot of the implementation work. The experience has been different from vibe coding in ways that matter. In the HGSE projects, I was optimizing for something working. Here, I&rsquo;m optimizing for something I can read six weeks later, find when I need it, and build on without second-guessing what&rsquo;s underneath.</p>
<p>That changes what I actually delegate.</p>
<h2 id="the-delegation-model">The Delegation Model</h2>
<p>The architectural decisions stay with me. What the data model looks like, how executables get resolved, where state lives. What I hand off is the implementation of decisions I&rsquo;ve already made. I describe the shape of what I want, review what comes back against that shape, and merge when it aligns. When it doesn&rsquo;t, I say so explicitly.</p>
<p>A concrete example: I&rsquo;ve been building an AI proxy backed by <a href="https://www.cloudflare.com/developer-platform/products/ai-gateway/">Cloudflare AI Gateway</a> that sits across all of my tools. I decided on the architecture, what the proxy needs to do, how it integrates with the <a href="https://jahvon.dev/notes/cloudflare-experience/">Cloudflare platform</a>, what observability I want. AI implemented it. The Cloudflare MCP server made the feedback loop tight enough that I could test and iterate without switching contexts.</p>
<p>What makes this work is having a single place to see everything. Everything I&rsquo;ve configured, discoverable from one surface.</p>
<video class="demo-video"
       autoplay loop muted playsinline preload="metadata"
       aria-label="The flow v2 terminal UI">
  <source src="https://jahvon.dev/images/flow-v2-tui.mp4" type="video/mp4">
</video>

<p>One of the real risks of AI-assisted development is ending up with code you can&rsquo;t navigate. Outputs that don&rsquo;t connect to anything, a project that sprawls in ways you can&rsquo;t audit. The workspace model keeps that from happening. I know where things live because I designed where they live.</p>
<p>I&rsquo;ve also started using AI to enrich Flow itself, generating executable metadata, adding descriptions and tags, making the library more useful as it grows. Flow has an MCP server, so AI tools can interact with it directly. Watching an AI tool work with Flow rather than just producing files has been one of the more interesting parts of this.</p>
<p>Licklider&rsquo;s framing from the last post still holds here. Set the goals, determine the criteria, perform the evaluations. That&rsquo;s still your job. What&rsquo;s changed is my confidence in what I can hand off once those things are set.</p>
<h2 id="what-it-produced">What It Produced</h2>
<p>The review and iterate phase is where the real work happens. AI gets you to a first draft faster. Whether that draft is right is still a judgment call only you can make.</p>
<p>A few months of this produced Flow v2 and something I&rsquo;ve been sitting on: <a href="https://mochiexec.io">Mochi</a>. Development workflows have a way of becoming invisible. They exist, they&rsquo;re just not anywhere you can see them. It&rsquo;s a local-first dev ops dashboard built on Flow. Point it at a directory and it finds your development scripts and automations, turns them into a unified, AI-enriched dashboard. No cloud, no accounts, works with whatever you&rsquo;re already running.</p>
<p><img src="https://jahvon.dev/images/mochi-executables_hu_3937508efceff644.png" srcset="https://jahvon.dev/images/mochi-executables_hu_e108e5df939cf6e.png 700w, https://jahvon.dev/images/mochi-executables_hu_3937508efceff644.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/mochi-executables.aec2ad9fd23d482d0003d84a995b6ff0c0972ba228d8c392f64f92a3378ad175.png" width="1400" height="1279"
     alt="Mochi Executables View"
     loading="lazy" decoding="async">

<em>Executables view. Everything Mochi found across my workspaces, tagged and filterable.</em></p>
<p>Still early. If it sounds useful, the waitlist is at <a href="https://mochiexec.io">mochiexec.io</a>.</p>
<p>I&rsquo;m more convinced than I was last fall that the gap worth closing isn&rsquo;t between what AI can produce and what you can prompt. It&rsquo;s between what AI produces and what you actually understand. Building in a system you designed is one way to stay honest about that.</p>
]]></content:encoded>
    </item>
    <item>
      <title>From Cloud Native to Serverless with Cloudflare</title>
      <link>https://jahvon.dev/notes/cloudflare-experience/</link>
      <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/cloudflare-experience/</guid>
      <description>Notes from a few months building on Cloudflare Workers after years of Kubernetes.</description>
      <content:encoded><![CDATA[<p>Over the last couple of months, I&rsquo;ve been building a few projects on Cloudflare Workers, and it&rsquo;s been a fun technology shift for me. Developing TypeScript applications on serverless infrastructure after several years of Go and Kubernetes has brought me lots of interesting challenges and opportunities. In many ways, it feels like the opposite of what I&rsquo;ve done with k8s. Infrastructure and component connections (bindings) are managed in a single configuration file instead of sprawling YAML files. Instead of working with containerized microservices and operators, I have to build light processes invoked through in-code APIs. The constraints are different, but working within them has given me a greater appreciation for the power of Kubernetes while also making me grow attached to the seamless developer experience of the Cloudflare platform.</p>
<p><img src="https://jahvon.dev/images/cloudflare.png" srcset="https://jahvon.dev/images/cloudflare_hu_7b39d94bdd6fdd88.png 576w, https://jahvon.dev/images/cloudflare.png 1153w" sizes="(min-width: 768px) 720px, 100vw" width="1153" height="409"
     alt="Cloudflare diagram"
     loading="lazy" decoding="async">
</p>
<p>The diagram above is roughly how a typical architecture with my most-used patterns comes together. It took a few annoying and painful lessons to land here, but the platform&rsquo;s binding model nudged me in the right direction. Workers are small by design, and the bindings gave me just what I needed to compose them into bigger patterns.</p>
<p>At the core of any architecture is communication, and service bindings and queues work insanely well. I use service bindings when I need fast, direct calls from one worker to another. In my Wrangler config, I just add:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">services</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">binding</span> <span class="p">=</span> <span class="s2">&#34;BOUND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">service</span> <span class="p">=</span> <span class="s2">&#34;my-worker&#34;</span>
</span></span></code></pre></div><p>Then in code I can easily send a request like this, with no HTTP overhead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Request</span><span class="p">(</span><span class="s1">&#39;api/v1/data&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;GET&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">env</span><span class="p">.</span><span class="nx">BOUND_SERVICE</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
</span></span></code></pre></div><p>This might seem simple, but coming from a k8s background it&rsquo;s a meaningful shift. At a previous role we spent real time and infrastructure trying to solve service discovery; trying to figuring out which services talk to each other, which host to use per environment, keeping that visible and manageable across teams. We end up reaching for tools just to answer the question &ldquo;who calls who.&rdquo; With service bindings, the answer lives right in the config. It&rsquo;s a couple of lines, and those lines double as documentation of your service communication graph without any extra infrastructure to maintain.</p>
<p>I use queues when failure actually matters. Anything I need to retry, delay, or handle gracefully goes through a queue. In k8s I would have reached for another tool like Kafka or SQS for this, which means more infrastructure to provision, configure, monitor, and reason about. Here it&rsquo;s all in the Wrangler config:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># In the producer&#39;s wrangler.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">queues</span><span class="p">.</span><span class="nx">producers</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">queue</span> <span class="p">=</span> <span class="s2">&#34;message-queue&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">binding</span> <span class="p">=</span> <span class="s2">&#34;MESSAGE_QUEUE&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># In the consumer&#39;s wrangler.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">queues</span><span class="p">.</span><span class="nx">consumers</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">queue</span> <span class="p">=</span> <span class="s2">&#34;message-queue&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">max_concurrency</span> <span class="p">=</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl"><span class="nx">max_batch_size</span> <span class="p">=</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl"><span class="nx">max_batch_timeout</span> <span class="p">=</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl"><span class="nx">max_retries</span> <span class="p">=</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl"><span class="nx">dead_letter_queue</span> <span class="p">=</span> <span class="s2">&#34;message-dlq&#34;</span>
</span></span></code></pre></div><p>Then in code I just end up with code blocks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// Sending messages
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">msg</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">key</span><span class="o">:</span> <span class="s1">&#39;value&#39;</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">env</span><span class="p">.</span><span class="nx">MESSAGE_QUEUE</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">msg</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Processing message
</span></span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="kr">async</span> <span class="nx">queue</span><span class="p">(</span><span class="nx">batch</span><span class="p">,</span> <span class="nx">env</span><span class="p">,</span> <span class="nx">ctx</span><span class="p">)</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">void</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">message</span> <span class="k">of</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">messages</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>No additional tooling. The queuing system is just there and the resilience story is config rather than code. That trade-off keeps showing up with Cloudflare and it&rsquo;s one of the things I&rsquo;ve genuinely enjoyed about working on the serverless side of things.</p>
<p>One thing to note is that workers run in a V8 sandbox with strict resource limits. While the resource limits are real, they are manageable once you stop fighting them. The pattern I kept coming back to was breaking long-running processes into chunks and passing a continuation token through queue messages or service calls. This essentially means checkpointing work. In some cases, that involves persisting temporary data to R2 or KV so nothing blows its budget in a single execution.</p>
<p>The other binding that comes up when I want to avoid some of the limitations of the worker runtime is containers (currently in beta). Some of the limits can be configured (and increased with a Workers paid subscription) a bit, but the runtime can&rsquo;t. When I needed to run workloads that didn&rsquo;t neatly fit into the model, a container binding let me attach an image with its own runtime and call into it the same way I call into service bindings. For example, if you need image processing with a library like Sharp for compression or transformation, you can&rsquo;t run that inside a worker but a container binding solves it cleanly.  It uses a slim wrapper for passing environment variables and spinning up instances in the bound worker&rsquo;s code.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// The container creates a &#34;Durable Object&#34;
</span></span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">class</span> <span class="nx">MyContainer</span> <span class="kr">extends</span> <span class="nx">Container</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">defaultPort</span> <span class="o">=</span> <span class="mi">8080</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">envVars</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">ENV_KEY1</span>: <span class="kt">env.ENV_KEY1</span> <span class="o">||</span> <span class="s1">&#39;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nx">ENV_KEY2</span><span class="o">:</span> <span class="s1">&#39;hello world&#39;</span>
</span></span><span class="line"><span class="cl">	<span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Send requests to the container
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nx">env</span><span class="p">.</span><span class="nx">CONTAINER</span><span class="p">.</span><span class="nx">getByName</span><span class="p">(</span><span class="s1">&#39;instance-1&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">container</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;api/v1/action&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;POST&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">(</span><span class="nx">request</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>Containers (and <a href="https://developers.cloudflare.com/durable-objects/">durable objects</a>, in general) provide a nice escape hatch. The rest of your architecture stays serverless and you only reach for a container when the runtime genuinely needs it.</p>
<p>The constraint that keeps biting me is D1&rsquo;s query limitations, especially the 100-parameter limit on batch queries. Every time my data models grew, I ran into sneaky bugs related to this. The fix is just chunking batches so that I never pass in so many parameters, but it took a few rounds before it became second nature. There are many other platform <a href="https://developers.cloudflare.com/workers/platform/limits/">limits</a> worth knowing too, but I rarely hit them when following the patterns that help me get around those other challenges.</p>
<p>I&rsquo;ve built on a lot of platforms where infrastructure problems and application problems are tangled together. On Workers, they&rsquo;re largely separate. It&rsquo;s been interesting working in a system where the constraints shift from operational to architectural.</p>
<p>There are definitely still some rough edges when working with Cloudflare, but in the short time I&rsquo;ve been using it, I&rsquo;ve also seen some great enhancements made to the ecosystem. Generally, I&rsquo;m left to think about the design of what I&rsquo;m building rather than how to keep it running. I like to pair it with tools like <a href="https://github.com/drizzle-team/drizzle-orm">Drizzle</a>, <a href="https://hono.dev/">Hono</a>, and <a href="https://localflare.dev/">Localflare</a> to improve my developer experience even more. Between the Wrangler CLI, MCP integration, GraphQL API, and solid documentation, Cloudflare provides a great suite of tools for extending architectures quickly with the help of coding assistants.</p>
<p>Coming from k8s, the biggest adjustment isn&rsquo;t the TypeScript or the serverless model; it&rsquo;s recalibrating how much infrastructure you actually need to think about. My home server still runs Kubernetes and I still genuinely enjoy that flexibility; being able to drop in open source software and wire it together however I want is something Cloudflare can&rsquo;t match. But for pipelines where I just need things to run, Workers is hard to beat. The mental shift is realizing those aren&rsquo;t competing opinions, they&rsquo;re just different problems. I came in expecting to feel constrained. I left thinking more carefully about what I actually need to own.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building Boston HERC&#39;s Website, Twice</title>
      <link>https://jahvon.dev/notes/herc-journey/</link>
      <pubDate>Tue, 23 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/herc-journey/</guid>
      <description>My experience redesigning Boston HERC&amp;rsquo;s website to reflect their growth and impact supporting first-generation students in the Boston area.</description>
      <content:encoded><![CDATA[<p>One of the most rewarding projects that I&rsquo;ve had the chance to work on this past year was one that allowed me to work closely with one of my favorite organizations, <a href="https://www.bostonherc.org/">Boston Higher Education Resource Center (HERC)</a>. The Boston HERC serves first-generation youth of color, providing support from 7th grade through postsecondary success. I first connected with HERC in 2019 through <a href="https://www.catchafire.org/">Catchafire</a> - a platform that matches skilled volunteers with mission-driven organizations. At the time, they needed help migrating from a single page Wix site to WordPress.</p>
<p>I didn&rsquo;t quite know what I was getting myself into, being early in my career and without any WordPress experience. The project ended up being much more than a simple migration but it was a great collaboration. I spent about 9 months working on my first project with the HERC team. We came up with a much more beautiful and informative site that they could feel proud to share with their community and donors. I was initially drawn to the HERC because of my own experience in high school but fell in love with the organization even more as I got to learn about their history and impact.</p>
<h3 id="staying-involved">Staying involved</h3>
<p>Naturally, I didn&rsquo;t want my involvement to end there. I&rsquo;ve continued to help them with their WordPress site over the years, mostly with small updates as new reports and events happened. I&rsquo;ve also had many chances to engage with the students that they support through panel discussions, STEM presentations, and a brief mentorship match. I was honored to receive their Make a Difference Award at this past May&rsquo;s annual celebration.</p>
<p>Since the first site design project, they&rsquo;d grown to reach over 1,600 students annually across 13 partner schools in 3 districts. They&rsquo;d launched an Alumni Success Program, expanded beyond Boston into Revere and Chelsea, and built up an even greater team of leaders and coaches empowering low-income youth across the Boston area. Unfortunately, their website was still telling the story of who they were, not who they&rsquo;d become. That led me to reaching out to offer some of my time to give it a complete refresh!</p>
<h3 id="redesign-goals">Redesign goals</h3>
<p>Looking through their existing site statistics, old and new content, and chatting with their team, I realized this wasn&rsquo;t just about updating copy and swapping out photos. Boston HERC needed their digital presence to reflect their empowering, student-first approach to supporting first-generation college students.</p>
<p>My focus for this redesign was on three key areas:</p>
<p><strong>Storytelling that matches their identity</strong>: Softening the overall feel of the site that allows the stories of their community and students to shine through more naturally.</p>
<p><strong>Data-driven messaging</strong>: Integrating their impressive outcomes data throughout the site, not buried in an annual report, but woven into the narrative of what makes Boston HERC different.</p>
<p><strong>Streamlined user journeys</strong>: Whether someone was a prospective student, a potential funder, or a school administrator looking to partner, the site needed to quickly communicate Boston HERC&rsquo;s value proposition and next steps.</p>
<p>There&rsquo;s something humbling about helping an organization that&rsquo;s been steadily doing the work for over 25 years. I admire how Boston HERC has been laser-focused on their mission to equip first-generation youth to access and thrive in higher education.</p>
<h3 id="technical-approach">Technical approach</h3>
<p>As a developer, I often get caught up in technical complexity and can be guilty of overengineering architecture at times. My latest WordPress administration work that I did for HERC was a nice way to challenge that inclination. My first time around, I used <a href="https://underscores.me/">underscores</a> as the base for the theme I created but spent a lot of time tweaking things, especially as I tried to make the site responsive for smaller devices. I had to write a lot of HTML, CSS, and JavaScript for the theme and had to figure out some of the oddities of WordPress PHP for some customizations I needed.</p>
<p>For my second time theming this site, I decided to lean into existing technologies, themes, widgets, and extensions so that it&rsquo;s easier for the HERC team to maintain and for me to make adjustments, without having to spend too much time in the weeds. I decided to use an <a href="https://elementor.com/">Elementor</a> theme as the basis of the latest site. I still had to inject in the HERC brand, create some custom assets, and figure out how I wanted to structure the pages but it was a much smoother experience this time.</p>
<p>I wanted to make sure that I was using my time on the most impactful side of this work: making sure their story is told effectively. When Boston HERC&rsquo;s website better reflects their sophistication and impact, it helps them reach more students, attract more funding, and partner with more schools. This project was a great reminder of how technology can serve a mission, not the other way around.</p>
<p><strong>You can see the live transformation at <a href="https://bostonherc.org/">bostonherc.org</a>.</strong></p>
<p><em>If their mission speaks to you too, consider <a href="https://www.bostonherc.org/get-involved/give/">giving to this great organization</a>!</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>AI as a Creative Partner</title>
      <link>https://jahvon.dev/notes/ai-creative-partner/</link>
      <pubDate>Wed, 19 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/ai-creative-partner/</guid>
      <description>An essay reflecting on my time using GenAI as a creative partner for a HGSE course that I was a teaching fellow for.</description>
      <content:encoded><![CDATA[<p>Since the start of the year, I&rsquo;ve been on a journey with learning about and with Large Language Models, settling into new AI tooling workflows, and reflecting on how these technologies have been showing up in my work. It&rsquo;s been quite impossible to avoid the constant AI buzz, so I wanted to figure out if my earlier AI skepticism was misplaced. I would only delegate teeny tiny tasks and easily confirmable questions to these systems. My time at Recurse Center this past summer accelerated that exploration even more. I tried several intentional experiments with a range AI development tools and processes. It gave me my first experiences with vibe coding during a weekly interest group that had formed. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>My own position has begun to develop even more over the last six weeks, as I served as a Teaching Fellow for a Harvard Graduate School of Education module on using generative AI as a creative partner. It followed a project-based structure where students sought to build vibe coded apps to respond to a weekly prompt. Build something that&hellip; &ldquo;makes your life easier&rdquo;, &ldquo;invites play&rdquo;, &ldquo;answers a question&rdquo;, etc. The studio group that I supported included 15 students coming from a variety of backgrounds but many have never coded or used AI tools, from grade school educators to EdTech entrepreneurs they all shared a similar desire of getting their hands dirty with AI so that they can learn how they can apply it with the work that they want to do. We used tools like Replit, Claude Code, Google Colab, and Figma Make to play with AI in a reflective space. Alongside each session and through 1:1 conversations, I got to engage in lots of thoughtful discussions about ideating, prompting, iterating, societal impacts of AI, limitations of the current tools, our routine usage of these tools, and much more. I deeply engaged in the coursework, not only as a teacher, but as a fellow learner.</p>
<h2 id="what-we-built">What We Built</h2>
<h3 id="the-collaborative-illusion">The Collaborative Illusion</h3>
<p>For the first project, the class was tasked with building something that tells a story. I decided to use Claude Code to create an interactive version of The Three Little Pigs. I didn&rsquo;t really have specific technologies in mind for this project so I just sent a straightforward prompt that described that I wanted animated visuals that matched the story as the viewer worked through it. I was inspired by the gentle animations of <a href="https://beta.hearingbirdsong.com/">Hearing Birdsong</a> so I tried to describe my experience with that site as a foundation for how I wanted my story to be. Claude&rsquo;s response to that design was far from what I imagined. I went back and forth a few times, trying to see if I could iterate to improve the size and positioning of the text, interactive actions, animations, and design elements but I was left unsatisfied overall.</p>
<p>I knew that Claude Code does not generate images but I would have loved to see it admit defeat. Explicitly tell me that it could not create a visually appealing animations without its current set of tools and assets. Or tell me that it made the wrong decision when it decided on the initial tech stack after getting more information from me. Instead, when I described what I wanted the pigs and homes to be modeled as, it stuck with unsatisfying SVG representations.</p>
<p>Reading about what Joseph Weizenbaum wrote in <em>Contextual Understandings by Computers</em> about ELIZA, his 1960s chatbot, a few weeks later reminded me of this experience:</p>
<blockquote>
<p>One of the principle aims of the DOCTOR program is to keep the conversation going&ndash;even at the price of having to conceal any misunderstandings on its own part.</p>
</blockquote>
<p>These modern AI systems seem to operate similarly - they&rsquo;re optimized to maintain the illusion of understanding and expertise rather than honestly calling out their limitations. Claude kept generating code, stating that it was making progress even though the questions that I continued to ask were clearly stating otherwise. I wasn&rsquo;t too surprised by this given my previous experiments with AI but many students struggled with this phenomena.</p>
<h3 id="drawing-the-line">Drawing the Line</h3>
<p>The fifth week of the course, we focused on building games! As a kid, I dreamed of creating my own video games. I ended up taking a different path with my software career so it felt a bit too ambitious for me to try to jump into as a side project. I decided to put Claude Code to test again for this. My vision was to create a game that combined two games that I played when I was a kid: Pokemon and Neopets. (Imagine being able to select a Neopet to go up against other wild Neopets) It was this week that I really started to feel the need for much more collaborative development with Claude. In the first three weeks, I stuck mostly to prompt-review-reprompt cycles but this week I was consistently unsatisfied with what was being created.</p>
<p>I decided to take a look at the code that was being written, edited some bits, and asked for clarification. Then eventually, I was able to tell it explicitly how I wanted it to implement some of the features that I needed. I also had to take a much more active role in getting the aesthetics to align with what I wanted. I did the work of researching assets that I can pull in, colors and fonts that I should use, and crafted detailed explanations for the placement of some elements.</p>
<blockquote>
<p>In the anticipated symbiotic partnership, men will set the goals, formulate the hypotheses, determine the criteria, and perform the evaluations. Computing machines will do the routinizable work that must be done to prepare the way for insights and decisions in technical and scientific thinking.</p>
<p><em>Man-Computer Symbiosis, J. C. Licklider</em></p>
</blockquote>
<p>Licklider&rsquo;s explanation of how he viewed the relationship between man and computer in his 1960 paper felt spot on in how my experience went. I was doing exactly that: formulating what &ldquo;good Pokemon-meets-Neopets gameplay&rdquo; meant. This productive collaboration only emerged when I stopped treating the AI as capable of independent creative judgment and started treating it as Licklider envisioned.</p>
<h2 id="what-we-uncovered">What We Uncovered</h2>
<h3 id="vibe-coding-in-practice">Vibe Coding in Practice</h3>
<p>I loved seeing the joy and excitement that spread across the room as students worked on and shared their projects. But I really appreciated the moments of shared frustration that brought up thoughtful questions as we wrestled with the limitations of using AI as a creative partner. Non-technical creators now have the ability to apply code to problems in their own lives and domains; in a way that was much more out of reach before. It was quite refreshing hearing how students want to use vibe coding to do things like spinning up interactive prototypes for professional development trainings they&rsquo;re building, teaching other entrepreneurs the strengths and limitations of AI use in the social innovation space, simplify the creation of classroom worksheets and activities, and much more.</p>
<p>To give you a sense of what <em>I</em> was able to create with AI, I vibe coded this interactive portfolio:</p>
<iframe src="https://vibes.jahvon.dev" width="100%" height="600px" frameborder="0"></iframe>
<p>We hear that the power is in the prompt but, for me, the whole process matters. I&rsquo;ve learned that you can come with a great, detailed prompt but without an understanding of what&rsquo;s possible and where AI should create versus where you should intervene, you&rsquo;ll end up disappointed or at risk. While vibe coding lowers the barrier to entry for creating, it doesn&rsquo;t guarantee that you won&rsquo;t get lost once you&rsquo;re inside. It can do very well with applying simple, common applications of code but fall apart in the obscure cases. And without AI having a full understanding of what you are intending to create and you having an idea of what it is creating, it can lead you down paths that may be harmful and unproductive.  A student shared how it has an &ldquo;addicting&rdquo; effect since you can instantly see an idea realized. As someone who has the understanding of the code these vibe coded projects produced, I would be hesitant to use it blindly for anything that requires care and attention. Especially not without some careful review and collaborative implementing&hellip; but I don&rsquo;t think it&rsquo;s vibe coding at that point.</p>
<h3 id="the-efficiency-trap">The Efficiency Trap</h3>
<p>A lot of the hype that I see with AI is around how much more efficient it makes people. I had many conversations with students about the potential for AI to take away jobs, weaken relationships, increase dependency on technology, and kill the individual learning and creative process.</p>
<p>Kate Crawford argues in <em>The Atlas of AI</em> that we need to ask &ldquo;what is being optimized, and for whom, and who gets to decide.&rdquo; When we optimize for speed in creating apps or generating content, what are we not optimizing for? Crawford points out that &ldquo;the true costs of this extraction is never borne by the industry itself&rdquo; - not the environmental costs of training models, not the labor costs of the workers who label data, not the costs to students whose critical thinking declines from over-reliance on generated answers.</p>
<p>The efficiency gains are real - I built 6 functional prototypes in hours that would have taken me weeks. But the costs are externalized: to my own learning, to the development of judgment and perspective, to the practice and growth of skills like problem solving.</p>
<h3 id="designing-dependency">Designing Dependency</h3>
<p>In one of my reading discussion, we talked about how companies like OpenAI, Google, and Anthropic are building LLMs with features that mimic human connection: memories of past conversations, empathetic language, customizable personalities, approachable voices. Someone shared how ChatGPT had referenced her previous chat about being sick in a completely unrelated conversation - unprompted, it checked in on her health. While the gesture may feel nice, it raised an unsettling question: should we be designing machines to provide emotional connection?</p>
<p>Crawford warns that AI systems &ldquo;are ultimately designed to serve existing dominant interests.&rdquo; What interests does artificial empathy serve? I think that the goal is to optimize for engagement metrics, not genuine human wellbeing - keeping users returning to the platform, deepening dependence on the system. These features don&rsquo;t seem to be about about connection; they&rsquo;re about retention.</p>
<p>I&rsquo;ve heard stories of people ending relationships based on the AI&rsquo;s advice or seeking emotional support primarily from chatbots. When we find ourselves turning to ChatGPT for thoughts on deeply personal matters, we should ask: Does it have the full context of our lives like a close friend would? Does it challenge us when needed, like a parent might? Can we trust its guidance when it doesn&rsquo;t know what we&rsquo;re not sharing?</p>
<p>Crawford describes AI as &ldquo;both embodied and material, made from natural resources, fuel, human labor, infrastructures, logistics, histories, and classifications.&rdquo; But these systems fundamentally lack what makes human connection meaningful: they have no stakes in our life, no shared history beyond collected data, no capacity to be changed by knowing us. A chatbot remembering you were sick is pattern-matching engineered to feel like care.</p>
<p>Sure, we may reach a point where AI convincingly simulates every feature of human relationship. These aspects may make the creative process feel more personal, but that still leaves actual messy, complicated, but irreplaceable connections at risk.</p>
<h2 id="a-working-philosophy">A Working Philosophy</h2>
<p>For quick MVPs and non-critical prototypes, these tools are genuinely useful. But they can&rsquo;t replace pair programming with a colleague who asks why you&rsquo;re solving the problem that way, whiteboarding with your team where someone sketches a better approach, or independent research that builds understanding from the ground up. The Pokemon-Neopets game required me to step in - researching assets, making aesthetic decisions, explicitly directing implementation. That&rsquo;s where I learned something. As one Recurser put it, LLMs are like e-bikes: great for getting somewhere quickly, but if your goal is to become stronger, they won&rsquo;t help you with that. I found most of the value with working with these tools when I critically engaged with what&rsquo;s being generated during the review and iterate phase.</p>
<p>A student told me she&rsquo;s learned to change her expectations when working with AI tools - we start with grand ideas of what they can do, but these systems lack the qualities that enable human imagination and creation. Earlier this year, I saw this work well when a friend asked if I could help him learn some Python. He was curious about automating data analysis that he does as a scientist in biotech. I decided to use Claude to help me craft a curriculum and some exercises for us to work through. After gathering some more information about the data formats, goals, and background for his work; we actually ended up with a decent set of lessons that got him comfortable with writing Python and using numpy and pandas to help with some tasks. When I sent him off on his own, he had both tools and understanding.</p>
<h3 id="ai-as-a-learning-partner">AI as a Learning Partner</h3>
<p>That difference between my earlier experience with AI and my more recent vibe coding experiences is in the way AI is collaboratively used as a scaffold for learning and creating versus replacement for it. LLMs risk creating a gap between the edge of what you can produce and what you can understand. I could see AI working as a much better learning partner than a creative partner. This requires more investment upfront from us but pays off in genuine capability rather than dependency. I&rsquo;ve started including explicit process instructions in my prompts: &ldquo;Before writing any code, summarize what you&rsquo;re about to do and ask for confirmation.&rdquo; &ldquo;Admit when questions are ambiguous.&rdquo; Unfortunately, some LLMs routinely ignore these instructions so you still have to be independently vigilant.</p>
<p>I&rsquo;ll keep using AI tools, but with clearer boundaries. For rapid prototyping where I need speed over quality. For handling boilerplate so I can focus on interesting problems. Always understanding that output requires review, refinement, and judgment only I can provide. This course reinforced something I suspected: the most important parts of learning and creating can&rsquo;t be automated, not because AI will never be technically capable, but because we must build our own mental structures. LLMs can give fast answers, but only you can determine which questions you care about, and which answers are meaningful. Being a teaching fellow for this module showed me that the students who thrived weren&rsquo;t the ones who generated the most code - they were the ones who asked the best questions, challenged the outputs, and built understanding through iteration. I&rsquo;m carrying forward a position, not of rejection or uncritical embrace, but of conscious engagement with these tools as supplements to my creative capability, never substitutes for it.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Check out RC&rsquo;s <a href="https://www.recurse.com/blog/191-developing-our-position-on-ai">position on AI</a> that dropped during my time in batch. The sentiments around balancing &ldquo;shipping mode&rdquo; and &ldquo;learning mode&rdquo; when considering AI usage really resonated with me and the experience that I had during that time.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>return recurse(): Summer of Building and Learning</title>
      <link>https://jahvon.dev/notes/rc-return-statement/</link>
      <pubDate>Tue, 09 Sep 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/rc-return-statement/</guid>
      <description>What I built and learned during 12 weeks of focused development at the Recurse Center.</description>
      <content:encoded><![CDATA[<p>I wrapped up my batch at <a href="https://www.recurse.com/scout/click?t=420ecb9ef5810758f6fe8dec816d80a8">Recurse Center</a> a few weeks ago and wanted to capture what I worked on during those 12 weeks.
RC gave me the space I needed to dive deep into a passion project, pick up new technologies, and push my
comfort zone across many areas of software engineering.</p>
<h3 id="learning-highlights">Learning highlights</h3>
<p><strong>Collaborative learning through pairing</strong> - Gained exposure to a diverse set of projects and development approaches. I didn&rsquo;t always
understand the technologies or projects I paired on, but they often provided inspiration for improving my own work or workflows. Notable
pairing sessions included a recipe management app with OpenAI integration, a BPE tokenizer implementation, and programming language development.</p>
<p><strong>Teaching through presentation</strong> - Regular demos of flow progress and architectural decisions solidified my understanding and gave me
practice explaining complex technical concepts to others. Take a look at my <a href="https://docs.google.com/presentation/d/11WBdY8pZ5IPJkBGb9A9pZIWUTCa9D2RAJaeNHPXNIQg/edit?usp=sharing">final presentation deck</a>
for the overview that I gave on flow&rsquo;s composable workflows!</p>
<p><strong>Discovering my learning patterns</strong> - After a few weeks of experimenting with time management, I settled on a three-day project focus
while leaving space for exploration. Using flow itself as a platform for trying new technologies safely turned out to be an effective
learning strategy.</p>
<p><strong>Balancing focus and community</strong> - The biggest challenge was finding rhythm between deep work and community engagement - there were so
many interesting things happening at RC. Writing regular check-ins and reading others&rsquo; updates helped me reflect and adjust that balance as desired.</p>
<h3 id="core-project">Core project</h3>
<p>As I described in <a href="https://jahvon.dev/notes/rc-6-week-flow/">6 Weeks of flow at Recurse Center</a>, I decided to expand my long-running side project <em>flow</em> as my
main focus. A little more than half of my flow work fell into these four major efforts, with the remainder spent on bug fixes and UX
improvements throughout.</p>
<p><strong>flow CLI v1.0 release</strong> - Stabilized platform with comprehensive testing, v2 of the integrated secrets vault, major documentation
updates, and CI integration via custom <a href="https://github.com/marketplace/actions/flow-execute">GitHub Action</a></p>
<p><strong>Desktop app POC</strong> - Built proof-of-concept with <a href="https://github.com/tauri-apps/tauri">Tauri</a>, establishing CLI-as-source-of-truth architecture for upcoming first release</p>
<p><strong>Executable generation</strong> - Added parsers for makefile, package.json, and docker-compose to streamline workflow migration</p>
<p><strong>MCP server</strong> - Built <a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol</a> integration enabling AI tools to understand flow workspaces and executables natively</p>
<p>Check out my new <a href="https://jahvon.dev/architecture/flow/">architecture doc</a> for more details on how I&rsquo;ve been building this local-first developer automation platform. To continue on with the &ldquo;learning generously&rdquo; mindset, I plan to keep it updated as the architecture evolves.</p>
<h3 id="honorable-mentions">Honorable mentions</h3>
<p><strong>Dev environment exploration</strong> - Tried various changes to my development editors and workflows, including AI-enhanced IDEs,
terminal-based applications, and improvements to local project and dotfile organization. This also included deeper integrations of
flow into my productivity, development, and operation workflows as I began to use it to standardize workflows across my side-projects.</p>
<p><strong>&ldquo;Impossible&rdquo; experiments</strong> - Single-day projects at the edge of my abilities, including a CGo process monitor using C for system process
information and a container runtime with runc. Also explored a WebAssembly plugin system for flow - not as impossible as it seemed but
informative for future feature planning.</p>
<p><strong>Creative coding</strong> - Completely new domain I fell into through RC events. These sessions became a refreshing creative outlet, including
mini projects with p5.js, tone.js, Motion Canvas, and the Python Imaging Library. Check out <a href="https://codepen.io/jahvon/pen/pvJXJjv">this Codepen</a> for an example of one of my creations.</p>
<p><strong>Vibe coding</strong> - Exploration of spinning up complete applications with AI development tools. These weekly sessions gave me better
appreciation of AI-assisted coding and helped me understand its current limitations. I&rsquo;ve since been vibe coding a few small apps
for my home lab. It&rsquo;s also been cool to also see how vibes + flow MCP can produce some neat, standardized workflows.</p>
<p><strong>Community programming</strong> - Beyond the creative and vibe coding sessions, I explored topics well outside my main focus through these
community activities. System design discussions, a workshop on building Obsidian plugins, and weekly non-programming presentations are
just a few that kept me curious about areas I wouldn&rsquo;t encounter naturally. As a &ldquo;never-graduated&rdquo; alum, I&rsquo;m looking forward to continuing
to participate whenever time allows.</p>
<p><strong>WordPress project</strong> - Pro bono work for nonprofit Boston HERC. Started before RC but got more time to work on this and
launched the first phase of the redesign of their core pages at <a href="https://bostonherc.org">bostonherc.org</a>. It was interesting finding
ways to apply my evolving learning style to evaluating and picking up newer, no-code frameworks like Elementor.</p>
<hr>
<p>My time at RC ended up giving me much more than I was hoping for. I got dedicated time to tackle ambitious ideas
I&rsquo;d been putting off for a while and learn new-to-me technologies in a supportive environment. The collaborative culture
pushed me to share my work regularly and learn from other skilled builders working on completely different problems.
As I move into the next phase of my career, I&rsquo;m carrying forward not just new technical skills but a better understanding
of how I learn best and what gets me excited about software engineering.</p>
<script async defer src="https://www.recurse-scout.com/loader.js?t=420ecb9ef5810758f6fe8dec816d80a8"></script>
]]></content:encoded>
    </item>
    <item>
      <title>6 Weeks of flow at Recurse Center</title>
      <link>https://jahvon.dev/notes/rc-6-week-flow/</link>
      <pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/rc-6-week-flow/</guid>
      <description>An update on my journey tackling developer tool chaos with a personal automation platform. 6 weeks of building desktop apps, cryptographic vaults, and feature planning at Recurse Center.</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve been thinking a lot about developer tooling and the idea of a &ldquo;personal developer platform&rdquo; - something that I could adapt to aid how <em>I</em> want to develop. Modern development can feel like tool chaos at times - we&rsquo;re juggling package managers, test runners, linters, deployment scripts, language-specific tooling, and dozens of open source CLI tools. Each project accumulates its own collection of scripts and commands that live in different places with different interfaces.</p>
<p>I introduced <a href="https://flowexec.io/">flow</a> in <a href="https://jahvon.dev/notes/forging-flow/">another post</a> a few months ago, but I&rsquo;ve had the amazing opportunity to start a sabbatical at the <a href="https://www.recurse.com/scout/click?t=420ecb9ef5810758f6fe8dec816d80a8">Recurse Center</a>, where I&rsquo;ve been able to think more deeply about the problems I was solving and the technologies I wanted to learn about! As I mentioned in that post, flow has been my &ldquo;learning platform&rdquo; over the last 2 years and I knew I wanted to take it a step further at Recurse.</p>
<p>I&rsquo;m at the halfway point of my time at RC and am excited to share how my first 6 weeks have been on my main coding project.</p>
<h2 id="flow-desktop">flow desktop</h2>
<p>I&rsquo;ve been itching to work on a frontend project for a while now. While building the flow TUI library, I had lots of fun thinking about how I could create a good experience through visuals and layout. <a href="https://github.com/charmbracelet/bubbletea">Bubble Tea</a> has been fun to use here, but I&rsquo;ve wanted to do some UI development with more possibilities. Doing this in the &ldquo;browser&rdquo; and using new-to-me technologies sounded like a great plan.</p>
<p>Coming into RC, this was something I knew I wanted to work on, but my excitement grew as I started to learn about the fascinating world of frontend development through RC pair programming, events, and chats.</p>
<p><strong>Architecture Decision: CLI as Single Source of Truth</strong></p>
<p>Instead of duplicating business logic in my desktop app, I&rsquo;ve been building it as a pure visualization layer over the existing CLI.</p>
<p><img src="https://jahvon.dev/images/flow-desktop-arch.png" srcset="https://jahvon.dev/images/flow-desktop-arch_hu_fde535ab93ee9ac8.png 600w, https://jahvon.dev/images/flow-desktop-arch.png 1201w" sizes="(min-width: 768px) 720px, 100vw" width="1201" height="446"
     alt="Desktop Architecture"
     loading="lazy" decoding="async">
</p>
<p>This felt risky at first - wouldn&rsquo;t spawning processes be too slow? Turns out CLI commands execute pretty fast thanks to some caching I do on the CLI side. The whole round trip feels instant with my current usage.</p>
<p><strong>Tech Stack</strong></p>
<p>All of the resources, pairing, feedback, and individual research I&rsquo;ve done has landed me on the following:</p>
<ul>
<li><strong>Tauri</strong>: Gives me Rust backend + web frontend without Electron&rsquo;s bloat. Bonus that it&rsquo;s an opportunity to learn some Rust.</li>
<li><strong>TypeScript</strong>: I was able to get TS and Rust types generated from the same JSON schema that I use to generate Go code. This has been making development much smoother across the 3 languages that flow now uses.</li>
<li><strong>React &amp; Mantine UI</strong>: VSCode-like components without building everything from scratch. Their <a href="https://mantine.dev/x/spotlight/">Spotlight</a> extension is what sold me - it could be a really cool search and command center for the UI!</li>
</ul>
<p><strong>Demo!</strong></p>
<p>Here is a quick demo of me using my current implementation of the desktop. This shows the workspace and executable viewer/runner in action - you can see me running an executable directly from the UI and playing with the theme picker I prototyped for customization.</p>
<video class="demo-video"
       controls preload="none" poster="https://jahvon.dev/images/desktop-demo-poster.png"
       aria-label="The flow desktop app, early build">
  <source src="https://jahvon.dev/images/flow-desktop-demo.mp4" type="video/mp4">
</video>

<p>I still have more work to do here, but it&rsquo;s been satisfying seeing my ideas come to life as I pick up these new technologies.</p>
<h2 id="vaults-v2">vaults v2</h2>
<p>I had a couple of pain points with the vault that I initially built for flow. The UX was pretty simple but limiting. I&rsquo;ve also been really wanting a way to integrate my Bitwarden secrets into flow seamlessly. This led me to brainstorm a new design for that feature. I spent my first 2 weeks at RC doing some light research on cryptography with Go, studying how other tools handle secrets, and building a simple POC.</p>
<p>I decided to introduce a &ldquo;provider&rdquo; concept that also improves the experience around having multiple vaults. I currently have an implementation for an updated version of my AES symmetrically encrypted vault, added an Age asymmetric encryption backend, and plan to add a backend for custom CLI-tool vault managers. You can see what I came up with in <a href="https://github.com/jahvon/vault">this repo</a>. From the flow perspective, the experience would be like this:</p>
<p><strong>Creating a vault</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Auto-generates everything for the AES vault</span>
</span></span><span class="line"><span class="cl">flow vault create development
</span></span><span class="line"><span class="cl"><span class="c1"># Create an Age vault with identity generated from age-keygen</span>
</span></span><span class="line"><span class="cl">flow vault create team --type age --recipients key1,key2,key3 --identityFile id.txt
</span></span><span class="line"><span class="cl"><span class="c1"># External needs CLI integration</span>
</span></span><span class="line"><span class="cl">flow vault create bitwarden --type external --interactive
</span></span></code></pre></div><p><strong>Vault Switching as Primary UX</strong></p>
<p>Borrowed the mental model from git/kubectl:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">flow vault switch development    <span class="c1"># Like git checkout</span>
</span></span><span class="line"><span class="cl">flow secret <span class="nb">set</span> api-key <span class="s2">&#34;dev-123&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">flow vault switch production
</span></span><span class="line"><span class="cl">flow secret <span class="nb">set</span> api-key <span class="s2">&#34;prod-456&#34;</span>
</span></span></code></pre></div><p>This allows for clean secret references in executables: <code>secretRef: &quot;api-key&quot;</code> uses current vault, <code>secretRef: &quot;production/api-key&quot;</code> is explicit.</p>
<h2 id="technical-decisions">Technical decisions</h2>
<h3 id="executable-composition">Executable composition</h3>
<p>Executables aren&rsquo;t just scripts - they&rsquo;re composable units with conditional logic:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serial</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">failFast</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">execs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">os == &#34;darwin&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;command -v mytool || brew install mytool&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">env[&#34;PUSH&#34;] == &#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="l">make image</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">deploy development</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">reviewRequired</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">  </span><span class="c"># Pauses for human confirmation</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">launch app</span><span class="w">
</span></span></span></code></pre></div><p>The expression language (using <a href="https://github.com/expr-lang/expr">Expr</a>) has access to OS info, environment variables, and flow&rsquo;s cache. It&rsquo;s like having bash conditionals but declarative.</p>
<h3 id="process-architecture">Process architecture</h3>
<p>The desktop app&rsquo;s process model is pretty simple. Each user action spawns a CLI process:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="cp">#[tauri::command]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">async</span><span class="w"> </span><span class="k">fn</span> <span class="nf">get_workspaces</span><span class="p">()</span><span class="w"> </span>-&gt; <span class="nb">Result</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Workspace</span><span class="o">&gt;</span><span class="p">,</span><span class="w"> </span><span class="nb">String</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="n">output</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Command</span>::<span class="n">new</span><span class="p">(</span><span class="s">&#34;flow&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">args</span><span class="p">([</span><span class="s">&#34;workspace&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;list&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;--output&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;json&#34;</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">output</span><span class="p">().</span><span class="k">await</span><span class="o">?</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">serde_json</span>::<span class="n">from_slice</span><span class="p">(</span><span class="o">&amp;</span><span class="n">output</span><span class="p">.</span><span class="n">stdout</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>This seems inefficient but has huge benefits:</p>
<ul>
<li>Desktop crashes don&rsquo;t corrupt CLI state</li>
<li>CLI updates immediately benefit desktop</li>
<li>No state synchronization between processes</li>
<li>Easy to debug - each operation is a discrete CLI command</li>
</ul>
<h2 id="rc-moments-that-shaped-the-code">RC moments that shaped the code</h2>
<p><strong>Pair Programming</strong>: I paired on setting up Tauri and trying to integrate it with the CLI. We came up with the great idea of using some of my existing CLI output formatting options to get data through the app. This turned out to be a great decision to make on the fly. Conversations with others helped me confirm that CLI-as-source-of-truth wasn&rsquo;t a compromise - it was the right abstraction.</p>
<p><strong>The Feedback Loop</strong>: RC&rsquo;s culture of sharing work-in-progress meant getting feedback on half-baked ideas. I&rsquo;ve enjoyed presenting and demoing my progress throughout my time. I&rsquo;d thought extensions would be a neat feature but I never actually needed them myself. Hearing about the different ways that others think flow could be extended convinced me to reopen an <a href="https://github.com/flowexec/flow/issues/185">issue</a> I closed.</p>
<p><strong>Community Inspiration</strong>: Seeing the variety of projects and approaches at RC has reinforced my belief that developer tools should be adaptable rather than prescriptive. Everyone has their own workflow, and the best tools are the ones that bend to fit how you think, not the other way around.</p>
<h2 id="next-up">Next up</h2>
<h3 id="flow-mcp-server">flow MCP server</h3>
<p>I&rsquo;ve started using Claude Code and this has inspired me to learn how to create a Model Context Protocol server that will allow AI to understand flow workspaces and executables natively. I&rsquo;d love to eventually have something that enables:</p>
<ul>
<li>Browsing flow files and suggesting syntax improvements</li>
<li>Generating new workflows from natural language</li>
<li>Debugging failures with full workspace context 🚀</li>
</ul>
<h3 id="wasm-plugin-system">WASM plugin system</h3>
<p>Extending flow with WASM-integrated extensions. I want to try to allow automations to be programmable in two ways:</p>
<ul>
<li><strong>Executable template generator</strong>: Plugins that run template generation for flow-discoverable executables (from APIs, templates, external sources). I&rsquo;m thinking of something like Taskfile/just → flow executable integrations to start</li>
<li><strong>WASM Runtime executable type</strong>: Plugin executables that run sandboxed through flow</li>
</ul>
<p>This will be my first time getting hands-on with WebAssembly and I already have many ideas for cool plugins that will allow me to tinker with a variety of languages in the future.</p>
<h3 id="test--release-improvements">Test &amp; release improvements</h3>
<p>The best way to try out some of these new and upcoming features would be to clone the flow repo and run the <code>build binary</code> executable to get a local go build. Note that the main branch is not guaranteed to be stable, though.</p>
<p>With a new component in the flow ecosystem, I need to level up the test and release process as I prepare for v1 over the next couple of months. This means learning frontend testing practices, updating my GitHub workflows to handle multi-language builds, and rethinking the installation process to bundle the desktop app alongside the CLI.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Distributing Work with Go Concurrency</title>
      <link>https://jahvon.dev/notes/distributing-work/</link>
      <pubDate>Tue, 27 May 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/distributing-work/</guid>
      <description>&lt;p&gt;A few months back, I worked through VictoriaMetrics&amp;rsquo; &lt;a href=&#34;https://victoriametrics.com/blog/go-sync-mutex/index.html&#34;&gt;Go concurrency series&lt;/a&gt; and wanted to get some practice. So I implemented a few distributed systems, work distribution patterns to see how the concurrency patterns translate.&lt;/p&gt;
&lt;p&gt;Work distribution is fundamental to building scalable systems - you need ways to spread processing across multiple components while coordinating the results. Go&amp;rsquo;s goroutines and channels map well to distributed system concepts - channels as service communication, goroutines as system components, WaitGroups for coordination. Here&amp;rsquo;s what I learned.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months back, I worked through VictoriaMetrics&rsquo; <a href="https://victoriametrics.com/blog/go-sync-mutex/index.html">Go concurrency series</a> and wanted to get some practice. So I implemented a few distributed systems, work distribution patterns to see how the concurrency patterns translate.</p>
<p>Work distribution is fundamental to building scalable systems - you need ways to spread processing across multiple components while coordinating the results. Go&rsquo;s goroutines and channels map well to distributed system concepts - channels as service communication, goroutines as system components, WaitGroups for coordination. Here&rsquo;s what I learned.</p>
<h2 id="producer-consumer-async-work-distribution">Producer-Consumer: Async Work Distribution</h2>
<p>Producers generate work and send it through channels while consumers process it asynchronously.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">ConsumerResult</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">ConsumerID</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Data</span><span class="w">       </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// start multiple consumers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">numConsumers</span><span class="p">;</span><span class="w"> </span><span class="nx">id</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">consumerID</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">defer</span><span class="w"> </span><span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="nx">msg</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">msgChan</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">result</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ConsumerResult</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nx">ConsumerID</span><span class="p">:</span><span class="w"> </span><span class="nx">consumerID</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nx">Data</span><span class="p">:</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;processed-%d&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">resultChan</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">result</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}(</span><span class="nx">id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// start a single producer that sends work into a channel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="nb">close</span><span class="p">(</span><span class="nx">msgChan</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="mi">25</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">msgChan</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">i</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}()</span><span class="w">
</span></span></span></code></pre></div><p>Buffered channels give you throttling - if consumers can&rsquo;t keep up, the producer blocks instead of consuming memory.</p>
<p>Use this pattern for event streaming, async processing, or decoupling generation speed from processing speed. It maps directly to Kafka or microservice event handling.</p>
<h2 id="worker-pools-controlled-work-distribution">Worker Pools: Controlled Work Distribution</h2>
<p>Worker pools give you structure - fixed number of workers pulling from the same job queue. It&rsquo;s like running N service instances behind a load balancer.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">PoolJob</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">ID</span><span class="w">   </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Data</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// start a fixed number of workers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">numWorkers</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">workerID</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">defer</span><span class="w"> </span><span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="nx">job</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobChan</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// do some work</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">100</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">results</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">PoolResult</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nx">WorkerID</span><span class="p">:</span><span class="w"> </span><span class="nx">workerID</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nx">JobID</span><span class="p">:</span><span class="w">    </span><span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nx">Value</span><span class="p">:</span><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;processed-%s&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">job</span><span class="p">.</span><span class="nx">Data</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}(</span><span class="nx">i</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>The job channel acts like a load balancer - work goes to whichever worker is available.</p>
<p>This pattern is good for CPU-heavy tasks or when you need predictable resource usage. It&rsquo;s similar to scaling microservice instances for ingress traffic.</p>
<h2 id="batch-processing-efficient-work-distribution">Batch Processing: Efficient Work Distribution</h2>
<p>Sometimes you need to group items into batches for efficiency or to respect downstream rate limits. This example handles batching by size and by time.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">p</span><span class="w"> </span><span class="o">*</span><span class="nx">BatchProcessor</span><span class="p">)</span><span class="w"> </span><span class="nf">startBatchAggregator</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">batch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nx">p</span><span class="p">.</span><span class="nx">batchSize</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">flushTimer</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nf">NewTimer</span><span class="p">(</span><span class="mi">2</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">sendBatch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="o">&lt;-</span><span class="nx">p</span><span class="p">.</span><span class="nx">rateLimiter</span><span class="p">.</span><span class="nx">C</span><span class="w"> </span><span class="c1">// wait for rate limiter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">batchCopy</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">batch</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nb">copy</span><span class="p">(</span><span class="nx">batchCopy</span><span class="p">,</span><span class="w"> </span><span class="nx">batch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">p</span><span class="p">.</span><span class="nx">batchChan</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">batchCopy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">batch</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">batch</span><span class="p">[:</span><span class="mi">0</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">case</span><span class="w"> </span><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">p</span><span class="p">.</span><span class="nx">itemChan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">batch</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                        </span><span class="nf">sendBatch</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="nb">close</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">batchChan</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nx">batch</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">batch</span><span class="p">,</span><span class="w"> </span><span class="nx">item</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">batch</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="nx">p</span><span class="p">.</span><span class="nx">batchSize</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="nf">sendBatch</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">flushTimer</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">batch</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="nf">sendBatch</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>The <code>select</code> with the flush timer gives you batches when they&rsquo;re full OR when time runs out. The rate limiter prevents overwhelming the batch processor and its external dependencies. I used a simple timer here, but you can replace it with much more sophisticated limiting logic as needed.</p>
<p>The batch processing pattern works well for database bulk operations, API integrations with rate limits, or protecting downstream services.</p>
<h2 id="a-few-notes">A Few Notes</h2>
<p>Working through these patterns reinforced a few things:</p>
<ul>
<li>Channels behave like message queues with capacity limits and natural flow control.</li>
<li>Multiple goroutines running the same function is basically horizontal scaling - same patterns you&rsquo;d use for scaling system components.</li>
<li>These patterns compose well. Producer-consumer provides the foundation, worker pools add structure, batching adds efficiency.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>Analogy</th>
          <th>Use Case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Producer-Consumer</td>
          <td>Message queues, event streams</td>
          <td>Event-driven architectures, async processing</td>
      </tr>
      <tr>
          <td>Worker Pools</td>
          <td>Load-balanced system components</td>
          <td>Controlled concurrency, predictable resources</td>
      </tr>
      <tr>
          <td>Batch Processing</td>
          <td>ETL pipelines, bulk APIs</td>
          <td>Rate limiting, bulk operations</td>
      </tr>
  </tbody>
</table>
<h3 id="error-handling">Error Handling</h3>
<p>Error handling in concurrent code needs to be explicit and planned upfront, similar to how distributed systems need circuit breakers and retry logic. I used result structs that carry either data or errors:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">WorkResult</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Data</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Err</span><span class="w">  </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">worker</span><span class="p">(</span><span class="nx">jobs</span><span class="w"> </span><span class="o">&lt;-</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">results</span><span class="w"> </span><span class="kd">chan</span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">WorkResult</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">job</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">job</span><span class="o">%</span><span class="mi">7</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// simulate some failures</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">results</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">WorkResult</span><span class="p">{</span><span class="nx">Err</span><span class="p">:</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;job %d failed&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">job</span><span class="p">)}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">continue</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">results</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">WorkResult</span><span class="p">{</span><span class="nx">Data</span><span class="p">:</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;processed-%d&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">job</span><span class="p">)}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>For timeouts and cancellation, <code>context.Context</code> works well:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">workerWithTimeout</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">jobs</span><span class="w"> </span><span class="o">&lt;-</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">results</span><span class="w"> </span><span class="kd">chan</span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">WorkResult</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="nx">job</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// process the job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nx">results</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">WorkResult</span><span class="p">{</span><span class="nx">Err</span><span class="p">:</span><span class="w"> </span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="beyond-the-basics">Beyond the basics</h3>
<p>These patterns scratch the surface of Go&rsquo;s concurrency toolkit. The VictoriaMetrics series I mentioned dives deep into more advanced primitives like <code>sync.Mutex</code> for protecting shared state, <code>sync.Pool</code> for object reuse, <code>sync.Once</code> for one-time initialization, and <code>sync.Map</code> for concurrent map access. I recommend checking it out if you haven&rsquo;t already!</p>
<p>I intentionally stuck to channels and WaitGroups in my examples here - they mirror message passing between services naturally and keep the code readable. Once those patterns are solid, adding mutexes and other synchronization primitives becomes intuitive because you already understand the coordination challenges.</p>
<p>As you build more complex systems, you&rsquo;ll need these other tools. Mutexes become your distributed locks, sync.Pool mirrors connection pooling in microservices, sync.Once handles singleton initialization across service instances (similar to leader election), and sync.Map acts like shared caches that multiple services access concurrently.</p>
<p><strong><a href="https://github.com/jahvon/go-concurrency-examples">Full code examples on GitHub</a></strong></p>
<p><em>Next: circuit breaker and fan-in/fan-out implementations.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Forging flow: My Journey of Creation and Learning</title>
      <link>https://jahvon.dev/notes/forging-flow/</link>
      <pubDate>Sat, 08 Feb 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/forging-flow/</guid>
      <description>&lt;p&gt;Over the last couple of years, I have been having fun experimenting with ways to streamline my developer experience. This may largely stem from my Developer Experience (DevX) focus as a software / platform engineer in the CarGurus DevX organization. (&lt;em&gt;side note: Check out &lt;a href=&#34;https://www.cargurus.dev/How-CarGurus-is-supercharging-our-microservice-developer-experience/&#34;&gt;this blog post&lt;/a&gt; I wrote on how we supercharged the experience for our Product Engineers&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;However, the challenges that &lt;em&gt;I&lt;/em&gt; face at work and on my side projects aren&amp;rsquo;t quite the same as the ones faced by CarGurus product engineers. I started to find myself drowning in a sea of scattered commands, scripts, and tools. This, combined with my desire to find opportunities to learn more about Go patterns and libraries in a low-risk way, motivated me to invest some free time developing an &amp;ldquo;integrated development platform&amp;rdquo; - or at least the foundations for one!&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Over the last couple of years, I have been having fun experimenting with ways to streamline my developer experience. This may largely stem from my Developer Experience (DevX) focus as a software / platform engineer in the CarGurus DevX organization. (<em>side note: Check out <a href="https://www.cargurus.dev/How-CarGurus-is-supercharging-our-microservice-developer-experience/">this blog post</a> I wrote on how we supercharged the experience for our Product Engineers</em>)</p>
<p>However, the challenges that <em>I</em> face at work and on my side projects aren&rsquo;t quite the same as the ones faced by CarGurus product engineers. I started to find myself drowning in a sea of scattered commands, scripts, and tools. This, combined with my desire to find opportunities to learn more about Go patterns and libraries in a low-risk way, motivated me to invest some free time developing an &ldquo;integrated development platform&rdquo; - or at least the foundations for one!</p>
<p>Enter flow: my open-source task runner and workflow automation tool. What started as a simple itch to scratch has evolved into the foundations of a comprehensive platform for wrangling dev workflows across projects. Looking back, I realize it would have been easier to just migrate everything into a tool like <a href="https://taskfile.dev/">Taskfile</a> or <a href="https://just.systems/">Just</a>, but my vision for my own personal platform doesn&rsquo;t stop at the CLI. Taking that route also would have left my learning desires unmet. While much of my influence for flow comes from cloud-native projects and ideals, I&rsquo;ve approached it from a local-first perspective - one where repeatable &ldquo;micro-workflows&rdquo; can be pieced together however <em>you</em> desire; making them easily discoverable, automated, and observable.</p>
<p>It&rsquo;s ambitious, but that&rsquo;s what excites me! There&rsquo;s so much experimenting and learning ahead. This is my first blog post, but if this interests you, please return! I&rsquo;ll be using it to document my learnings and progress on this project and some of my other side projects.</p>
<h2 id="building-the-foundation">Building the Foundation</h2>
<p>My first build of flow centered around two simple concepts driven by YAML files:</p>
<ul>
<li>Workspaces for organizing tasks across projects/repos</li>
<li>Executables for defining those tasks</li>
</ul>
<p>I included a simple <a href="https://github.com/rivo/tview/">tview</a> terminal UI implementation to simplify the discovery of workspaces and executables across my system. While I&rsquo;ve since moved away from that library, it helped me conceptualize much of the current TUI.</p>
<p>Through usage, I found myself iterating <em>a lot</em>. As I onboarded more workspaces and as those workspaces grew in complexity, flow&rsquo;s feature set had to grow. My favorite components to implement have been the <a href="https://github.com/charmbracelet/bubbletea">bubbletea</a> TUI framework, an internal documentation generator for the <a href="https://flowexec.io/">flowexec.io</a> site, the <a href="https://flowexec.io/#/guide/templating">templating</a> workflow, and the <a href="https://flowexec.io/#/guide/state">state</a> and <a href="https://flowexec.io/#/guide/conditional">conditional</a> management of  serial and parallel executable types. &rsquo;ll dive deeper into those in follow-up posts, but I invite you to explore the guides at flowexec.io for a complete overview of where flow stands today.</p>
<p>At it&rsquo;s core, the flow CLI is a YAML-driven task runner. Here is an example of a flow file that I have for my Authentik server deployed in my home cluster; it uses the <code>exec</code> executable type to define the command that&rsquo;s run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w"> </span><span class="c"># optional, additional grouping in a workspace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">k8s, auth]</span><span class="w"> </span><span class="c"># useful for filtering the `flow library` command</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># this description is rendered as markdown (alongside other executable info)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># when viewing in the `flow library`.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">  **References:**
</span></span></span><span class="line"><span class="cl"><span class="sd">  - https://goauthentik.io/
</span></span></span><span class="line"><span class="cl"><span class="sd">  - https://github.com/goauthentik/helm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">executables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># flow install authentik:app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">verb</span><span class="p">:</span><span class="w"> </span><span class="l">install</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">aliases</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">chart]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Upgrade/install Authentik Helm chart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># secrets are managed with the integrated vault via the `flow secret` command</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">secretRef</span><span class="p">:</span><span class="w"> </span><span class="l">authentik-secret-key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">AUTHENTIK_SECRET_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">secretRef</span><span class="p">:</span><span class="w"> </span><span class="l">authentik-db-password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">AUTHENTIK_DB_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        helm upgrade --install authentik authentik/authentik \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --version 2024.10.4 \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --namespace auth --create-namespace \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --set authentik.secret_key=$AUTHENTIK_SECRET_KEY \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --set authentik.postgresql.password=$AUTHENTIK_DB_PASSWORD \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --set postgresql.auth.password=$AUTHENTIK_DB_PASSWORD \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --set postgresql.postgresqlPassword=$AUTHENTIK_DB_PASSWORD \
</span></span></span><span class="line"><span class="cl"><span class="sd">          -f values.yaml</span><span class="w">
</span></span></span></code></pre></div><p>The flow CLI provides a consistent experience for all executable runs and searches. This includes automatically generating a summary markdown document viewable with the <code>flow library</code> command, log formatting and archiving, and a configurable TUI experience.</p>
<p>This will show up in the <code>flow library</code> as rendered markdown:</p>
<p><img src="https://jahvon.dev/images/library-authentik-exec_hu_ce3ceb83172b1543.png" srcset="https://jahvon.dev/images/library-authentik-exec_hu_69fabeb981d37580.png 700w, https://jahvon.dev/images/library-authentik-exec_hu_ce3ceb83172b1543.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/library-authentik-exec.72a7791a5d00574821c54e4666a9a5366f5c4632ead429e14bff952cc2d5cc21.png" width="1400" height="1375"
     alt="Authentik Executable"
     loading="lazy" decoding="async">
</p>
<p>As my needs evolved, I added more executable configurations and types. Here&rsquo;s an example of a common <code>request</code> executable I use to pause my home&rsquo;s pi.hole blocking:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">executables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># flow pause pihole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">verb</span><span class="p">:</span><span class="w"> </span><span class="l">pause</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pihole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">method</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;POST&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">pos</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">DURATION</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="m">300</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">secretRef</span><span class="p">:</span><span class="w"> </span><span class="l">pihole-pwhash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">envKey</span><span class="p">:</span><span class="w"> </span><span class="l">PWHASH</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">http://pi.hole/admin/api.php?disable=$DURATION&amp;auth=$PWHASH</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">validStatusCodes</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">200</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">logResponse</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">transformResponse</span><span class="p">:</span><span class="w"> </span><span class="l">if .status == &#34;disabled&#34; then .status = &#34;paused&#34; else . end</span><span class="w">
</span></span></span></code></pre></div><p><img src="https://jahvon.dev/images/library-pihole-exec_hu_560c97ea9463da37.png" srcset="https://jahvon.dev/images/library-pihole-exec_hu_ed7791a134bf4c1.png 700w, https://jahvon.dev/images/library-pihole-exec_hu_560c97ea9463da37.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/library-pihole-exec.4ce8f7efff753ae1192643673548abb8e8b632e935e64f543592eaf06ca5076b.png" width="1400" height="1165"
     alt="PiHole Executable"
     loading="lazy" decoding="async">
</p>
<p>Here&rsquo;s an example of the log output:</p>
<p><img src="https://jahvon.dev/images/log-pihole-exec_hu_8f60b8d8976ed9f2.png" srcset="https://jahvon.dev/images/log-pihole-exec_hu_a24131036a77d8be.png 700w, https://jahvon.dev/images/log-pihole-exec_hu_8f60b8d8976ed9f2.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/log-pihole-exec.43caacb1e63c7fa66f70a912185fbc43945225d4ceef18e2695c7c3558309b42.png" width="1400" height="111"
     alt="PiHole Executable Logs"
     loading="lazy" decoding="async">
</p>
<h2 id="lessons-learned">Lessons Learned</h2>
<p>Building flow has been an incredible opportunity to deepen my understanding of Go and its ecosystem. Here are a few key lessons that have significantly shaped how I write Go now:</p>
<h3 id="small-packages-and-interfaces-made-testing-a-breeze">Small Packages and Interfaces Made Testing a Breeze</h3>
<p>This approach makes testing easier, improves code organization, and makes refactoring as ideas evolve a joy.</p>
<p>Each executable type has its own Runner, making it simple to extend the system with new types. Here&rsquo;s a snippet that demonstrates the ease of using this type when assigning an executable to a <code>Runner</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="cp">//go:generate mockgen -destination=mocks/mock_runner.go -package=mocks github.com/jahvon/flow/internal/runner Runner</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Runner</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">Name</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">Exec</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="o">*</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">e</span><span class="w"> </span><span class="o">*</span><span class="nx">executable</span><span class="p">.</span><span class="nx">Executable</span><span class="p">,</span><span class="w"> </span><span class="nx">eng</span><span class="w"> </span><span class="nx">engine</span><span class="p">.</span><span class="nx">Engine</span><span class="p">,</span><span class="w"> </span><span class="nx">inputEnv</span><span class="w"> </span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">IsCompatible</span><span class="p">(</span><span class="nx">executable</span><span class="w"> </span><span class="o">*</span><span class="nx">executable</span><span class="p">.</span><span class="nx">Executable</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>This interface allows me to easily add new executable types by just implementing these three methods. The core execution logic remains clean and extensible:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Exec</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">ctx</span><span class="w"> </span><span class="o">*</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">executable</span><span class="w"> </span><span class="o">*</span><span class="nx">executable</span><span class="p">.</span><span class="nx">Executable</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">eng</span><span class="w"> </span><span class="nx">engine</span><span class="p">.</span><span class="nx">Engine</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">inputEnv</span><span class="w"> </span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">assignedRunner</span><span class="w"> </span><span class="nx">Runner</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">runner</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">registeredRunners</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">if</span><span class="w"> </span><span class="nx">runner</span><span class="p">.</span><span class="nf">IsCompatible</span><span class="p">(</span><span class="nx">executable</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nx">assignedRunner</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">runner</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="k">break</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">assignedRunner</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">return</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;compatible runner not found for executable %s&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">executable</span><span class="p">.</span><span class="nf">ID</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">executable</span><span class="p">.</span><span class="nx">Timeout</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">return</span><span class="w"> </span><span class="nx">assignedRunner</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">executable</span><span class="p">,</span><span class="w"> </span><span class="nx">eng</span><span class="p">,</span><span class="w"> </span><span class="nx">inputEnv</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">done</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">error</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="nx">done</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">assignedRunner</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">executable</span><span class="p">,</span><span class="w"> </span><span class="nx">eng</span><span class="p">,</span><span class="w"> </span><span class="nx">inputEnv</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">case</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">time</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">executable</span><span class="p">.</span><span class="nx">Timeout</span><span class="p">):</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">return</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;timeout after %v&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">executable</span><span class="p">.</span><span class="nx">Timeout</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>For testing, I use <a href="https://onsi.github.io/ginkgo/">ginkgo</a> for its expressive BDD-style syntax and <a href="https://github.com/uber-go/mock">GoMock</a> to generate a mock runner. This mock simulates serial and parallel execution without the complexity of managing real subprocesses or network calls. This approach has been invaluable for verifying complex concurrent behaviors, especially when testing features like timeout handling, parallel execution limits, and failure modes in a reliable, repeatable way.</p>
<h3 id="build-better-abstractions-with-service-layers">Build Better Abstractions with Service Layers</h3>
<p>When working with third-party modules or I/O components, wrapping your interaction with a service layer is invaluable. It keeps business logic decoupled from implementation details and simplifies testing and refactoring. In flow, I use this pattern extensively for components like shell operations, file system operations, and process management.</p>
<p>For example, my run service abstracts away the complexities of running shell operations with the <a href="https://github.com/mvdan/sh">github.com/mvdan/sh</a> library.  This means if I need to change how shell commands are executed or add new shell features, I only need to update the service implementation, not the core application logic. You can explore some of my service implementations in the <a href="https://github.com/jahvon/flow/tree/main/internal/services">source code</a>.</p>
<h3 id="good-tools-are-worth-the-investment">Good Tools Are Worth the Investment</h3>
<p>Investing in custom tooling or incoproating open source, especially for patterns like code generation, can significantly streamline your development workflow and reduce boilerplate. In flow, I define all types in YAML and use <a href="https://github.com/atombender/go-jsonschema">go-jsonschema</a> for <code>codegen</code>.</p>
<p>Here&rsquo;s an example of how my <code>Launch</code> executable type is defined:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">LaunchExecutableType</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">uri]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Launches an application or opens a URI.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	  </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/definitions/ParameterList&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/definitions/ArgumentList&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">The application to launch the URI with.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">uri</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">The URI to launch. This can be a file path or a web URL.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">wait</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">boolean</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">If set to true, the executable will wait for the launched application to exit before continuing.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span></code></pre></div><p>This generates both the Go type and its documentation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Launches an application or opens a URI.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">LaunchExecutableType</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// The application to launch the URI with.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">App</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="s">`json:&#34;app,omitempty&#34; yaml:&#34;app,omitempty&#34; mapstructure:&#34;app,omitempty&#34;`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// Args corresponds to the JSON schema field &#34;args&#34;.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Args</span><span class="w"> </span><span class="nx">ArgumentList</span><span class="w"> </span><span class="s">`json:&#34;args,omitempty&#34; yaml:&#34;args,omitempty&#34; mapstructure:&#34;args,omitempty&#34;`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// Params corresponds to the JSON schema field &#34;params&#34;.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Params</span><span class="w"> </span><span class="nx">ParameterList</span><span class="w"> </span><span class="s">`json:&#34;params,omitempty&#34; yaml:&#34;params,omitempty&#34; mapstructure:&#34;params,omitempty&#34;`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// The URI to launch. This can be a file path or a web URL.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">URI</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="s">`json:&#34;uri&#34; yaml:&#34;uri&#34; mapstructure:&#34;uri&#34;`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// If set to true, the executable will wait for the launched application to exit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// before continuing.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Wait</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="s">`json:&#34;wait,omitempty&#34; yaml:&#34;wait,omitempty&#34; mapstructure:&#34;wait,omitempty&#34;`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>I&rsquo;ve also built a <code>docsgen</code> tool that uses this same schema to generate structured documentation. This means my types, code, and documentation all stay in sync automatically. You can see the generated type documentation for Launch <a href="https://flowexec.io/#/types/flowfile?id=executablelaunchexecutabletype">here</a>.</p>
<p>This investment in tooling has paid off repeatedly, especially as flow&rsquo;s type system has grown more complex. It reduces errors, ensures consistency, and lets me focus on implementing features rather than maintaining boilerplate code.</p>
<h2 id="the-road-ahead">The Road Ahead</h2>
<p>Looking forward, I&rsquo;m excited to explore building extensions around the flow CLI, from allowing users to BYO-vault to providing a local browser-based UI for executing workflows and discovering what&rsquo;s on your machine.</p>
<p>flow is a reflection of my passion for crafting tools that make developers&rsquo; lives easier. What started as a personal project has grown into something I believe can help other developers take control of their development experience. I invite you to <a href="https://flowexec.io/#/development">contribute</a>, star the <a href="https://github.com/jahvon/flow">repo</a> to show your support, and to open issues to report bugs or suggest features!</p>
]]></content:encoded>
    </item>
    <item>
      <title>About Me</title>
      <link>https://jahvon.dev/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/about/</guid>
      <description>about</description>
      <content:encoded><![CDATA[<p>👋🏾 Hey there! I&rsquo;m Jahvon, a software and platform engineer based in Boston with 8+ years building resilient systems and leading cross-functional teams. My work tends to cluster around developer tooling and cloud-native systems, but I follow my curiosity into a lot of different spaces. That has taken me through teaching, community work, and more than a few projects I couldn&rsquo;t fully explain when I started them.</p>
<p>I build things to understand them. This site is a record of what I&rsquo;m working on, what I&rsquo;m figuring out, and occasionally what I got wrong.</p>



  
    <div class="latest-post-box ">
      Check out my latest lab note: <a href="https://jahvon.dev/notes/rollout-vllm/">Nowhere to Put the Canary: Argo Rollouts &#43; vLLM</a>
    </div>
  


<h3 id="where-ive-been">Where I&rsquo;ve Been</h3>
<ul>
<li>
<p><strong>Founding Engineer at Klipster (2025 - now)</strong></p>
<p>Building an AI-enriched video studio and automation platform for an early-stage Proptech startup, on Cloudflare Workers, Containers, D1, R2, and Queues.</p>
</li>
<li>
<p><strong><a href="https://labs.jahvon.dev">Dockery Labs</a> (2025 - now)</strong></p>
<p>My personal R&amp;D space for building and experimenting with new ideas.</p>
</li>
<li>
<p><strong>Teaching Fellow at <a href="https://www.gse.harvard.edu/">Harvard Graduate School of Education</a> (2025)</strong></p>
<p>Teaching and learning through &ldquo;Vibe Coding,&rdquo; a module exploring generative AI as a creative partner in programming.</p>
</li>
<li>
<p><strong><a href="https://www.recurse.com/scout/click?t=420ecb9ef5810758f6fe8dec816d80a8">Recurse Center</a> (2025)</strong></p>
<p>Took a sabbatical to deepen my coding practice across multiple languages and frameworks, with a focus on <a href="https://jahvon.dev/tags/flow/">flow</a> - my open-source developer automation platform.</p>
</li>
<li>
<p><strong>Principal Software Engineer at <a href="https://www.cargurus.com">CarGurus</a> (2022-2025)</strong></p>
<p>Built Kubernetes controllers, deployment pipelines, and developer tools (like <a href="https://www.cargurus.dev/How-CarGurus-is-supercharging-our-microservice-developer-experience/">Mach5</a>) used by engineering teams across the entire product development organization.</p>
</li>
<li>
<p><strong>Kubernetes Networking at <a href="https://www.solo.io/">solo.io</a> (2021-2022)</strong></p>
<p>Worked on open-source release pipelines and Kubernetes controllers for Istio service mesh and Envoy-based API gateway technologies.</p>
</li>
<li>
<p><strong>EdTech Data Platform at <a href="https://www.panoramaed.com/">Panorama Education</a> (2018-2021)</strong></p>
<p>Built and optimized ETL pipelines that transform school data into actionable insights for improving student outcomes.</p>
</li>
</ul>
<h3 id="also">Also</h3>
<p>Certified Kubernetes Application Developer (CKAD), 2024. B.S. in Computer Science and Business
Administration from the University of Pittsburgh.</p>
<p>You can also find me on <a href="https://www.linkedin.com/in/jahvon/">LinkedIn</a> and <a href="https://github.com/jahvon">GitHub</a>.</p>
<div class="about-buttons">
  <a href="https://jahvon.dev/notes" class="about-btn">
    <span class="btn-text">Lab Notes</span>
  </a>
  <div class="icon">
    <center><img src="https://jahvon.dev/icon.png" alt="Icon" width="150" height="150"></center>
  </div>
  <a href="https://jahvon.dev/architecture" class="about-btn">
    <span class="btn-text">Architecture</span>
  </a>
</div>
]]></content:encoded>
    </item>
  </channel>
</rss>
