home
diamond Go Premium
Data Engineering Path  ·  Airflow
Apache Airflow Logo

Writing Your First Production DAG

From Zero to Production — A Complete Walkthrough

This guide walks you through building a complete, production-ready DAG step by step. We'll build a real-world ETL pipeline that extracts data from an API, transforms it, loads it to a database, and sends a notification.


Step 1: The DAG File Structure

Every DAG file follows this structure:

# ============================================================
# File: dags/daily sales etl.py
# Description: Daily ETL pipeline for sales data
# Author: Data Engineering Team
# ============================================================

# --- 1. Imports ---
from airflow.sdk import dag, task
from datetime import datetime, timedelta
import json

# --- 2. Default Arguments ---
default_args = {
    "owner": "data-engineering",
    "depends_on_past": False,
    "email": ["data-team@company.com"],
    "email_on_failure": True,
    "email_on_retry": False,
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
    "execution_timeout": timedelta(hours=1),
}

# --- 3. DAG Definition ---
@dag(
    dag_id="daily_sales_etl",
    default_args=default_args,
    description="Extract sales data from API, transform, and load to warehouse",
    schedule="0 6 * * *",
    start_date=datetime(2024, 1, 1),
    catchup=False,
    tags=["production", "sales", "etl"],
    doc_md=__doc__,
    max_active_runs=1,
)
def daily_sales_etl():

    # --- 4. Task Definitions ---
    @task()
    def extract() -> dict:
        """Extract data from the sales API."""
        import requests
        response = requests.get(
            "https://api.company.com/v1/sales",
            headers={"Authorization": f"Bearer {Variable.get('api_token')}"},
            params={"date": "{{ ds }}"},
        )
        response.raise_for_status()
        data = response.json()
        return {"records": data, "count": len(data)}

    @task()
    def validate(raw: dict) -> dict:
        """Validate extracted data."""
        records = raw["records"]
        assert len(records) > 0, "No records extracted!"
        assert all("amount" in r for r in records), "Missing 'amount' field!"
        return raw

    @task()
    def transform(validated: dict) -> list:
        """Clean and transform data."""
        records = validated["records"]
        return [
            {
                "sale_id": r["id"],
                "amount": round(float(r["amount"]), 2),
                "currency": r.get("currency", "USD"),
                "timestamp": r["created_at"],
            }
            for r in records
            if float(r["amount"]) > 0
        ]

    @task()
    def load(transformed: list):
        """Load data to PostgreSQL warehouse."""
        from airflow.providers.postgres.hooks.postgres import PostgresHook
        hook = PostgresHook(postgres_conn_id="warehouse")
        hook.insert_rows(
            table="sales_fact",
            rows=[(r["sale_id"], r["amount"], r["currency"], r["timestamp"])
                  for r in transformed],
            target_fields=["sale_id", "amount", "currency", "timestamp"],
        )
        return len(transformed)

    @task()
    def notify(row_count: int):
        """Send completion notification."""
        from airflow.providers.slack.hooks.slack import SlackHook
        hook = SlackHook(slack_conn_id="slack_data_team")
        hook.send(
            text=f"Sales ETL completed: {row_count} records loaded for {{ ds }}",
            channel="#data-pipeline-alerts",
        )

    # --- 5. Task Dependencies ---
    raw = extract()
    validated = validate(raw)
    transformed = transform(validated)
    rows = load(transformed)
    notify(rows)

# --- 6. DAG Instantiation ---
daily_sales_etl()

Step 2: Understanding Each Section

Section Purpose
Imports Import Airflow modules and any external libraries
Default Args Set common parameters (retries, alerts, timeouts) for all tasks
DAG Definition Configure the DAG's ID, schedule, tags, and behavior
Task Definitions Define the actual work each task performs
Dependencies Specify the execution order
Instantiation Call the @dag function to register it with Airflow
Best Practices
  • Set catchup=False unless you specifically need backfilling
  • Set max_active_runs=1 to prevent overlapping runs
  • Always add tags for easy filtering in the UI
  • Use execution_timeout to prevent runaway tasks

Step 3: Save It, and Watch Airflow Find It

Writing the file is only half the job — here's what actually happens next, end to end.

3.1 — Save the file into your DAGs folder

Airflow only looks in one place: the folder configured as [core] dags_folder in airflow.cfg (by default, $AIRFLOW_HOME/dags). The file above needs to physically live there:

# The file from Step 1, saved exactly where Airflow expects it
$AIRFLOW_HOME/dags/daily_sales_etl.py

Nothing else is required — no registration command, no restart. A background process called the DAG File Processor continuously rescans this folder (by default every 30 seconds, [scheduler] dag_dir_list_interval) and parses any .py file that defines a DAG.

3.2 — Confirm it shows up in the Airflow UI

If the file is syntactically valid Python and the DAG is well-formed, it appears in the DAGs list on its own — no button to click, no manual step:

Airflow DAGs list — daily_sales_etl freshly discovered, correctly showing its tags, schedule, and owner exactly as defined in the code Figure — this is the DAG from Step 1, moments after being saved. Note it's paused by default (the toggle on the left) — new DAGs never run automatically until you explicitly turn them on, exactly as intended.

Everything here is read straight from the code you wrote: the tags=["production", "sales", "etl"] list, the schedule="0 6 * * *" (rendered as 0 6 * * *), and owner="data-engineering" from default_args. If any of these look wrong, the fix is always the same — edit the file, save it, wait for the next scan.

If it doesn't show up
Check DAG Import Errors at the top of the DAGs list first — a syntax error or bad import fails silently from the UI's perspective (no crash, the DAG just never appears) but is always reported there with a full traceback.

3.3 — Trigger it and read the Graph View

Toggle the DAG on, click the play button to trigger a manual run, and open the Graph tab to see every task and how they connect:

Airflow Graph View — extract, validate, transform, load, and notify, all connected left to right and all succeeded Figure — the exact five tasks from Step 1's code, in the exact order raw = extract(); validated = validate(raw); ... wires them. Green means success; click any box to see its logs.

This is the full loop: write the code → save it in the DAGs folder → Airflow discovers and parses it automatically → the UI shows you exactly the structure you wrote → running it confirms it actually works. Every DAG you build for the rest of this course follows this same loop.

lock

This content is reserved for Premium Members.

Upgrade to Premium

Entity Details

Create New Item

help

Submit Technical Query

Have a question or run into an issue? Describe it below, upload an optional screenshot, and our engineering team will answer it!

image Attach image (optional)

Submit Feedback

build Free Developer Utility Free Tool
gavel

Privacy & Legal Disclaimer

1. Client-Side Browser Processing

All utility tools on DeepEngineerHub (including Image to PDF, Text Formatters, JSON Converters, and Encryptors) execute 100% locally within your client browser using WebAssembly and JavaScript. No uploaded images, text, or documents are transmitted, collected, or stored on remote servers.

2. Limitation of Liability ("As-Is" Provision)

Tools and services are provided free of charge for convenience and educational purposes "as-is" without warranties of any kind. DeepEngineerHub shall not be held liable for any data loss, formatting inconsistencies, or indirect damages resulting from tool usage.

3. Open Source & Third-Party Software

Certain utilities utilize open-source client libraries (such as jsPDF, Mermaid.js, Pyodide) licensed under MIT, Apache, or BSD open licenses. All intellectual property remains with their respective copyright holders.