<?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: Zoy</title>
    <description>The latest articles on DEV Community by Zoy (@zoyluofakeall).</description>
    <link>https://dev.to/zoyluofakeall</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%2F4021455%2F507bd587-4367-42d3-8f21-643db4936c7a.jpg</url>
      <title>DEV Community: Zoy</title>
      <link>https://dev.to/zoyluofakeall</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zoyluofakeall"/>
    <language>en</language>
    <item>
      <title>Cron is too dumb. Airflow is too heavy. Meet cronova.</title>
      <dc:creator>Zoy</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:50:31 +0000</pubDate>
      <link>https://dev.to/zoyluofakeall/cron-is-too-dumb-airflow-is-too-heavy-meet-cronova-4chj</link>
      <guid>https://dev.to/zoyluofakeall/cron-is-too-dumb-airflow-is-too-heavy-meet-cronova-4chj</guid>
      <description>&lt;p&gt;&lt;em&gt;A self-hosted workflow scheduler that fits in a single Go binary — DAGs, retries, backfill, a web console, and a built-in endpoint for AI agents. No JVM, no Python, no database to run.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 Originally published by the team at &lt;strong&gt;&lt;a href="https://www.zoytown.com" rel="noopener noreferrer"&gt;zoytown.com&lt;/a&gt;&lt;/strong&gt;. cronova is a free, open-source (MIT) project — source, docs and releases are on GitHub: &lt;strong&gt;&lt;a href="https://github.com/zoyluoblue/cronova" rel="noopener noreferrer"&gt;github.com/zoyluoblue/cronova&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Every team that runs scheduled jobs eventually hits the same wall.&lt;/p&gt;

&lt;p&gt;You start with &lt;code&gt;cron&lt;/code&gt;. It's perfect — until the day one job needs to run &lt;em&gt;after&lt;/em&gt; another, or retry on failure, or backfill a week you missed, or just tell you &lt;em&gt;why&lt;/em&gt; last night's run died. &lt;code&gt;cron&lt;/code&gt; does none of that. So you start bolting on shell glue, lock files, and a Slack webhook, and six months later you've built a bad workflow engine by accident.&lt;/p&gt;

&lt;p&gt;The "grown-up" answer is &lt;strong&gt;Apache Airflow&lt;/strong&gt;. It's genuinely great — and it's also a Python stack, a Postgres, a Redis/Celery broker, and a small pile of YAML and Docker to keep alive. For a real data platform, worth it. For "I have a dozen jobs on one box," it's a second full-time system to babysit.&lt;/p&gt;

&lt;p&gt;I wanted the middle: &lt;strong&gt;real DAG scheduling, none of the operational weight.&lt;/strong&gt; That's cronova.&lt;/p&gt;

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

&lt;p&gt;cronova is an open-source &lt;strong&gt;workflow scheduler / job orchestrator&lt;/strong&gt; written in Go. It schedules &lt;strong&gt;DAGs&lt;/strong&gt; — directed acyclic graphs of tasks — on cron or interval triggers, runs each task as an OS subprocess, and ships with a web console, a REST API, a CLI, and an MCP endpoint for AI agents.&lt;/p&gt;

&lt;p&gt;The whole thing is &lt;strong&gt;one static binary&lt;/strong&gt; with an &lt;strong&gt;embedded SQLite&lt;/strong&gt; database. No JVM. No Python runtime. No external database. No message broker. No containers required. You install it with one command and you're done:&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/zoyluoblue/cronova/main/deploy/bootstrap.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That drops a native service under systemd (Linux) or launchd (macOS) and runs a short setup wizard. Upgrades are &lt;code&gt;cronova update&lt;/code&gt;; removal is &lt;code&gt;cronova uninstall&lt;/code&gt;. It manages its own lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first DAG in 60 seconds
&lt;/h2&gt;

&lt;p&gt;A DAG is a single YAML file. Drop this in &lt;code&gt;./dags/&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;dag_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily_etl&lt;/span&gt;
&lt;span class="na"&gt;schedule&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="c1"&gt;# cron; or "@every 30s"; omit for manual-only&lt;/span&gt;
&lt;span class="na"&gt;start_date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-06-01&lt;/span&gt;
&lt;span class="na"&gt;catchup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;             &lt;span class="c1"&gt;# backfill missed periods&lt;/span&gt;
&lt;span class="na"&gt;default_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&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;extract&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;shell&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;extract.py&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--date&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;logical_date&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&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;transform&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transform.py&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--date&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;logical_date&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;extract&lt;/span&gt;&lt;span class="pi"&gt;]&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;load&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psql&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;load.sql"&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;transform&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1800&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cronova serve                 &lt;span class="c"&gt;# scheduler + console at http://localhost:8090&lt;/span&gt;
cronova trigger daily_etl     &lt;span class="c"&gt;# run it now&lt;/span&gt;
cronova runs daily_etl        &lt;span class="c"&gt;# watch task states advance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{{ logical_date }}&lt;/code&gt; is the run's &lt;em&gt;logical&lt;/em&gt; date — the period it represents — which is what makes catchup meaningful: a backfilled run processes the data for &lt;em&gt;that&lt;/em&gt; day, not wall-clock "now". The same values land in your process as &lt;code&gt;CRONOVA_*&lt;/code&gt; environment variables, so your scripts can just read them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that make it feel finished
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A web console that isn't an afterthought.&lt;/strong&gt; Open &lt;code&gt;http://localhost:8090&lt;/code&gt; and you get a dashboard, run history with sparklines, a dependency graph, and &lt;strong&gt;live log tailing&lt;/strong&gt; while a task runs.&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%2Fpfc0sfxjngvpwi1abpgv.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%2Fpfc0sfxjngvpwi1abpgv.png" alt="cronova dashboard — self-hosted workflow scheduler" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The task editor is the piece I'm proudest of. You don't type &lt;code&gt;{{ }}&lt;/code&gt; template syntax — every variable is a color-coded &lt;strong&gt;pill&lt;/strong&gt; you click or drag in from a grouped palette (built-in run variables, your shared variables, connections, and per-run params).&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%2Fmz6u0s7o3gguej8i08bj.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%2Fmz6u0s7o3gguej8i08bj.png" alt="cronova task editor — drag-and-drop template variable pills" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polyglot tasks.&lt;/strong&gt; Every task runs as a subprocess, so it can be &lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;sql&lt;/code&gt;, a &lt;code&gt;jar&lt;/code&gt;, or an &lt;code&gt;http&lt;/code&gt; call — any language on the host. You can even drag-and-drop a whole project folder (or a &lt;code&gt;.zip&lt;/code&gt;) into the console and point a task at it; each run gets a fresh, isolated copy as its working directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash-recoverable execution.&lt;/strong&gt; Run tasks in the optional decoupled gRPC executor and you can restart — or upgrade — the scheduler &lt;em&gt;without killing running jobs&lt;/em&gt;. On recovery it re-attaches to in-flight tasks, with no double execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It speaks Airflow's vocabulary.&lt;/strong&gt; Dependencies, cron and interval schedules, cross-DAG triggers, catchup/backfill, per-task retries and timeouts, resource pools, and trigger rules — the primitives you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody else ships: it's AI-native
&lt;/h2&gt;

&lt;p&gt;cronova has a built-in &lt;strong&gt;Model Context Protocol (MCP) server&lt;/strong&gt;. Point Claude (or any MCP client) at it and an AI agent can list, create, validate, trigger, and inspect DAGs — through the &lt;em&gt;same&lt;/em&gt; token-authenticated, role-gated API a human uses, not a bolted-on hack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cronova tokens create my-agent &lt;span class="nt"&gt;-role&lt;/span&gt; admin
cronova mcp                                   &lt;span class="c"&gt;# MCP server over stdio&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a remote JSON CLI for scripting:&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;export &lt;/span&gt;&lt;span class="nv"&gt;CRONOVA_SERVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8090 &lt;span class="nv"&gt;CRONOVA_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cnv_pat_…
cronova dags &lt;span class="nt"&gt;-o&lt;/span&gt; json
cronova api POST /api/dags/validate &lt;span class="s1"&gt;'{"dag_id":"x","tasks":[…]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where it fits (and where it doesn't)
&lt;/h2&gt;

&lt;p&gt;cronova is for the sweet spot between a bare &lt;code&gt;crontab&lt;/code&gt; and a full data platform: &lt;strong&gt;you want DAGs, retries, backfill, pools, a UI and an API — but not a stack to operate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Being honest about the other end: if you need hundreds of provider integrations, a managed cloud offering, or Python-native dynamically-generated DAGs at massive scale, &lt;strong&gt;Airflow&lt;/strong&gt; (or &lt;strong&gt;Dagster&lt;/strong&gt; / &lt;strong&gt;Prefect&lt;/strong&gt;) is still the richer choice. cronova deliberately trades that ecosystem for &lt;em&gt;operational simplicity&lt;/em&gt;: one binary, YAML DAGs, tens of megabytes, Linux &amp;amp; macOS on amd64/arm64.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/zoyluoblue/cronova" rel="noopener noreferrer"&gt;github.com/zoyluoblue/cronova&lt;/a&gt; — one-line install, sample DAGs, MIT license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://zoyluoblue.github.io/cronova/" rel="noopener noreferrer"&gt;zoyluoblue.github.io/cronova&lt;/a&gt; — a full tutorial, a console guide, and a complete CLI reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it saves you from standing up an Airflow cluster for a handful of jobs, a ⭐ on GitHub genuinely helps other people find it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;cronova is an open-source project from the team behind *&lt;/em&gt;&lt;a href="https://www.zoytown.com" rel="noopener noreferrer"&gt;zoytown.com&lt;/a&gt;*&lt;em&gt;. Built in Go, MIT-licensed. Questions and issues welcome on &lt;a href="https://github.com/zoyluoblue/cronova" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>devops</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
