3. Create the Airflow DAG
In this step you'll write the DAG that orchestrates the full pipeline.
A DAG (Directed Acyclic Graph) defines the tasks and the order they run. Airflow reads every .py file in the dags/ folder and registers them automatically.
The generated template
DEGEN already created a starter DAG at dags/pipeline_dag.py. Open it:
| dags/pipeline_dag.py |
|---|
| from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime
with DAG(
dag_id="sales_pipeline",
start_date=datetime(2024, 1, 1),
schedule="@daily",
catchup=False,
) as dag:
def extract():
print("TODO: add extract logic")
extract_task = PythonOperator(
task_id="extract",
python_callable=extract,
)
|
It's minimal on purpose. You'll expand it now.
Replace with the full DAG
Open dags/pipeline_dag.py and replace its contents with the following:
| dags/pipeline_dag.py |
|---|
| from __future__ import annotations
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.operators.bash import BashOperator
# Absolute path to the project root (where degen.yaml lives)
PROJECT_ROOT = Path(__file__).parent.parent
default_args = {
"owner": "degen",
"retries": 1,
"retry_delay": timedelta(minutes=2),
}
def run_extract(**context):
"""Generate synthetic daily sales data and write to data/raw/."""
import csv
import random
from datetime import date
output = PROJECT_ROOT / "data" / "raw" / "sales.csv"
output.parent.mkdir(parents=True, exist_ok=True)
products = ["Widget A", "Widget B", "Widget C", "Gadget X", "Gadget Y"]
regions = ["north", "south", "east", "west"]
today = context["ds"] # Airflow injects the execution date
rows = [
{
"date": today,
"product": random.choice(products),
"region": random.choice(regions),
"quantity": random.randint(1, 50),
"price": round(random.uniform(9.99, 199.99), 2),
}
for _ in range(200)
]
with open(output, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(f"Extracted {len(rows)} records for {today} → {output}")
def run_pyspark(**context):
"""Run the PySpark transformation job."""
script = PROJECT_ROOT / "src" / "pyspark_transform.py"
venv_python = PROJECT_ROOT / ".venv" / "bin" / "python"
result = subprocess.run(
[str(venv_python), str(script)],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
)
print(result.stdout)
if result.returncode != 0:
print(result.stderr)
raise RuntimeError(f"PySpark job failed (exit {result.returncode})")
with DAG(
dag_id="sales_pipeline",
description="Daily sales ETL: extract → PySpark → dbt",
default_args=default_args,
start_date=datetime(2024, 1, 1),
schedule="@daily",
catchup=False,
tags=["degen", "sales"],
) as dag:
extract = PythonOperator(
task_id="extract",
python_callable=run_extract,
)
pyspark_transform = PythonOperator(
task_id="pyspark_transform",
python_callable=run_pyspark,
)
dbt_run = BashOperator(
task_id="dbt_run",
bash_command=(
f"cd {PROJECT_ROOT}/sales_pipeline && "
f"{PROJECT_ROOT}/.venv/bin/dbt run --profiles-dir {PROJECT_ROOT}"
),
)
dbt_test = BashOperator(
task_id="dbt_test",
bash_command=(
f"cd {PROJECT_ROOT}/sales_pipeline && "
f"{PROJECT_ROOT}/.venv/bin/dbt test --profiles-dir {PROJECT_ROOT}"
),
)
# Pipeline order: extract → pyspark → dbt run → dbt test
extract >> pyspark_transform >> dbt_run >> dbt_test
|
Save the file. No restart needed — Airflow rescans dags/ every 30 seconds.
Verify in Airflow UI
Open http://localhost:8080 and refresh the page.
You should see sales_pipeline in the list with 4 tasks:
extract → pyspark_transform → dbt_run → dbt_test
Click on the DAG name → Graph tab to see the task dependency visualization.
Tip
If the DAG doesn't appear after 60 seconds, check for syntax errors:
.venv/bin/python dags/pipeline_dag.py
No output means no errors.
What >> means
The >> operator in Airflow sets the execution order:
extract >> pyspark_transform >> dbt_run >> dbt_test
This means:
- pyspark_transform only starts after extract succeeds
- dbt_run only starts after pyspark_transform succeeds
- dbt_test only starts after dbt_run succeeds
If any task fails, the pipeline stops there and retries up to retries=1 times.
Next: Write the PySpark Job →