<?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>Flow on Jahvon Dockery</title>
    <link>https://jahvon.dev/architecture/flow/</link>
    <description>Recent content in Flow 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>
    <atom:link href="https://jahvon.dev/architecture/flow/index.xml" rel="self" type="application/rss+xml" />
    <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>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>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>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>
  </channel>
</rss>
