Data Engineering
Hands-onIntermediate 40 min· Pipelines · 8 of 18

Orchestration with Apache Airflow

Learn what an orchestrator does, then build a daily Apache Airflow DAG with the TaskFlow API: tasks, dependencies, retries, XCom and idempotent loads. Run it locally and read its logs in the web UI.

What you will build: an Airflow pipeline (a DAG) that runs every day, extracts orders, calculates revenue and saves a CSV report, with automatic retries and a clear view of what succeeded or failed.

Prerequisites: Python, the command line, and the ideas from *ETL vs ELT and pipeline design*. Apache Airflow does not run natively on Windows. Windows users should use WSL 2 (Ubuntu) or Docker; macOS and Linux work directly. This lesson is written for Airflow 3 (the current major version at the time of writing, 2026); notes mark what differs in Airflow 2.

What an orchestrator does

A pipeline is rarely one script. It is several steps that must run in order, at the right time, and recover when something fails: extract from an API, load to the warehouse, run dbt, refresh a dashboard. You could glue these together with cron, but cron cannot tell you that step 2 failed, retry just that step, wait for step 1 to finish, or re-run last Tuesday.

Apache Airflow is an open-source orchestrator. You describe the workflow in Python; Airflow schedules it, runs the steps in the right order, retries failures, keeps a history and shows everything in a web UI. Airflow does not process big data itself. It tells other tools (Spark, dbt, a SQL warehouse, a Python script) when to run. Think of it as the shift manager, not the cook.

The core vocabulary

Airflow vocabulary

TermMeaningBrew & Bean example
DAGDirected acyclic graph: tasks plus the order between them. No loops allowedThe daily sales pipeline
TaskOne unit of workextract, transform, load
OperatorA template for a task type (run Bash, run SQL, call Python)BashOperator, SQL operators from providers
Task instanceOne run of one task for one dateload for 2026-10-03
DAG runOne run of the whole DAGThe run for 3 October
SchedulerThe process that decides what should run nowTriggers the 3 October run after that day ends
Executor and workersWhere tasks actually runYour laptop in this lesson
XComSmall messages passed between tasksThe list of rows from extract to transform

A DAG is "acyclic" so that there is always a clear order: a task can depend on earlier tasks but never on itself through a loop. A DAG run covers a data interval (for example one day), and by default Airflow starts the run for an interval after that interval has ended. A @daily run covering 3 October starts just after midnight on 4 October, because only then is all of 3 October's data available.

Install Airflow

  1. 1In WSL, macOS or Linux, create a folder and a virtual environment: mkdir airflow-demo && cd airflow-demo && python3 -m venv .venv && source .venv/bin/activate.
  2. 2Choose a home for Airflow's files: export AIRFLOW_HOME=$(pwd)/airflow. Airflow will keep its database, logs and dags folder here.
  3. 3Install Airflow using the official constraints file, which pins compatible versions of its many dependencies. Copy the exact install command for your Python version from the Airflow documentation page 'Installation from PyPI'. It has the form pip install "apache-airflow==X.Y.Z" --constraint "<constraints-url>".
  4. 4Start everything with one command: airflow standalone. It sets up a local database, starts the scheduler, API server and other components, and prints a generated admin password for the web UI.
  5. 5Open http://localhost:8080 and sign in with the username and password shown in the terminal output.

airflow standalone is for learning on one machine. Production uses a proper database (PostgreSQL), separate services, and often Docker Compose, Kubernetes or a managed service.

Installation trouble is normal

Pip installs of Airflow break easily without the constraints file. If you hit dependency errors, delete the virtual environment and reinstall with the official command. The Docker Compose file from the Airflow docs is a good alternative on any OS.

Write your first DAG

Airflow reads Python files from the dags folder inside AIRFLOW_HOME (so airflow-demo/airflow/dags). Create the folder if it does not exist. Airflow's TaskFlow API turns plain Python functions into tasks with the @task decorator and wires dependencies from how you call them.

airflow/dags/brew_daily_sales.py
import csv
import json
from datetime import datetime, timedelta
from pathlib import Path

from airflow.sdk import dag, task   # Airflow 3. On Airflow 2: from airflow.decorators import dag, task

OUT_DIR = Path.home() / "brew_reports"

MENU = {
    "espresso": 120, "cappuccino": 180, "cold-brew": 220, "filter-coffee": 80,
    "masala-chai": 90, "croissant": 150, "banana-bread": 130, "veg-sandwich": 160,
}


@dag(
    dag_id="brew_daily_sales",
    schedule="@daily",
    start_date=datetime(2026, 10, 1),
    catchup=False,
    default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
    tags=["brew-and-bean"],
)
def brew_daily_sales():

    @task
    def extract(ds=None):
        """Pretend to fetch yesterday's orders. In real life: call the API or read the DB."""
        print(f"Extracting orders for {ds}")
        return [
            {"order_id": 1, "item_id": "cappuccino", "qty": 2},
            {"order_id": 2, "item_id": "espresso", "qty": 1},
            {"order_id": 3, "item_id": "croissant", "qty": 2},
        ]

    @task
    def transform(rows):
        total = 0
        for row in rows:
            if row["item_id"] not in MENU:
                raise ValueError(f"Unknown item_id: {row['item_id']}")
            row["revenue"] = row["qty"] * MENU[row["item_id"]]
            total += row["revenue"]
        return {"rows": rows, "total": total}

    @task
    def load(result, ds=None):
        OUT_DIR.mkdir(parents=True, exist_ok=True)
        path = OUT_DIR / f"sales_{ds}.csv"
        with open(path, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=["order_id", "item_id", "qty", "revenue"])
            writer.writeheader()
            writer.writerows(result["rows"])
        print(f"Wrote {path}; day total = {result['total']}")

    load(transform(extract()))


brew_daily_sales()

A three-task DAG. extract returns a list; transform checks and prices it; load writes sales_<date>.csv to a brew_reports folder in your home directory. The ds argument is the run's date as YYYY-MM-DD, which Airflow fills in.

Walk through what each part does:

@dag(...) defines the DAG. schedule="@daily" means once a day. start_date is the first date Airflow considers. catchup=False stops Airflow from creating one run for every day since the start date when you first switch the DAG on (with catchup=True it would backfill them all, which is useful for history and surprising when unexpected).

default_args applies to every task. Here each task retries twice, waiting five minutes between tries, which handles brief network failures without waking anyone.

Each @task function becomes a task. The last line, load(transform(extract())), says: run extract, pass its result to transform, pass that to load. Airflow draws the dependency graph from this.

In transform, an unknown item_id raises an error. That makes the task fail loudly, which is what you want: bad data should stop the pipeline, not sneak into reports.

Do not pass big data through XCom

XCom values are stored in Airflow's metadata database. Pass small things between tasks (ids, counts, file paths, a few rows), not whole datasets. For real data, write it to storage (a file in S3 or a table in the warehouse) and pass only the location.

Run and inspect it

  1. 1Run a single test of the whole DAG without the scheduler: airflow dags test brew_daily_sales 2026-10-01. It prints each task's logs in the terminal.
  2. 2Check the report: cat ~/brew_reports/sales_2026-10-01.csv (the file name uses the date you passed).
  3. 3In the web UI, find brew_daily_sales in the DAG list. New DAGs may start paused; use the toggle to unpause.
  4. 4Click the DAG to see the graph view: three boxes joined by arrows. Green means success.
  5. 5Trigger a run manually with the play button, then open a task and read its log.
  6. 6Make a task fail on purpose: change one item_id to "unknown", wait for the DAG file to be re-read, trigger a run and watch transform retry, then fail. Fix the code and clear the failed task to re-run it.
What the end of airflow dags test should show
[... scheduler and task log lines ...]
Extracting orders for 2026-10-01
...
Wrote /home/<you>/brew_reports/sales_2026-10-01.csv; day total = 780

The total is 2 x 180 + 120 + 2 x 150 = 780. Airflow also gives you airflow dags list to confirm it found your file, and airflow dags list-import-errors to see why a DAG file failed to load (a typo shows up there).

Make pipelines safe to re-run

Failures and re-runs are normal in orchestration, so design tasks to be idempotent: running the same task twice for the same date gives the same result as running it once. Our load task is idempotent because the file name contains the date and the file is overwritten. A task that blindly appends rows to a table would create duplicates on a retry. Use "delete that day's rows then insert", MERGE/upsert, or overwrite a date partition instead.

Always base work on the run's date (ds, or the data interval) and never on datetime.now(). Then a backfill for last month processes last month's data, not today's.

What orchestration adds beyond cron

FeatureWhat it gives you
Retries and retry_delayAutomatic recovery from temporary failures
Backfill / catchupRe-run past dates when logic changes or data arrives late
SensorsWait for a file or an upstream result before continuing
Task groups and dependenciesFan-out and fan-in, for example one extract per source then one load
Pools and concurrency limitsStop 50 tasks hammering one database
Alerts and callbacksEmail or message on failure
ProvidersPackages that add operators for Spark, dbt Cloud, AWS, GCP, databases and more

Where Airflow fits

Typical production use: Airflow triggers an ingestion job, then a Spark or dbt run (see *Apache Spark and PySpark basics* and *Transformations with dbt*), then data quality checks (see *Data quality, testing and observability*), then a notification. Alternatives you may meet are Dagster, Prefect, and the schedulers built into cloud platforms (for example Databricks Workflows or cloud-managed Airflow services). The ideas (DAG, retries, idempotency, backfills) transfer to all of them, which is why interviewers ask about them.

How this comes up in interviews: "What is a DAG?", "What is the difference between catchup and backfill?", "How do you make a task idempotent?", "How do tasks share data?" (XCom for small values, external storage for large), and "Why not just use cron?".

Prompt · Review my DAG
You are a senior data engineer. Review this Airflow DAG for production readiness. Check: idempotency, use of the run date instead of now(), retries, XCom size, secrets in code, and clarity of task boundaries. List problems in order of importance with a fixed code snippet for each.

[paste your DAG file]

What to expect: A good review points out hard-coded passwords, non-idempotent writes, use of datetime.now(), large XCom payloads and tasks that do too much. Check each suggestion before applying it.

Try it: Add a fourth task

Add a task check_total between transform and load that raises an error if the day's total is zero or negative. Then add a second DAG file that runs every hour (schedule="@hourly") and just prints the run date.

Check yourself

  1. The DAG appears in airflow dags list and the UI
  2. airflow dags test finishes and writes the CSV
  3. You can explain catchup, retries and idempotency in one sentence each
  4. No passwords or API keys are written in the DAG file (use Airflow Connections or environment variables)

Key takeaways

  1. An orchestrator schedules steps, orders them, retries failures and records history; Airflow describes this as Python code called a DAG.
  2. A DAG is tasks plus dependencies with no loops; a DAG run covers one data interval and starts after the interval ends.
  3. Use the TaskFlow API (@dag, @task) and pass only small values between tasks; store big data externally.
  4. Make tasks idempotent and base them on the run date so retries and backfills are safe.
  5. Airflow triggers other tools such as Spark and dbt; it is not where heavy data processing happens.

Check yourself

Finishing the quiz marks the lesson as done.

1. Why is a DAG 'acyclic'?

2. A DAG with catchup=True and a start date three months ago is switched on today. What happens?

3. Your load task appends rows to a table. A retry runs it twice for the same day. What is the problem and the fix?

4. What should you pass between tasks using XCom?

5. Why should a task use the run date (ds) instead of datetime.now()?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free