<?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: Bechir Bejaoui</title>
    <description>The latest articles on DEV Community by Bechir Bejaoui (@bejaouibechir).</description>
    <link>https://dev.to/bejaouibechir</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%2F4134552%2Fe4bd4d74-bd01-4006-b48b-2772e2ed25be.jpeg</url>
      <title>DEV Community: Bechir Bejaoui</title>
      <link>https://dev.to/bejaouibechir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bejaouibechir"/>
    <language>en</language>
    <item>
      <title>From YAML validator to event-driven projects: 14 releases of Hydra ETL in six weeks</title>
      <dc:creator>Bechir Bejaoui</dc:creator>
      <pubDate>Wed, 04 Nov 2026 22:34:34 +0000</pubDate>
      <link>https://dev.to/bejaouibechir/from-yaml-validator-to-event-driven-projects-14-releases-of-hydra-etl-in-six-weeks-3lgc</link>
      <guid>https://dev.to/bejaouibechir/from-yaml-validator-to-event-driven-projects-14-releases-of-hydra-etl-in-six-weeks-3lgc</guid>
      <description>&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%2F8pt7umpam64fyy77tbvu.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%2F8pt7umpam64fyy77tbvu.png" alt="http://www.hydra.com" width="192" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the end of September, Hydra ETL did one thing well: you described a data job in four YAML files, &lt;code&gt;hdrctl validate&lt;/code&gt; checked them before any data was read, and the same job ran from the terminal, a REST API or a visual editor.&lt;/p&gt;

&lt;p&gt;Six weeks and 14 releases later, a Hydra ETL project can start itself when a file lands in a folder, roll back what it wrote when something fails, aggregate 20 million rows in about half a minute, and call another project as a step. The latest one, 0.19.0, &lt;strong&gt;shipped on November 4, which also happens to be my birthday&lt;/strong&gt;. Not a bad way to celebrate.&lt;br&gt;
This post walks through that evolution in five steps, and what stayed the same.&lt;/p&gt;
&lt;h2&gt;
  
  
  The starting point: a job you can read and check
&lt;/h2&gt;

&lt;p&gt;A Hydra ETL job is four files: &lt;code&gt;sources.yaml&lt;/code&gt;, &lt;code&gt;transformations.yaml&lt;/code&gt;, &lt;code&gt;destinations.yaml&lt;/code&gt;, &lt;code&gt;pipeline.yaml&lt;/code&gt;. Nothing runs until the manifests are valid. That was the promise in September, and every release since has kept it: each new feature comes with its own checks, run before anything touches your data.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. The project becomes the unit (0.11.4 → 0.12.3)
&lt;/h2&gt;

&lt;p&gt;Teams don't run one job; they run a set of them in a certain order.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hdrctl init&lt;/code&gt; now scaffolds a &lt;strong&gt;project&lt;/strong&gt;: jobs, workflows, parameters, environments.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workflows/thread.yaml&lt;/code&gt; declares the order: stages, sequential or parallel, delays, loops.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hdrctl run &amp;lt;project&amp;gt;&lt;/code&gt; checks &lt;strong&gt;the whole project&lt;/strong&gt; — every workflow, every job, every &lt;code&gt;pipeline.yaml&lt;/code&gt; — and lists every problem before starting anything.&lt;/li&gt;
&lt;li&gt;Studio stopped overwriting files edited outside it: a change made in your editor or by git is detected, and the latest change wins.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  2. Failures stop leaving half-written data (0.13 → 0.14)
&lt;/h2&gt;

&lt;p&gt;This is the part I care most about. A pipeline that fails halfway is worse than one that never starts.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ZvDhzJc3VRI?start=74" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All-or-nothing load per job&lt;/strong&gt;: a job writes through one load session. A failure or Ctrl+C leaves the destination as it was (transactions on PostgreSQL, MySQL/MariaDB and SQL Server; temporary file then swap on CSV and Parquet).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow rollback&lt;/strong&gt;: with &lt;code&gt;rollback: true&lt;/code&gt;, a failed workflow undoes the steps it already did, last one first. Actions that touch the outside world (webhook, email, scripts) must declare how to undo them, or explicitly say they can't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback workflows&lt;/strong&gt;: a backup workflow can take over — before the start, when a condition is true (&lt;code&gt;not source_available('crm_db')&lt;/code&gt;), or after a failure, but &lt;strong&gt;only if the failed workflow published nothing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts&lt;/strong&gt; per workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Volume (0.15 → 0.16)
&lt;/h2&gt;

&lt;p&gt;pandas stays the default, but above two million estimated rows Hydra ETL can read CSV with pyarrow and run aggregations, sorts and joins in DuckDB. Results are identical to pandas, sums included.&lt;/p&gt;

&lt;p&gt;Measured on 20 million rows, 2 CPUs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aggregation: &lt;strong&gt;112.8 s / 5.4 GB → 34.3 s / 1.4 GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;right join then aggregation: &lt;strong&gt;71.8 s / 3.4 GB → 27.7 s / 2.6 GB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No silent fallback: when a step can't run in DuckDB, the log says so. And a new &lt;code&gt;engine:&lt;/code&gt; key chooses the engine per job.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Secrets and environments (0.17 → 0.18)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Secrets are &lt;strong&gt;masked everywhere&lt;/strong&gt; Hydra ETL writes text: logs, errors, tracebacks, the output of scripts, stored runs.&lt;/li&gt;
&lt;li&gt;Secrets can be encrypted with &lt;strong&gt;SOPS&lt;/strong&gt; (age, PGP, cloud KMS) and are decrypted in memory only.&lt;/li&gt;
&lt;li&gt;One project, several environments: global variables per machine, parameter values and secrets per environment, managed with &lt;code&gt;hdrctl env&lt;/code&gt;, &lt;code&gt;hdrctl param&lt;/code&gt; and &lt;code&gt;hdrctl secret&lt;/code&gt;. A secret value is never passed on the command line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Projects that start themselves (0.19)
&lt;/h2&gt;

&lt;p&gt;The latest release adds &lt;strong&gt;project triggers&lt;/strong&gt;. A &lt;code&gt;triggers.yaml&lt;/code&gt; at the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;1.0"&lt;/span&gt;
&lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nightly&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cron&lt;/span&gt;
    &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/Paris&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;new_orders&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inbox/*.csv&lt;/span&gt;
    &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;after_crm&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;after_project&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../crm&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hdrctl trigger validate&lt;/code&gt; checks it, then &lt;code&gt;hdrctl watch&lt;/code&gt; runs it. Dropping a CSV into &lt;code&gt;inbox/&lt;/code&gt;, then finishing the &lt;code&gt;crm&lt;/code&gt; project, gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Waiting for events. Ctrl+C to stop.
  2026-11-04 21:29:36 ▶ sales/new_orders  files: inbox/orders_2026-11-04.csv  env: none
  2026-11-04 21:29:39 ✓ sales/new_orders  2.6s
  2026-11-04 21:30:07 ▶ sales/after_crm  project crm ended: success  env: none
  2026-11-04 21:30:12 ✓ sales/after_crm  4.9s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven trigger types in total: cron, interval, file, marker file, change in a source table, webhook with a token, and the end of another project.&lt;/p&gt;

&lt;p&gt;The guarantees are what make this usable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a file is only read once it has stopped changing;&lt;/li&gt;
&lt;li&gt;a trigger already running is not started twice;&lt;/li&gt;
&lt;li&gt;a change to &lt;code&gt;triggers.yaml&lt;/code&gt; is picked up without restarting;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;after_project&lt;/code&gt; loops are refused.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same release adds &lt;strong&gt;sub-projects&lt;/strong&gt;: a workflow step &lt;code&gt;type: project&lt;/code&gt; runs another project, with cycle detection and results the next steps can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six weeks at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.11.3–0.11.4&lt;/td&gt;
&lt;td&gt;Sep 27 – Oct 17&lt;/td&gt;
&lt;td&gt;Secrets resolve; PostgreSQL upsert fixed; projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.12.x&lt;/td&gt;
&lt;td&gt;Oct 22–24&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;thread.yaml&lt;/code&gt;, &lt;code&gt;hdrctl run &amp;lt;project&amp;gt;&lt;/code&gt;, loops, Studio ↔ disk sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.13–0.14&lt;/td&gt;
&lt;td&gt;Oct 26–28&lt;/td&gt;
&lt;td&gt;All-or-nothing loads, rollback, fallback, timeouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.15–0.16&lt;/td&gt;
&lt;td&gt;Oct 28–29&lt;/td&gt;
&lt;td&gt;DuckDB large-volume engine, &lt;code&gt;engine:&lt;/code&gt; per job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.17–0.18&lt;/td&gt;
&lt;td&gt;Nov 1–2&lt;/td&gt;
&lt;td&gt;Masked secrets, SOPS, environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.19&lt;/td&gt;
&lt;td&gt;Nov 4&lt;/td&gt;
&lt;td&gt;Project triggers, &lt;code&gt;hdrctl watch&lt;/code&gt;, sub-projects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What didn't change
&lt;/h2&gt;

&lt;p&gt;Still one &lt;code&gt;pip install&lt;/code&gt;, no database, no broker, no Node. Still YAML you can review in a pull request. Still checked before it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not there yet
&lt;/h2&gt;

&lt;p&gt;Hydra ETL is in beta, with one maintainer. File triggers watch local folders; SFTP and S3 are planned. If you try it and something breaks, an issue helps more than a star.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Hydra Studio Quick tour
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/d_yaYXV1Hao?start=12" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Try Hydra ETL in playground: &lt;a href="https://killercoda.com/hydra-etl" rel="noopener noreferrer"&gt;killercoda.com/hydra-etl&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every release, with its notes: &lt;a href="https://hydraetl.com/changelog/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=evolution" rel="noopener noreferrer"&gt;hydraetl.com/changelog&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Source: &lt;a href="https://github.com/bejaouibechir/Hydra" rel="noopener noreferrer"&gt;github.com/bejaouibechir/Hydra&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install: &lt;code&gt;pip install -U hydra-etl&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>etl</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Clean a million rows with Hydra ETL — no database, no Docker</title>
      <dc:creator>Bechir Bejaoui</dc:creator>
      <pubDate>Sun, 20 Sep 2026 20:07:00 +0000</pubDate>
      <link>https://dev.to/bejaouibechir/clean-a-million-rows-with-hydra-etl-no-database-no-docker-nm1</link>
      <guid>https://dev.to/bejaouibechir/clean-a-million-rows-with-hydra-etl-no-database-no-docker-nm1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://hydraetl.com/blog/one-million-rows" rel="noopener noreferrer"&gt;hydraetl.com&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything below runs on a laptop with &lt;strong&gt;one install and no service to start&lt;/strong&gt;: no database, no Docker, no cluster. You will generate a million-row CSV, describe the job in four small YAML files, check it before it runs, execute it, then run the exact same job on the optional Rust engine and compare.&lt;/p&gt;

&lt;p&gt;Total time: about five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;hydra-etl        &lt;span class="c"&gt;# 0.10.2 or later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.9+, Linux, macOS or Windows. Nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One rule for the whole tutorial:&lt;/strong&gt; create an empty folder, and run every command from it. That folder &lt;em&gt;is&lt;/em&gt; the Hydra ETL job — the manifests sit at its root and &lt;code&gt;data/&lt;/code&gt; sits inside it. By the end it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orders-demo/            ← your terminal stays here, always
├── gen.py
├── sources.yaml
├── transformations.yaml
├── destinations.yaml
├── pipeline.yaml
├── workflow.yaml
└── data/
    ├── orders.csv       (generated in step 2)
    └── orders_clean.csv (written in step 5)
&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;&lt;span class="nb"&gt;mkdir &lt;/span&gt;orders-demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;orders-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use a virtual environment, create it here too (&lt;code&gt;python -m venv .venv&lt;/code&gt;) and activate it before installing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make the data (no download, no hunting on your disk)
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;gen.py&lt;/code&gt; and run &lt;code&gt;python gen.py&lt;/code&gt;. It uses the standard library only and takes a few seconds.&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;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&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;data/orders.csv&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;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&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;customer_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;country&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;price&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;qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1_000_001&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TN&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;FR&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;DE&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;ES&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;PT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                    &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/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;&lt;span class="nb"&gt;mkdir &lt;/span&gt;data &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python gen.py     &lt;span class="c"&gt;# from orders-demo/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get &lt;code&gt;data/orders.csv&lt;/code&gt;, 1,000,000 rows, about 25 MB. Note the negative prices: that is the dirt we are going to filter out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,customer_id,country,price,qty
1,41906,TN,7.63,5
2,16050,FR,65.47,2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Describe the job — four files, 30 lines
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sources.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;1.0"&lt;/span&gt;
&lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;src_orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;csv&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;base_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
    &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders.csv&lt;/span&gt;
      &lt;span class="na"&gt;batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;transformations.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;1.0"&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int&lt;/span&gt;
        &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int&lt;/span&gt;
        &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;float&lt;/span&gt;
        &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;calculate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;qty"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;destinations.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;1.0"&lt;/span&gt;
&lt;span class="na"&gt;destinations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dst_clean&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;csv&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;base_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
    &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders_clean.csv&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pipeline.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;1.0"&lt;/span&gt;
&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src_orders&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dst_clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole job: fix the types, drop invalid rows, compute a column. A reviewer can read it in a pull request without running anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Check it before it runs
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.&lt;/code&gt; means "the job in the current folder", so run this from &lt;code&gt;orders-demo/&lt;/code&gt;, not from &lt;code&gt;data/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hdrctl validate &lt;span class="nt"&gt;--strict&lt;/span&gt; &lt;span class="nb"&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 plaintext"&gt;&lt;code&gt;  ok  sources.yaml            — Pydantic valid
  ok  destinations.yaml       — Pydantic valid
  ok  transformations.yaml    — Pydantic valid
  ok  pipeline.from           — resolved: src_orders
  ok  pipeline.to             — resolved: dst_clean

  Strict checks
  ok  Recognized operations   — cast, filter, calculate
  ok  filter.expr             — 'price &amp;gt; 0'
  ok  calculate.total         — 'price * qty'

  ✅ DSL valid — no errors detected.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No data was read and nothing was written. Try breaking something on purpose — rename &lt;code&gt;dst_clean&lt;/code&gt; in &lt;code&gt;pipeline.yaml&lt;/code&gt;, or write &lt;code&gt;price * (qty&lt;/code&gt; — and run it again: validation fails with exit code 1, which is what you want in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run it
&lt;/h2&gt;

&lt;p&gt;Still from &lt;code&gt;orders-demo/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hdrctl run &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result lands in &lt;code&gt;data/orders_clean.csv&lt;/code&gt;: 990,055 rows (about 1% of the rows had a negative price and were dropped) plus the new &lt;code&gt;total&lt;/code&gt; column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,customer_id,country,price,qty,total
1,41906,TN,7.63,5,38.15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Same job, Rust engine
&lt;/h2&gt;

&lt;p&gt;Part of the engine has an optional Rust implementation. It is off by default; turning it on changes nothing else about the job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"hydra-etl[native]"&lt;/span&gt;
&lt;span class="nv"&gt;HYDRA_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rust hdrctl run &lt;span class="nb"&gt;.&lt;/span&gt;          &lt;span class="c"&gt;# from orders-demo/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows PowerShell, set the variable first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HYDRA_BACKEND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rust"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;hdrctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured on two machines, three runs each, on the 1M-row file above:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;Python (default)&lt;/th&gt;
&lt;th&gt;Rust (&lt;code&gt;HYDRA_BACKEND=rust&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Faster by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows 11 (25H2), Python 3.13.7&lt;/td&gt;
&lt;td&gt;6.4 · 6.5 · 6.7 s&lt;/td&gt;
&lt;td&gt;3.5 · 3.7 · 3.8 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~42%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux VM, Python 3.10.12&lt;/td&gt;
&lt;td&gt;8.5 · 8.7 · 9.2 s&lt;/td&gt;
&lt;td&gt;6.4 · 6.8 · 7.6 s&lt;/td&gt;
&lt;td&gt;~22%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Between 20% and 45% faster end to end, depending on the machine.&lt;/strong&gt; Be careful with bigger claims you may read elsewhere, including ours: the Rust part accelerates CSV &lt;em&gt;reading&lt;/em&gt;, and reading is only one stage of this pipeline. Speeding up one stage by 4× does not make the whole job 4× faster — and the share it represents depends on your disk, your filesystem and what else runs on the machine.&lt;/p&gt;

&lt;p&gt;One run out of the six on the Windows laptop came out at 7.7 s with the Rust engine, twice the other two. That is what a background process looks like in a benchmark; we report it rather than hide it. Run each engine at least three times before drawing a conclusion on your own hardware.&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%2Fex8tfkjxh60qfhtvsrp2.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%2Fex8tfkjxh60qfhtvsrp2.png" alt="Same job, same 1,000,000 rows 65 s with the Python engine, 38 s with the Rust engine" width="798" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output is the same, byte for byte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;md5sum &lt;/span&gt;data/orders_clean.csv   &lt;span class="c"&gt;# identical after a Python run and after a Rust run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the point of the Rust backend: same results, less time on the read, and an automatic fallback to Python whenever that guarantee cannot be kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Let Hydra ETL time itself
&lt;/h2&gt;

&lt;p&gt;Timing a job with a stopwatch is fine once. In practice you want the pipeline to report on itself — and that is what workflows are for. A workflow orders several steps in a DAG; a step is either a &lt;strong&gt;job&lt;/strong&gt; or an &lt;strong&gt;action&lt;/strong&gt; (Python, Bash, PowerShell, SSH, webhook, email…).&lt;/p&gt;

&lt;p&gt;Here, three steps: start a timer, run the job, report. The actions are Python, so the same file works on Linux, macOS and Windows.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;workflow.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timed_clean_orders"&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;million&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;report&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;long&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;took"&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_timer"&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;action&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;time,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pathlib;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pathlib.Path('.hydra_t0').write_text(str(time.time()));&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;print('timer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;started')"&lt;/span&gt;
      &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clean_orders"&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;job&lt;/span&gt;
      &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;
      &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_timer"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;on_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fail&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report_time"&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;action&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;import time, pathlib&lt;/span&gt;
          &lt;span class="s"&gt;t0 = float(pathlib.Path(".hydra_t0").read_text())&lt;/span&gt;
          &lt;span class="s"&gt;rows = sum(1 for _ in open("data/orders_clean.csv")) - 1&lt;/span&gt;
          &lt;span class="s"&gt;seconds = time.time() - t0&lt;/span&gt;
          &lt;span class="s"&gt;print(f"{rows:,} rows written in {seconds:.1f} s ({rows/seconds:,.0f} rows/s)")&lt;/span&gt;
      &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clean_orders"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workflows are validated before they run, exactly like jobs:&lt;/p&gt;

&lt;p&gt;Again from &lt;code&gt;orders-demo/&lt;/code&gt;, where &lt;code&gt;workflow.yaml&lt;/code&gt; now sits next to the four manifests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hdrctl workflow validate ./workflow.yaml
hdrctl workflow run ./workflow.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Workflow  timed_clean_orders
  ────────────────────────────────────────────────────────

  ✓  Step 'start_timer' OK (0.0s)
     │ timer started
  ✓  Step 'clean_orders' OK (7.8s)
  ✓  Step 'report_time' OK (0.2s)
     │ 990,055 rows written in 7.9 s (124,977 rows/s)

  ✅ Workflow 'timed_clean_orders' completed in 8.0s — 3/3 steps OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happened here. The runner reported the duration of &lt;strong&gt;each step&lt;/strong&gt;, which is the cheapest profiling you will ever set up. And the report came out of the pipeline itself, so the same action could just as well push it to a webhook, send it by email, or write it next to the data.&lt;/p&gt;

&lt;p&gt;Run the same workflow with &lt;code&gt;HYDRA_BACKEND=rust&lt;/code&gt; and you get the Python-versus-Rust comparison above without touching a stopwatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this shows
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A pipeline is a few readable files, versioned with your code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hdrctl validate&lt;/code&gt; fails fast on your laptop or in CI, before a single row is read.&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;pip install&lt;/code&gt; and no infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When not to use Hydra ETL
&lt;/h2&gt;

&lt;p&gt;If you orchestrate hundreds of heterogeneous tasks across a cluster, use Airflow or Dagster. If your data already lives in a warehouse, dbt is the natural fit. Hydra ETL targets file-and-database pipelines that should stay readable and run without infrastructure. It is beta: pin your version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code and docs: &lt;a href="https://github.com/bejaouibechir/Hydra" rel="noopener noreferrer"&gt;github.com/bejaouibechir/Hydra&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try it in the browser: &lt;a href="https://hydraetl.com" rel="noopener noreferrer"&gt;hydraetl.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>rust</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
