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
| Term | Meaning | Brew & Bean example |
|---|---|---|
| DAG | Directed acyclic graph: tasks plus the order between them. No loops allowed | The daily sales pipeline |
| Task | One unit of work | extract, transform, load |
| Operator | A template for a task type (run Bash, run SQL, call Python) | BashOperator, SQL operators from providers |
| Task instance | One run of one task for one date | load for 2026-10-03 |
| DAG run | One run of the whole DAG | The run for 3 October |
| Scheduler | The process that decides what should run now | Triggers the 3 October run after that day ends |
| Executor and workers | Where tasks actually run | Your laptop in this lesson |
| XCom | Small messages passed between tasks | The 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
- 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. - 2Choose a home for Airflow's files:
export AIRFLOW_HOME=$(pwd)/airflow. Airflow will keep its database, logs and dags folder here. - 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>". - 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. - 5Open
http://localhost:8080and 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.
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
- 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. - 2Check the report:
cat ~/brew_reports/sales_2026-10-01.csv(the file name uses the date you passed). - 3In the web UI, find
brew_daily_salesin the DAG list. New DAGs may start paused; use the toggle to unpause. - 4Click the DAG to see the graph view: three boxes joined by arrows. Green means success.
- 5Trigger a run manually with the play button, then open a task and read its log.
- 6Make a task fail on purpose: change one
item_idto"unknown", wait for the DAG file to be re-read, trigger a run and watchtransformretry, then fail. Fix the code and clear the failed task to re-run it.
[... scheduler and task log lines ...]
Extracting orders for 2026-10-01
...
Wrote /home/<you>/brew_reports/sales_2026-10-01.csv; day total = 780The 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
| Feature | What it gives you |
|---|---|
| Retries and retry_delay | Automatic recovery from temporary failures |
| Backfill / catchup | Re-run past dates when logic changes or data arrives late |
| Sensors | Wait for a file or an upstream result before continuing |
| Task groups and dependencies | Fan-out and fan-in, for example one extract per source then one load |
| Pools and concurrency limits | Stop 50 tasks hammering one database |
| Alerts and callbacks | Email or message on failure |
| Providers | Packages 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?".
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
- The DAG appears in
airflow dags listand the UI airflow dags testfinishes and writes the CSV- You can explain catchup, retries and idempotency in one sentence each
- No passwords or API keys are written in the DAG file (use Airflow Connections or environment variables)
Key takeaways
- An orchestrator schedules steps, orders them, retries failures and records history; Airflow describes this as Python code called a DAG.
- A DAG is tasks plus dependencies with no loops; a DAG run covers one data interval and starts after the interval ends.
- Use the TaskFlow API (
@dag,@task) and pass only small values between tasks; store big data externally. - Make tasks idempotent and base them on the run date so retries and backfills are safe.
- 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.