<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Tartrau</title>
    <description>The latest articles on DEV Community by Thomas Tartrau (@thomastartrau).</description>
    <link>https://dev.to/thomastartrau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121170%2F457058e8-1ea7-405b-9521-f490a6fcd86f.png</url>
      <title>DEV Community: Thomas Tartrau</title>
      <link>https://dev.to/thomastartrau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomastartrau"/>
    <language>en</language>
    <item>
      <title>JSON vs Programmatic Tool Calling with Claude</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:54:27 +0000</pubDate>
      <link>https://dev.to/thomastartrau/json-vs-programmatic-tool-calling-with-claude-17i4</link>
      <guid>https://dev.to/thomastartrau/json-vs-programmatic-tool-calling-with-claude-17i4</guid>
      <description>&lt;p&gt;When building an application with the Claude API, connecting tools is the first step toward a functional agent. The model can call functions, query databases, send messages. But how you wire those tools drastically changes performance, cost, and code complexity.&lt;/p&gt;

&lt;p&gt;In 2026, the Claude API offers three distinct approaches. This article compares them with code, numbers, and practical experience from using all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: round trips
&lt;/h2&gt;

&lt;p&gt;Traditional tool calling works in a loop: the model requests a tool, your server executes it, sends back the result, the model reasons over the result, requests another tool, and so on. Each iteration is a full round trip with the model.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;The problem becomes concrete as the number of tools grows. Checking expenses for 20 employees? 20 round trips. Every intermediate result piles into the context. According to Anthropic's measurements, tool definitions alone can consume over &lt;a href="https://www.anthropic.com/engineering/advanced-tool-use" rel="noopener noreferrer"&gt;134,000 tokens before any conversation&lt;/a&gt; in a multi-server setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: JSON schema + manual loop
&lt;/h2&gt;

&lt;p&gt;This is the original approach. You define each tool with a JSON schema, send the request to the API, parse the &lt;code&gt;tool_use&lt;/code&gt; response, execute the tool, and send back the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Execute a SQL query. Returns rows as JSON.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SQL query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the revenue by region?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}]&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;while True&lt;/code&gt; loop is the critical part. You handle the request/response cycle, parse &lt;code&gt;tool_use&lt;/code&gt; blocks, accumulate messages, and manage error cases yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: full control over every step. You can log, filter results, inject business logic between calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: lots of boilerplate. Every tool result passes through the model, even if it's just an intermediate value. With 10 tool calls, that's 10 full inference passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Tool Runner SDK
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner" rel="noopener noreferrer"&gt;Tool Runner&lt;/a&gt; is a helper in the official Anthropic SDKs (Python, TypeScript, Go, Java, C#, PHP, Ruby). It automates the agentic loop: tool definition, execution, conversation state management, and type validation.&lt;/p&gt;

&lt;p&gt;In Python, the &lt;code&gt;@beta_tool&lt;/code&gt; decorator generates the JSON schema from type hints and the docstring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta_tool&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@beta_tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Execute a SQL query on the sales database.

    Args:
        sql: SQL query to execute
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query_database&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the revenue by region?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In TypeScript, two options: &lt;code&gt;betaZodTool&lt;/code&gt; with Zod validation (recommended), or &lt;code&gt;betaTool&lt;/code&gt; with a plain JSON schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;



&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryDatabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betaZodTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query_database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Execute a SQL query on the sales database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SQL query to execute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toolRunner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;queryDatabase&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What's the revenue by region?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Tool Runner handles the loop automatically: when Claude requests a tool, the runner executes it and sends back the result. No more &lt;code&gt;while True&lt;/code&gt;, no manual &lt;code&gt;tool_use&lt;/code&gt; block parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: less code, type safety with Zod or Python type hints, built-in error handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: same execution model as the manual approach under the hood. Each tool call is still a round trip with the model. The gain is in DX, not performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: programmatic tool calling
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling" rel="noopener noreferrer"&gt;Programmatic tool calling&lt;/a&gt; changes the execution model. Instead of requesting one tool at a time through the API, Claude writes Python code that calls your tools directly inside a code execution container. Intermediate results stay in the container and only enter the model's context when Claude explicitly sends them.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;To enable this approach, you need two things: include the &lt;code&gt;code_execution&lt;/code&gt; tool in the request, and add &lt;code&gt;allowed_callers&lt;/code&gt; to the tools Claude can call from code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Compare revenue across the West, East, and Central regions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_execution_20260120&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_execution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Execute a SQL query. Returns rows as JSON.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SQL query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_callers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_execution_20260120&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude then generates a Python script that calls &lt;code&gt;query_database&lt;/code&gt; in a loop or in parallel via &lt;code&gt;asyncio.gather&lt;/code&gt;, filters the results, and only sends back the summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;West&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;East&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Central&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;query_database&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT SUM(revenue) as total FROM sales WHERE region = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Highest revenue region: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; USD)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Details: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of 3 model round trips, a single inference produced the code, and all 3 tool calls execute inside the container. The model only receives the final &lt;code&gt;print()&lt;/code&gt; output.&lt;/p&gt;

&lt;p&gt;Tools are exposed as async Python functions. The &lt;code&gt;allowed_callers&lt;/code&gt; field controls who can call each tool:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;["direct"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard API call (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;["code_execution_20260120"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Call only from code execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;["direct", "code_execution_20260120"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both modes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anthropic recommends choosing a single mode per tool to avoid ambiguity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: massive token and latency reduction. BrowseComp and DeepSearchQA benchmarks show &lt;a href="https://www.anthropic.com/engineering/advanced-tool-use" rel="noopener noreferrer"&gt;+11% performance and -24% input tokens&lt;/a&gt;. On complex research tasks, average usage drops from 43,588 to 27,297 tokens, a &lt;a href="https://www.anthropic.com/engineering/advanced-tool-use" rel="noopener noreferrer"&gt;37% reduction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: requires the &lt;code&gt;code_execution&lt;/code&gt; tool (beta). Containers have a limited lifetime (~5 minutes idle). Not available on Amazon Bedrock or Google Cloud. The field is not a security boundary: &lt;code&gt;allowed_callers&lt;/code&gt; guides Claude but does not strictly block direct calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;JSON + loop&lt;/th&gt;
&lt;th&gt;Tool Runner SDK&lt;/th&gt;
&lt;th&gt;Programmatic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inferences per N tools&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1 + returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate tokens&lt;/td&gt;
&lt;td&gt;In context&lt;/td&gt;
&lt;td&gt;In context&lt;/td&gt;
&lt;td&gt;In container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code complexity&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-step control&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Via generated code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;N x inference&lt;/td&gt;
&lt;td&gt;N x inference&lt;/td&gt;
&lt;td&gt;1 inference + exec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Zod / type hints&lt;/td&gt;
&lt;td&gt;N/A (generated code)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;GA, all clouds&lt;/td&gt;
&lt;td&gt;Beta, all clouds&lt;/td&gt;
&lt;td&gt;Beta, direct API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSON + manual loop&lt;/strong&gt; when you need fine-grained control between each tool call. For example: a workflow with human validation between steps, detailed logging, or conditional business logic. It's also the only option if you target Bedrock or Google Cloud without the Tool Runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool Runner SDK&lt;/strong&gt; for most use cases. The code is cleaner, type validation is automatic, and the behavior is identical to the manual loop under the hood. This is the approach I use by default in my TypeScript and Python projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Programmatic tool calling&lt;/strong&gt; when the number of tool calls per request is high or intermediate results are large. The typical example: aggregating data from 20 sources, filtering, and only returning the summary. It's also the most efficient approach for research tasks where Claude needs to explore and sort before concluding.&lt;/p&gt;

&lt;p&gt;You can combine approaches. A tool marked &lt;code&gt;["direct"]&lt;/code&gt; will be called through the standard loop, while another marked &lt;code&gt;["code_execution_20260120"]&lt;/code&gt; will be orchestrated by the code. In the same project.&lt;/p&gt;

&lt;h2&gt;
  
  
  In practice
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/blog/en/automate-code-review-ai-coderift"&gt;CodeRift&lt;/a&gt;, I use the Tool Runner for review agents: each agent has 2-3 tools (read a file, search symbols, post a comment) and the number of calls is predictable. The Tool Runner eliminates boilerplate without sacrificing visibility.&lt;/p&gt;

&lt;p&gt;For data exploration tasks in my &lt;a href="https://dev.to/blog/en/why-rust-workflow-engine-ironflow"&gt;IronFlow&lt;/a&gt; workflows, programmatic tool calling would be the right choice: an agent scanning 50 GitLab projects, filtering open MRs, and aggregating stats. The heavy lifting happens in the container, and only the summary enters the context.&lt;/p&gt;

&lt;p&gt;The manual JSON approach stays for cases where the Tool Runner isn't available (unsupported SDK, or integration with a third-party framework that manages its own loop).&lt;/p&gt;

&lt;p&gt;The choice isn't permanent. Start with the Tool Runner, migrate to programmatic when the token bill justifies it.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Automated code review with CodeRift</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:54:23 +0000</pubDate>
      <link>https://dev.to/thomastartrau/automated-code-review-with-coderift-49g9</link>
      <guid>https://dev.to/thomastartrau/automated-code-review-with-coderift-49g9</guid>
      <description>&lt;p&gt;Code review is the bottleneck of most teams. The merge request is open, the author waits, the reviewer is busy with something else. When the review finally comes, it's often superficial: a quick glance at the diff, a "LGTM", merge. Logic bugs, security flaws and architecture violations slip through.&lt;/p&gt;

&lt;p&gt;I built CodeRift to solve this problem. It's an automated code review platform that analyzes every GitLab merge request with a 7-step pipeline: diff parsing, AST analysis via tree-sitter, TOML rule engine, parallel specialized AI agents, cross-file correlation, false positive validation, and inline comment publishing directly in the MR. The whole thing is orchestrated by &lt;a href="https://dev.to/blog/en/why-rust-workflow-engine-ironflow"&gt;IronFlow&lt;/a&gt;, my Rust workflow engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;CodeRift follows an API + Workers model. The API (Rust/Axum/SQLx) handles persistence, GitLab OAuth authentication and webhooks. IronFlow workers poll the API for pending reviews, execute them, and post results.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;The API/Worker separation is the same as &lt;a href="https://dev.to/blog/en/why-rust-workflow-engine-ironflow"&gt;IronFlow&lt;/a&gt;: the API owns persistence and never executes. Scaling means launching more workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7-step review pipeline
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;review-merge-request&lt;/code&gt; workflow implements IronFlow's &lt;code&gt;WorkflowHandler&lt;/code&gt; trait. Each step is an IronFlow operation with automatic retry and structured logging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps 1-3: Preparation
&lt;/h3&gt;

&lt;p&gt;The worker updates the review status in the database, posts a "review in progress" note on the GitLab MR, and sets the commit status to &lt;code&gt;running&lt;/code&gt;. The developer immediately sees that the AI review has started.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.operation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update-review-status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;update_status_op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"post-review-started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;HttpConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;notes_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PRIVATE-TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="py"&gt;.gitlab.bot_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;review_started_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nn"&gt;commit_status&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"set-commit-status-running"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;proj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="py"&gt;.head_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="py"&gt;.review_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"running"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"AI review in progress..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: The review pipeline (the core)
&lt;/h3&gt;

&lt;p&gt;This is where the work happens. The pipeline follows this sequence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOML rules&lt;/strong&gt;: CodeRift loads a set of rules embedded at compile time (&lt;code&gt;include_dir!&lt;/code&gt;). Each rule targets a language, file patterns, and defines a regex pattern with exceptions. For example, the &lt;code&gt;rust-unwrap&lt;/code&gt; rule detects &lt;code&gt;.unwrap()&lt;/code&gt; in production code while excluding test files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[rules]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"rust-unwrap"&lt;/span&gt;
&lt;span class="py"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error_handling"&lt;/span&gt;
&lt;span class="py"&gt;score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;".unwrap() in production code"&lt;/span&gt;
&lt;span class="py"&gt;languages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rust"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;file_patterns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"*.rs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;exclude_patterns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"*_test.rs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tests/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"regex"&lt;/span&gt;
&lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'\.unwrap\(\)'&lt;/span&gt;
&lt;span class="py"&gt;negative_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'#\[test\]|#\[cfg\(test\)\]|mod tests'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules cover Rust, TypeScript, Python, Go, SQL, and common patterns (OWASP, prompt injection). Each project can add its own rules or disable server rules via a &lt;code&gt;.coderift/context.md&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AST analysis&lt;/strong&gt;: tree-sitter parses the modified files and extracts a symbol index (definitions, references, cross-file edges). This index serves two purposes: building a dependency graph to group related files into the same chunks, and detecting structural patterns that regex can't see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunking&lt;/strong&gt;: files are grouped into chunks of 5 (configurable) for parallel review. When the dependency graph is available, related files stay in the same chunk to give the agent context on both sides of an interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specialized AI agents&lt;/strong&gt;: each chunk is reviewed by 3 agents in parallel, each with a different focus:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Opus 4.6&lt;/td&gt;
&lt;td&gt;Injections, XSS, SSRF, access control, secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bugs&lt;/td&gt;
&lt;td&gt;Opus 4.6&lt;/td&gt;
&lt;td&gt;Incorrect logic, edge cases, type errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;N+1, unnecessary allocations, algorithmic complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each agent has a configurable USD budget and a maximum of 4 turns. The IronFlow provider manages execution and cost tracking. If an agent fails (schema error, timeout), the pipeline continues with results from the others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.system_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OPUS_46&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.max_turns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.max_budget_usd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="py"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FindingsOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cross-file correlation&lt;/strong&gt;: a Sonnet 4.6 agent receives all findings from all chunks and identifies vulnerabilities that span multiple files. For example, an unvalidated input in a handler that reaches a SQL query in another file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False positive validation&lt;/strong&gt;: a final agent filters out irrelevant findings. Common false positives: an &lt;code&gt;.unwrap()&lt;/code&gt; in test code, a pattern flagged in a comment, a finding targeting unchanged code, or a score inflated relative to actual risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps 5-6: Publishing
&lt;/h3&gt;

&lt;p&gt;The worker posts results to the GitLab MR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;summary&lt;/strong&gt; with a walkthrough of the MR, files grouped by functional area, Mermaid sequence diagrams for non-trivial flows, and pre-merge checks (security, error handling, tests).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline comments&lt;/strong&gt; on the relevant lines, with severity (Bug, Security, Performance, Error Handling, Suggestion), a criticality score (Critical/Major/Moderate/Minor), and a fix suggestion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 7: Commit status
&lt;/h3&gt;

&lt;p&gt;The GitLab commit status changes to &lt;code&gt;success&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. If the review has Critical findings, the pipeline can block the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;Each finding has a precise structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Finding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;end_line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Bug | Security | Performance | ErrorHandling | Suggestion&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// 0-10, maps to Critical/Major/Moderate/Minor&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;analysis_chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;ai_fix_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;analysis_chain&lt;/code&gt; field contains the agent's step-by-step reasoning. The &lt;code&gt;ai_fix_prompt&lt;/code&gt; field is a ready-to-use prompt to automatically fix the finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just call Claude Code?
&lt;/h2&gt;

&lt;p&gt;The first version of this system used &lt;a href="https://dev.to/projects/n8n-claude-code"&gt;an n8n node that invoked Claude Code CLI&lt;/a&gt;. It worked for small MRs, but the limitations showed up fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No structured context&lt;/strong&gt;: Claude saw a raw diff without understanding the relationships between files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rules&lt;/strong&gt;: impossible to reliably encode project conventions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No deduplication&lt;/strong&gt;: the same findings came back in duplicates or triples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cost control&lt;/strong&gt;: a 2000-line diff could cost $15&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation&lt;/strong&gt;: no filter on false positives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CodeRift solves each of these problems. The structured pipeline (rules + AST + specialized agents + validation) produces more accurate findings than the "send the entire diff to an LLM and hope" approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-project configuration
&lt;/h2&gt;

&lt;p&gt;Each project can customize the review via &lt;code&gt;.coderift/context.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Architecture&lt;/span&gt;

4 strict layers: rest, services, repositories, entities.
Violations to flag:
&lt;span class="p"&gt;-&lt;/span&gt; rest importing repositories directly
&lt;span class="p"&gt;-&lt;/span&gt; services doing raw SQL
&lt;span class="p"&gt;-&lt;/span&gt; repositories containing business logic

&lt;span class="gh"&gt;# Frontend&lt;/span&gt;

API types in app/types/generated.ts are auto-generated.
Never manually redefine a type that exists in this file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is injected into agent prompts as untrusted context (sandboxed, with no ability to alter review instructions). Agents use it to understand project conventions and flag specific violations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;CodeRift is built with the ecosystem I developed: &lt;a href="https://dev.to/blog/en/why-rust-workflow-engine-ironflow"&gt;IronFlow&lt;/a&gt; for workflow orchestration, the same layered architecture (entities/repositories/services/rest) as &lt;a href="https://dev.to/projects/netir"&gt;Netir&lt;/a&gt;, and &lt;a href="https://dev.to/blog/en/mcp-rtk-reduce-token-usage-mcp-servers"&gt;MCP RTK&lt;/a&gt; optionally to reduce tokens on connected MCP servers. The &lt;a href="https://dev.to/blog/en/claude-code-setup-2026"&gt;Claude Code setup&lt;/a&gt; and &lt;a href="https://dev.to/blog/en/writing-effective-claude-code-skills"&gt;skills&lt;/a&gt; I use daily served as the foundation for structuring CodeRift's review prompts.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>codereview</category>
      <category>devops</category>
    </item>
    <item>
      <title>Layered Architecture: Rust API with Axum, SQLx</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:54:18 +0000</pubDate>
      <link>https://dev.to/thomastartrau/layered-architecture-rust-api-with-axum-sqlx-m9h</link>
      <guid>https://dev.to/thomastartrau/layered-architecture-rust-api-with-axum-sqlx-m9h</guid>
      <description>&lt;p&gt;Most Rust/Axum APIs I see on GitHub put everything in the same file: the handler extracts parameters, runs the SQL query, applies business logic, and returns JSON. That works for a demo project. On a production API with 30+ endpoints, state machines, SSE, JWT auth, and a worker lease system, it falls apart.&lt;/p&gt;

&lt;p&gt;I structured &lt;a href="https://dev.to/projects/ironflow"&gt;IronFlow&lt;/a&gt; - a workflow engine where workflows are Rust code (see &lt;a href="https://dev.to/blog/en/why-rust-workflow-engine-ironflow"&gt;why I chose Rust for this project&lt;/a&gt;) - into strict layers spread across a 20-crate Cargo workspace. Dependencies only go in one direction: downward. A REST handler cannot call a SQL query directly, and an entity has no idea Axum exists.&lt;/p&gt;

&lt;p&gt;This article shows this architecture with real code, the decisions that worked, and the ones I would reconsider.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workspace structure
&lt;/h2&gt;

&lt;p&gt;The Cargo workspace contains 20 crates. The main layers are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ironflow-store/         # Entities + data access (traits + implementations)
  src/entities/         # Structs, enums, FSM
  src/postgres/         # PostgreSQL implementation (SQLx)
  src/memory/           # In-memory implementation (tests/dev)
ironflow-engine/        # Orchestration, execution, events
ironflow-api/           # Axum handlers, DTOs, middleware, OpenAPI
  src/entities/         # API DTOs (separate from store entities)
  src/routes/           # Handlers by domain
ironflow-core/          # AI providers, shell/HTTP/agent operations
ironflow-auth/          # JWT, authentication extractors
ironflow-types/         # Shared types (JSON envelopes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer is a separate crate in the workspace. The &lt;code&gt;ironflow-api&lt;/code&gt; crate cannot access &lt;code&gt;sqlx&lt;/code&gt; directly: it goes through the &lt;code&gt;Store&lt;/code&gt; trait defined in &lt;code&gt;ironflow-store&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;/p&gt;



&lt;h2&gt;
  
  
  Entities: the domain without a framework
&lt;/h2&gt;

&lt;p&gt;The entities layer lives in &lt;code&gt;ironflow-store/src/entities/&lt;/code&gt;. It defines domain types without depending on Axum or SQLx for queries. One file per concept: &lt;code&gt;run.rs&lt;/code&gt;, &lt;code&gt;step.rs&lt;/code&gt;, &lt;code&gt;run_status.rs&lt;/code&gt;, &lt;code&gt;step_status.rs&lt;/code&gt;, &lt;code&gt;trigger_kind.rs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The FSM in the type system
&lt;/h3&gt;

&lt;p&gt;The core of IronFlow is a finite state machine (FSM) that manages the lifecycle of each run. Valid transitions are defined in an exhaustive &lt;code&gt;matches!&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"snake_case"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Retrying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AwaitingApproval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;can_transition_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.is_terminal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// idempotent&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nd"&gt;matches!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// lease expired&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Retrying&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AwaitingApproval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Retrying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Retrying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Retrying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AwaitingApproval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AwaitingApproval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AwaitingApproval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_terminal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;matches!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Completed&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;
                &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;matches!&lt;/code&gt; macro makes the transition table readable at a glance. Adding a transition means adding a line. Removing a state triggers a compiler warning everywhere it is used.&lt;/p&gt;

&lt;p&gt;An important detail: terminal-to-same-terminal transitions are idempotent. A run that is already &lt;code&gt;Failed&lt;/code&gt; receiving &lt;code&gt;Failed&lt;/code&gt; is not an error. This simplifies concurrency scenarios between workers.&lt;/p&gt;

&lt;h3&gt;
  
  
  The generic &lt;code&gt;FsmState&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For SQL-side transitions (via the &lt;code&gt;lib_fsm&lt;/code&gt; library), IronFlow wraps the status with the state machine ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;FsmState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Clone&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Copy&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;state_machine_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handlers pattern-match on &lt;code&gt;run.status.state&lt;/code&gt;, while SQL queries use &lt;code&gt;run.status.state_machine_id&lt;/code&gt; for atomic transitions. A single type carries both pieces of information.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;Run&lt;/code&gt; entity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;workflow_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FsmState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TriggerKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;retry_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;completed_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;scheduled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;created_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RunActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Run&lt;/code&gt; is the store's internal model. The API never exposes it directly - it uses a &lt;code&gt;RunResponse&lt;/code&gt; (DTO) that controls what goes out. IDs are UUID v7 (chronologically sorted, good for B-tree index performance).&lt;/p&gt;

&lt;h2&gt;
  
  
  The store: traits and two implementations
&lt;/h2&gt;

&lt;p&gt;The store layer defines async traits for data access, with two implementations: &lt;code&gt;PostgresStore&lt;/code&gt; for production and &lt;code&gt;InMemoryStore&lt;/code&gt; for tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;RunStore&lt;/code&gt; trait
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;RunStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;create_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NewRun&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RunCreation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;list_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunFilter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;update_run_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;pick_next_pending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LeaseRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;reap_expired_leases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReapedRun&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ... create_step, update_step, list_steps, get_stats, delete_run&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StoreFuture&amp;lt;'a, T&amp;gt;&lt;/code&gt; is a &lt;code&gt;Pin&amp;lt;Box&amp;lt;dyn Future&amp;lt;Output = Result&amp;lt;T, StoreError&amp;gt;&amp;gt; + Send + 'a&amp;gt;&amp;gt;&lt;/code&gt; - needed for object safety so the store can be used as &lt;code&gt;Arc&amp;lt;dyn RunStore&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Store&lt;/code&gt; trait unifies all capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;RunStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UserStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ApiKeyStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;SecretStore&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AuditLogStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ArtifactStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;LogStore&lt;/span&gt;
&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UserStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ApiKeyStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;SecretStore&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AuditLogStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ArtifactStore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;LogStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blanket impl means any type that implements all 7 sub-traits is automatically a &lt;code&gt;Store&lt;/code&gt;. Both &lt;code&gt;InMemoryStore&lt;/code&gt; and &lt;code&gt;PostgresStore&lt;/code&gt; implement all 7.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two interchangeable backends
&lt;/h3&gt;

&lt;p&gt;The PostgreSQL implementation uses &lt;code&gt;SELECT FOR UPDATE SKIP LOCKED&lt;/code&gt; for concurrent run picking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;RunStore&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;PostgresStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;pick_next_pending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LeaseRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StoreFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// SELECT FOR UPDATE SKIP LOCKED inside a transaction&lt;/span&gt;
            &lt;span class="c1"&gt;// Atomic transition Pending -&amp;gt; Running&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The in-memory implementation uses an &lt;code&gt;RwLock&lt;/code&gt; and a sorted &lt;code&gt;Vec&lt;/code&gt;. Same API, same behavior, zero PostgreSQL required for tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InMemoryStore&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="nf"&gt;.create_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewRun&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;workflow_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"deploy"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;TriggerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Manual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt;
    &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.into_run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ironflow-api&lt;/code&gt; crate tests use &lt;code&gt;InMemoryStore&lt;/code&gt;. No Docker, no migrations, no cleanup between tests. The &lt;code&gt;PostgresStore&lt;/code&gt; has its own integration tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store error handling
&lt;/h3&gt;

&lt;p&gt;Store errors are storage errors, not HTTP errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Error)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;StoreError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"run not found: {0}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;RunNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"step not found: {0}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;StepNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"invalid status transition: {from} -&amp;gt; {to}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;InvalidTransition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"lease lost on run {run_id}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;LeaseLost&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"artifact {name:?} already exists on step {step_id}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;DuplicateArtifact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;step_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"database error: {0}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conversion to HTTP status codes happens in the API crate, not here. The store has no concept of a &lt;code&gt;StatusCode&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API layer: Axum handlers and DTOs
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ironflow-api&lt;/code&gt; crate assembles everything. It contains Axum handlers, response DTOs (separate from store entities), middleware, and OpenAPI documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  DTOs separate from entities
&lt;/h3&gt;

&lt;p&gt;The API crate defines its own response types in &lt;code&gt;src/entities/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ironflow-api/src/entities/run.rs - API DTO&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;RunResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;workflow_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TriggerKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;created_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="nb"&gt;From&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;RunResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Explicit conversion, controls what goes out&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RunResponse&lt;/code&gt; is the public contract. The store's &lt;code&gt;Run&lt;/code&gt; is the internal model. The &lt;code&gt;From&amp;lt;Run&amp;gt;&lt;/code&gt; conversion is the control point: you choose what is exposed and how.&lt;/p&gt;

&lt;h3&gt;
  
  
  A typical handler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[utoipa::path(&lt;/span&gt;
    &lt;span class="nd"&gt;get,&lt;/span&gt;
    &lt;span class="nd"&gt;path&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/api/v1/runs/{id}"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;tags&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"runs"&lt;/span&gt;&lt;span class="nd"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Run ID"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nf"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RunDetailResponse&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;security&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;IntoResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="nf"&gt;.get_run_or_404&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;join!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.store&lt;/span&gt;&lt;span class="nf"&gt;.list_steps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.store&lt;/span&gt;&lt;span class="nf"&gt;.list_step_dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.store&lt;/span&gt;&lt;span class="nf"&gt;.list_artifacts_for_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RunDetailResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;RunResponse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StepResponse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler does 3 things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify authentication (&lt;code&gt;Authenticated&lt;/code&gt; extractor)&lt;/li&gt;
&lt;li&gt;Fetch data in parallel via &lt;code&gt;tokio::join!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Convert to DTOs and return&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; converts &lt;code&gt;StoreError&lt;/code&gt; to &lt;code&gt;ApiError&lt;/code&gt; via the &lt;code&gt;From&lt;/code&gt; impl. Tracing is automatic. OpenAPI docs are generated by &lt;code&gt;#[utoipa::path]&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error conversion
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;StoreError&lt;/code&gt; converts automatically to &lt;code&gt;ApiError&lt;/code&gt; via &lt;code&gt;#[from]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Error)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ApiError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"run not found"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;RunNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"authentication required"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"invalid credentials"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;InvalidCredentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"{0}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;Conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"database error"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;#[from]&lt;/span&gt; &lt;span class="n"&gt;StoreError&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;IntoResponse&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ApiError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;into_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;RunNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Unauthorized&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StoreError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LeaseLost&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CONFLICT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;INTERNAL_SERVER_ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ErrorEnvelope&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.code&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;&lt;span class="nf"&gt;.into_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;StoreError&lt;/code&gt; is translated to a precise HTTP code. A &lt;code&gt;LeaseLost&lt;/code&gt; is a 409 Conflict (the client can retry), not a 500. A &lt;code&gt;RunNotFound&lt;/code&gt; is a 404. The store does not decide the HTTP code, the API does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Router assembly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;create_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouterConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Router&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;internal_routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs/next"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pick_next_run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs/{id}/status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_run_status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs/{id}/lease"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renew_lease&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;from_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_token_auth&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;api_v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list_runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;create_run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs/{id}/cancel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancel_run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/runs/{id}/approve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approve_run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/workflows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list_workflows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/stats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_stats&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.nest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;internal_routes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.nest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_v1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RequestBodyLimitLayer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;from_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security_headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate route groups: internal routes (worker-to-API, protected by a dedicated token) and public routes (JWT authentication). Internal routes use &lt;code&gt;worker_token_auth&lt;/code&gt;, public routes use &lt;code&gt;Authenticated&lt;/code&gt;. Both go through the same &lt;code&gt;AppState&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  AppState: dependency injection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Clone)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;AppState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;jwt_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JwtConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;worker_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Arc&amp;lt;dyn Store&amp;gt;&lt;/code&gt; is the injection point. In production, it is a &lt;code&gt;PostgresStore&lt;/code&gt;. In tests, it is an &lt;code&gt;InMemoryStore&lt;/code&gt;. The handler does not know which one it uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests: the concrete benefit of the architecture
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;get_run&lt;/code&gt; handler tests illustrate the benefit of traits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;existing_run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InMemoryStore&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="nf"&gt;.create_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewRun&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;workflow_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;TriggerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Manual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt;
        &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into_run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;test_state_with_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.with_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.oneshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="py"&gt;.id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="nf"&gt;.status&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Docker, no database, no migrations. The test instantiates an &lt;code&gt;InMemoryStore&lt;/code&gt;, creates a run, and verifies the handler returns 200. Execution takes a few milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Store traits.&lt;/strong&gt; Two interchangeable implementations (&lt;code&gt;PostgresStore&lt;/code&gt; and &lt;code&gt;InMemoryStore&lt;/code&gt;) simplify testing and allow starting the project without a database. The &lt;code&gt;Arc&amp;lt;dyn Store&amp;gt;&lt;/code&gt; in &lt;code&gt;AppState&lt;/code&gt; makes injection transparent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DTOs separate from entities.&lt;/strong&gt; The API's &lt;code&gt;RunResponse&lt;/code&gt; and the store's &lt;code&gt;Run&lt;/code&gt; are distinct types. Modifying the internal model does not break the API contract. The &lt;code&gt;From&amp;lt;Run&amp;gt;&lt;/code&gt; conversion is the single control point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End-to-end typed errors.&lt;/strong&gt; From &lt;code&gt;StoreError&lt;/code&gt; to &lt;code&gt;ApiError&lt;/code&gt;, every conversion is explicit. The &lt;code&gt;matches!&lt;/code&gt; in &lt;code&gt;IntoResponse&lt;/code&gt; documents the error-to-HTTP-code mapping. No hidden &lt;code&gt;.unwrap()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated OpenAPI documentation.&lt;/strong&gt; &lt;code&gt;utoipa&lt;/code&gt; annotates handlers and types. The spec is always in sync with the code. The embedded dashboard consumes this spec directly.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The missing service layer.&lt;/strong&gt; Today, business logic lives in handlers (for simple cases) or in the engine (for orchestration). An explicit &lt;code&gt;ironflow-services&lt;/code&gt; crate with status transition logic, cost limit validation, and event construction would make handlers thinner and tests more targeted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boxed &lt;code&gt;StoreFuture&lt;/code&gt;s.&lt;/strong&gt; The &lt;code&gt;Pin&amp;lt;Box&amp;lt;dyn Future&amp;gt;&amp;gt;&lt;/code&gt; is needed for object safety of &lt;code&gt;dyn RunStore&lt;/code&gt;, but adds one allocation per call. For non-dynamic usage (when the concrete type is known), direct async methods would be more performant. This is the classic flexibility vs. performance tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;The IronFlow workspace:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crates in the workspace&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST endpoints (public + internal)&lt;/td&gt;
&lt;td&gt;30+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of Rust code&lt;/td&gt;
&lt;td&gt;~25,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;td&gt;150+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store backends&lt;/td&gt;
&lt;td&gt;2 (PostgreSQL + in-memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported AI providers&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The project is open source on &lt;a href="https://gitlab.com/ThomasTartrau/ironflow" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. The release profile uses &lt;code&gt;lto = true&lt;/code&gt;, &lt;code&gt;strip = true&lt;/code&gt;, &lt;code&gt;codegen-units = 1&lt;/code&gt;, and &lt;code&gt;panic = "abort"&lt;/code&gt;. The resulting binary is compact and starts in under a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Layered architecture is not an invention. It is a classic pattern from Java/C#/.NET. What is specific to Rust is that the type system and Cargo workspaces make this separation &lt;strong&gt;enforced by the compiler&lt;/strong&gt;, not by convention. A handler that tries to import &lt;code&gt;sqlx&lt;/code&gt; directly will not compile if the &lt;code&gt;ironflow-api&lt;/code&gt; crate does not list it in its dependencies.&lt;/p&gt;

&lt;p&gt;The key point of IronFlow: store traits with two implementations. &lt;code&gt;PostgresStore&lt;/code&gt; for production, &lt;code&gt;InMemoryStore&lt;/code&gt; for tests. This is what makes handler tests fast and reliable without external infrastructure.&lt;/p&gt;

&lt;p&gt;The cost is real: more crates, more &lt;code&gt;From&lt;/code&gt; impls, more boilerplate for error conversions. But on a project with concurrent workers, leases, and state machines, the maintainability gains easily justify the investment.&lt;/p&gt;

&lt;p&gt;If you are starting a Rust API with Axum, begin by separating entities from the rest. Add a store trait when you want to test without a database. Add DTOs when the internal model diverges from the API contract. And split into crates when compilation times or responsibility boundaries justify it.&lt;/p&gt;

&lt;p&gt;To see how this architecture supports real use cases, read &lt;a href="https://dev.to/blog/en/automate-code-review-ai-coderift"&gt;how IronFlow orchestrates AI agents for automated code review&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>api</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why I Chose Rust for a Workflow Engine</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:54:14 +0000</pubDate>
      <link>https://dev.to/thomastartrau/why-i-chose-rust-for-a-workflow-engine-4mk4</link>
      <guid>https://dev.to/thomastartrau/why-i-chose-rust-for-a-workflow-engine-4mk4</guid>
      <description>&lt;p&gt;Most workflow engines work the same way: define a step graph in a YAML file or DSL, let an engine interpret it, and hope the execution matches what you imagined. I used this approach for years with n8n, Airflow, and even Temporal in an earlier version, before hitting the limits of declarative definitions when business logic gets complex.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://gitlab.com/ThomasTartrau/ironflow" rel="noopener noreferrer"&gt;IronFlow&lt;/a&gt; to solve this. It's a workflow engine where workflows are imperative Rust code: no YAML, no DSL. The engine persists every step, tracks costs, and exposes everything through a REST API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with declarative workflows
&lt;/h2&gt;

&lt;p&gt;A YAML workflow file works for simple cases: A then B then C. The trouble starts when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested conditions&lt;/strong&gt;: if step 2 fails and step 1 produced a certain flag, skip step 3 but run step 4 with different parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional parallelism&lt;/strong&gt;: fan-out on N items, but some items need different processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granular error handling&lt;/strong&gt;: retry with backoff on some steps, fail-fast on others, structured logs for debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In YAML, these scenarios produce condition trees that become unreadable. You end up writing code in "hooks" or "scripts" embedded in the YAML, which is coding in a template language instead of a real one.&lt;/p&gt;

&lt;p&gt;Temporal solved this by offering workflows as code (Go, Java, TypeScript, Python). Their approach is right. But Temporal requires heavy infrastructure: a cluster with Cassandra or MySQL, a frontend service, a history service, a matching service. For a project that needs to orchestrate AI agents and shell commands, it's overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust and not Go or Node
&lt;/h2&gt;

&lt;p&gt;The language choice for a workflow engine isn't neutral. The engine is an infrastructure component that runs continuously, manages concurrency, and manipulates state machines. Here's what motivated the Rust choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The type system for state machines
&lt;/h3&gt;

&lt;p&gt;IronFlow's core is an FSM (finite state machine) managing each run's lifecycle. A run passes through precise states - &lt;code&gt;Pending&lt;/code&gt;, &lt;code&gt;Running&lt;/code&gt;, &lt;code&gt;AwaitingApproval&lt;/code&gt;, &lt;code&gt;Retrying&lt;/code&gt;, &lt;code&gt;Completed&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt;, &lt;code&gt;Cancelled&lt;/code&gt; - and transitions between these states are constrained.&lt;/p&gt;

&lt;p&gt;In Rust, this constraint is expressed in the type system. The FSM rejects invalid transitions &lt;strong&gt;at compile time&lt;/strong&gt;, not at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;RunEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PickedUp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AllStepsCompleted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StepFailed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StepFailedRetryable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RetryStarted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MaxRetriesExceeded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancelRequested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ApprovalRequested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Approved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Rejected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each event can only be applied from certain states. &lt;code&gt;Approved&lt;/code&gt; is only valid from &lt;code&gt;AwaitingApproval&lt;/code&gt;. &lt;code&gt;PickedUp&lt;/code&gt; is only valid from &lt;code&gt;Pending&lt;/code&gt;. The transition table is explicit in the code:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;In Go, this same logic would use &lt;code&gt;switch&lt;/code&gt; on &lt;code&gt;string&lt;/code&gt; or &lt;code&gt;int&lt;/code&gt;. An invalid transition error would only appear at runtime. In Node, you wouldn't even have guarantees on event types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tokio and zero-compromise parallelism
&lt;/h3&gt;

&lt;p&gt;A workflow engine must handle concurrency everywhere: multiple runs in parallel, concurrent steps within a single run, pending HTTP calls, workers polling the API. Tokio provides all of this with near-metal performance.&lt;/p&gt;

&lt;p&gt;IronFlow uses an API + workers model. The API owns persistence and never executes anything. Workers poll the API for pending runs, execute them locally, and stream steps and logs back. Scaling out means starting more workers.&lt;/p&gt;

&lt;p&gt;Concretely, a workflow can fan-out on parallel steps with &lt;code&gt;ctx.parallel()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;
    &lt;span class="nf"&gt;.parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StepConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo test"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StepConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo clippy"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"audit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StepConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo audit"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// fail-fast: stop everything if one step fails&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step runs in its own Tokio task. The &lt;code&gt;?&lt;/code&gt; propagates errors naturally. No callback hell, no promise chaining.&lt;/p&gt;

&lt;h3&gt;
  
  
  A single binary, zero dependencies
&lt;/h3&gt;

&lt;p&gt;Go also produces a static binary, and it's a common argument in its favor. But Rust goes further with &lt;code&gt;lto = true&lt;/code&gt;, &lt;code&gt;strip = true&lt;/code&gt; and &lt;code&gt;codegen-units = 1&lt;/code&gt; in the release profile: the binary is more compact and starts faster.&lt;/p&gt;

&lt;p&gt;For IronFlow, this means trivial deployment: a single file to copy to the server. No Node runtime, no JVM, no Python with its virtualenvs. The worker runs with a few MB of RAM, even under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Rust makes natural in IronFlow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Imperative code workflows
&lt;/h3&gt;

&lt;p&gt;An IronFlow workflow is a &lt;code&gt;WorkflowHandler&lt;/code&gt; trait implementation. You receive a &lt;code&gt;WorkflowContext&lt;/code&gt; and chain operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;WorkflowHandler&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"deploy"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;WorkflowContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HandlerFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo build --release"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;
                &lt;span class="nf"&gt;.parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StepConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo test"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StepConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo clippy"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"audit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StepConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cargo audit"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(());&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.approval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;ApprovalConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ship to production?"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;ShellConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./deploy.sh"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The control flow is standard Rust. If tests fail, &lt;code&gt;?&lt;/code&gt; propagates the error and the run transitions to &lt;code&gt;Failed&lt;/code&gt;. The approval gate suspends the run until a human acts. No DSL to learn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interchangeable AI providers
&lt;/h3&gt;

&lt;p&gt;IronFlow supports 10 AI providers, all behind the same &lt;code&gt;AgentProvider&lt;/code&gt; trait. A workflow written for Claude runs on any other provider without modification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ProviderRouter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ProviderMatcher&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;ModelPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nvidia/"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;nvidia&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Review"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SONNET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Review"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nvidia/deepseek-v4-flash"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ProviderRouter&lt;/code&gt; dispatches on the model name. A workflow can mix vendors within the same execution. Providers include Claude Code (local), SSH, Docker, Kubernetes (ephemeral and persistent), Anthropic API, OpenAI, Gemini, Mistral, and NVIDIA NIM.&lt;/p&gt;

&lt;p&gt;In Go, this routing-by-trait pattern would use interfaces - similar on the surface, but without the lifetime guarantees Rust provides. In Node, you'd use duck typing and discover errors in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture in 12 crates
&lt;/h3&gt;

&lt;p&gt;The workspace is split into 12 crates, each with a clear responsibility:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;Cargo's feature system lets you include only what you need. &lt;code&gt;ironflow-core&lt;/code&gt; works as a standalone library without a server or database. Providers like SSH, Docker, and Kubernetes are behind feature flags (&lt;code&gt;transport-ssh&lt;/code&gt;, &lt;code&gt;transport-docker&lt;/code&gt;, &lt;code&gt;transport-k8s&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest comparison with alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;IronFlow&lt;/th&gt;
&lt;th&gt;Temporal&lt;/th&gt;
&lt;th&gt;Windmill&lt;/th&gt;
&lt;th&gt;n8n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Definition&lt;/td&gt;
&lt;td&gt;Imperative Rust code&lt;/td&gt;
&lt;td&gt;Code (Go/Java/TS/Python)&lt;/td&gt;
&lt;td&gt;Scripts (Python/TS/Go) + UI&lt;/td&gt;
&lt;td&gt;GUI + JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;API + workers (Postgres)&lt;/td&gt;
&lt;td&gt;Cluster (Cassandra/MySQL)&lt;/td&gt;
&lt;td&gt;Server (Postgres)&lt;/td&gt;
&lt;td&gt;Server (SQLite/Postgres)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native AI agents&lt;/td&gt;
&lt;td&gt;10 providers, built-in budgeting&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Via plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Single binary&lt;/td&gt;
&lt;td&gt;Multi-service cluster&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Go (server) + multi-SDK&lt;/td&gt;
&lt;td&gt;Rust (server)&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Temporal is the obvious choice for teams that need durable execution at scale and accept the operational complexity. Temporal actually &lt;a href="https://temporal.io/blog/why-rust-powers-core-sdk" rel="noopener noreferrer"&gt;chose Rust for their Core SDK&lt;/a&gt;, citing "fearless concurrency" and the fact that "if it compiles, it probably works."&lt;/p&gt;

&lt;p&gt;Windmill chose Rust for its backend and &lt;a href="https://www.windmill.dev/blog/launch-week-1/fastest-workflow-engine" rel="noopener noreferrer"&gt;reports 10 to 13x better performance than Airflow&lt;/a&gt; thanks to the PostgreSQL + Rust combination. Same stack as IronFlow.&lt;/p&gt;

&lt;p&gt;n8n is perfect for no-code and quick automation, but its Node.js base and GUI model limit infrastructure use cases.&lt;/p&gt;

&lt;p&gt;IronFlow sits between Temporal (too heavy for AI agent workflows) and no-code tools (too limited for complex logic). The bet is simple: if the workflow is complex enough to need conditions, loops, and granular error handling, it should be code. And if it's code, you should use a language that guarantees at compile time that states are correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Rust is not without trade-offs. Build time for a 12-crate workspace is significant - several minutes for a release build. And the Rust developer pool is smaller than Go's or TypeScript's.&lt;/p&gt;

&lt;p&gt;If IronFlow were an internal enterprise tool with a 10-person team, Go would probably be a better choice. But for an open-source project where engine correctness is critical and performance matters, Rust is the right trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code is open source
&lt;/h2&gt;

&lt;p&gt;IronFlow is published under the MIT license on &lt;a href="https://gitlab.com/ThomasTartrau/ironflow" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt; (mirror on &lt;a href="https://github.com/ThomasTartrau/ironflow" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). All crates are on &lt;a href="https://crates.io/crates/ironflow-core" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;. The project is part of a tooling ecosystem I'm building around automation and Claude Code, with &lt;a href="https://dev.to/blog/en/mcp-rtk-reduce-token-usage-mcp-servers"&gt;MCP RTK&lt;/a&gt; (MCP filtering proxy) and the &lt;a href="https://dev.to/blog/en/writing-effective-claude-code-skills"&gt;Claude Code skills&lt;/a&gt; I use daily. The &lt;a href="https://dev.to/blog/en/claude-code-setup-2026"&gt;complete setup&lt;/a&gt; tying all these tools together is detailed in a previous post. The &lt;a href="https://dev.to/projects/ironflow"&gt;project page&lt;/a&gt; has installation links and documentation. For a deep dive into the API's internal architecture (entities, store traits, Axum handlers), see &lt;a href="https://dev.to/blog/en/layered-architecture-rust-api-axum-sqlx"&gt;Layered Architecture for a REST API in Rust with Axum and SQLx&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>architecture</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Writing effective Claude Code skills</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:54:10 +0000</pubDate>
      <link>https://dev.to/thomastartrau/writing-effective-claude-code-skills-145l</link>
      <guid>https://dev.to/thomastartrau/writing-effective-claude-code-skills-145l</guid>
      <description>&lt;p&gt;Skills are the most underrated feature in &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;. I use dozens of them covering everything from Git commits to blog article creation. Most developers have none, or one or two copied from a tutorial.&lt;/p&gt;

&lt;p&gt;This article shows how to write skills that work in production: the structure, the loading mechanism, the patterns I've identified after months of iteration, and the common mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a skill is
&lt;/h2&gt;

&lt;p&gt;A skill is a folder containing a &lt;code&gt;SKILL.md&lt;/code&gt; file. The file combines a YAML frontmatter (metadata) and a Markdown body (instructions). When the user types &lt;code&gt;/skill-name&lt;/code&gt; or phrases a request that matches the description, Claude loads the instructions and follows them. The &lt;a href="https://docs.anthropic.com/en/docs/claude-code/skills" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; covers installation and basic syntax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit-push/
  SKILL.md          # required: metadata + instructions
  references/       # optional: detailed docs
  scripts/          # optional: executable code
  assets/           # optional: templates, static files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SKILL.md format is an open standard created by Anthropic and adopted by other agent products like &lt;a href="https://docs.cursor.com/context/skills" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;. A skill written for Claude Code works as-is in these tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progressive disclosure: why it matters
&lt;/h2&gt;

&lt;p&gt;The loading mechanism is the most important aspect to understand. Without it, you write skills that are too large and waste context, or too vague and never trigger.&lt;/p&gt;

&lt;p&gt;Loading happens in three levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1 - Discovery (~100 tokens per skill).&lt;/strong&gt; Only the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; from the frontmatter are injected into the system prompt at the start of each session. Claude knows the skill exists and when it applies. Even with dozens of active skills, that's only a few thousand tokens - negligible in a 200K context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2 - Activation (&amp;lt;5,000 tokens).&lt;/strong&gt; When the user's request matches a skill's description, Claude reads the full &lt;code&gt;SKILL.md&lt;/code&gt; body. This is where the detailed instructions, step-by-step workflows, and checklists live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3 - Execution (on demand, unlimited size).&lt;/strong&gt; The agent reads files from the &lt;code&gt;references/&lt;/code&gt; folder or runs scripts from &lt;code&gt;scripts/&lt;/code&gt; only when the Level 2 instructions tell it to. A blog skill might have 20 reference files, but Claude only loads 2 or 3 per execution.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;The practical consequence: the &lt;code&gt;SKILL.md&lt;/code&gt; body should stay under 5,000 tokens. Anything beyond that belongs in &lt;code&gt;references/&lt;/code&gt;. If the skill is too large, Claude loads thousands of context tokens every activation, even when it only needs a fraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a SKILL.md
&lt;/h2&gt;

&lt;p&gt;Here's the minimal structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-skill&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|"&lt;/span&gt;
  &lt;span class="s"&gt;What this skill does AND when to use it.&lt;/span&gt;
  &lt;span class="s"&gt;Include trigger phrases in the languages your users speak.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# My Skill&lt;/span&gt;

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; First step
&lt;span class="p"&gt;2.&lt;/span&gt; Second step
&lt;span class="p"&gt;3.&lt;/span&gt; Third step

&lt;span class="gu"&gt;## Output format&lt;/span&gt;
What the user should receive in return.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The frontmatter: the routing contract
&lt;/h3&gt;

&lt;p&gt;The frontmatter is the most critical component. The &lt;code&gt;description&lt;/code&gt; is the &lt;strong&gt;only text&lt;/strong&gt; Claude sees before deciding whether to activate the skill. If it's vague, the skill won't trigger.&lt;/p&gt;

&lt;p&gt;Technical constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; in lowercase with hyphens only, 1-64 characters&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; must match the parent folder name exactly&lt;/li&gt;
&lt;li&gt;Invalid YAML &lt;strong&gt;silently&lt;/strong&gt; prevents loading - no error, the skill just disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the description from one of my production skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;commit-push&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Commit all changes with an auto-generated conventional commit message&lt;/span&gt;
  &lt;span class="s"&gt;and push to remote, all in one step.&lt;/span&gt;
  &lt;span class="s"&gt;Use when: "commit &amp;amp; push", "commit and push", "commit push",&lt;/span&gt;
  &lt;span class="s"&gt;"/commit-push", "commit et pousse", "pousse ca", "fais un commit&lt;/span&gt;
  &lt;span class="s"&gt;et push", "commit tout".&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three elements to note:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What it does&lt;/strong&gt; - "commit all changes with auto-generated conventional commit message and push"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use it&lt;/strong&gt; - explicit list of trigger phrases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bilingual&lt;/strong&gt; - triggers in both English and French&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I had only written "Commit and push changes", the skill would trigger on "commit and push" but not on "pousse ca" or "commit tout".&lt;/p&gt;

&lt;h3&gt;
  
  
  The body: keep it light
&lt;/h3&gt;

&lt;p&gt;The SKILL.md body contains the workflow Claude should follow. Here's a simplified excerpt from my &lt;code&gt;/lint-check&lt;/code&gt; skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Lint Check&lt;/span&gt;

Run the full Rust lint pipeline and auto-fix errors.

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Run &lt;span class="sb"&gt;`cargo fmt -- --check`&lt;/span&gt; to detect formatting issues
&lt;span class="p"&gt;2.&lt;/span&gt; If formatting issues found, run &lt;span class="sb"&gt;`cargo fmt`&lt;/span&gt; to fix them
&lt;span class="p"&gt;3.&lt;/span&gt; Run &lt;span class="sb"&gt;`cargo clippy -- -D warnings`&lt;/span&gt; to detect lint issues
&lt;span class="p"&gt;4.&lt;/span&gt; If clippy issues found, fix them one by one
&lt;span class="p"&gt;5.&lt;/span&gt; Run &lt;span class="sb"&gt;`cargo check`&lt;/span&gt; to verify compilation
&lt;span class="p"&gt;6.&lt;/span&gt; If errors remain after 3 fix attempts, stop and report

&lt;span class="gu"&gt;## Error handling&lt;/span&gt;

| Scenario | Action |
|----------|--------|
| cargo fmt fails | Report the error, do not continue |
| clippy warns but compiles | Fix warnings, re-run |
| cargo check fails | Show the error, suggest a fix |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill is 30 lines, not 300. It says what to do, in what order, and how to handle errors. Claude doesn't need an explanation of what &lt;code&gt;cargo clippy&lt;/code&gt; is - it already knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two design philosophies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern A: tool wrappers
&lt;/h3&gt;

&lt;p&gt;The skill is a thin wrapper around a CLI or deterministic script. The logic lives in the code; the skill just orchestrates it.&lt;/p&gt;

&lt;p&gt;Examples from my setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/lint-check&lt;/code&gt; orchestrates &lt;code&gt;cargo fmt&lt;/code&gt;, &lt;code&gt;cargo clippy&lt;/code&gt;, &lt;code&gt;cargo check&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/commit-push&lt;/code&gt; orchestrates &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git diff&lt;/code&gt;, &lt;code&gt;git log&lt;/code&gt;, &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/seo-scan&lt;/code&gt; orchestrates an SEO crawler and updates a tracking file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dev.to/blog/en/mcp-rtk-reduce-token-usage-mcp-servers"&gt;MCP RTK&lt;/a&gt; is a good example of this pattern: the skill orchestrates a token filtering proxy via CLI commands, with no logic in prose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B: cognitive disciplines
&lt;/h3&gt;

&lt;p&gt;The skill encodes a methodology the agent must follow. It's pure prompt engineering - no script to run, just a thinking process.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/systematic-debugging&lt;/code&gt; enforces a 5-step debugging methodology (reproduce, isolate, hypothesize, verify, fix)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/security-audit&lt;/code&gt; defines an OWASP top 10 checklist with patterns to search by category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pattern B is harder to write well. The temptation is to over-explain. Claude knows how to debug - the skill adds &lt;strong&gt;structure&lt;/strong&gt; to the process, not knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns for effective skills
&lt;/h2&gt;

&lt;p&gt;After months of iterating, here are the patterns that work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bash first, prose second
&lt;/h3&gt;

&lt;p&gt;A code block the agent can execute beats a paragraph describing what to do.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Check if there are uncommitted modified files in the current
git repository by using the git status command.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;1.&lt;/span&gt; Run &lt;span class="sb"&gt;`git status`&lt;/span&gt; to check for uncommitted changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude knows what &lt;code&gt;git status&lt;/code&gt; does. The short version is clearer and more reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  State-check before action
&lt;/h3&gt;

&lt;p&gt;Always verify the current state before modifying anything. Without this, Claude acts on assumptions and breaks things.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run &lt;span class="sb"&gt;`git status`&lt;/span&gt; to verify clean working tree
&lt;span class="p"&gt;2.&lt;/span&gt; Run &lt;span class="sb"&gt;`git log --oneline -5`&lt;/span&gt; to confirm current branch
&lt;span class="p"&gt;3.&lt;/span&gt; Only then: create the feature branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Validation loops
&lt;/h3&gt;

&lt;p&gt;After each action, verify the result is correct before moving to the next. My &lt;code&gt;/dev-pipeline&lt;/code&gt; skill does this at every step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;4.&lt;/span&gt; Run &lt;span class="sb"&gt;`cargo clippy -- -D warnings`&lt;/span&gt;
&lt;span class="p"&gt;5.&lt;/span&gt; If clippy reports errors:
   a. Fix the errors
   b. Re-run clippy
   c. If errors persist after 3 attempts, stop and report
&lt;span class="p"&gt;6.&lt;/span&gt; Only if clippy passes: proceed to tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without validation loops, Claude chains steps even when one fails. It ends up "completing" a pipeline where every step has failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compose primitives
&lt;/h3&gt;

&lt;p&gt;Don't bundle entire workflows into a single skill. Compose simple skills together.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;/dev-pipeline&lt;/code&gt; skill doesn't reimplement linting - it calls &lt;code&gt;/lint-check&lt;/code&gt;. It doesn't reimplement commits - it calls &lt;code&gt;/commit-push&lt;/code&gt;. Each skill does one thing, and workflows compose these primitives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Dev Pipeline&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Plan the implementation (use EnterPlanMode)
&lt;span class="p"&gt;2.&lt;/span&gt; Implement the changes
&lt;span class="p"&gt;3.&lt;/span&gt; Run &lt;span class="sb"&gt;`/lint-check`&lt;/span&gt; to verify code quality
&lt;span class="p"&gt;4.&lt;/span&gt; Run tests
&lt;span class="p"&gt;5.&lt;/span&gt; Run &lt;span class="sb"&gt;`/commit-push`&lt;/span&gt; to commit and push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Document output formats
&lt;/h3&gt;

&lt;p&gt;The agent must know exactly what it produces. Without an output specification, Claude improvises a different format every execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Output format&lt;/span&gt;

Deliver a summary with:
&lt;span class="p"&gt;-&lt;/span&gt; Files modified: list of paths
&lt;span class="p"&gt;-&lt;/span&gt; Tests: pass/fail count
&lt;span class="p"&gt;-&lt;/span&gt; Lint: pass/fail with details
&lt;span class="p"&gt;-&lt;/span&gt; Commit: the conventional commit message used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Anti-patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't re-teach what the model knows
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Bad&lt;/span&gt;
JSON (JavaScript Object Notation) is a structured data format
used for data exchange...

&lt;span class="gh"&gt;# Good&lt;/span&gt;
Generate a JSON response matching the schema in references/schema.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude knows what JSON is. Every token wasted on unnecessary pedagogy is a context token lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't write vague descriptions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Bad - will almost never trigger&lt;/span&gt;
name: helper
description: Helps with dev stuff

&lt;span class="gh"&gt;# Good - clear and specific triggers&lt;/span&gt;
name: lint-check
description: |
  Run the full Rust lint pipeline (cargo fmt, clippy, check)
  and auto-fix errors. Trigger on: "lint check", "lance le lint",
  "cargo fmt &amp;amp;&amp;amp; cargo clippy", "check my Rust code".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A vague description means Claude doesn't know when to activate the skill. It has dozens of descriptions to compare against the user's request - precision is essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't create monolithic mega-skills
&lt;/h3&gt;

&lt;p&gt;One skill = one capability. If the description contains "and" between two independent actions, it's probably two skills.&lt;/p&gt;

&lt;p&gt;My first &lt;code&gt;/dev-pipeline&lt;/code&gt; was 500 lines with everything inline: linting, tests, review, commit, push, MR creation. Today it's 40 lines and composes five specialized skills.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't ignore failure modes
&lt;/h3&gt;

&lt;p&gt;Document what can go wrong and how to react. Without this, Claude stops or invents a solution when a command fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Error handling&lt;/span&gt;

| Scenario | Action |
|----------|--------|
| No git remote configured | Stop, ask user to configure |
| Pre-commit hook fails | Fix the issue, retry once |
| Push rejected (not fast-forward) | Run &lt;span class="sb"&gt;`git pull --rebase`&lt;/span&gt;, retry |
| Merge conflict after rebase | Stop, show conflicts to user |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Don't use absolute paths
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Bad&lt;/span&gt;
Read /Users/thomas/.claude/scripts/validate.py

&lt;span class="gh"&gt;# Good&lt;/span&gt;
Read scripts/validate.py from the skill directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Absolute paths break when the skill is shared or used on another machine. Paths relative to the skill directory or environment variables are portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project-specific skills
&lt;/h2&gt;

&lt;p&gt;The most powerful skills are those adapted to a specific project. In &lt;a href="https://dev.to/blog/en/claude-code-setup-2026"&gt;my setup&lt;/a&gt;, the Netir project has six skills that encode the project's conventions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# netir-cpm/SKILL.md (excerpt)&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;netir-cpm&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Commit, push and create a GitLab Merge Request for the Netir&lt;/span&gt;
  &lt;span class="s"&gt;project. Netir conventions applied: assignee ThomasTartrau,&lt;/span&gt;
  &lt;span class="s"&gt;reviewer netir-bot, label "MR::en attente de review".&lt;/span&gt;
  &lt;span class="s"&gt;Use when: "/cpm", "create the MR", "commit push mr".&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference from the generic &lt;code&gt;/cpm&lt;/code&gt;: Netir conventions (labels, reviewer, assignee) are hardcoded. I don't need to re-specify them for every MR.&lt;/p&gt;

&lt;p&gt;Another example: &lt;code&gt;/netir-qa-swarm&lt;/code&gt; launches four review agents in parallel, each with a different focus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Agents&lt;/span&gt;

| Agent | Focus |
|-------|-------|
| Architecture | Layers, separation of concerns |
| Security | OWASP, injections, auth, rate limiting |
| Rust quality | Idioms, clippy, performance, unwrap |
| Business patterns | Domain coherence, naming, edge cases |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent has instructions specific to the Netir codebase (the &lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;Axum&lt;/a&gt;/SQLx stack, naming conventions, error patterns). A generic review skill doesn't know these conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a skill
&lt;/h2&gt;

&lt;p&gt;The most important test: invoke the skill with varied phrasings and verify it triggers.&lt;/p&gt;

&lt;p&gt;Users don't say "/invoke-my-skill". They say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"commit and push"&lt;/li&gt;
&lt;li&gt;"run the linter"&lt;/li&gt;
&lt;li&gt;"create a MR"&lt;/li&gt;
&lt;li&gt;"check the code"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these natural phrasings don't trigger the skill, the description needs work. I add failing phrasings to the frontmatter triggers.&lt;/p&gt;

&lt;p&gt;The second test: verify the instructions produce the expected result on a real case. No imaginary dry-run - run the skill on an actual project and check the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before publishing a skill
&lt;/h2&gt;

&lt;p&gt;Before deploying a new skill to my configuration repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;name&lt;/code&gt; matches the folder name exactly&lt;/li&gt;
&lt;li&gt;The description includes triggers in French and English&lt;/li&gt;
&lt;li&gt;The SKILL.md body is under 5,000 tokens&lt;/li&gt;
&lt;li&gt;Details are in &lt;code&gt;references/&lt;/code&gt;, not the body&lt;/li&gt;
&lt;li&gt;Failure modes are documented&lt;/li&gt;
&lt;li&gt;Output format is specified&lt;/li&gt;
&lt;li&gt;No secrets, tokens or absolute paths in the skill&lt;/li&gt;
&lt;li&gt;Destructive commands are protected by confirmations&lt;/li&gt;
&lt;li&gt;The skill has been tested with at least 3 different phrasings&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;The description is the router.&lt;/strong&gt; Invest as much time on the description as on the body. A skill with perfect instructions but a vague description will never trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code beats prose.&lt;/strong&gt; A deterministic script is always more reliable than ambiguous instructions. If a task has a single correct answer, put the logic in a script, not in Markdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cheapest context is context you don't load.&lt;/strong&gt; Progressive disclosure exists for a reason. A 200-line skill that could be 40 with references wastes context on every activation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with real phrasings.&lt;/strong&gt; Users don't type clean commands. They write "commit this", "push it", "lint" - triggers must cover these variants.&lt;/p&gt;

&lt;p&gt;Skills transform Claude Code from a generic assistant into a tool adapted to your specific workflow. The &lt;a href="https://dev.to/projects"&gt;projects page&lt;/a&gt; lists the other tools I've built around this ecosystem.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>MCP RTK: cut 90% of MCP server tokens</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:54:06 +0000</pubDate>
      <link>https://dev.to/thomastartrau/mcp-rtk-cut-90-of-mcp-server-tokens-aen</link>
      <guid>https://dev.to/thomastartrau/mcp-rtk-cut-90-of-mcp-server-tokens-aen</guid>
      <description>&lt;p&gt;I use &lt;a href="https://dev.to/blog/en/claude-code-setup-2026"&gt;Claude Code&lt;/a&gt; every day for development. Like many developers, I've connected several MCP servers (GitLab, Grafana, Sentry...) to give Claude direct access to my tools. The problem: every MCP call injects tens of thousands of tokens into the context, and the bill spirals fast.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://gitlab.com/ThomasTartrau/mcp-rtk" rel="noopener noreferrer"&gt;MCP RTK&lt;/a&gt; to fix this. It's an MCP proxy written in Rust that sits between Claude Code and MCP servers, filtering responses before they reach the model. Result: &lt;strong&gt;over 267 million tokens saved&lt;/strong&gt; across my 38,000+ commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: oversized MCP responses
&lt;/h2&gt;

&lt;p&gt;The MCP protocol (&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;) lets Claude interact with external tools. When Claude calls an MCP tool, the server returns a JSON response. The issue is that these responses often contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;empty or null fields that add nothing&lt;/li&gt;
&lt;li&gt;technical metadata (internal timestamps, pagination IDs, HTTP headers)&lt;/li&gt;
&lt;li&gt;very long values (full logs, HTML descriptions, entire diffs)&lt;/li&gt;
&lt;li&gt;arrays with dozens of entries when 5 would suffice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single &lt;code&gt;list_issues&lt;/code&gt; call on GitLab can consume over 180,000 tokens. Claude only needs a fraction to answer the question. The rest is pure waste.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;



&lt;p&gt;Over a typical work session with 50 to 100 MCP calls, that easily adds up to 500,000 wasted tokens injected into the context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: an 8-step filtering proxy
&lt;/h2&gt;

&lt;p&gt;MCP RTK sits transparently between Claude Code and MCP servers. No workflow change needed: Claude keeps calling the same tools, but responses pass through a filtering pipeline before reaching the context.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;The pipeline has 8 steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove null and empty fields&lt;/strong&gt; - fields without values are stripped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truncate long strings&lt;/strong&gt; - values exceeding a configurable threshold are cut&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field whitelist&lt;/strong&gt; - only useful fields are kept&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field blacklist&lt;/strong&gt; - known useless fields are removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication&lt;/strong&gt; - identical entries are merged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Array compression&lt;/strong&gt; - long arrays are sampled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical metadata removal&lt;/strong&gt; - API-internal fields are stripped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalization&lt;/strong&gt; - output format is standardized&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step is independently configurable. You can enable or disable each filter, adjust thresholds, and define server-specific rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration with presets
&lt;/h2&gt;

&lt;p&gt;Configuration uses a TOML file. MCP RTK ships with community presets for popular servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[servers.gitlab]&lt;/span&gt;
&lt;span class="py"&gt;preset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gitlab"&lt;/span&gt;

&lt;span class="nn"&gt;[servers.grafana]&lt;/span&gt;
&lt;span class="py"&gt;preset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"grafana"&lt;/span&gt;

&lt;span class="nn"&gt;[servers.sentry]&lt;/span&gt;
&lt;span class="py"&gt;preset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sentry"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each preset defines which fields to keep, which to exclude, and appropriate truncation thresholds for the server. For custom servers, you define rules directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[servers.my-api]&lt;/span&gt;
&lt;span class="py"&gt;whitelist&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;max_string_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="py"&gt;max_array_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP RTK auto-detects installed MCP servers and offers to configure them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Across my 38,000+ commands, MCP RTK has saved &lt;strong&gt;267 million tokens&lt;/strong&gt; with an average reduction rate of 87%. On Opus 4.6 ($15/M input tokens), that's roughly &lt;strong&gt;$4,000 in savings&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;p&gt;Useful information is preserved. Claude responds with the same accuracy, but consumes far fewer tokens per session. The &lt;code&gt;mcp-rtk gain&lt;/code&gt; command lets you track savings in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tokens saved:      267.1M (86.7%)
Efficiency meter: █████████████████████░░░ 86.7%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;MCP RTK is distributed as a single binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;mcp-rtk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line change in your Claude Code config - wrap the existing MCP command with &lt;code&gt;mcp-rtk --&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gitlab"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-rtk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@nicepkg/gitlab-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"GITLAB_PERSONAL_ACCESS_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glpat-..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP RTK detects the upstream server from the command and loads the matching preset automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust?
&lt;/h2&gt;

&lt;p&gt;Rust was a deliberate choice. The proxy must process every MCP response with minimal latency to avoid slowing down the workflow. Rust provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;near-instant startup (no JVM, no runtime)&lt;/li&gt;
&lt;li&gt;minimal memory footprint (a few MB)&lt;/li&gt;
&lt;li&gt;a single binary with no dependencies to install&lt;/li&gt;
&lt;li&gt;compile-time memory safety guarantees&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The code is open source
&lt;/h2&gt;

&lt;p&gt;MCP RTK is published under the MIT license on &lt;a href="https://gitlab.com/ThomasTartrau/mcp-rtk" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt; (mirror on &lt;a href="https://github.com/ThomasTartrau/mcp-rtk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). Community presets are maintained by users: anyone can contribute their own configurations for new MCP servers.&lt;/p&gt;

&lt;p&gt;The project is part of a tooling ecosystem I'm building around Claude Code, alongside &lt;a href="https://gitlab.com/ThomasTartrau/skill-radar" rel="noopener noreferrer"&gt;Skill Radar&lt;/a&gt; (detecting repetitive patterns in sessions) and &lt;a href="https://github.com/ThomasTartrau/claude-deck" rel="noopener noreferrer"&gt;Claude Deck&lt;/a&gt; (multi-workspace for parallel sessions). To learn how to write your own skills, see the guide on &lt;a href="https://dev.to/blog/en/writing-effective-claude-code-skills"&gt;effective skills&lt;/a&gt;. The &lt;a href="https://dev.to/projects/mcp-rtk"&gt;project page&lt;/a&gt; has installation links and documentation.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>rust</category>
      <category>claudecode</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My Claude Code setup in 2026</title>
      <dc:creator>Thomas Tartrau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:44:18 +0000</pubDate>
      <link>https://dev.to/thomastartrau/my-claude-code-setup-in-2026-aca</link>
      <guid>https://dev.to/thomastartrau/my-claude-code-setup-in-2026-aca</guid>
      <description>&lt;p&gt;I've been using &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; as my primary development tool since early 2025. After hundreds of sessions on Rust, TypeScript and React projects, I've built a configuration that evolves every week. This article shows the setup as it is today, with real files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure: where config lives
&lt;/h2&gt;

&lt;p&gt;Everything starts from &lt;code&gt;~/.claude/&lt;/code&gt;. That's Claude Code's global directory. Here's how mine is organized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/
  CLAUDE.md              # global instructions
  settings.json          # model, hooks, permissions, skill overrides
  RTK.md                 # MCP RTK reference (loaded via @RTK.md)
  agents/
    netir-reviewer/      # custom Rust review agent
  skills/                # symlinks to the config repo
    blog -&amp;gt; .../global/skills/blog/
    commit-push -&amp;gt; .../global/skills/commit-push/
    netir-cpm -&amp;gt; .../projects/netir/skills/netir-cpm/
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick: skills are symlinks to a &lt;strong&gt;Git configuration repo&lt;/strong&gt; that I maintain separately. This lets me version, sync across machines, and separate global skills from project-specific ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLAUDE.md: the most important file
&lt;/h2&gt;

&lt;p&gt;The global CLAUDE.md is loaded into &lt;strong&gt;every session&lt;/strong&gt;. This is where I put rules that apply everywhere, regardless of the project.&lt;/p&gt;

&lt;p&gt;Mine is under 30 lines. Each line was added after a real problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## French language -- Strict orthography&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Always respond in French with all correct diacritical marks.
&lt;span class="p"&gt;-&lt;/span&gt; FORBIDDEN to write without accents.
&lt;span class="p"&gt;
-&lt;/span&gt; Never launch subagents or delegate tasks unless the user explicitly asks.
&lt;span class="p"&gt;-&lt;/span&gt; Output code first, explanation after - only if non-obvious.
&lt;span class="p"&gt;-&lt;/span&gt; No compliments or affirmations in code reviews. State the issue, show the fix, stop.
&lt;span class="p"&gt;-&lt;/span&gt; No em dashes, smart quotes, or decorative Unicode.

&lt;span class="gu"&gt;## Git &amp;amp; GitLab&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Simple feature branch workflow: feature branches merge into main.
&lt;span class="p"&gt;-&lt;/span&gt; Always use conventional commit messages.

@RTK.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The language rule&lt;/strong&gt; comes first because without it, Claude switches to English or drops accents after context compaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Never launch subagents unless asked"&lt;/strong&gt; prevents Claude from delegating to sub-agents when it's unnecessary. Without this rule, it spawns agents for simple searches and wastes context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@RTK.md&lt;/code&gt;&lt;/strong&gt; is a reference to a separate file documenting RTK commands. Claude loads it automatically when it needs the context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rules: contextual instructions
&lt;/h2&gt;

&lt;p&gt;Rules are Markdown files in &lt;code&gt;~/.claude/rules/&lt;/code&gt; (or in the config repo). The difference from CLAUDE.md: they have a &lt;strong&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/memory#rule-files" rel="noopener noreferrer"&gt;&lt;code&gt;paths&lt;/code&gt; header&lt;/a&gt;&lt;/strong&gt; that activates them only on matching files.&lt;/p&gt;

&lt;p&gt;I have 15. Here are the most useful ones:&lt;/p&gt;

&lt;h3&gt;
  
  
  Code discipline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.{rs,ts,tsx,jsx,js,sql,toml}"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Code discipline&lt;/span&gt;

&lt;span class="gu"&gt;## Prior art - search before writing&lt;/span&gt;
Before writing a helper or abstraction: grep 2-3 variants
of the concept. If an equivalent exists, reuse it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule activates on all code files. It prevents Claude from recreating utilities that already exist in the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.{rs,ts,tsx,sql,toml,yml,yaml},**/Dockerfile"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;-&lt;/span&gt; Secrets: never hardcoded. Use env vars or vault.
&lt;span class="p"&gt;-&lt;/span&gt; Injections - zero tolerance: Always use parameterized queries.
&lt;span class="p"&gt;-&lt;/span&gt; JWT stateless by default. Short-lived access + long-lived refresh.
&lt;span class="p"&gt;-&lt;/span&gt; HTTP security headers: CORS, X-Content-Type-Options, HSTS, CSP.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Anti "fake done"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Agent Verification -- 11 "Fake Done" Shortcuts&lt;/span&gt;

Before marking any task as complete, verify the diff against each shortcut.
If any applies, the task is NOT done.
&lt;span class="p"&gt;
1.&lt;/span&gt; Relaxed tests - assertions weakened to make red go green
&lt;span class="p"&gt;2.&lt;/span&gt; Swallowed errors - try/catch that hides the failure
&lt;span class="p"&gt;3.&lt;/span&gt; Stub returns - hardcoded return values
&lt;span class="p"&gt;4.&lt;/span&gt; Comment-as-fix - the bug is now a TODO
&lt;span class="p"&gt;5.&lt;/span&gt; Happy-path only - 500s, empty inputs unhandled
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is agent-specific. When Claude marks a task as done, it must check its own diff against these 11 patterns. It catches cases where the code compiles but doesn't solve the actual problem.&lt;/p&gt;

&lt;p&gt;Other rules cover: Rust imports, layered architecture, database migrations, JavaScript/TypeScript pitfalls, performance, refactoring, SQLx compile-time checks, and testing.&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/blog/config-layers-en.svg" class="article-body-image-wrapper"&gt;&lt;img src="/images/blog/config-layers-en.svg" alt="Three layers of Claude Code configuration: CLAUDE.md always loaded, Rules loaded if paths match, Skills loaded on demand"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills: custom slash commands
&lt;/h2&gt;

&lt;p&gt;Skills are &lt;code&gt;SKILL.md&lt;/code&gt; files in folders under &lt;code&gt;~/.claude/skills/&lt;/code&gt;. Each skill defines a slash command (e.g., &lt;code&gt;/commit-push&lt;/code&gt;) with detailed instructions.&lt;/p&gt;

&lt;p&gt;I have &lt;strong&gt;55 skills installed&lt;/strong&gt; with &lt;strong&gt;40 active&lt;/strong&gt; (15 disabled via &lt;code&gt;skillOverrides&lt;/code&gt; in settings.json). They fall into three categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  Daily workflow skills
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/commit-push&lt;/code&gt;&lt;/strong&gt; - auto-generated conventional commit message + push in one command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/cpm&lt;/code&gt;&lt;/strong&gt; - commit, push and GitLab Merge Request creation in one command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/dev-pipeline&lt;/code&gt;&lt;/strong&gt; - plan, implement, lint, test, review, ship - the full pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/lint-check&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;cargo fmt &amp;amp;&amp;amp; cargo clippy &amp;amp;&amp;amp; cargo check&lt;/code&gt; with auto-fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/work-on-issue&lt;/code&gt;&lt;/strong&gt; - fetch a GitLab issue, assign, plan, implement, test, verify&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Review and quality skills
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/security-audit&lt;/code&gt;&lt;/strong&gt; - OWASP top 10 audit on the codebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/systematic-debugging&lt;/code&gt;&lt;/strong&gt; - structured debugging methodology&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/netir-qa-swarm&lt;/code&gt;&lt;/strong&gt; - 4 reviewers in parallel on a MR (architecture, security, Rust quality, business patterns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/netir-review-triage&lt;/code&gt;&lt;/strong&gt; - sorts review comments into actionable/nit/ambiguous&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Content skills
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/blog&lt;/code&gt;&lt;/strong&gt; - complete article creation pipeline (research, writing, SEO, scoring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/linkedin-post-coach&lt;/code&gt;&lt;/strong&gt; - interactive coaching for LinkedIn posts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/useful-for-me&lt;/code&gt;&lt;/strong&gt; - analyzes an external repo/tool and tells me if it's useful for my projects&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project-specific skills
&lt;/h3&gt;

&lt;p&gt;The Netir setup illustrates project-specific skills well. These skills live in &lt;code&gt;projects/netir/skills/&lt;/code&gt; and only activate inside the Netir directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;netir-cpm          # CPM with Netir conventions (labels, reviewer)
netir-qa-swarm     # multi-agent review
netir-review-triage # review sorting
netir-alert-triage # monitoring alert triage
netir-tech-debt    # tech debt analysis
netir-next         # next task selection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A skill is just a Markdown file with instructions. Here's a simplified excerpt of &lt;code&gt;/commit-push&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Commit &amp;amp; Push&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; git status to see modified files
&lt;span class="p"&gt;2.&lt;/span&gt; git diff to analyze changes
&lt;span class="p"&gt;3.&lt;/span&gt; git log -5 for existing commit style
&lt;span class="p"&gt;4.&lt;/span&gt; Generate a conventional message (feat/fix/refactor...)
&lt;span class="p"&gt;5.&lt;/span&gt; git add relevant files
&lt;span class="p"&gt;6.&lt;/span&gt; git commit
&lt;span class="p"&gt;7.&lt;/span&gt; git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The power comes from composition: &lt;code&gt;/dev-pipeline&lt;/code&gt; internally calls &lt;code&gt;/lint-check&lt;/code&gt;, then chains with review and commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disabling unused skills
&lt;/h3&gt;

&lt;p&gt;The 15 disabled skills are set in &lt;code&gt;settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skillOverrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cost-estimate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email-campaigns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tdd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"teach"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids cluttering autocompletion and prevents Claude from invoking them accidentally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks: automate without thinking
&lt;/h2&gt;

&lt;p&gt;Hooks are shell commands executed automatically on certain events. The most important one in my setup:&lt;/p&gt;

&lt;h3&gt;
  
  
  The RTK integration
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/nicholasgasior/rtk" rel="noopener noreferrer"&gt;RTK&lt;/a&gt; (Rust Token Killer) is an open source tool that integrates with Claude Code via a &lt;code&gt;PreToolUse&lt;/code&gt; hook. It intercepts Bash commands and filters outputs to reduce token consumption.&lt;/p&gt;

&lt;p&gt;The configuration is a single entry in &lt;code&gt;settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rtk hook claude"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RTK runs &lt;strong&gt;before every Bash command&lt;/strong&gt;. It intercepts CLI commands (&lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;ps aux&lt;/code&gt;...) and rewrites them to go through its filtering proxy. Result: CLI outputs are filtered automatically, without Claude or me having to think about it.&lt;/p&gt;

&lt;p&gt;The numbers on my current setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total commands:    39,646
Tokens saved:      276.4M (86.8%)
Top commands:
  rtk read    3,067 calls   217.8M tokens saved
  rtk grep    5,408 calls    13.8M tokens saved
  rtk find      573 calls    11.8M tokens saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Opus ($15/M tokens for input), &lt;strong&gt;276 million tokens saved&lt;/strong&gt; represents roughly &lt;strong&gt;$4,100 in savings&lt;/strong&gt;. The hook pays for itself in the first session.&lt;/p&gt;

&lt;p&gt;I also built &lt;a href="https://dev.to/blog/en/mcp-rtk-reduce-token-usage-mcp-servers"&gt;MCP RTK&lt;/a&gt;, a complementary proxy that applies the same filtering principle to MCP server responses (GitLab, Grafana, Sentry...).&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom agents
&lt;/h2&gt;

&lt;p&gt;Agents are files in &lt;code&gt;~/.claude/agents/&lt;/code&gt;. They define a specialized profile with restricted tools.&lt;/p&gt;

&lt;p&gt;I have a single custom agent: &lt;strong&gt;netir-reviewer&lt;/strong&gt;, a Rust code reviewer specific to the Netir project. It checks layered architecture, SQLx conventions, error handling, imports, OpenAPI registration, and tracing.&lt;/p&gt;

&lt;p&gt;The difference between a skill and an agent: a skill gives instructions to the main session, an agent is a &lt;strong&gt;separate session&lt;/strong&gt; with its own context and tools. A review agent only has access to &lt;code&gt;Read&lt;/code&gt; and &lt;code&gt;Grep&lt;/code&gt;, not &lt;code&gt;Edit&lt;/code&gt; or &lt;code&gt;Write&lt;/code&gt; - it can't modify code, only read it and flag issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The configuration repo
&lt;/h2&gt;

&lt;p&gt;This entire setup is managed in a private Git repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Documents/dev/claude/personal-config/
  install.sh           # setup script (symlinks, copies)
  global/
    CLAUDE.md           # global instructions
    rules/              # 15 rule files
    skills/             # global skills (blog, commit-push, etc.)
  projects/
    _template/          # template for new projects
    netir/              # Netir-specific config
      agents/
      rules/
      skills/
    jarvis/             # Jarvis-specific config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;install.sh&lt;/code&gt; creates symlinks from &lt;code&gt;~/.claude/skills/&lt;/code&gt; to the repo. When I add a skill or modify a rule, a &lt;code&gt;git pull&lt;/code&gt; on another machine is enough to sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  The project template
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;_template/&lt;/code&gt; folder contains a base structure for starting a new project with Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; projects/_template projects/my-new-project
&lt;span class="c"&gt;# Adapt rules and skills to the project&lt;/span&gt;
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;After months of iterating on this config:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep CLAUDE.md short.&lt;/strong&gt; Early versions were 200+ lines. Claude would skim them and forget rules. Under 30 well-chosen lines are more effective than an exhaustive document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contextual rules beat a big CLAUDE.md.&lt;/strong&gt; Loading Rust rules only on .rs files saves context and avoids confusion between languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable unused skills.&lt;/strong&gt; 55 skills is too many. The 15 disabled ones don't serve my current workflows. Keeping them active slows autocompletion and adds noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A single well-placed hook is enough.&lt;/strong&gt; The RTK hook on PreToolUse covers 90% of optimization needs. No need for complex hooks on every event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version your config.&lt;/strong&gt; A Git repo for Claude Code config seems overkill at first. In practice, it lets you roll back when a change breaks something, and sync between machines without friction.&lt;/p&gt;

&lt;p&gt;The setup keeps evolving. Every repeated friction in a Claude Code session becomes a candidate for a new rule, a new skill, or a hook adjustment. To go further: my guide on &lt;a href="https://dev.to/blog/en/writing-effective-claude-code-skills"&gt;writing effective skills&lt;/a&gt; details the patterns and anti-patterns, and the &lt;a href="https://dev.to/projects/mcp-rtk"&gt;projects page&lt;/a&gt; lists the other tools I've built around this ecosystem.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
