<?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: Alisson Rosa</title>
    <description>The latest articles on DEV Community by Alisson Rosa (@neochaotic).</description>
    <link>https://dev.to/neochaotic</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%2F3966804%2F90ddac9a-4a01-4e29-a602-652f4b934c50.jpg</url>
      <title>DEV Community: Alisson Rosa</title>
      <link>https://dev.to/neochaotic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neochaotic"/>
    <language>en</language>
    <item>
      <title>Leoflow Lite: a local development environment for Apache Airflow</title>
      <dc:creator>Alisson Rosa</dc:creator>
      <pubDate>Wed, 01 Jul 2026 11:54:06 +0000</pubDate>
      <link>https://dev.to/neochaotic/leoflow-lite-a-local-development-environment-for-apache-airflow-4p8g</link>
      <guid>https://dev.to/neochaotic/leoflow-lite-a-local-development-environment-for-apache-airflow-4p8g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;strong&gt;Leoflow Lite is a local development environment for Apache Airflow.&lt;/strong&gt;&lt;br&gt;
Run &lt;code&gt;leoflow lite&lt;/code&gt; and you get a Docker-free, Kubernetes-free local loop that&lt;br&gt;
compiles and runs &lt;strong&gt;standard Airflow 3.2 DAGs&lt;/strong&gt; — real provider operators, sensors,&lt;br&gt;
connections, variables, XCom — with &lt;strong&gt;hot-reload on save&lt;/strong&gt; and the &lt;strong&gt;Airflow UI&lt;/strong&gt;,&lt;br&gt;
and it picks up your &lt;strong&gt;local GCP / AWS / Azure credentials&lt;/strong&gt; automatically. Install,&lt;br&gt;
point it at a DAG, watch it run.&lt;/p&gt;
&lt;/blockquote&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%2Fi11le8dkw8ewhlogobqn.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%2Fi11le8dkw8ewhlogobqn.png" alt="Leoflow Lite — the real Apache Airflow 3.2 UI running locally: the LITE badge, an all-green pipeline run, and an in-browser IDE button, with no Docker and no Kubernetes" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;If you write Apache Airflow DAGs, you know the local-dev pain: a &lt;code&gt;docker-compose&lt;/code&gt; with&lt;br&gt;
a scheduler, a webserver, a metadata DB, a Redis, and a worker — minutes to boot,&lt;br&gt;
heavy to keep running, awkward to iterate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leoflow Lite is the opposite.&lt;/strong&gt; One command, no containers required, and you're&lt;br&gt;
editing a DAG and watching it run against the &lt;strong&gt;real Airflow UI&lt;/strong&gt;:&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="c"&gt;# install (pin the version on the sh side of the pipe)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh

leoflow lite &lt;span class="nt"&gt;--postgres&lt;/span&gt; managed     &lt;span class="c"&gt;# embedded Postgres, no Docker needed&lt;/span&gt;
&lt;span class="c"&gt;# → scaffolds a starter DAG, serves the Airflow 3.2 UI at http://localhost:8088,&lt;/span&gt;
&lt;span class="c"&gt;#   and hot-reloads on every save.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;strong&gt;standard &lt;code&gt;airflow.sdk&lt;/code&gt; DAGs&lt;/strong&gt; — no new DSL:&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;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.standard.operators.bash&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;greet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hi {{ ds }}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;

    &lt;span class="n"&gt;greet&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the file and the DAG reloads in the UI in a couple of seconds.&lt;/p&gt;

&lt;p&gt;Lite ships a &lt;strong&gt;simple in-browser editor for quick edits&lt;/strong&gt; — hit the &lt;strong&gt;IDE&lt;/strong&gt; button in&lt;br&gt;
the UI (bottom-right in the screenshot above) to tweak &lt;code&gt;dag.py&lt;/code&gt; / &lt;code&gt;leoflow.yaml&lt;/code&gt;&lt;br&gt;
without leaving the page. For real work, use &lt;strong&gt;your favorite IDE&lt;/strong&gt; (VS Code, PyCharm,&lt;br&gt;
Vim — whatever) on the files locally; either way, every save hot-reloads.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's supported
&lt;/h2&gt;

&lt;p&gt;Lite runs the same Airflow-compatible engine as Leoflow's production mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Airflow 3.2 DAGs&lt;/strong&gt; via &lt;code&gt;airflow.sdk&lt;/code&gt; — &lt;code&gt;@task&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;, schedules, trigger rules,
fan-in/fan-out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operators &amp;amp; sensors&lt;/strong&gt; — &lt;code&gt;BashOperator&lt;/code&gt;/&lt;code&gt;PythonOperator&lt;/code&gt; natively, and &lt;strong&gt;any
provider operator&lt;/strong&gt; (e.g. &lt;code&gt;SQLExecuteQueryOperator&lt;/code&gt;, cloud transfer operators) runs
for real; sensors too, including &lt;code&gt;mode='reschedule'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connections&lt;/strong&gt; — create them in the UI; they're delivered to the task as
&lt;code&gt;AIRFLOW_CONN_&amp;lt;ID&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables&lt;/strong&gt; — &lt;code&gt;{{ var.value.x }}&lt;/code&gt; and the Admin → Variables UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XCom&lt;/strong&gt; between tasks, Jinja templating, the run context (&lt;code&gt;{{ ds }}&lt;/code&gt;, …).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Your local cloud credentials just work
&lt;/h2&gt;

&lt;p&gt;Lite's subprocess executor runs each task as a &lt;strong&gt;local process under your user&lt;/strong&gt;,&lt;br&gt;
inheriting your environment and &lt;code&gt;$HOME&lt;/code&gt;. So whatever you've already signed into with&lt;br&gt;
your cloud's normal CLI is exactly what the task uses — authenticate once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GCP&lt;/strong&gt; — &lt;code&gt;gcloud auth application-default login&lt;/code&gt;
(&lt;a href="https://cloud.google.com/docs/authentication/application-default-credentials" rel="noopener noreferrer"&gt;Application Default Credentials&lt;/a&gt;),
or set &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS&lt;/strong&gt; — &lt;code&gt;aws configure&lt;/code&gt; or &lt;code&gt;aws sso login&lt;/code&gt;
(&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html" rel="noopener noreferrer"&gt;AWS CLI configuration&lt;/a&gt;),
or &lt;code&gt;AWS_PROFILE&lt;/code&gt; / &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; in the env.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure&lt;/strong&gt; — &lt;code&gt;az login&lt;/code&gt;
(&lt;a href="https://learn.microsoft.com/cli/azure/authenticate-azure-cli" rel="noopener noreferrer"&gt;sign in with the Azure CLI&lt;/a&gt;),
or &lt;code&gt;AZURE_*&lt;/code&gt; in the env.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then a task just uses the provider SDK — &lt;strong&gt;no Connection needed for local dev&lt;/strong&gt;:&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;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gcs_peek&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_buckets&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;list&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;              &lt;span class="c1"&gt;# leoflow.yaml: dependencies: [google-cloud-storage]
&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list_buckets&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;   &lt;span class="c1"&gt;# uses your local ADC
&lt;/span&gt;    &lt;span class="nf"&gt;list_buckets&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;storage.Client()&lt;/code&gt; resolves your &lt;code&gt;gcloud&lt;/code&gt; Application Default Credentials through&lt;br&gt;
Google's normal chain, exactly as it would in a script you run by hand — same idea&lt;br&gt;
for &lt;code&gt;boto3&lt;/code&gt; (AWS) and the Azure SDKs. Author a DAG that hits BigQuery, S3, or Blob&lt;br&gt;
Storage and run it against your &lt;strong&gt;real&lt;/strong&gt; accounts, locally, in the dev loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In production/cluster mode the same DAG gets its credentials from managed&lt;br&gt;
&lt;strong&gt;Connections&lt;/strong&gt; instead — pods don't inherit your laptop's env.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Leoflow Lite vs the usual local-Airflow setups
&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;
&lt;code&gt;docker-compose&lt;/code&gt; Airflow&lt;/th&gt;
&lt;th&gt;&lt;code&gt;astro dev&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Leoflow Lite&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker required&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;no&lt;/strong&gt; (&lt;code&gt;--postgres managed&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time&lt;/td&gt;
&lt;td&gt;minutes&lt;/td&gt;
&lt;td&gt;~a minute&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;seconds&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot-reload&lt;/td&gt;
&lt;td&gt;restart-ish&lt;/td&gt;
&lt;td&gt;restart-ish&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;on save&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real provider operators&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local cloud creds&lt;/td&gt;
&lt;td&gt;manual mounts&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;inherited automatically&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airflow UI&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes (3.2)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use Leoflow to develop Apache Airflow DAGs locally?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;leoflow lite&lt;/code&gt; is a local development environment for Airflow: write a standard&lt;br&gt;
&lt;code&gt;airflow.sdk&lt;/code&gt; DAG, run it, and iterate with hot-reload — no Docker or Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it run real Airflow operators and sensors?&lt;/strong&gt;&lt;br&gt;
Yes. Native operators (&lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;) run directly; any other provider operator or&lt;br&gt;
sensor runs the genuine Airflow class in the task. Reschedule-mode sensors are&lt;br&gt;
supported.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Airflow version is it compatible with?&lt;/strong&gt;&lt;br&gt;
Apache Airflow 3.2 — including the Airflow 3.2 web UI, which Lite serves locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Docker?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;leoflow lite --postgres managed&lt;/code&gt; uses an embedded Postgres and the subprocess&lt;br&gt;
executor. (If Docker is present it can use it; if Docker is wedged, Lite falls back to&lt;br&gt;
the managed Postgres automatically.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it use my existing GCP/AWS/Azure credentials?&lt;/strong&gt;&lt;br&gt;
Yes — the local task inherits your environment and &lt;code&gt;$HOME&lt;/code&gt;, so the provider SDKs find&lt;br&gt;
your credentials through their normal default chains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it production-ready / what about deploying?&lt;/strong&gt;&lt;br&gt;
Lite is for local development; the same DAGs deploy to Leoflow's Kubernetes mode (one&lt;br&gt;
pod per task) for production. Same authoring, two runtimes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh
leoflow lite &lt;span class="nt"&gt;--postgres&lt;/span&gt; managed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open source, Apache 2.0: &lt;strong&gt;&lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
If you've been running Airflow under &lt;code&gt;docker-compose&lt;/code&gt; just to develop a DAG, this is&lt;br&gt;
the lighter loop.&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How we parse Apache Airflow DAGs without importing Airflow</title>
      <dc:creator>Alisson Rosa</dc:creator>
      <pubDate>Mon, 29 Jun 2026 22:16:11 +0000</pubDate>
      <link>https://dev.to/neochaotic/how-we-parse-apache-airflow-dags-without-importing-airflow-9d</link>
      <guid>https://dev.to/neochaotic/how-we-parse-apache-airflow-dags-without-importing-airflow-9d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Leoflow runs a Go control plane that &lt;strong&gt;never imports Apache Airflow&lt;/strong&gt;,&lt;br&gt;
yet compiles standard &lt;code&gt;airflow.sdk&lt;/code&gt; DAGs. It does it with a &lt;strong&gt;structural shim&lt;/strong&gt;: a&lt;br&gt;
pure-stdlib stand-in for &lt;code&gt;airflow&lt;/code&gt; that the parser puts on the import path, then&lt;br&gt;
&lt;code&gt;exec&lt;/code&gt;s your &lt;code&gt;dag.py&lt;/code&gt; to &lt;em&gt;record&lt;/em&gt; the graph (without running task bodies or&lt;br&gt;
installing a single provider). Arbitrary provider operators are &lt;strong&gt;captured by&lt;br&gt;
class + kwargs&lt;/strong&gt; at compile time and run &lt;strong&gt;for real in the task pod&lt;/strong&gt; at runtime.&lt;br&gt;
The parser drops from &lt;strong&gt;262 MB / 136 packages&lt;/strong&gt; to &lt;strong&gt;~44 KB / zero dependencies&lt;/strong&gt;.&lt;br&gt;
This is the engineering behind Leoflow v0.1.0.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Where the shim sits
&lt;/h2&gt;

&lt;p&gt;Before the trick, the shape of the whole system. The shim is one small box —&lt;br&gt;
the &lt;strong&gt;parser&lt;/strong&gt;, at compile time — and &lt;em&gt;everything downstream of &lt;code&gt;dag.json&lt;/code&gt; is Go,&lt;/em&gt;&lt;br&gt;
with the real Airflow operator only ever appearing inside the task's pod:&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%2Fl00kjdgr0u5mshbjgeop.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%2Fl00kjdgr0u5mshbjgeop.png" alt="Leoflow architecture — leoflow compile (parser + shim) produces dag.json + image; a Go control plane (API, scheduler, executor router) dispatches one pod per task; the worker pod runs the agent over gRPC with the real provider operator; Postgres holds metadata, Redis on Pro" width="798" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep that picture in mind: the &lt;strong&gt;only&lt;/strong&gt; place Python (and Airflow) lives is the&lt;br&gt;
parser sidecar and the worker pod. The scheduling path in the middle is pure Go.&lt;/p&gt;
&lt;h2&gt;
  
  
  The constraint that forces the design
&lt;/h2&gt;

&lt;p&gt;Leoflow's scheduler is Go — no GIL, no Python in the hot path (that's the whole&lt;br&gt;
point: Airflow's Python control plane is what makes it slow). But a Leoflow DAG is a&lt;br&gt;
&lt;strong&gt;standard Apache Airflow 3.2 DAG&lt;/strong&gt;, written against &lt;code&gt;airflow.sdk&lt;/code&gt;:&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;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.standard.operators.bash&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[1,2,3]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; /tmp/raw.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&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;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/raw.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

    &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: &lt;strong&gt;how does a control plane that never imports Airflow read a DAG written against&lt;br&gt;
the Airflow SDK?&lt;/strong&gt; Importing real Airflow into the parser would drag in the GIL, the&lt;br&gt;
dependency tree, and parse-time side effects — exactly what we're escaping. The&lt;br&gt;
answer (ADR 0024) is to not import Airflow at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  The weight you don't carry
&lt;/h2&gt;

&lt;p&gt;This isn't a micro-optimization. We measured the "just import the SDK" path —&lt;br&gt;
&lt;code&gt;pip install ./parser apache-airflow-task-sdk&lt;/code&gt; on Python 3.12:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parser dependency&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Third-party packages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real &lt;code&gt;apache-airflow-task-sdk&lt;/code&gt; (→ pulls &lt;code&gt;apache-airflow-core&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;262 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;136&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leoflow's structural shim&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~44 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 262 MB is grpc, babel, cryptography, sqlalchemy, libcst, pydantic,&lt;br&gt;
opentelemetry, aiohttp… &lt;strong&gt;none&lt;/strong&gt; of which a &lt;em&gt;parser&lt;/em&gt; uses — it constructs DAG and&lt;br&gt;
operator objects and reads a handful of attributes. And it can't be trimmed:&lt;br&gt;
&lt;code&gt;task-sdk → core&lt;/code&gt;, and &lt;code&gt;providers-standard/http → apache-airflow (meta) → core&lt;/code&gt;.&lt;br&gt;
Dropping Airflow makes the parser &lt;strong&gt;pure Python and small enough to embed in the Go&lt;br&gt;
binary&lt;/strong&gt; — no parser venv, no &lt;code&gt;pip&lt;/code&gt; at install time, no Airflow-version coupling.&lt;/p&gt;

&lt;p&gt;To be precise: &lt;strong&gt;this is the &lt;em&gt;parser's&lt;/em&gt; weight, not the whole system's.&lt;/strong&gt; The real&lt;br&gt;
task SDK and a DAG's providers &lt;em&gt;do&lt;/em&gt; get installed — in the &lt;strong&gt;task image, per DAG&lt;/strong&gt;&lt;br&gt;
(&lt;code&gt;pip install&lt;/code&gt; at build time, or the Lite venv), because that's where the operator&lt;br&gt;
actually runs them. Leoflow doesn't &lt;em&gt;delete&lt;/em&gt; that weight; it moves it &lt;strong&gt;off the&lt;br&gt;
scheduling hot path&lt;/strong&gt; (the Go control plane never installs or imports Airflow) and&lt;br&gt;
&lt;strong&gt;splits it per DAG&lt;/strong&gt; (each image carries only its own providers — never one fat&lt;br&gt;
shared worker for all 1,500). The parser is the part that gets to be ~44 KB.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shim: a structural stand-in for &lt;code&gt;airflow&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The parser ships a &lt;strong&gt;pure-standard-library&lt;/strong&gt; package that &lt;em&gt;looks&lt;/em&gt; like &lt;code&gt;airflow&lt;/code&gt; —&lt;br&gt;
same import paths, same attribute surface the compiler reads — and &lt;strong&gt;nothing else&lt;/strong&gt;.&lt;br&gt;
It's put ahead of any real Airflow on the import path, and then the parser simply&lt;br&gt;
&lt;strong&gt;exec's your &lt;code&gt;dag.py&lt;/code&gt;&lt;/strong&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;runpy&lt;/span&gt;
&lt;span class="n"&gt;runpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dag.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__leoflow_dag__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# `airflow` resolves to the shim
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the file &lt;em&gt;builds structure&lt;/em&gt;. Here's the core of the shim (paraphrased):&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;_CURRENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;     &lt;span class="c1"&gt;# stack of DAGs being defined
&lt;/span&gt;&lt;span class="n"&gt;COLLECTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;    &lt;span class="c1"&gt;# dag_id -&amp;gt; DAG, filled as each DAG context is entered
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;COLLECTED&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__enter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="n"&gt;_CURRENT&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="n"&gt;self&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;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__exit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;_CURRENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseOperator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upstream_task_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;downstream_task_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&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="c1"&gt;# attach to the active DAG and store every kwarg as an attribute
&lt;/span&gt;        &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kwargs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_CURRENT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&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="n"&gt;_CURRENT&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&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;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__rshift__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;    &lt;span class="c1"&gt;# a &amp;gt;&amp;gt; b records the edge
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;downstream_task_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upstream_task_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&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;other&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fuq5sd8zku553e6e3ria2.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%2Fuq5sd8zku553e6e3ria2.png" alt="The shim flow — dag.py is exec'd under a structural stand-in for airflow; DAG/operators register into COLLECTED, which the compiler turns into dag.json" width="276" height="838"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;with DAG(...)&lt;/code&gt; registers; constructing an operator attaches it to the active DAG and&lt;br&gt;
stores its kwargs; &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; records edges; &lt;code&gt;@task&lt;/code&gt; builds the node but &lt;strong&gt;never runs the&lt;br&gt;
body&lt;/strong&gt;. The compiler then reads &lt;code&gt;COLLECTED&lt;/code&gt; — exactly the attributes it needs&lt;br&gt;
(&lt;code&gt;dag_id&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;task_dict&lt;/code&gt;, and per task &lt;code&gt;task_id&lt;/code&gt;, &lt;code&gt;upstream_task_ids&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;trigger_rule&lt;/code&gt;, &lt;code&gt;python_callable&lt;/code&gt;, &lt;code&gt;op_args&lt;/code&gt;/&lt;code&gt;op_kwargs&lt;/code&gt;, &lt;code&gt;bash_command&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;endpoint&lt;/code&gt;/&lt;code&gt;method&lt;/code&gt;) — and emits an immutable &lt;code&gt;dag.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two properties fall straight out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unsupported constructs can't be faked.&lt;/strong&gt; A &lt;code&gt;from airflow.&amp;lt;thing&amp;gt;&lt;/code&gt; the shim doesn't
model raises &lt;code&gt;ModuleNotFoundError&lt;/code&gt;, which the loader turns into a clear &lt;em&gt;"not
supported by Leoflow"&lt;/em&gt; error — at compile time, never a silent half-run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parsing has no side effects.&lt;/strong&gt; &lt;code&gt;@task&lt;/code&gt; bodies never execute during parsing, so a
DAG file can't trigger its own work just by being read — the thing that makes
Airflow's dag-parsing both slow and risky.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The control plane now has the graph &lt;strong&gt;without importing Airflow or installing one&lt;br&gt;
provider&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The long tail: capture, don't reimplement
&lt;/h2&gt;

&lt;p&gt;Modeling all &lt;strong&gt;1,500+&lt;/strong&gt; provider operators in the shim would be a treadmill. So for&lt;br&gt;
anything beyond the native handful (&lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;empty&lt;/code&gt;), the shim has a&lt;br&gt;
&lt;strong&gt;meta-path finder&lt;/strong&gt; (ADR 0040) that synthesizes &lt;em&gt;any&lt;/em&gt;&lt;br&gt;
&lt;code&gt;airflow.providers.&amp;lt;x&amp;gt;.{operators,sensors,transfers}.&amp;lt;Class&amp;gt;&lt;/code&gt; on demand. It doesn't&lt;br&gt;
implement the operator — it &lt;strong&gt;captures&lt;/strong&gt; it: records the operator's &lt;strong&gt;real dotted&lt;br&gt;
class path&lt;/strong&gt; and its &lt;strong&gt;constructor kwargs&lt;/strong&gt;, then registers it like any node:&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="c1"&gt;# in the dag.py — a provider operator the shim has never heard of
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.common.sql.operators.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQLExecuteQueryOperator&lt;/span&gt;
&lt;span class="nc"&gt;SQLExecuteQueryOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rollup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insert into ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# captured as: { class: "airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator",
#                kwargs: { conn_id: "warehouse", sql: "insert into ..." } }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No provider is installed in the parser. The dotted path and kwargs are just data in&lt;br&gt;
&lt;code&gt;dag.json&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the shim won't do (on purpose)
&lt;/h2&gt;

&lt;p&gt;The shim does &lt;em&gt;all&lt;/em&gt; the structural parsing — but it draws three deliberate lines, and&lt;br&gt;
each one is a feature, not a gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It never runs your task bodies.&lt;/strong&gt; &lt;code&gt;@task&lt;/code&gt; and operators only &lt;em&gt;build structure&lt;/em&gt; at
parse time; the code inside a task runs later, in the pod. Reading a DAG can't
trigger its work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't capture hooks.&lt;/strong&gt; &lt;code&gt;airflow.providers.*.hooks.*&lt;/code&gt; is intentionally &lt;em&gt;not&lt;/em&gt;
synthesized — a hook is a runtime client (it opens real connections), so it belongs
inside a &lt;code&gt;@task&lt;/code&gt; body that runs in the pod, never at parse time. Operators and
sensors are captured; hooks are left to the runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It rejects what it can't model, loudly.&lt;/strong&gt; Anything outside the supported surface
and the generic provider path — an unknown &lt;code&gt;from airflow.&amp;lt;thing&amp;gt;&lt;/code&gt;, or a file with no
&lt;code&gt;dag_id&lt;/code&gt; / multiple DAGs — fails at compile time with a precise message, instead of
being silently mis-parsed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;em&gt;"does the shim do everything?"&lt;/em&gt; — it does everything &lt;strong&gt;structural&lt;/strong&gt;, and&lt;br&gt;
deliberately hands &lt;strong&gt;execution&lt;/strong&gt; (and hooks) to the runtime. That boundary &lt;em&gt;is&lt;/em&gt; the&lt;br&gt;
design.&lt;/p&gt;
&lt;h2&gt;
  
  
  The seam: the &lt;em&gt;real&lt;/em&gt; operator runs in the pod
&lt;/h2&gt;

&lt;p&gt;At runtime, inside the task's own pod — where the provider &lt;em&gt;is&lt;/em&gt; installed, baked into&lt;br&gt;
that DAG's image — the agent reconstructs and runs the genuine operator:&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="nf"&gt;import_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dotted_class&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;captured_kwargs&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;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real Airflow operator executes, with the real provider, against the real&lt;br&gt;
connection — while the control plane that scheduled it never imported either. Here is&lt;br&gt;
the whole life of a DAG, with the shim wired to every component it touches across the&lt;br&gt;
three phases:&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%2Frkk6t22pduj1ti6gots2.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%2Frkk6t22pduj1ti6gots2.png" alt="Drill-down — compile time: dag.py is exec'd under the shim (_core for supported ops, _generic captures provider operators as class + kwargs) while leoflow.yaml connectors pip-install the real providers into the DAG image; the shim emits dag.json. Schedule time: the Go control plane reads dag.json and dispatches a pod, never parsing. Run time: the worker pod's agent does import_string(class)(kwargs).execute(context) — the captured class + kwargs arrive via the task spec, the real provider is present in the image" width="800" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read it left to right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compile time (no Airflow).&lt;/strong&gt; The shim's &lt;code&gt;_core&lt;/code&gt; handles the supported ops;
&lt;code&gt;_generic&lt;/code&gt; captures every provider operator as &lt;code&gt;class + kwargs&lt;/code&gt;. In parallel,
&lt;code&gt;connectors:&lt;/code&gt; in &lt;code&gt;leoflow.yaml&lt;/code&gt; &lt;code&gt;pip install&lt;/code&gt;s the &lt;em&gt;real&lt;/em&gt; providers into the DAG's
image. Out comes &lt;code&gt;dag.json&lt;/code&gt; (graph + task types + captured class/kwargs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule time (Go).&lt;/strong&gt; The control plane consumes &lt;code&gt;dag.json&lt;/code&gt; and dispatches a pod
per task. It &lt;strong&gt;never parses&lt;/strong&gt; a DAG — it reads an immutable artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run time (the pod).&lt;/strong&gt; The agent reconstructs the operator from the captured
&lt;code&gt;class + kwargs&lt;/code&gt; (delivered in the task spec) and &lt;code&gt;execute()&lt;/code&gt;s it — and the real
provider is right there in the image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Compile time: structure, dependency-free, in Go's world. Run time: the real Airflow&lt;br&gt;
operator, in an isolated pod.&lt;/strong&gt; That split is the entire design — it's how you get&lt;br&gt;
Airflow's ecosystem fidelity without Airflow's control plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fidelity: golden tests are the contract
&lt;/h2&gt;

&lt;p&gt;A shim is only safe if it produces &lt;em&gt;exactly&lt;/em&gt; what real Airflow would. So fidelity is&lt;br&gt;
pinned by &lt;strong&gt;golden tests&lt;/strong&gt;: for every shipped example, the shim's structural output is&lt;br&gt;
asserted &lt;strong&gt;byte-equal to the real Airflow-based compiler's&lt;/strong&gt; output. Drift is caught&lt;br&gt;
in CI &lt;strong&gt;without installing Airflow&lt;/strong&gt; (the golden corpus is regenerated from the real&lt;br&gt;
compiler only when the supported surface changes). When we first built it, those&lt;br&gt;
golden diffs caught two real fidelity gaps — duplicate &lt;code&gt;task_id&lt;/code&gt; auto-suffixing and&lt;br&gt;
list fan-in — which the shim now handles. There's also an escape hatch:&lt;br&gt;
&lt;code&gt;LEOFLOW_PARSER_BACKEND=airflow&lt;/code&gt; runs the real &lt;code&gt;DagBag&lt;/code&gt; for side-by-side diffing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No GIL, no Airflow imports in scheduling&lt;/strong&gt; — the control plane stays fast and
Go-native.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dependency hell&lt;/strong&gt; — each DAG owns its image; the parser needs zero providers,
and 136 transitive packages leave your supply-chain surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No parse-time surprises&lt;/strong&gt; — reading a DAG can't run it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full operator fidelity&lt;/strong&gt; — the actual provider operator runs in the pod, guarded
by golden tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's all open source (Apache 2.0): &lt;strong&gt;&lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
ADR 0024 (the shim) and ADR 0040 (operator capture) have the gory details.&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>python</category>
      <category>go</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Leoflow v0.1.0 — run your Airflow DAGs on a Go control plane (no Airflow in the hot path)</title>
      <dc:creator>Alisson Rosa</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:09:06 +0000</pubDate>
      <link>https://dev.to/neochaotic/leoflow-v010-run-your-airflow-dags-on-a-go-control-plane-no-airflow-in-the-hot-path-1i7c</link>
      <guid>https://dev.to/neochaotic/leoflow-v010-run-your-airflow-dags-on-a-go-control-plane-no-airflow-in-the-hot-path-1i7c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Leoflow &lt;code&gt;v0.1.0&lt;/code&gt; is the first stable release. You write &lt;strong&gt;standard&lt;br&gt;
Apache Airflow 3.2 DAGs in Python&lt;/strong&gt;; Leoflow's parser turns them into an immutable&lt;br&gt;
&lt;code&gt;dag.json&lt;/code&gt; using a &lt;strong&gt;structural shim&lt;/strong&gt; that imports &lt;em&gt;zero&lt;/em&gt; Airflow, the scheduler&lt;br&gt;
(Go, no GIL) runs &lt;strong&gt;one pod per task&lt;/strong&gt;, and the real Airflow provider operator&lt;br&gt;
executes &lt;em&gt;in the pod&lt;/em&gt; — so the control plane never imports Airflow, but your tasks&lt;br&gt;
get full provider fidelity. Ships with &lt;strong&gt;86 connection types&lt;/strong&gt;, &lt;strong&gt;generic provider&lt;br&gt;
operators + sensors&lt;/strong&gt; (including reschedule-mode), and a &lt;strong&gt;Docker-free local Lite&lt;br&gt;
edition&lt;/strong&gt; that self-heals. GitHub: &lt;strong&gt;&lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The bet
&lt;/h2&gt;

&lt;p&gt;Airflow's execution model is right: a DAG of tasks, each task in its own pod&lt;br&gt;
(&lt;code&gt;KubernetesExecutor&lt;/code&gt; proved it). What's slow is the &lt;strong&gt;Python control plane&lt;/strong&gt; — a&lt;br&gt;
scheduler that imports your DAGs (and all their dependencies) into a GIL-bound&lt;br&gt;
process, re-parses them constantly, and turns "add a provider" into a dependency-hell&lt;br&gt;
negotiation across every DAG.&lt;/p&gt;

&lt;p&gt;Leoflow keeps the model and rewrites the control plane in &lt;strong&gt;Go&lt;/strong&gt;. No GIL. No Airflow&lt;br&gt;
in the scheduling path. Each DAG is its &lt;strong&gt;own container image&lt;/strong&gt;, so dependencies are&lt;br&gt;
the DAG's problem, not the platform's. And the public API speaks &lt;strong&gt;Airflow 3.2&lt;/strong&gt;, so&lt;br&gt;
the &lt;strong&gt;real Airflow UI&lt;/strong&gt; runs on top, unmodified.&lt;/p&gt;

&lt;p&gt;The catch: if the control plane is Go and never imports Airflow, how does it read a&lt;br&gt;
DAG written against the Airflow SDK? That's the shim — and it's the most interesting&lt;br&gt;
piece of v0.1.0.&lt;/p&gt;

&lt;p&gt;Here's the shape of it — your DAG becomes an immutable artifact, and a Go control&lt;br&gt;
plane schedules it onto a pod per task:&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%2Fc6uliuc9cpbdz8gt6haz.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%2Fc6uliuc9cpbdz8gt6haz.png" alt="Leoflow architecture — dag.py compiles to dag.json + image, a Go control plane schedules one pod per task" width="800" height="99"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  You write real Airflow DAGs
&lt;/h2&gt;

&lt;p&gt;No new DSL. This is a Leoflow DAG:&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;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.standard.operators.bash&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[1, 2, 3]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; /tmp/raw.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&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;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/raw.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

    &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;transform&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;@task&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;schedule&lt;/code&gt;, trigger rules, fan-in/fan-out, &lt;code&gt;PythonOperator&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;BashOperator&lt;/code&gt; — the constructs you already know. You compile it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;leoflow compile ./sales   &lt;span class="c"&gt;# → dag.json + a container image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How the shim works (&lt;code&gt;dag.py → dag.json&lt;/code&gt;, no Airflow imported)
&lt;/h2&gt;

&lt;p&gt;Here's the trick (ADR 0024). The parser &lt;strong&gt;exec's your &lt;code&gt;dag.py&lt;/code&gt;&lt;/strong&gt; — but with a&lt;br&gt;
&lt;strong&gt;structural stand-in for &lt;code&gt;airflow&lt;/code&gt;&lt;/strong&gt; on the import path. Pure standard library,&lt;br&gt;
zero third-party deps. It reproduces &lt;em&gt;exactly&lt;/em&gt; the attribute surface the compiler&lt;br&gt;
reads, and nothing else:&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%2Fuq5sd8zku553e6e3ria2.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%2Fuq5sd8zku553e6e3ria2.png" alt="The shim flow — dag.py is exec'd under a structural stand-in for airflow; DAG/operators register into COLLECTED, which the compiler turns into dag.json" width="276" height="838"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two consequences fall straight out of this design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An unsupported construct can't be faked.&lt;/strong&gt; A &lt;code&gt;from airflow.providers.foo...&lt;/code&gt;
that the shim doesn't model raises &lt;code&gt;ModuleNotFoundError&lt;/code&gt; — which the loader turns
into a clear &lt;em&gt;"not supported by Leoflow"&lt;/em&gt; error at compile time, never a silent
half-run. Loud beats subtle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task bodies never execute during parsing.&lt;/strong&gt; &lt;code&gt;@task&lt;/code&gt; calls only build the graph.
Parsing a DAG can't trigger its side effects — the thing that makes Airflow's
DAG-parsing both slow and dangerous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the control plane gets the graph &lt;strong&gt;without importing Airflow or installing a&lt;br&gt;
single provider&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  …but the &lt;em&gt;real&lt;/em&gt; operator runs in the pod
&lt;/h3&gt;

&lt;p&gt;Airflow's ecosystem is &lt;strong&gt;1,500+ operators&lt;/strong&gt;; modeling each in the shim would be a&lt;br&gt;
treadmill. So Leoflow splits them (ADR 0040):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A native fast path&lt;/strong&gt; for the hottest few — &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;empty&lt;/code&gt; —
which Leoflow runs with its &lt;em&gt;own&lt;/em&gt; Go/runtime code. No Airflow in the pod at all;
this is the "no Python in the hot path" part. A deliberate, growing whitelist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A generic path&lt;/strong&gt; for the long tail. The shim's &lt;strong&gt;meta-path finder&lt;/strong&gt; synthesizes
&lt;em&gt;any&lt;/em&gt; &lt;code&gt;airflow.providers.&amp;lt;x&amp;gt;.{operators,sensors,transfers}.&amp;lt;Class&amp;gt;&lt;/code&gt; on demand and
&lt;strong&gt;captures&lt;/strong&gt; it — recording the operator's &lt;strong&gt;real dotted class path&lt;/strong&gt; and its
&lt;strong&gt;constructor kwargs&lt;/strong&gt;, without the provider installed in the parser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, at runtime, inside the task's own pod (where the provider &lt;em&gt;is&lt;/em&gt; installed, via&lt;br&gt;
the image), the agent does essentially:&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="nf"&gt;import_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dotted_class&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;captured_kwargs&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;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The genuine Airflow operator runs, with the genuine provider, in an isolated pod —&lt;br&gt;
while the control plane that scheduled it never imported either. &lt;strong&gt;Compile-time:&lt;br&gt;
structure, dependency-free. Run-time: the real thing, in a pod.&lt;/strong&gt; That seam is the&lt;br&gt;
whole design.&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%2Fhjtlbuoz9uhxmze6figl.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%2Fhjtlbuoz9uhxmze6figl.png" alt="Native fast path vs generic capture — bash/python/http run natively; everything else is captured by class+kwargs and the real operator runs in the pod" width="586" height="675"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Operators, sensors &amp;amp; 86 connectors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A provider operator is just an import.&lt;/strong&gt; Anything outside the native fast path is&lt;br&gt;
captured at compile time and runs &lt;em&gt;for real&lt;/em&gt; in the pod — e.g. a SQL rollup against&lt;br&gt;
your warehouse, its &lt;code&gt;conn_id&lt;/code&gt; resolving to a managed connection:&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;airflow.providers.common.sql.operators.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQLExecuteQueryOperator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rollup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&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;example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nc"&gt;SQLExecuteQueryOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_rollup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# a managed Connection (created in the UI)
&lt;/span&gt;        &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insert into rollup select day, count(*) from events group by day&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;BashOperator&lt;/code&gt;/&lt;code&gt;PythonOperator&lt;/code&gt;/&lt;code&gt;HttpOperator&lt;/code&gt; are the &lt;em&gt;native&lt;/em&gt; path — Leoflow runs&lt;br&gt;
those itself, no Airflow in the pod. Everything else takes the generic path above.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Run it locally:&lt;/strong&gt; in &lt;code&gt;leoflow lite&lt;/code&gt;, add a &lt;code&gt;warehouse&lt;/code&gt; Postgres connection&lt;br&gt;
(Admin → Connections) plus &lt;code&gt;events&lt;/code&gt;/&lt;code&gt;rollup&lt;/code&gt; tables, then trigger the DAG — the&lt;br&gt;
real &lt;code&gt;SQLExecuteQueryOperator&lt;/code&gt; resolves the connection and writes the rollup. This&lt;br&gt;
exact path is validated end-to-end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Providers as a one-liner.&lt;/strong&gt; A DAG declares what it needs; &lt;code&gt;connectors:&lt;/code&gt; is sugar&lt;br&gt;
(ADR 0038) that expands to the &lt;code&gt;apache-airflow-providers-*&lt;/code&gt; packages and bakes them&lt;br&gt;
into &lt;em&gt;that DAG's&lt;/em&gt; image — no shared worker, no platform-wide dependency vote:&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;# leoflow.yaml&lt;/span&gt;
&lt;span class="na"&gt;dag_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sales&lt;/span&gt;
&lt;span class="na"&gt;connectors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# → providers baked into THIS dag's image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;86 connection types&lt;/strong&gt;, generated from real Airflow (ADR 0039) so the connection&lt;br&gt;
forms match field-for-field, are available in the UI. A managed connection is&lt;br&gt;
delivered to the task pod as &lt;code&gt;AIRFLOW_CONN_&amp;lt;ID&amp;gt;&lt;/code&gt; — the credential never lives in the&lt;br&gt;
image:&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="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
    &lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AIRFLOW_CONN_PG_TARGET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# a managed Connection, injected in-pod
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO cats VALUES (%s, %s)&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sensors, including reschedule mode.&lt;/strong&gt; A &lt;code&gt;mode='reschedule'&lt;/code&gt; sensor &lt;strong&gt;releases its&lt;br&gt;
pod&lt;/strong&gt; between checks:&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;airflow.providers.standard.sensors.date_time&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DateTimeSensor&lt;/span&gt;

&lt;span class="nc"&gt;DateTimeSensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_until_six&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{ ds }}T06:00:00+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reschedule&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;A sensor waiting six hours isn't holding a pod for six hours: each not-ready poke&lt;br&gt;
surfaces &lt;code&gt;up_for_reschedule&lt;/code&gt;, frees the pod, and is re-dispatched when it's time to&lt;br&gt;
check again.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lite: zero to a local grid, Docker-free
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;leoflow lite&lt;/code&gt; is the local edition — no Kubernetes, no cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;leoflow lite &lt;span class="nt"&gt;--postgres&lt;/span&gt; managed     &lt;span class="c"&gt;# embedded Postgres, no Docker required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It scaffolds a starter DAG, brings up an embedded Postgres, starts the control&lt;br&gt;
plane, and serves the &lt;strong&gt;Airflow 3.2 UI&lt;/strong&gt; at &lt;code&gt;localhost:8088&lt;/code&gt; — hot-reloading on every&lt;br&gt;
save. v0.1.0 hardened it to be resilient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker wedged? It keeps working.&lt;/strong&gt; If the Docker daemon is present but
unresponsive, Lite falls back to the managed (Docker-free) Postgres instead of
failing on &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It self-heals its state.&lt;/strong&gt; Reusing a metadata DB used to leave "ghost" DAGs and
stale import errors you couldn't remove from the UI. Now, on boot, Lite reconciles
the registered DAGs against your workspace — deregistering what's gone and clearing
orphan import errors — fail-safe (it never wipes on an unreachable control plane).
It's gated by an end-to-end CI test so it can't silently regress.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Leoflow vs an Airflow control plane
&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;Airflow&lt;/th&gt;
&lt;th&gt;Leoflow v0.1.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;td&gt;Python (GIL)&lt;/td&gt;
&lt;td&gt;Go (no GIL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG parsing&lt;/td&gt;
&lt;td&gt;imports Airflow + your deps; bodies can run&lt;/td&gt;
&lt;td&gt;structural shim, zero deps, bodies never run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider deps&lt;/td&gt;
&lt;td&gt;shared, platform-wide&lt;/td&gt;
&lt;td&gt;per-DAG image (&lt;code&gt;connectors:&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator fidelity&lt;/td&gt;
&lt;td&gt;real&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;real&lt;/strong&gt; (runs in the pod via captured class+kwargs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task isolation&lt;/td&gt;
&lt;td&gt;pod-per-task (K8s executor)&lt;/td&gt;
&lt;td&gt;pod-per-task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG artifact&lt;/td&gt;
&lt;td&gt;mutable in the dagbag&lt;/td&gt;
&lt;td&gt;immutable &lt;code&gt;dag.json&lt;/code&gt; + image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Airflow UI&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;same&lt;/strong&gt; Airflow 3.2 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local dev&lt;/td&gt;
&lt;td&gt;needs the stack&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;leoflow lite&lt;/code&gt;, Docker-free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Why use it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You already write Airflow DAGs&lt;/strong&gt; — keep them. The shim reads standard
&lt;code&gt;airflow.sdk&lt;/code&gt;; your operators run for real in the pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're tired of dependency hell&lt;/strong&gt; — each DAG owns its image; adding a provider to
one DAG never touches another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the control plane off the critical path&lt;/strong&gt; — Go, no GIL, no DAG imports,
no parse-time side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the Airflow UI without the Airflow scheduler&lt;/strong&gt; — v0.1.0 serves the real
3.2 UI on a Go core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a real local loop&lt;/strong&gt; — &lt;code&gt;leoflow lite&lt;/code&gt;, no Kubernetes, that doesn't fall
over when your Docker does.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Status
&lt;/h2&gt;

&lt;p&gt;v0.1.0 is the first &lt;strong&gt;stable&lt;/strong&gt; release (the &lt;code&gt;v0.1.0-rc.N&lt;/code&gt; series soaked and promoted —&lt;br&gt;
SemVer carries the maturity; no alpha/beta). It ships the shim, 86 connectors,&lt;br&gt;
generic provider operators + sensors (reschedule included), the resilient Lite&lt;br&gt;
edition, and the embedded Airflow 3.2 UI. dbt-native rendering is next (v0.1.1).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it in 30 seconds:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh
leoflow lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt; — point&lt;br&gt;
&lt;code&gt;leoflow lite&lt;/code&gt; at a DAG and watch it light up the grid. Tell us where it bites.&lt;/p&gt;

&lt;p&gt;Apache 2.0. Thanks for reading.&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>go</category>
      <category>dataengineering</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>We rewrote Apache Airflow's control plane in Go (and kept the UI)</title>
      <dc:creator>Alisson Rosa</dc:creator>
      <pubDate>Wed, 03 Jun 2026 18:23:45 +0000</pubDate>
      <link>https://dev.to/neochaotic/we-rewrote-apache-airflows-control-plane-in-go-and-kept-the-ui-42l6</link>
      <guid>https://dev.to/neochaotic/we-rewrote-apache-airflows-control-plane-in-go-and-kept-the-ui-42l6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Leoflow &lt;code&gt;v0.0.1&lt;/code&gt; just shipped. It speaks the Airflow API, runs the Airflow 3.2.x UI &lt;strong&gt;unmodified&lt;/strong&gt;, but replaces the Python control plane with Go. Pod-per-task is the only execution mode. Each DAG is its own container image. Fan-in (map-reduce) is a Python list comprehension. Install: &lt;code&gt;curl -fsSL https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh&lt;/code&gt;. GitHub: &lt;strong&gt;&lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt; — stars and issues warmly accepted.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The 3 AM pager
&lt;/h2&gt;

&lt;p&gt;You know the one. The scheduler stalled again. Or the triggerer suffocated under 500 sensors. Or a worker leaked file descriptors until Kubernetes OOMKilled it mid-run. Or someone bumped &lt;code&gt;pandas&lt;/code&gt; for the new DAG and broke six legacy ones because they all share the same image.&lt;/p&gt;

&lt;p&gt;Apache Airflow is the most widely deployed workflow orchestrator on earth. It is also the one that bleeds the most in production. None of those wounds are bugs — they are &lt;strong&gt;structural consequences of running orchestration through a Python control plane&lt;/strong&gt;. You cannot patch the GIL. You cannot make &lt;code&gt;DagBag&lt;/code&gt; reparse cheap. You cannot make Celery workers ephemeral without rewriting them.&lt;/p&gt;

&lt;p&gt;So we did the only thing left: we kept everything Airflow got right and replaced everything that bleeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "kept" means
&lt;/h2&gt;

&lt;p&gt;We did not invent a new model. Airflow's &lt;code&gt;KubernetesExecutor&lt;/code&gt; proved years ago that &lt;strong&gt;pod-per-task is correct&lt;/strong&gt;: each task gets its own container, its own resources, its own lifecycle. You can't leak a process that exits.&lt;/p&gt;

&lt;p&gt;We also did not invent a new UI. The Airflow 3.2.x React SPA ships embedded inside the Leoflow server binary. Your team's muscle memory survives the migration.&lt;/p&gt;

&lt;p&gt;What we kept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The pod-per-task execution model&lt;/li&gt;
&lt;li&gt;The Airflow 3.2.x UI (literally the same React build, served from &lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The HTTP API shape (&lt;code&gt;/api/v2/dags/...&lt;/code&gt;, &lt;code&gt;/api/v2/dagRuns/...&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;The vocabulary: DAG, TaskInstance, DagRun, XCom, Trigger Rules&lt;/li&gt;
&lt;li&gt;The DAG-authoring dialect — &lt;code&gt;from airflow.sdk import DAG, task&lt;/code&gt;, TaskFlow, classic operators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we threw out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Python scheduler. It's Go now.&lt;/li&gt;
&lt;li&gt;The Python triggerer. Sensors are 2 KB goroutines.&lt;/li&gt;
&lt;li&gt;The shared &lt;code&gt;/dags&lt;/code&gt; folder. Each DAG is its own immutable container image.&lt;/li&gt;
&lt;li&gt;The "long-lived Celery worker" model. Every task is an ephemeral pod.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What it looks like to write a DAG
&lt;/h2&gt;

&lt;p&gt;Two files. That's the whole project.&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;# leoflow.yaml — your deploy concerns&lt;/span&gt;
&lt;span class="na"&gt;dag_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;etl_sales&lt;/span&gt;
&lt;span class="na"&gt;python_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pandas==2.1.0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;requests==2.31.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dag.py — your DAG, in real Airflow SDK 3.2.x
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/orders&lt;/span&gt;&lt;span class="sh"&gt;"&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;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&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;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etl_sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 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;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;leoflow compile &lt;span class="nb"&gt;.&lt;/span&gt;              &lt;span class="c"&gt;# generates Dockerfile, builds image, emits dag.json&lt;/span&gt;
leoflow push ./dag.json        &lt;span class="c"&gt;# registers a new versioned DAG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;Dockerfile&lt;/code&gt;. No &lt;code&gt;requirements.txt&lt;/code&gt;. No &lt;code&gt;Helm values.yaml&lt;/code&gt; for this DAG. No &lt;code&gt;pyproject.toml&lt;/code&gt;. The compiler reads &lt;code&gt;leoflow.yaml&lt;/code&gt;, generates a Dockerfile against the official base image (&lt;code&gt;leoflow/python-runtime:3.11&lt;/code&gt;), builds, pushes to your registry, and registers a versioned &lt;code&gt;dag.json&lt;/code&gt; with the control plane. That's the whole inner loop.&lt;/p&gt;

&lt;p&gt;For local development, &lt;code&gt;leoflow lite&lt;/code&gt; provisions a managed Postgres, hot-reloads on every save, gives each DAG its own per-DAG virtualenv at &lt;code&gt;~/.leoflow/dev/venvs/&amp;lt;dag_id&amp;gt;/&lt;/code&gt;, and &lt;strong&gt;auto-detects &lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt; on &lt;code&gt;PATH&lt;/code&gt;&lt;/strong&gt; for 5–10× faster cold installs. Two DAGs that pin conflicting versions of the same package coexist without interference. This is the bit that made me file the issue against Airflow for the first time, ten years ago. We finally have it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Map-reduce, as a Python list comprehension
&lt;/h2&gt;

&lt;p&gt;Hyperparameter search. K-fold cross-validation. Ensemble training. Monte Carlo. Every parallel ML workload is &lt;strong&gt;map-reduce&lt;/strong&gt;. Most orchestrators make you build it: an operator per fan-out, a broker for the intermediate values, shared storage for the artifacts, a custom reducer that knows how to find them all.&lt;/p&gt;

&lt;p&gt;Leoflow expresses the whole pattern in &lt;strong&gt;two lines of Python&lt;/strong&gt;:&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;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;dict&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;train_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="c1"&gt;# map
&lt;/span&gt;
&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_best&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&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;dict&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trials&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="k"&gt;lambda&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="c1"&gt;# reduce
&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hparam_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;select_best&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&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;lr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;[trial(lr) for lr in …]&lt;/code&gt; is the whole map. &lt;code&gt;trials: list[dict]&lt;/code&gt; is the whole reduce. &lt;strong&gt;No XCom plumbing, no broker, no shared filesystem, no special operator.&lt;/strong&gt; The parser captures the list shape at compile time; the runtime assembles upstream XComs in declaration order and delivers them as a real Python list. Per-trial isolation (own pod, own process, own venv if you want). Per-trial retry. Deterministic ordering. A 256 KB cap per upstream value. A &lt;code&gt;null&lt;/code&gt; slot for any upstream that legitimately produced no result.&lt;/p&gt;

&lt;p&gt;If you have ever written a Celery chord by hand, take a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                          Author / CI                             │
│  leoflow.yaml  +  dag.py  +  (auto-generated) Dockerfile         │
└───────────────────────────────┬─────────────────────────────────┘
                                │  leoflow compile / push
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Control plane — Go                           │
│ ┌───────────────────────────────────────────────────────────┐   │
│ │ HTTP API  /api/v2  ·  JWT · RBAC · multi-tenant           │   │
│ ├───────────────────────────────────────────────────────────┤   │
│ │ Scheduler   ·  state machine · cron · catchup             │   │
│ │             ·  PG-advisory-lock leader election           │   │
│ │             ·  retries with backoff                       │   │
│ ├───────────────────────────────────────────────────────────┤   │
│ │ Agent gRPC service  ·  task spec · state · XCom · logs     │   │
│ └───────────────────────────────────────────────────────────┘   │
│       │                                  │                       │
│       │ Postgres (metadata)              │ Redis (XCom + log)    │
└───────┼──────────────────────────────────┼──────────────────────┘
        │                                  │
        │     dispatch: one pod per task   │
        ▼                                  │
┌───────────────────────────────────────┐ │
│              Kubernetes               │ │
│  ┌─────────────────────────────────┐  │ │
│  │  Worker pod = your DAG image    │  │ │
│  │  leoflow-agent (15 MB Go bin)   │  │ │
│  │     ⇅ gRPC                      │  │ │
│  │  your Python / Bash code        │──┼─┘
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short-lived &lt;code&gt;http_api&lt;/code&gt; tasks skip the pod and run inline as goroutines (capped). Everything else runs &lt;strong&gt;pod-per-task&lt;/strong&gt;, every time. Concurrency is goroutines and pods — no Celery, no triggerer process, no shared worker pool.&lt;/p&gt;

&lt;p&gt;A few specifics worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leader election&lt;/strong&gt; is a Postgres advisory lock. No external coordinator. No ZooKeeper, no etcd, no Raft library. It is the kind of decision you can explain to a new hire in 30 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XCom&lt;/strong&gt; lives in &lt;strong&gt;Postgres on Lite&lt;/strong&gt; (small, no Redis required for laptop dev) and &lt;strong&gt;Redis on Pro&lt;/strong&gt;. 256 KB cap, optional schema validation, last-write-wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connections&lt;/strong&gt; are encrypted at rest with AES-256-GCM and delivered to tasks via Airflow's standard &lt;code&gt;AIRFLOW_CONN_&amp;lt;ID&amp;gt;&lt;/code&gt; env var. Postgres / MySQL / SQLite / MSSQL / Redis / HTTP / GCS connectors ship with chain-of-custody-tested integration tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent&lt;/strong&gt; is a static Go binary, ~15 MB. PID 1 of the task pod. Talks gRPC back to the control plane. Forks one process per task. Does not buffer Python output (&lt;code&gt;-u&lt;/code&gt; plus &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt;), because watching a SIGKILL race steal half of the user's &lt;code&gt;print()&lt;/code&gt; output is its own kind of torment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The numbers (the only honest part of any orchestration README)
&lt;/h2&gt;

&lt;p&gt;We are not going to claim "1000× faster" because nobody who has run real pipelines believes you. Here is what falls out of replacing the control plane:&lt;/p&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;Airflow today&lt;/th&gt;
&lt;th&gt;Leoflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler decision latency&lt;/td&gt;
&lt;td&gt;3–10 s per task&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&amp;lt;200 ms&lt;/strong&gt; — native Go, no GIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensor concurrency&lt;/td&gt;
&lt;td&gt;~500 (asyncio Triggerer)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100,000+&lt;/strong&gt; — each sensor is a 2 KB goroutine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG parsing cost&lt;/td&gt;
&lt;td&gt;Re-parsed every scheduler loop&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Zero&lt;/strong&gt; — &lt;code&gt;dag.json&lt;/code&gt; is precompiled, immutable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker lifecycle&lt;/td&gt;
&lt;td&gt;Long-lived, leak-prone&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ephemeral pod per task&lt;/strong&gt; — spawn, run, die&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker image size&lt;/td&gt;
&lt;td&gt;1.5 GB+ Airflow base&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~200 MB typical&lt;/strong&gt; — each DAG is its own slim image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency isolation&lt;/td&gt;
&lt;td&gt;Workaround via &lt;code&gt;KubernetesPodOperator&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Native&lt;/strong&gt; — every DAG is a container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;15–45 s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2–5 s target&lt;/strong&gt; — agent is a 15 MB static binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;Retrofitted with effort&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Native&lt;/strong&gt; — Prometheus + OpenTelemetry + structured logs from commit one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the structural wins. The marketing-grade "X× faster" depends on your DAG. The scheduler latency drop is universal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it is not (because we have all read those launch posts)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not v1.0.&lt;/strong&gt; Per &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0037-release-version-scheme.md" rel="noopener noreferrer"&gt;ADR 0037&lt;/a&gt;, &lt;code&gt;v0.0.1&lt;/code&gt; ends the pre-alpha series; every release after is &lt;code&gt;vX.Y.Z-rc.N → vX.Y.Z&lt;/code&gt;. The HTTP API, CLI surface, and Helm values may change between minor versions until &lt;code&gt;v1.0.0&lt;/code&gt; locks them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI is still Airflow 3.2.x.&lt;/strong&gt; It is a tactical choice (your team's muscle memory). A purpose-built Leoflow UI is on the roadmap (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0018-airflow-ui-as-mvp.md" rel="noopener noreferrer"&gt;ADR 0018&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro is Kubernetes-only.&lt;/strong&gt; Lite runs anywhere. Pro means a real cluster, external Postgres + Redis, the Helm chart. There is deliberately no Docker-Compose "Pro" path; we explained why in &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0015-kubernetes-only-execution.md" rel="noopener noreferrer"&gt;ADR 0015&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a drop-in for every Airflow plugin.&lt;/strong&gt; The Airflow operator catalog has 30+ years of accreted Python; we ship a closed set (&lt;code&gt;python&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;http_api&lt;/code&gt;) plus first-party connectors. ADR 0036 defines a runtime shim for &lt;code&gt;from airflow.providers.&amp;lt;X&amp;gt;.hooks.&amp;lt;Y&amp;gt; import &amp;lt;Z&amp;gt;Hook&lt;/code&gt; so the common cases keep working — but if your DAG depends on three obscure providers we have not vendored, you will hit a wall today. File an issue; we are gating the next batch by demand.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lite: the zero-deploy path
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprises people. To run Leoflow on your laptop you do &lt;strong&gt;not&lt;/strong&gt; need a Kubernetes cluster. You do not need Docker. You do not need a container registry, a &lt;code&gt;compile&lt;/code&gt;, a &lt;code&gt;push&lt;/code&gt;, or a single line of deploy YAML. You need one shell command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh
leoflow lite                &lt;span class="c"&gt;# → http://localhost:8088 (LITE badge, top-center)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer is a single shell script: three static Go binaries into &lt;code&gt;~/.leoflow/bin&lt;/code&gt;, then &lt;code&gt;leoflow setup&lt;/code&gt; provisions a &lt;strong&gt;managed CPython&lt;/strong&gt;, the parser, and a &lt;strong&gt;managed local Postgres&lt;/strong&gt; — nothing touches your system Python or your global packages. Then &lt;code&gt;leoflow lite&lt;/code&gt; boots a full control plane (scheduler, API, UI) against that managed Postgres. No system services, no Compose file, no cluster. Close the terminal and it's gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  There is no "dags/" folder — there is &lt;em&gt;your&lt;/em&gt; folder
&lt;/h3&gt;

&lt;p&gt;This trips up everyone coming from Airflow, so let's be explicit. Leoflow has &lt;strong&gt;no magic &lt;code&gt;dags/&lt;/code&gt; directory&lt;/strong&gt;. During &lt;code&gt;leoflow setup&lt;/code&gt; you pick a &lt;strong&gt;workspace folder&lt;/strong&gt; (default &lt;code&gt;~/leoflow&lt;/code&gt;) — that folder &lt;em&gt;is&lt;/em&gt; the runtime. Every subdirectory that contains a &lt;code&gt;leoflow.yaml&lt;/code&gt; is a DAG project; the watcher scans them and hot-reloads on save. Your tree looks like what you'd actually keep in git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/leoflow/                     ← the workspace you chose at install
├── etl_sales/
│   ├── leoflow.yaml           ← makes this folder a DAG
│   └── dag.py
└── hparam_search/
    ├── leoflow.yaml
    └── dag.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No central registry file, no &lt;code&gt;dag_folder&lt;/code&gt; setting to fight, no "why isn't my DAG showing up." A folder with a &lt;code&gt;leoflow.yaml&lt;/code&gt; is a DAG. That's the whole rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit DAGs from the browser — and get examples in one click
&lt;/h3&gt;

&lt;p&gt;Lite ships a small &lt;strong&gt;embedded web editor&lt;/strong&gt; so you can go from install to a running DAG without leaving the browser. Click the &lt;code&gt;&amp;lt; &amp;gt;&lt;/code&gt; &lt;strong&gt;IDE&lt;/strong&gt; button (bottom-right of the UI) and you get a real &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;Monaco&lt;/a&gt; editor — the engine behind VS Code — scoped to your workspace:&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.amazonaws.com%2Fuploads%2Farticles%2Fe9861fdl44pnim3dp154.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.amazonaws.com%2Fuploads%2Farticles%2Fe9861fdl44pnim3dp154.png" alt="The Leoflow Lite web editor: a file tree on the left with leoflow.yaml and a dag.py open, Python syntax highlighting, and Download examples / New file / Save buttons" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python + YAML syntax highlighting&lt;/strong&gt;, a workspace file tree, open/save (&lt;strong&gt;⌘S&lt;/strong&gt;), create/rename/delete with a "create target" chip that always tells you where a new file will land, collapse/expand carets that remember their state, and a recursive folder delete that says so out loud before it nukes a tree.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;"Download examples"&lt;/strong&gt; button in the header. Click it and Leoflow materializes the bundled example DAGs straight into your workspace — fan-out/aggregate, Monte Carlo π, an HTTP-load DAG, a daily-sales ETL — so you have real, runnable DAGs in the UI in seconds instead of staring at an empty home screen.&lt;/li&gt;
&lt;li&gt;Every save hits disk, the watcher picks it up, and the DAG &lt;strong&gt;hot-reloads&lt;/strong&gt;. (One gotcha: the Airflow tab doesn't auto-refresh DAG &lt;em&gt;structure&lt;/em&gt; — reload it.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is deliberately &lt;em&gt;not&lt;/em&gt; a full IDE — no extensions, no terminal, no debugger. For those, point your own editor at the same workspace folder; it's just files on disk. The editor is a Lite convenience and is never registered in Pro.&lt;/p&gt;

&lt;p&gt;Recover the admin password any time with &lt;code&gt;leoflow lite reset-password&lt;/code&gt;. The &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/lite-web-editor.md" rel="noopener noreferrer"&gt;Lite web-editor guide&lt;/a&gt; and the &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/dev-workflow.md" rel="noopener noreferrer"&gt;Lite cookbook&lt;/a&gt; cover the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pro: when you outgrow the laptop
&lt;/h2&gt;

&lt;p&gt;For production, the Helm chart deploys against external Postgres 13+ and Redis 6+ (Cloud SQL / RDS / Memorystore / ElastiCache / Azure Cache all work):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace leoflow
helm &lt;span class="nb"&gt;install &lt;/span&gt;lf oci://ghcr.io/neochaotic/leoflow &lt;span class="nt"&gt;-n&lt;/span&gt; leoflow &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; v0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; database.url&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'postgres://USER:PASS@HOST:5432/leoflow?sslmode=verify-full'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; redis.url&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'rediss://HOST:6380/0'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; auth.jwtSecret&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;secretKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/helm-chart.md" rel="noopener noreferrer"&gt;chart docs&lt;/a&gt; for every value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering discipline (the stuff you would actually want to know)
&lt;/h2&gt;

&lt;p&gt;If you are evaluating Leoflow as a load-bearing piece of your data platform, the answers are in the repo, but the short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict TDD.&lt;/strong&gt; Every line of production code is preceded by a failing test (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0011-tdd-strict.md" rel="noopener noreferrer"&gt;ADR 0011&lt;/a&gt;). Per-package coverage floors enforced in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go Report Card A+&lt;/strong&gt; as the quality floor (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0012-code-quality-standards.md" rel="noopener noreferrer"&gt;ADR 0012&lt;/a&gt;). &lt;code&gt;gocyclo ≤ 15&lt;/code&gt; per function. GoDocs on every exported identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply chain from commit one.&lt;/strong&gt; &lt;code&gt;govulncheck&lt;/code&gt;, &lt;code&gt;gosec&lt;/code&gt;, &lt;code&gt;Trivy&lt;/code&gt;, &lt;code&gt;CodeQL&lt;/code&gt;, &lt;code&gt;gitleaks&lt;/code&gt;, OpenSSF Scorecard, OpenSSF Best Practices badge. Releases signed with &lt;code&gt;cosign&lt;/code&gt;. SBOMs published per platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every architectural decision is an ADR.&lt;/strong&gt; 37 of them at the time of writing. They are the single best place to start if you want to understand the &lt;em&gt;why&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo also runs an end-to-end install smoke against &lt;strong&gt;seven Linux distros&lt;/strong&gt; (&lt;code&gt;ubuntu:24.04&lt;/code&gt;, &lt;code&gt;debian:12&lt;/code&gt;, &lt;code&gt;fedora:41&lt;/code&gt;, &lt;code&gt;alpine:3.20&lt;/code&gt;, &lt;code&gt;opensuse/leap:15&lt;/code&gt;, &lt;code&gt;archlinux:latest&lt;/code&gt;, &lt;code&gt;rockylinux:9&lt;/code&gt;) on every release, plus a &lt;code&gt;prealpha.N → v0.0.1&lt;/code&gt; upgrade smoke. The &lt;code&gt;v0.0.1&lt;/code&gt; release got the green light because every one of those passed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Help us ship the next milestone
&lt;/h2&gt;

&lt;p&gt;We are at &lt;code&gt;v0.0.1&lt;/code&gt;. The next steps are visible — pick any:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Star the repo&lt;/strong&gt; at &lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;. It is the cheapest way to tell us this matters to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open an issue&lt;/strong&gt; with a chronic Airflow pain we have not closed yet. Pre-1.0 is &lt;em&gt;the&lt;/em&gt; time to shape the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File a PR.&lt;/strong&gt; Strict TDD applies. The &lt;a href="https://github.com/neochaotic/leoflow/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING guide&lt;/a&gt; is the entry point. We review fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try the Lite quick start&lt;/strong&gt; (60 seconds, above). If something does not work, that is an issue worth filing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the Helm chart&lt;/strong&gt; against a real cluster and tell us what bit you. Pro alpha needs real-world miles before it gets a Pro &lt;code&gt;v1.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What is on the post-&lt;code&gt;v0.0.1&lt;/code&gt; table
&lt;/h2&gt;

&lt;p&gt;Visible work toward &lt;code&gt;v0.1.0&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimized backfill (parallel execution with throttling)&lt;/li&gt;
&lt;li&gt;UI scaling for 10,000+ DAGs (caching, server-side pagination)&lt;/li&gt;
&lt;li&gt;Dynamic task mapping&lt;/li&gt;
&lt;li&gt;OIDC authentication (Google, Azure AD, Keycloak, Okta)&lt;/li&gt;
&lt;li&gt;Deferrable tasks (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0016-deferrable-tasks.md" rel="noopener noreferrer"&gt;ADR 0016&lt;/a&gt;) — efficient dispatch + long-poll pattern, native Go, &lt;strong&gt;no separate Triggerer process&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A purpose-built Leoflow UI (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0018-airflow-ui-as-mvp.md" rel="noopener noreferrer"&gt;ADR 0018&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are deliberately keeping the surface small until it is &lt;em&gt;boring and reliable&lt;/em&gt;. Workflow orchestrators have to be boring to be useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  License &amp;amp; credits
&lt;/h2&gt;

&lt;p&gt;Apache 2.0. We stand on the shoulders of Airflow — the team behind it defined the vocabulary, proved the architecture, and built the UI we reuse without modification today. We also studied Argo Workflows, Prefect, and Dagster carefully; each made decisions worth borrowing, and we did.&lt;/p&gt;

&lt;p&gt;If you have ever waited five seconds for an Airflow task to start, you know why we built this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt; — the v0.0.1 release notes are at &lt;a href="https://github.com/neochaotic/leoflow/releases/tag/v0.0.1" rel="noopener noreferrer"&gt;/releases/tag/v0.0.1&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>go</category>
      <category>kubernetes</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>We rewrote Apache Airflow's control plane in Go (and kept the UI)</title>
      <dc:creator>Alisson Rosa</dc:creator>
      <pubDate>Wed, 03 Jun 2026 16:22:27 +0000</pubDate>
      <link>https://dev.to/neochaotic/we-rewrote-apache-airflows-control-plane-in-go-and-kept-the-ui-1fh5</link>
      <guid>https://dev.to/neochaotic/we-rewrote-apache-airflows-control-plane-in-go-and-kept-the-ui-1fh5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Leoflow &lt;code&gt;v0.0.1&lt;/code&gt; just shipped. It speaks the Airflow API, runs the Airflow 3.2.x UI &lt;strong&gt;unmodified&lt;/strong&gt;, but replaces the Python control plane with Go. Pod-per-task is the only execution mode. Each DAG is its own container image. Fan-in (map-reduce) is a Python list comprehension. Install: &lt;code&gt;curl -fsSL https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh&lt;/code&gt;. GitHub: &lt;strong&gt;&lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt; — stars and issues warmly accepted.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The 3 AM pager
&lt;/h2&gt;

&lt;p&gt;You know the one. The scheduler stalled again. Or the triggerer suffocated under 500 sensors. Or a worker leaked file descriptors until Kubernetes OOMKilled it mid-run. Or someone bumped &lt;code&gt;pandas&lt;/code&gt; for the new DAG and broke six legacy ones because they all share the same image.&lt;/p&gt;

&lt;p&gt;Apache Airflow is the most widely deployed workflow orchestrator on earth. It is also the one that bleeds the most in production. None of those wounds are bugs — they are &lt;strong&gt;structural consequences of running orchestration through a Python control plane&lt;/strong&gt;. You cannot patch the GIL. You cannot make &lt;code&gt;DagBag&lt;/code&gt; reparse cheap. You cannot make Celery workers ephemeral without rewriting them.&lt;/p&gt;

&lt;p&gt;So we did the only thing left: we kept everything Airflow got right and replaced everything that bleeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "kept" means
&lt;/h2&gt;

&lt;p&gt;We did not invent a new model. Airflow's &lt;code&gt;KubernetesExecutor&lt;/code&gt; proved years ago that &lt;strong&gt;pod-per-task is correct&lt;/strong&gt;: each task gets its own container, its own resources, its own lifecycle. You can't leak a process that exits.&lt;/p&gt;

&lt;p&gt;We also did not invent a new UI. The Airflow 3.2.x React SPA ships embedded inside the Leoflow server binary. Your team's muscle memory survives the migration.&lt;/p&gt;

&lt;p&gt;What we kept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The pod-per-task execution model&lt;/li&gt;
&lt;li&gt;The Airflow 3.2.x UI (literally the same React build, served from &lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The HTTP API shape (&lt;code&gt;/api/v2/dags/...&lt;/code&gt;, &lt;code&gt;/api/v2/dagRuns/...&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;The vocabulary: DAG, TaskInstance, DagRun, XCom, Trigger Rules&lt;/li&gt;
&lt;li&gt;The DAG-authoring dialect — &lt;code&gt;from airflow.sdk import DAG, task&lt;/code&gt;, TaskFlow, classic operators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we threw out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Python scheduler. It's Go now.&lt;/li&gt;
&lt;li&gt;The Python triggerer. Sensors are 2 KB goroutines.&lt;/li&gt;
&lt;li&gt;The shared &lt;code&gt;/dags&lt;/code&gt; folder. Each DAG is its own immutable container image.&lt;/li&gt;
&lt;li&gt;The "long-lived Celery worker" model. Every task is an ephemeral pod.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What it looks like to write a DAG
&lt;/h2&gt;

&lt;p&gt;Two files. That's the whole project.&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;# leoflow.yaml — your deploy concerns&lt;/span&gt;
&lt;span class="na"&gt;dag_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;etl_sales&lt;/span&gt;
&lt;span class="na"&gt;python_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pandas==2.1.0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;requests==2.31.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dag.py — your DAG, in real Airflow SDK 3.2.x
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/orders&lt;/span&gt;&lt;span class="sh"&gt;"&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;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&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;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etl_sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 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;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;leoflow compile &lt;span class="nb"&gt;.&lt;/span&gt;              &lt;span class="c"&gt;# generates Dockerfile, builds image, emits dag.json&lt;/span&gt;
leoflow push ./dag.json        &lt;span class="c"&gt;# registers a new versioned DAG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;Dockerfile&lt;/code&gt;. No &lt;code&gt;requirements.txt&lt;/code&gt;. No &lt;code&gt;Helm values.yaml&lt;/code&gt; for this DAG. No &lt;code&gt;pyproject.toml&lt;/code&gt;. The compiler reads &lt;code&gt;leoflow.yaml&lt;/code&gt;, generates a Dockerfile against the official base image (&lt;code&gt;leoflow/python-runtime:3.11&lt;/code&gt;), builds, pushes to your registry, and registers a versioned &lt;code&gt;dag.json&lt;/code&gt; with the control plane. That's the whole inner loop.&lt;/p&gt;

&lt;p&gt;For local development, &lt;code&gt;leoflow lite&lt;/code&gt; provisions a managed Postgres, hot-reloads on every save, gives each DAG its own per-DAG virtualenv at &lt;code&gt;~/.leoflow/dev/venvs/&amp;lt;dag_id&amp;gt;/&lt;/code&gt;, and &lt;strong&gt;auto-detects &lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt; on &lt;code&gt;PATH&lt;/code&gt;&lt;/strong&gt; for 5–10× faster cold installs. Two DAGs that pin conflicting versions of the same package coexist without interference. This is the bit that made me file the issue against Airflow for the first time, ten years ago. We finally have it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Map-reduce, as a Python list comprehension
&lt;/h2&gt;

&lt;p&gt;Hyperparameter search. K-fold cross-validation. Ensemble training. Monte Carlo. Every parallel ML workload is &lt;strong&gt;map-reduce&lt;/strong&gt;. Most orchestrators make you build it: an operator per fan-out, a broker for the intermediate values, shared storage for the artifacts, a custom reducer that knows how to find them all.&lt;/p&gt;

&lt;p&gt;Leoflow expresses the whole pattern in &lt;strong&gt;two lines of Python&lt;/strong&gt;:&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;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;dict&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;train_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="c1"&gt;# map
&lt;/span&gt;
&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_best&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&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;dict&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trials&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="k"&gt;lambda&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="c1"&gt;# reduce
&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hparam_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;select_best&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&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;lr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;[trial(lr) for lr in …]&lt;/code&gt; is the whole map. &lt;code&gt;trials: list[dict]&lt;/code&gt; is the whole reduce. &lt;strong&gt;No XCom plumbing, no broker, no shared filesystem, no special operator.&lt;/strong&gt; The parser captures the list shape at compile time; the runtime assembles upstream XComs in declaration order and delivers them as a real Python list. Per-trial isolation (own pod, own process, own venv if you want). Per-trial retry. Deterministic ordering. A 256 KB cap per upstream value. A &lt;code&gt;null&lt;/code&gt; slot for any upstream that legitimately produced no result.&lt;/p&gt;

&lt;p&gt;If you have ever written a Celery chord by hand, take a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                          Author / CI                             │
│  leoflow.yaml  +  dag.py  +  (auto-generated) Dockerfile         │
└───────────────────────────────┬─────────────────────────────────┘
                                │  leoflow compile / push
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Control plane — Go                           │
│ ┌───────────────────────────────────────────────────────────┐   │
│ │ HTTP API  /api/v2  ·  JWT · RBAC · multi-tenant           │   │
│ ├───────────────────────────────────────────────────────────┤   │
│ │ Scheduler   ·  state machine · cron · catchup             │   │
│ │             ·  PG-advisory-lock leader election           │   │
│ │             ·  retries with backoff                       │   │
│ ├───────────────────────────────────────────────────────────┤   │
│ │ Agent gRPC service  ·  task spec · state · XCom · logs     │   │
│ └───────────────────────────────────────────────────────────┘   │
│       │                                  │                       │
│       │ Postgres (metadata)              │ Redis (XCom + log)    │
└───────┼──────────────────────────────────┼──────────────────────┘
        │                                  │
        │     dispatch: one pod per task   │
        ▼                                  │
┌───────────────────────────────────────┐ │
│              Kubernetes               │ │
│  ┌─────────────────────────────────┐  │ │
│  │  Worker pod = your DAG image    │  │ │
│  │  leoflow-agent (15 MB Go bin)   │  │ │
│  │     ⇅ gRPC                      │  │ │
│  │  your Python / Bash code        │──┼─┘
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short-lived &lt;code&gt;http_api&lt;/code&gt; tasks skip the pod and run inline as goroutines (capped). Everything else runs &lt;strong&gt;pod-per-task&lt;/strong&gt;, every time. Concurrency is goroutines and pods — no Celery, no triggerer process, no shared worker pool.&lt;/p&gt;

&lt;p&gt;A few specifics worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leader election&lt;/strong&gt; is a Postgres advisory lock. No external coordinator. No ZooKeeper, no etcd, no Raft library. It is the kind of decision you can explain to a new hire in 30 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XCom&lt;/strong&gt; lives in &lt;strong&gt;Postgres on Lite&lt;/strong&gt; (small, no Redis required for laptop dev) and &lt;strong&gt;Redis on Pro&lt;/strong&gt;. 256 KB cap, optional schema validation, last-write-wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connections&lt;/strong&gt; are encrypted at rest with AES-256-GCM and delivered to tasks via Airflow's standard &lt;code&gt;AIRFLOW_CONN_&amp;lt;ID&amp;gt;&lt;/code&gt; env var. Postgres / MySQL / SQLite / MSSQL / Redis / HTTP / GCS connectors ship with chain-of-custody-tested integration tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent&lt;/strong&gt; is a static Go binary, ~15 MB. PID 1 of the task pod. Talks gRPC back to the control plane. Forks one process per task. Does not buffer Python output (&lt;code&gt;-u&lt;/code&gt; plus &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt;), because watching a SIGKILL race steal half of the user's &lt;code&gt;print()&lt;/code&gt; output is its own kind of torment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The numbers (the only honest part of any orchestration README)
&lt;/h2&gt;

&lt;p&gt;We are not going to claim "1000× faster" because nobody who has run real pipelines believes you. Here is what falls out of replacing the control plane:&lt;/p&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;Airflow today&lt;/th&gt;
&lt;th&gt;Leoflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler decision latency&lt;/td&gt;
&lt;td&gt;3–10 s per task&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&amp;lt;200 ms&lt;/strong&gt; — native Go, no GIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensor concurrency&lt;/td&gt;
&lt;td&gt;~500 (asyncio Triggerer)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100,000+&lt;/strong&gt; — each sensor is a 2 KB goroutine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG parsing cost&lt;/td&gt;
&lt;td&gt;Re-parsed every scheduler loop&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Zero&lt;/strong&gt; — &lt;code&gt;dag.json&lt;/code&gt; is precompiled, immutable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker lifecycle&lt;/td&gt;
&lt;td&gt;Long-lived, leak-prone&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ephemeral pod per task&lt;/strong&gt; — spawn, run, die&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker image size&lt;/td&gt;
&lt;td&gt;1.5 GB+ Airflow base&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~200 MB typical&lt;/strong&gt; — each DAG is its own slim image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency isolation&lt;/td&gt;
&lt;td&gt;Workaround via &lt;code&gt;KubernetesPodOperator&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Native&lt;/strong&gt; — every DAG is a container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;15–45 s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2–5 s target&lt;/strong&gt; — agent is a 15 MB static binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;Retrofitted with effort&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Native&lt;/strong&gt; — Prometheus + OpenTelemetry + structured logs from commit one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the structural wins. The marketing-grade "X× faster" depends on your DAG. The scheduler latency drop is universal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it is not (because we have all read those launch posts)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not v1.0.&lt;/strong&gt; Per &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0037-release-version-scheme.md" rel="noopener noreferrer"&gt;ADR 0037&lt;/a&gt;, &lt;code&gt;v0.0.1&lt;/code&gt; ends the pre-alpha series; every release after is &lt;code&gt;vX.Y.Z-rc.N → vX.Y.Z&lt;/code&gt;. The HTTP API, CLI surface, and Helm values may change between minor versions until &lt;code&gt;v1.0.0&lt;/code&gt; locks them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI is still Airflow 3.2.x.&lt;/strong&gt; It is a tactical choice (your team's muscle memory). A purpose-built Leoflow UI is on the roadmap (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0018-airflow-ui-as-mvp.md" rel="noopener noreferrer"&gt;ADR 0018&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro is Kubernetes-only.&lt;/strong&gt; Lite runs anywhere. Pro means a real cluster, external Postgres + Redis, the Helm chart. There is deliberately no Docker-Compose "Pro" path; we explained why in &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0015-kubernetes-only-execution.md" rel="noopener noreferrer"&gt;ADR 0015&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a drop-in for every Airflow plugin.&lt;/strong&gt; The Airflow operator catalog has 30+ years of accreted Python; we ship a closed set (&lt;code&gt;python&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;http_api&lt;/code&gt;) plus first-party connectors. ADR 0036 defines a runtime shim for &lt;code&gt;from airflow.providers.&amp;lt;X&amp;gt;.hooks.&amp;lt;Y&amp;gt; import &amp;lt;Z&amp;gt;Hook&lt;/code&gt; so the common cases keep working — but if your DAG depends on three obscure providers we have not vendored, you will hit a wall today. File an issue; we are gating the next batch by demand.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lite: the zero-deploy path
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprises people. To run Leoflow on your laptop you do &lt;strong&gt;not&lt;/strong&gt; need a Kubernetes cluster. You do not need Docker. You do not need a container registry, a &lt;code&gt;compile&lt;/code&gt;, a &lt;code&gt;push&lt;/code&gt;, or a single line of deploy YAML. You need one shell command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/neochaotic/leoflow/main/install.sh | sh
leoflow lite                &lt;span class="c"&gt;# → http://localhost:8088 (LITE badge, top-center)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer is a single shell script: three static Go binaries into &lt;code&gt;~/.leoflow/bin&lt;/code&gt;, then &lt;code&gt;leoflow setup&lt;/code&gt; provisions a &lt;strong&gt;managed CPython&lt;/strong&gt;, the parser, and a &lt;strong&gt;managed local Postgres&lt;/strong&gt; — nothing touches your system Python or your global packages. Then &lt;code&gt;leoflow lite&lt;/code&gt; boots a full control plane (scheduler, API, UI) against that managed Postgres. No system services, no Compose file, no cluster. Close the terminal and it's gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  There is no "dags/" folder — there is &lt;em&gt;your&lt;/em&gt; folder
&lt;/h3&gt;

&lt;p&gt;This trips up everyone coming from Airflow, so let's be explicit. Leoflow has &lt;strong&gt;no magic &lt;code&gt;dags/&lt;/code&gt; directory&lt;/strong&gt;. During &lt;code&gt;leoflow setup&lt;/code&gt; you pick a &lt;strong&gt;workspace folder&lt;/strong&gt; (default &lt;code&gt;~/leoflow&lt;/code&gt;) — that folder &lt;em&gt;is&lt;/em&gt; the runtime. Every subdirectory that contains a &lt;code&gt;leoflow.yaml&lt;/code&gt; is a DAG project; the watcher scans them and hot-reloads on save. Your tree looks like what you'd actually keep in git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/leoflow/                     ← the workspace you chose at install
├── etl_sales/
│   ├── leoflow.yaml           ← makes this folder a DAG
│   └── dag.py
└── hparam_search/
    ├── leoflow.yaml
    └── dag.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No central registry file, no &lt;code&gt;dag_folder&lt;/code&gt; setting to fight, no "why isn't my DAG showing up." A folder with a &lt;code&gt;leoflow.yaml&lt;/code&gt; is a DAG. That's the whole rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit DAGs from the browser — and get examples in one click
&lt;/h3&gt;

&lt;p&gt;Lite ships a small &lt;strong&gt;embedded web editor&lt;/strong&gt; so you can go from install to a running DAG without leaving the browser. Click the &lt;code&gt;&amp;lt; &amp;gt;&lt;/code&gt; &lt;strong&gt;IDE&lt;/strong&gt; button (bottom-right of the UI) and you get a real &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;Monaco&lt;/a&gt; editor — the engine behind VS Code — scoped to your workspace:&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.amazonaws.com%2Fuploads%2Farticles%2Fe9861fdl44pnim3dp154.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.amazonaws.com%2Fuploads%2Farticles%2Fe9861fdl44pnim3dp154.png" alt="The Leoflow Lite web editor: a file tree on the left with leoflow.yaml and a dag.py open, Python syntax highlighting, and Download examples / New file / Save buttons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python + YAML syntax highlighting&lt;/strong&gt;, a workspace file tree, open/save (&lt;strong&gt;⌘S&lt;/strong&gt;), create/rename/delete with a "create target" chip that always tells you where a new file will land, collapse/expand carets that remember their state, and a recursive folder delete that says so out loud before it nukes a tree.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;"Download examples"&lt;/strong&gt; button in the header. Click it and Leoflow materializes the bundled example DAGs straight into your workspace — fan-out/aggregate, Monte Carlo π, an HTTP-load DAG, a daily-sales ETL — so you have real, runnable DAGs in the UI in seconds instead of staring at an empty home screen.&lt;/li&gt;
&lt;li&gt;Every save hits disk, the watcher picks it up, and the DAG &lt;strong&gt;hot-reloads&lt;/strong&gt;. (One gotcha: the Airflow tab doesn't auto-refresh DAG &lt;em&gt;structure&lt;/em&gt; — reload it.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is deliberately &lt;em&gt;not&lt;/em&gt; a full IDE — no extensions, no terminal, no debugger. For those, point your own editor at the same workspace folder; it's just files on disk. The editor is a Lite convenience and is never registered in Pro.&lt;/p&gt;

&lt;p&gt;Recover the admin password any time with &lt;code&gt;leoflow lite reset-password&lt;/code&gt;. The &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/lite-web-editor.md" rel="noopener noreferrer"&gt;Lite web-editor guide&lt;/a&gt; and the &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/dev-workflow.md" rel="noopener noreferrer"&gt;Lite cookbook&lt;/a&gt; cover the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pro: when you outgrow the laptop
&lt;/h2&gt;

&lt;p&gt;For production, the Helm chart deploys against external Postgres 13+ and Redis 6+ (Cloud SQL / RDS / Memorystore / ElastiCache / Azure Cache all work):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace leoflow
helm &lt;span class="nb"&gt;install &lt;/span&gt;lf oci://ghcr.io/neochaotic/leoflow &lt;span class="nt"&gt;-n&lt;/span&gt; leoflow &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; v0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; database.url&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'postgres://USER:PASS@HOST:5432/leoflow?sslmode=verify-full'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; redis.url&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'rediss://HOST:6380/0'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; auth.jwtSecret&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;secretKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/helm-chart.md" rel="noopener noreferrer"&gt;chart docs&lt;/a&gt; for every value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering discipline (the stuff you would actually want to know)
&lt;/h2&gt;

&lt;p&gt;If you are evaluating Leoflow as a load-bearing piece of your data platform, the answers are in the repo, but the short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict TDD.&lt;/strong&gt; Every line of production code is preceded by a failing test (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0011-tdd-strict.md" rel="noopener noreferrer"&gt;ADR 0011&lt;/a&gt;). Per-package coverage floors enforced in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go Report Card A+&lt;/strong&gt; as the quality floor (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0012-code-quality-standards.md" rel="noopener noreferrer"&gt;ADR 0012&lt;/a&gt;). &lt;code&gt;gocyclo ≤ 15&lt;/code&gt; per function. GoDocs on every exported identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply chain from commit one.&lt;/strong&gt; &lt;code&gt;govulncheck&lt;/code&gt;, &lt;code&gt;gosec&lt;/code&gt;, &lt;code&gt;Trivy&lt;/code&gt;, &lt;code&gt;CodeQL&lt;/code&gt;, &lt;code&gt;gitleaks&lt;/code&gt;, OpenSSF Scorecard, OpenSSF Best Practices badge. Releases signed with &lt;code&gt;cosign&lt;/code&gt;. SBOMs published per platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every architectural decision is an ADR.&lt;/strong&gt; 37 of them at the time of writing. They are the single best place to start if you want to understand the &lt;em&gt;why&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo also runs an end-to-end install smoke against &lt;strong&gt;seven Linux distros&lt;/strong&gt; (&lt;code&gt;ubuntu:24.04&lt;/code&gt;, &lt;code&gt;debian:12&lt;/code&gt;, &lt;code&gt;fedora:41&lt;/code&gt;, &lt;code&gt;alpine:3.20&lt;/code&gt;, &lt;code&gt;opensuse/leap:15&lt;/code&gt;, &lt;code&gt;archlinux:latest&lt;/code&gt;, &lt;code&gt;rockylinux:9&lt;/code&gt;) on every release, plus a &lt;code&gt;prealpha.N → v0.0.1&lt;/code&gt; upgrade smoke. The &lt;code&gt;v0.0.1&lt;/code&gt; release got the green light because every one of those passed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Help us ship the next milestone
&lt;/h2&gt;

&lt;p&gt;We are at &lt;code&gt;v0.0.1&lt;/code&gt;. The next steps are visible — pick any:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Star the repo&lt;/strong&gt; at &lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;. It is the cheapest way to tell us this matters to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open an issue&lt;/strong&gt; with a chronic Airflow pain we have not closed yet. Pre-1.0 is &lt;em&gt;the&lt;/em&gt; time to shape the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File a PR.&lt;/strong&gt; Strict TDD applies. The &lt;a href="https://github.com/neochaotic/leoflow/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING guide&lt;/a&gt; is the entry point. We review fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try the Lite quick start&lt;/strong&gt; (60 seconds, above). If something does not work, that is an issue worth filing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the Helm chart&lt;/strong&gt; against a real cluster and tell us what bit you. Pro alpha needs real-world miles before it gets a Pro &lt;code&gt;v1.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What is on the post-&lt;code&gt;v0.0.1&lt;/code&gt; table
&lt;/h2&gt;

&lt;p&gt;Visible work toward &lt;code&gt;v0.1.0&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimized backfill (parallel execution with throttling)&lt;/li&gt;
&lt;li&gt;UI scaling for 10,000+ DAGs (caching, server-side pagination)&lt;/li&gt;
&lt;li&gt;Dynamic task mapping&lt;/li&gt;
&lt;li&gt;OIDC authentication (Google, Azure AD, Keycloak, Okta)&lt;/li&gt;
&lt;li&gt;Deferrable tasks (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0016-deferrable-tasks.md" rel="noopener noreferrer"&gt;ADR 0016&lt;/a&gt;) — efficient dispatch + long-poll pattern, native Go, &lt;strong&gt;no separate Triggerer process&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A purpose-built Leoflow UI (&lt;a href="https://github.com/neochaotic/leoflow/blob/main/docs/adr/0018-airflow-ui-as-mvp.md" rel="noopener noreferrer"&gt;ADR 0018&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are deliberately keeping the surface small until it is &lt;em&gt;boring and reliable&lt;/em&gt;. Workflow orchestrators have to be boring to be useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  License &amp;amp; credits
&lt;/h2&gt;

&lt;p&gt;Apache 2.0. We stand on the shoulders of Airflow — the team behind it defined the vocabulary, proved the architecture, and built the UI we reuse without modification today. We also studied Argo Workflows, Prefect, and Dagster carefully; each made decisions worth borrowing, and we did.&lt;/p&gt;

&lt;p&gt;If you have ever waited five seconds for an Airflow task to start, you know why we built this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/neochaotic/leoflow" rel="noopener noreferrer"&gt;github.com/neochaotic/leoflow&lt;/a&gt;&lt;/strong&gt; — the v0.0.1 release notes are at &lt;a href="https://github.com/neochaotic/leoflow/releases/tag/v0.0.1" rel="noopener noreferrer"&gt;/releases/tag/v0.0.1&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>go</category>
      <category>kubernetes</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
