MLOpsThe MLOps lifecycle · MLflow 3.17 · DVC 3.67 · FastAPI · Docker · GitHub Actions · Evidently · Python 3.11+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
50 small wins to finish your pathNext lesson →

Orchestrating with Airflow

Apache Airflow is an orchestrator: it holds a pipeline as a graph of tasks, runs them in dependency order on a schedule, retries the ones that fail, and keeps a record of every run. The retraining loop needs exactly that, and a cron line does not provide it.

Last updated: 19 Oct, 2026 · MLOps

Retraining strategies left the retrain needing something to start it every week. This lesson gives it one, as a small DAG over the two stages the training pipeline already has.

What a DAG is, and the three tasks of an ETL run · from the Apache Airflow One Shot- Building End To End ETL Pipeline Using AirFlow And Astro · 18:07 to 21:08

The clip's DAG extracts weather data, transforms it and loads it into Postgres; this DAG has two tasks that run the retraining pipeline, and the graph, the task and the dependency between them mean the same thing.

Reading a DAG as a graph of tasks

The video builds an extract, transform and load pipeline and defines the shape first. A DAG is a directed acyclic graph: directed because each arrow points from one task to the next, acyclic because the arrows never come back around to where they started. Each node is a task, one unit of work, and an arrow from one task to another says the second runs only after the first has succeeded. The pipeline here is the same shape with two nodes: pull the data, then fit the model.

Declaring the DAG and its schedule

The with DAG(...) block names the pipeline and says when it runs. catchup=False matters more than it looks: without it, a DAG switched on today with a start date in January would immediately queue every weekly run it missed, and a backlog of stale retrains helps nobody.

python
from datetime import datetime, timedelta

from airflow.sdk import DAG, task

# a missed week is skipped rather than queued up, so no stale retrain runs late
with DAG(
    dag_id="sp_retrain",
    start_date=datetime(2026, 1, 1),
    schedule=timedelta(days=7),
    catchup=False,
    default_args={"owner": "mlops", "retries": 1},
) as dag:
    ...   # the tasks are defined inside this block
Watch out. Two names moved in Airflow 3, and the video's code uses the Airflow 2 spelling of both, but they fail differently. schedule_interval was removed, not renamed to an alias: passing it raises TypeError: DAG.__init__() got an unexpected keyword argument 'schedule_interval' on 3.3.2, so the DAG file does not even import. The argument is schedule. The decorator is the softer case: importing task from airflow.decorators still resolves and only warns, The `airflow.decorators.task` attribute is deprecated. Please use `'airflow.sdk.task'`.

Writing the two tasks

A function under @task becomes a task. The imports sit inside the functions rather than at the top of the file, because the scheduler parses this file constantly to find DAGs and a heavy import at module level slows every parse down.

python
    @task
    def ingest() -> int:
        # imported inside the task: the scheduler parses this file often and should stay light
        from sp.components.data_ingestion import DataIngestion
        from sp.config.configuration import ConfigurationManager

        cfg = ConfigurationManager()
        ingestion = DataIngestion(cfg.get_data_ingestion_config())
        frame = ingestion.download()
        ingestion.split(frame)
        return len(frame)
python
    @task
    def train(rows: int) -> dict:
        from sp.pipeline.train_pipeline import main

        return main()

    # calling one task with the other's result is what creates the dependency
    train(ingest())

That last line is the whole wiring. Passing ingest() into train() does not run anything; it tells Airflow that train depends on ingest, and the row count travels between them as the task's return value.

The file the scheduler reads

python
from datetime import datetime, timedelta

from airflow.sdk import DAG, task

# a missed week is skipped rather than queued up, so no stale retrain runs late
with DAG(
    dag_id="sp_retrain",
    start_date=datetime(2026, 1, 1),
    schedule=timedelta(days=7),
    catchup=False,
    default_args={"owner": "mlops", "retries": 1},
) as dag:

    @task
    def ingest() -> int:
        # imported inside the task: the scheduler parses this file often and should stay light
        from sp.components.data_ingestion import DataIngestion
        from sp.config.configuration import ConfigurationManager

        cfg = ConfigurationManager()
        ingestion = DataIngestion(cfg.get_data_ingestion_config())
        frame = ingestion.download()
        ingestion.split(frame)
        return len(frame)

    @task
    def train(rows: int) -> dict:
        from sp.pipeline.train_pipeline import main

        return main()

    train(ingest())
The sp_retrain DAG drawn left to right. A pill labelled schedule 7 days points to the ingest task, which downloads stud.csv and writes train.csv and test.csv. The ingest task points to the train task, which validates, transforms, fits the forest and writes model.joblib and metrics.json. The train task points to a rounded artifacts node. Underneath, a panel titled what the parse reports shows tasks ingest and train, deps ingest mapping to train and train mapping to nothing, a schedule of 7 days 0 hours, catchup False and retries 1.

Parsing the DAG before trusting it

The first test of a DAG file is whether it imports at all. A typo or a missing package here shows up in the Airflow interface as a broken DAG with no tasks, which is a slow way to find a one-line error. Importing the file and reading the DAG object back is quicker, and it needs no scheduler.

bash
$ cd dags
$ python -c "import sp_retrain as m; d = m.dag; print(d.dag_id, '|', d.schedule, '|', sorted(t.task_id for t in d.tasks), '|', d.catchup)"
sp_retrain | 7 days, 0:00:00 | ['ingest', 'train'] | False

The schedule comes back as a seven-day interval, both tasks are there, and catchup is off. The dependency is readable the same way: ingest has train downstream of it, and train has nothing downstream.

Running it on a real schedule

Parsing proves the file is valid. Running it on a schedule needs more than a Python interpreter: Airflow keeps the state of every run in a metadata database, and a scheduler process has to be alive to create those runs. The quick start documents three commands for a local install, and airflow standalone rolls them into one.

bash
# from the Airflow quick start, shown as the docs give it, not run here
$ airflow db migrate
$ airflow api-server --port 8080
$ airflow scheduler

# or all of it in one process, for a local trial
$ airflow standalone

Those are the commands the Airflow 3 quick start gives at airflow.apache.org/docs/apache-airflow/stable/start.html, and the generated admin password lands in $AIRFLOW_HOME/simple_auth_manager_passwords.json.generated. A one-off run needs less than that: airflow dags test sp_retrain executes a single DagRun in the foreground against the metadata database, with no scheduler process involved, which the documentation describes as a simulated execution flow similar to a LocalExecutor session.

Airflow 2 against Airflow 3

Airflow 2, the release in the videoAirflow 3.3.2, the release here
Importing DAG and taskfrom airflow import DAG, from airflow.decorators import taskfrom airflow.sdk import DAG, task
The cadence argumentschedule_interval="@daily"schedule="@daily" or a timedelta. schedule_interval was removed: passing it raises TypeError, so the file will not import
The old DAG and task importsThe only wayfrom airflow import DAG and from airflow.decorators import task both still resolve, with a deprecation warning
The web processairflow webserverairflow api-server
Everything at onceairflow standaloneairflow standalone

What an orchestrator buys over a cron line

  • Dependencies are declared, not implied. The train task cannot start before ingest finishes, and that rule lives in one place rather than in the order of two cron entries.
  • Retries are per task. A failed download retries on its own without refitting the model, which is what retries: 1 in the default arguments sets up.
  • Every run has a record. Which runs happened, what each task returned, how long it took, where it failed: the interface the video walks through is a log of the pipeline's history.
  • Backfills are a decision. catchup makes replaying missed windows a choice you state, rather than something that happens to you.
Watch out. Airflow is a scheduler, not a worker: a task that fits a model inside the scheduler's own process will fight the scheduler for memory. For anything heavier than this example, a task should launch the work somewhere else, a container or a training job, and wait for it.
Try it yourself
  • Change schedule to "@daily" and parse the file again: the reported schedule changes to the cron preset.
  • Add a third task that reads metrics.json and prints r2, and make it depend on train by passing train's result into it.
  • Set catchup=True, parse the file, and work out how many runs a start date of January would queue.

Slow is fine. Stopping is the only problem.