<?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: Andy Cho</title>
    <description>The latest articles on DEV Community by Andy Cho (@splendidz).</description>
    <link>https://dev.to/splendidz</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%2F4010164%2Fae8894f0-0527-4acc-bc05-b39b86575b21.png</url>
      <title>DEV Community: Andy Cho</title>
      <link>https://dev.to/splendidz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/splendidz"/>
    <language>en</language>
    <item>
      <title>Vibe-coding "just add one more feature" always rots. Here's the C++ skeleton I built to stop it.</title>
      <dc:creator>Andy Cho</dc:creator>
      <pubDate>Wed, 01 Jul 2026 00:17:49 +0000</pubDate>
      <link>https://dev.to/splendidz/vibe-coding-just-add-one-more-feature-always-rots-heres-the-c-skeleton-i-built-to-stop-it-3922</link>
      <guid>https://dev.to/splendidz/vibe-coding-just-add-one-more-feature-always-rots-heres-the-c-skeleton-i-built-to-stop-it-3922</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Repo (MIT): &lt;a href="https://github.com/splendidz/uniflow" rel="noopener noreferrer"&gt;https://github.com/splendidz/uniflow&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a long one. dev.to feels like the right place for that, so I am going to actually explain the thing instead of just dropping a link. If you only want the gist: I built a small framework that gives your control logic a fixed skeleton, so it stops rotting into spaghetti as you add features, and so an LLM has exactly one place to put each change. Everything runs cooperatively on one thread, so there are no locks. That's the whole pitch. The rest of this post is the "why" and the "how it actually works", because that part is what I actually care about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing up front, before you decide this is not for you because of the language.&lt;/strong&gt; It is C++ first (single header, no deps, C++17), but I did not stop there. I ported the &lt;em&gt;entire&lt;/em&gt; framework and &lt;em&gt;every&lt;/em&gt; demo to &lt;strong&gt;Python&lt;/strong&gt; and &lt;strong&gt;C#&lt;/strong&gt; as well, with the public APIs named to mirror each other (&lt;code&gt;Task&lt;/code&gt; / &lt;code&gt;StartFlow&lt;/code&gt; / &lt;code&gt;SubmitAsync&lt;/code&gt; / &lt;code&gt;AsyncResult&lt;/code&gt; / &lt;code&gt;SetScale&lt;/code&gt; / &lt;code&gt;Freeze&lt;/code&gt;, and so on). So whatever stack you are on, there is a version you can actually run. The code samples below are C++, but every one of them has a 1:1 equivalent file you can open and read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;C++: &lt;a href="https://github.com/splendidz/uniflow/tree/main/cpp/examples" rel="noopener noreferrer"&gt;&lt;code&gt;cpp/examples/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python: &lt;a href="https://github.com/splendidz/uniflow/tree/main/python/examples" rel="noopener noreferrer"&gt;&lt;code&gt;python/examples/&lt;/code&gt;&lt;/a&gt; (run with just &lt;code&gt;python python/examples/&amp;lt;name&amp;gt;.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;C#: &lt;a href="https://github.com/splendidz/uniflow/tree/main/cs/examples" rel="noopener noreferrer"&gt;&lt;code&gt;cs/examples/&lt;/code&gt;&lt;/a&gt; (run with &lt;code&gt;dotnet run --project cs/examples/&amp;lt;name&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are a Python or C# person, do not let the C++ snippets scare you off. Go open the same demo in your language, it reads almost the same.&lt;/p&gt;

&lt;p&gt;I will also warn you up front: my English is not perfect. I am going to write plainly and a bit slowly so the ideas come through. Bear with me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ugfy09byplklb8er9s9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ugfy09byplklb8er9s9.gif" alt="Dozens of cars and traffic lights driving through a city, each an independent module, all running on a single pump thread with no locks" width="360" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;I do equipment and motion control for a living. Machines, motors, sensors, that kind of thing. For about 14 years now. So the code I write all day is "ordered logic on a single thread": do this, wait for that sensor, then move the axis, then check the result, then go to the next step. If you have done embedded or automation work you know exactly the shape of it.&lt;/p&gt;

&lt;p&gt;And for years, the shape of that code was always the same. One big &lt;code&gt;switch&lt;/code&gt; on an integer &lt;code&gt;step_no&lt;/code&gt;, a &lt;code&gt;sleep(10)&lt;/code&gt; loop around it, and a pile of bool flags to remember where I was.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the way I wrote it for years&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;step_no&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while&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="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_no&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;              &lt;span class="n"&gt;step_no&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="n"&gt;step_no&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="n"&gt;step_no&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// always 10ms, whether we are busy or doing nothing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. It ships. Machines run on code that looks exactly like this all over the world right now. But you already know the problem, because everyone who writes this hits it: a few months of "just add one more case" later, the file is huge, the flags fight each other, and only the person who wrote it can read it.&lt;/p&gt;

&lt;p&gt;The worst version of this I ever inherited was a single &lt;code&gt;.cpp&lt;/code&gt; file with tens of thousands of lines of control logic in it. One guy had owned it for eight years. When I joined and could not instantly find my way around his file, he looked at me like I was the slow one. Honestly, to me, he was the one who looked bad. The code was readable only to him, and that was the whole problem, not me. That file basically held the team hostage.&lt;/p&gt;

&lt;p&gt;So I have wanted a better default shape for this kind of code for a long time. The framework in this post is my attempt at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then vibe-coding made it worse, and also made it matter more
&lt;/h2&gt;

&lt;p&gt;Here is the new part. I lean on LLMs a lot now. I vibe-code big chunks of my work.&lt;/p&gt;

&lt;p&gt;And the failure mode is brutal and familiar. You tell the model "add feature X." It says yes. The feature lands wherever it happens to fit. You say "now add Y." It says yes again. Three weeks later you open the file and it is the exact same flag soup I described above, except now it was generated faster, so there is more of it.&lt;/p&gt;

&lt;p&gt;The model is not stupid. It is doing what the structure allows. If the structure allows a mess, you get a mess. There is no skeleton telling the model "the new step goes &lt;em&gt;here&lt;/em&gt;, and nowhere else."&lt;/p&gt;

&lt;p&gt;So that pushed me to finally build the thing. Not because AI is magic, but because AI removes the last bit of friction that used to keep code somewhat in shape. When a human adds the 40th feature by hand, the pain of touching the giant function at least makes them think. The model feels no pain. It will happily make it worse forever. So the structure has to be enforced by something that is not "a careful developer."&lt;/p&gt;

&lt;p&gt;I called it &lt;strong&gt;uniflow&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea, in plain words
&lt;/h2&gt;

&lt;p&gt;The model is deliberately boring. Two concepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Flow is one thing that can only be in one state at a time.&lt;/strong&gt; A car cannot drive forward and backward at once, so one car is one Flow. A device, a connection, one agent in a sim, one event processor: each is one Flow. If only one operation can run on it at a time, it is a Flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Task is one unit of work inside that Flow, and it is a chain of small named steps.&lt;/strong&gt; Each step is a function. A step runs from start to finish, never blocks, and at the end it returns &lt;em&gt;one&lt;/em&gt; of four things telling the framework what to do next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Next(...)&lt;/code&gt; go to the next named step&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Stay()&lt;/code&gt; stay on this step, run me again next round (this is how you wait without blocking)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Done()&lt;/code&gt; this task finished normally&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Fail()&lt;/code&gt; this task failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the entire vocabulary. A step never sleeps, never spins a loop waiting for something. If it needs to wait, it returns &lt;code&gt;Stay()&lt;/code&gt; and gets called again later. The waiting is the framework's job, not yours.&lt;/p&gt;

&lt;p&gt;Here is the smallest real shape. Notice that the declaration (the list of step names) and the bodies are separate. The declaration is the skeleton, and you can read the whole flow just by reading the function names top to bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"uniflow.hpp"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// One flow = one module.&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Flow_Example&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Uniflow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;explicit&lt;/span&gt; &lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Uniflow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Example"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AddTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// wire the task to the flow, one line, once&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// A task owns its own steps. Entry is public; the rest are private.&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyTask&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&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;Step1_Begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// the only entry point&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                          &lt;span class="c1"&gt;// these are reachable only via Entry / Next&lt;/span&gt;
        &lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step1_Begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step2_Work&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step3_Done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;task_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ready_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// the flow holds the state; steps read it via flow()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the bodies (in a real project these live in the &lt;code&gt;.cpp&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MyTask&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Step1_Begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"initialization complete"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// one line that shows up in the trace later&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step2_Work&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;        &lt;span class="c1"&gt;// go to the next step of THIS task&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MyTask&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Step2_Work&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;ready_&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;Stay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// not ready yet? re-run me next round. no blocking.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step3_Done&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="n"&gt;Flow_Example&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MyTask&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Step3_Done&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="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                          &lt;span class="c1"&gt;// task ends, module goes idle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;Step2_Work&lt;/code&gt;. That &lt;code&gt;if (...) return Stay();&lt;/code&gt; is the whole trick for waiting. There is no &lt;code&gt;while&lt;/code&gt;, no &lt;code&gt;sleep&lt;/code&gt;. You check the condition once. If it is not ready, you bail out and the framework calls you again on the next round. From the outside it looks like the step is "waiting" on the condition, but the thread is never stuck inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this shape can't sprawl (the part that matters)
&lt;/h2&gt;

&lt;p&gt;This is the actual point of the framework, more than the async stuff. The structure is not a style guide you hope people follow. It is enforced by the types and by visibility.&lt;/p&gt;

&lt;p&gt;Three rules do all the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A step can only point to a sibling step in the same task.&lt;/strong&gt; &lt;code&gt;Next(UF_FN(Step2_Work))&lt;/code&gt; only compiles for steps that belong to this task. You cannot jump into the middle of some other task's chain. To cross into another unit of work, you have to do it explicitly with &lt;code&gt;StartTask(...)&lt;/code&gt;, and that boundary shows up right there in the code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Steps are private. Entry is the single public door.&lt;/strong&gt; You literally cannot call &lt;code&gt;Step3_Done()&lt;/code&gt; from outside the task. The only way in is &lt;code&gt;Entry()&lt;/code&gt;. So there are no hidden entry points, no "oh someone set &lt;code&gt;step_no = 2&lt;/code&gt; from over here" surprises. The flag-based version lets anyone, anywhere, write &lt;code&gt;step_no = 1&lt;/code&gt; and cut into the middle of a sequence. This does not allow that. The compiler does not allow that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every transition is named, in code, at the point it happens.&lt;/strong&gt; A step says &lt;code&gt;return Next(UF_FN(Step3_Save))&lt;/code&gt;. So to understand the order of the logic, you read the list of step names. That's it. The flow is fixed by declaration, not by tracing flag mutations across 600 lines.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So now when I tell the model "add a step that slows down for pedestrians before the car waits at the light," the only legal place for that change is: one new private step function, plus one &lt;code&gt;Next(...)&lt;/code&gt; rewire. It physically cannot spread the change across the file, because the file does not have a place to spread it into. Feature #50 has the same shape as feature #1.&lt;/p&gt;

&lt;p&gt;That is the property I was chasing. Not "the AI writes good code." It is "the structure makes the bad version not expressible."&lt;/p&gt;

&lt;h2&gt;
  
  
  Let me show you the flag soup it replaces
&lt;/h2&gt;

&lt;p&gt;I want to make this concrete, because "spaghetti" is too abstract. Here is a fairly honest version of what real flag-based control code turns into. This is a connect / send-command / wait-for-ack loop with retries, reconnects, e-stop, and fault. Read it and feel the dread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estop_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// e-stop can hit at ANY stage, so every progress flag must be rolled back here&lt;/span&gt;
        &lt;span class="n"&gt;connecting_&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;cmd_sent_&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;waiting_ack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// oops, forgot draining_ -&amp;gt; next cycle waits for a ghost ack. that's a real bug.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSafe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;estop_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;connected_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;connecting_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginConnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;connecting_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;connect_timer_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsConnected&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;connected_&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;connecting_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect_timer_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Passed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mx"&gt;5000ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;reconnect_count_&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;fault_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// fault_ is checked way down somewhere else, good luck&lt;/span&gt;
            &lt;span class="n"&gt;connecting_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// and where do we reset reconnect_count_? who knows&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cmd_sent_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;draining_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="n"&gt;cmd_sent_&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;waiting_ack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// these two flags must ALWAYS move as a pair. nothing enforces that.&lt;/span&gt;
            &lt;span class="n"&gt;cmd_timer_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Restart&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;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waiting_ack_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasAck&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// forget even one of these 5 lines and the next command is blocked forever&lt;/span&gt;
            &lt;span class="n"&gt;cmd_sent_&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;waiting_ack_&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;retry_count_&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;reconnect_count_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;draining_&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd_timer_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Passed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mx"&gt;3000ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;retry_count_&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;cmd_sent_&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// "go back to the send step" expressed as flag juggling&lt;/span&gt;
                &lt;span class="n"&gt;waiting_ack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;fault_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;fault_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;connected_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// roll back the connection too... and reconnect_count_...?&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="c1"&gt;// what handles fault_? what if estop_ AND fault_ at once? no branch actually says.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have written code like this. You have too. Every flag is implicitly responsible for resetting some other flag, the e-stop and fault paths can cut in anywhere, and "forgetting to reset one flag" is a whole category of bug. The brace depth grows. Nobody can review it because reviewing it means simulating it in your head.&lt;/p&gt;

&lt;p&gt;Here is the same logic in uniflow. Each state is one named step. The brace depth is flat. The transitions are explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step1_Connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginConnect&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;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step2_WaitConnected&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step2_WaitConnected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsConnected&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;Stay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// poll until connected, no blocking&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step3_WaitRequest&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step3_WaitRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;input_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasRequest&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;Stay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;input_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Take&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;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step4_WaitAck&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step4_WaitAck&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;device_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasAck&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;OnSuccess&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;Done&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;StayTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mx"&gt;3000ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step5_Timeout&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// no ack in 3s -&amp;gt; jump to the timeout step&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step5_Timeout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;retry_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&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;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step3_WaitRequest&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// retry_count is a task member&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;OnFail&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;Fail&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;Adding a stage now means: add one function, change one &lt;code&gt;Next&lt;/code&gt;. The other stages do not move. The e-stop / fault paths become explicit transitions instead of flags that secretly reset each other. That &lt;code&gt;StayTimeout&lt;/code&gt; is a nice one too: it means "keep polling this step, but if too much time passes, go over there instead." A timeout is just another named transition, not yet another timer flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The execution model: one thread, no locks
&lt;/h2&gt;

&lt;p&gt;Okay, so how does "return Stay() and get called again" actually run? This is the part people poke at the most, so let me go slow.&lt;/p&gt;

&lt;p&gt;The unit of execution is a &lt;strong&gt;pump thread&lt;/strong&gt;. One &lt;code&gt;Runtime&lt;/code&gt; owns one pump thread. You attach as many Flow modules as you want to that one Runtime. The pump goes around in rounds. Each round has three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drain the post queue.&lt;/strong&gt; Other threads can hand work to the pump by calling &lt;code&gt;Post(...)&lt;/code&gt;. Those callbacks run here, on the pump thread. So they can touch module state without a lock, because they are running on the same thread as everything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run each module once.&lt;/strong&gt; For every active module, call its current step body exactly one time. The step returns one of &lt;code&gt;Stay / Next / Done / Fail&lt;/code&gt;. Round-robin. Nobody blocks. If module A is sitting on &lt;code&gt;Stay()&lt;/code&gt; waiting for a sensor, that is fine, it returns immediately and module B gets its turn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick how long to sleep before the next round.&lt;/strong&gt; This is the "smart" part, see below.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now here is the thing that falls out of this, and it is the single biggest reason I like the model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because every module runs on the same one thread, one at a time, shared state between modules needs no lock.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No mutex. No spinlock. No rwlock. No "did every thread remember to take the right lock in the right order." That entire category of bug is just gone, because there is only ever one thread touching the shared world. In my traffic demo, 15 cars and the traffic lights all read and write a shared &lt;code&gt;World&lt;/code&gt; object (where is every car, what is each light doing) and there is not a single lock on it. There cannot be a data race, because there is no second thread to race with.&lt;/p&gt;

&lt;p&gt;I made a diagram for this because it is really the heart of it:&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;(left: a pile of threads all reaching into shared state through locks, hoping everyone took the right one. right: one pump thread visiting each flow in turn, shared state with no lock at all.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is what running several modules on one thread looks like. This is two machine axes homing "at the same time" on a single thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;uniflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt; &lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// one pump thread&lt;/span&gt;

&lt;span class="n"&gt;Flow_XAxis&lt;/span&gt; &lt;span class="n"&gt;x_axis&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;Flow_YAxis&lt;/span&gt; &lt;span class="n"&gt;y_axis&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;x_axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_home_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartFlow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// start homing X&lt;/span&gt;
&lt;span class="n"&gt;y_axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_home_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartFlow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// start homing Y, at the same time&lt;/span&gt;

&lt;span class="c1"&gt;// what the pump actually does, round by round:&lt;/span&gt;
&lt;span class="c1"&gt;//   Round 1: X.Step1 (send move cmd) -&amp;gt; Next   |  Y.Step1 (send move cmd) -&amp;gt; Next&lt;/span&gt;
&lt;span class="c1"&gt;//   Round 2: X.Step2 (still moving)   -&amp;gt; Stay   |  Y.Step2 (still moving)   -&amp;gt; Stay&lt;/span&gt;
&lt;span class="c1"&gt;//   Round N: X.Step2 (in position)    -&amp;gt; Done   |  Y.Step2 (still moving)   -&amp;gt; Stay&lt;/span&gt;
&lt;span class="c1"&gt;//   Round M: (X is idle now)                    |  Y.Step2 (in position)    -&amp;gt; Done&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// while X is parked on Stay(), Y runs, and vice versa.&lt;/span&gt;
&lt;span class="c1"&gt;// both axes move together, and there is no mutex anywhere.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old way to get two axes moving together was two threads and a &lt;code&gt;join&lt;/code&gt;. Each &lt;code&gt;GoHome()&lt;/code&gt; was a blocking call, so you needed a thread per blocking call, and then you needed locks around anything they shared. Here, "moving" is just a step that returns &lt;code&gt;Stay()&lt;/code&gt; until the motor reports it is in position. No thread blocks, so you do not need a second thread, so you do not need a lock. The complexity does not move somewhere else. It actually goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But a single thread can't scale / will block"
&lt;/h2&gt;

&lt;p&gt;Two objections always come, so let me answer both directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"One thread won't use my cores."&lt;/strong&gt; Correct, one Runtime is one core, on purpose. If you want more cores, you make more Runtimes. Each Runtime is its own pump thread. The thread boundary is a decision &lt;em&gt;you&lt;/em&gt; make as the designer ("these modules go on pump A, those go on pump B"), not something forced on you per unit of work. And if you decide two Runtimes should actually share one thread again, &lt;code&gt;Runtime::Link()&lt;/code&gt; merges them back onto a single pump, and they are lock-free with each other again. You choose where the thread lines are. That is a feature, not a workaround.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"A long task will freeze the whole pump."&lt;/strong&gt; This is the real one, and the answer is the same answer libuv and Node use. The pump thread must never block. So genuinely blocking work (network I/O, a heavy computation, a slow device call) does not run on the pump. You throw it to a built-in thread pool with &lt;code&gt;SubmitAsync&lt;/code&gt;, and you get the result back later as just another step. While that work runs on a pool thread, the pump keeps spinning and every other module keeps making progress.&lt;/p&gt;

&lt;p&gt;It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step1_FetchData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetching"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// hand the blocking call to the pool. returns an id. 0 means it was rejected.&lt;/span&gt;
    &lt;span class="n"&gt;AsyncId&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SubmitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DoFetch&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;chrono&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;milliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Fail&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;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step2_Process&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// carry the id to the step that reads the result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;StepResult&lt;/span&gt; &lt;span class="nf"&gt;Step2_Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncId&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AsyncResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&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="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&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;// not done yet -&amp;gt; just Stay and poll&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;StayTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mx"&gt;5000ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step_GaveUp&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_timeout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&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="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;r&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// filled in only when it actually finished&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UF_FN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Step3_Save&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// runs on a pool thread, so it has to be static. no access to instance state.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;DoFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;url&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="n"&gt;http_&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="n"&gt;url&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 nice side effect: the step that &lt;em&gt;starts&lt;/em&gt; the work and the step that &lt;em&gt;reads&lt;/em&gt; the result are separate functions. So the flow stays explicit, the wait shows up in the trace, and the continuation runs back on the pump thread, which means even the result handling touches shared state with no lock. When the pool work finishes, it calls &lt;code&gt;Wake()&lt;/code&gt; on the pump so you catch the result immediately instead of waiting out a poll interval. Honestly the longer the work takes, the more this model pays off, because the "start" and "finish" being separate steps is exactly what you want for something slow.&lt;/p&gt;

&lt;p&gt;You can also fire several jobs and join them in one step, set per-step timeouts, abandon a worker you gave up on (it keeps running in the background and the framework logs that you abandoned it, so the leak is visible). I will not dump all of that here, it is in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart polling: it is not a busy-loop
&lt;/h2&gt;

&lt;p&gt;Back to step 3 of the round, picking the sleep. The old &lt;code&gt;sleep(10)&lt;/code&gt; had a dumb problem: it waits the same 10ms whether the machine is in the middle of a fast sequence of transitions or sitting completely idle. So it is both too slow (it adds 10ms of lag between back-to-back steps) and too wasteful (it burns a wakeup every 10ms doing nothing).&lt;/p&gt;

&lt;p&gt;The pump picks the wait based on what just happened this round:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;what happened this round&lt;/th&gt;
&lt;th&gt;next wait&lt;/th&gt;
&lt;th&gt;why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;at least one module advanced (&lt;code&gt;Next&lt;/code&gt;/&lt;code&gt;Done&lt;/code&gt;/&lt;code&gt;Fail&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0ms (default)&lt;/td&gt;
&lt;td&gt;things are happening, go straight to the next round, no lag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everyone was just polling (&lt;code&gt;Stay&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;20ms (default)&lt;/td&gt;
&lt;td&gt;normal idle-ish polling, CPU stays near zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everyone is idle&lt;/td&gt;
&lt;td&gt;1ms (default)&lt;/td&gt;
&lt;td&gt;pick up brand new work fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And when an external event shows up (a packet arrived, a sensor fired), any thread can call &lt;code&gt;rt.Wake()&lt;/code&gt; and the pump wakes up &lt;em&gt;right now&lt;/em&gt; instead of waiting out its sleep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnNetworkReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Packet&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;module_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetPendingPacket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;runtime_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// wake the pump immediately, don't sit through the sleep&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it reacts instantly when there is work, and it nearly fully releases the CPU when there is not. That is the difference between this and a naive tick loop. Same mental model as the old &lt;code&gt;switch&lt;/code&gt;/&lt;code&gt;sleep&lt;/code&gt;, but the framework owns the timing and does it well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I did not expect to like the most: the trace
&lt;/h2&gt;

&lt;p&gt;Because every single thing that runs reduces to "a step function got called once," there is exactly one place to measure. So the framework can just &lt;em&gt;tell you&lt;/em&gt; what your code is doing, with no logging code from you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[JobWorker    ] FLOW START  caller=main.cpp:42 main()
[JobWorker    ] Entry -&amp;gt; Step2_Validate                         #00 elapsed=0.01ms  tick x8 avg=0.01ms
[JobWorker    ]                 ASYNC SUBMIT  CallApi
[JobWorker    ]                 ASYNC DONE    CallApi  wait=124.38ms
[JobWorker    ] Step2_Validate -&amp;gt; Step3_WaitSave  inserted=3000  #01 elapsed=124.42ms tick x1 avg=0.03ms
[JobWorker    ] Step3_WaitSave -&amp;gt; Done                           #02 elapsed=18.71ms  tick x1
[JobWorker    ] FLOW END  DONE  steps=#02  wall=143.21ms  step=0.07ms  async=143.09ms  tick x10 avg=0.01ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line is a transition, the time spent in that stage, how many times the body ran, the async wait, and whatever you wrote in &lt;code&gt;Describe()&lt;/code&gt;. You can set "warn me if a step takes longer than 10ms" or "warn me if an async job takes longer than 500ms" with a couple of config lines, and you can plug your own observer in to push this to your metrics system. The measurement point is in one place, so your instrumentation and your business logic stay separate. For debugging machine behaviour at 2am this has saved me more than once.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Where it is the wrong tool
&lt;/h2&gt;

&lt;p&gt;I do not want to waste your time, so here is where I would tell you &lt;em&gt;not&lt;/em&gt; to use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU-bound work that needs every core.&lt;/strong&gt; One pump is one core. You can push heavy compute to the pool, but if raw parallel number-crunching is the actual goal, use a real parallel tool, not this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard real-time microsecond paths.&lt;/strong&gt; The cooperative round is your latency floor. If you need guaranteed microsecond response, this is not it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A third-party blocking loop you cannot break up.&lt;/strong&gt; You can isolate it with &lt;code&gt;SubmitAsync&lt;/code&gt;, but if your whole program is one big blocking library call, there is not much for uniflow to structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It shines for ordered, async-ish control logic with a mix of sync and async steps: equipment and motion control, protocol handlers (connect / handshake / send / reconnect), backend job pipelines (pull from queue / validate / call API / retry / store), simulations with many agents over shared state. Anything where the honest description is "a sequence of steps, some of which wait."&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares, quickly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vs C++20 coroutines:&lt;/strong&gt; coroutines are great but they are a language feature, so they enforce no structure. How you split a coroutine is up to each developer's taste, so over time it fragments again, same as before. They also need C++20. uniflow is C++17 and enforces the flow/task shape. They also compose fine, you can &lt;code&gt;co_await&lt;/code&gt; inside a step if you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vs Boost.Asio:&lt;/strong&gt; Asio is powerful but adopting it means restructuring your code onto &lt;code&gt;io_context&lt;/code&gt; / &lt;code&gt;awaitable&lt;/code&gt; / executors, and it is shaped for sockets. uniflow is one header, no deps, and it does not absorb your existing objects. It only structures the &lt;em&gt;logic&lt;/em&gt; that drives them. You can still use Asio for the actual I/O inside a step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vs an actor framework:&lt;/strong&gt; this stays plain C++ objects with no message-passing ceremony, and it drops into existing code without a rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vs Boost.SML / statecharts:&lt;/strong&gt; those are transition-table DSLs. uniflow is just plain functions returning a result, so normal control flow, breakpoints, and debugging all work the way you expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest closing
&lt;/h2&gt;

&lt;p&gt;Quick facts again: one header, zero dependencies, C++17, builds on MSVC / GCC / Clang. Ported to Python and C# with matching APIs. There are six demos (a virtual CNC line, a city of cars on one thread, a time-scaling simulator, and so on), each provided identically in all three languages. MIT licensed.&lt;/p&gt;

&lt;p&gt;Here is the real reason it is MIT and the real reason I wrote it all out. After 14 years I have a pretty firm opinion: working-but-messy code is worse than clean code that does not run yet. Code that does not run gets fixed fast and has room to grow. Messy code that happens to work just grinds down your good engineers, while the people who cannot do better wall off their corner and call it job security. It becomes a kind of tumor in the team. I have watched it happen more times than I can count, and that eight-year single-file guy is just the clearest example.&lt;/p&gt;

&lt;p&gt;uniflow is overkill for a tiny bit of logic, sure. But unless it is a project you will own alone forever, use &lt;em&gt;some&lt;/em&gt; methodology, it does not have to be mine, and ideally one that can be enforced a little, so the shape of the code does not rot as it grows. That is the entire reason this exists. I would rather hand the idea out and let people argue with it than keep it sitting in my repo.&lt;/p&gt;

&lt;p&gt;So: tell me where it breaks. Tell me where it is the wrong call. I will be in the comments answering, and honestly the technical pushback is the part I am here for.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/splendidz/uniflow" rel="noopener noreferrer"&gt;https://github.com/splendidz/uniflow&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>architecture</category>
      <category>vibecoding</category>
      <category>fsm</category>
    </item>
  </channel>
</rss>
