| name | airflow-dag-patterns |
| description | Build production Apache Airflow DAGs with best practices for operators, sensors, testing, and deployment. Use when creating data pipelines, orchestrating workflows, or scheduling batch jobs. |
| version | 1.0.0 |
| cluster | databases-data |
Apache Airflow DAG Patterns
Production-ready patterns for Apache Airflow including DAG design, operators, sensors, testing, and deployment strategies.
When to Use This Skill
- Creating data pipeline orchestration with Airflow
- Designing DAG structures and dependencies
- Implementing custom operators and sensors
- Testing Airflow DAGs locally
- Setting up Airflow in production
- Debugging failed DAG runs
Core Concepts
1. DAG Design Principles
| Principle | Description |
|---|
| Idempotent | Running twice produces same result |
| Atomic | Tasks succeed or fail completely |
| Incremental | Process only new/changed data |
| Observable | Logs, metrics, alerts at every step |
2. Task Dependencies
task1 >> task2 >> task3
task1 >> [task2, task3, task4]
[task1, task2, task3] >> task4
task1 >> task2 >> task4
task1 >> task3 >> task4
Quick Start
from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.operators.empty import EmptyOperator
default_args = {
'owner': 'data-team',
'depends_on_past': False,
'email_on_failure': True,
'email_on_retry': False,
'retries': 3,
'retry_delay': timedelta(minutes=5),
'retry_exponential_backoff': True,
'max_retry_delay': timedelta(hours=1),
}
with DAG(
dag_id='example_etl',
default_args=default_args,
description='Example ETL pipeline',
schedule='0 6 * * *',
start_date=datetime(2024, 1, 1),
catchup=False,
tags=['etl', 'example'],
max_active_runs=1,
) as dag:
start = EmptyOperator(task_id='start')
def extract_data(**context):
execution_date = context['ds']
return {'records': 1000}
extract = PythonOperator(
task_id=,
python_callable=extract_data,
)
end = EmptyOperator(task_id=)
start >> extract >> end
Patterns
Pattern 1: TaskFlow API (Airflow 2.0+)
from datetime import datetime
from airflow.decorators import dag, task
from airflow.models import Variable
@dag(
dag_id='taskflow_etl',
schedule='@daily',
start_date=datetime(2024, 1, 1),
catchup=False,
tags=['etl', 'taskflow'],
)
def taskflow_etl():
"""ETL pipeline using TaskFlow API"""
@task()
def extract(source: str) -> dict:
"""Extract data from source"""
import pandas as pd
df = pd.read_csv(f's3://bucket/{source}/{{ ds }}.csv')
return {'data': df.to_dict(), 'rows': len(df)}
@task()
def transform(extracted: dict) -> dict:
"""Transform extracted data"""
import pandas as pd
df = pd.DataFrame(extracted['data'])
df['processed_at'] = datetime.now()
df = df.dropna()
return {'data': df.to_dict(), : (df)}
():
pandas pd
df = pd.DataFrame(transformed[])
df.to_parquet()
transformed[]
():
()
extracted = extract(source=)
transformed = transform(extracted)
loaded = load(transformed, target=)
notify(loaded)
taskflow_etl()
Pattern 2: Dynamic DAG Generation
from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.models import Variable
import json
PIPELINE_CONFIGS = [
{'name': 'customers', 'schedule': '@daily', 'source': 's3://raw/customers'},
{'name': 'orders', 'schedule': '@hourly', 'source': 's3://raw/orders'},
{'name': 'products', 'schedule': '@weekly', 'source': 's3://raw/products'},
]
def create_dag(config: dict) -> DAG:
"""Factory function to create DAGs from config"""
dag_id = f"etl_{config['name']}"
default_args = {
'owner': 'data-team',
'retries': 3,
'retry_delay': timedelta(minutes=5),
}
dag = DAG(
dag_id=dag_id,
default_args=default_args,
schedule=config['schedule'],
start_date=datetime(2024, 1, 1),
catchup=,
tags=[, , config[]],
)
dag:
():
()
():
()
():
()
extract = PythonOperator(
task_id=,
python_callable=extract_fn,
op_kwargs={: config[]},
)
transform = PythonOperator(
task_id=,
python_callable=transform_fn,
)
load = PythonOperator(
task_id=,
python_callable=load_fn,
op_kwargs={: config[]},
)
extract >> transform >> load
dag
config PIPELINE_CONFIGS:
()[] = create_dag(config)
Pattern 3: Branching and Conditional Logic
from airflow.decorators import dag, task
from airflow.operators.python import BranchPythonOperator
from airflow.operators.empty import EmptyOperator
from airflow.utils.trigger_rule import TriggerRule
@dag(
dag_id='branching_pipeline',
schedule='@daily',
start_date=datetime(2024, 1, 1),
catchup=False,
)
def branching_pipeline():
@task()
def check_data_quality() -> dict:
"""Check data quality and return metrics"""
quality_score = 0.95
return {'score': quality_score, 'rows': 10000}
def choose_branch(**context) -> str:
"""Determine which branch to execute"""
ti = context['ti']
metrics = ti.xcom_pull(task_ids='check_data_quality')
if metrics['score'] >= 0.9:
return 'high_quality_path'
elif metrics['score'] >= 0.7:
return 'medium_quality_path'
:
quality_check = check_data_quality()
branch = BranchPythonOperator(
task_id=,
python_callable=choose_branch,
)
high_quality = EmptyOperator(task_id=)
medium_quality = EmptyOperator(task_id=)
low_quality = EmptyOperator(task_id=)
join = EmptyOperator(
task_id=,
trigger_rule=TriggerRule.NONE_FAILED_MIN_ONE_SUCCESS,
)
quality_check >> branch >> [high_quality, medium_quality, low_quality] >> join
branching_pipeline()
Pattern 4: Sensors and External Dependencies
from datetime import datetime, timedelta
from airflow import DAG
from airflow.sensors.filesystem import FileSensor
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor
from airflow.sensors.external_task import ExternalTaskSensor
from airflow.operators.python import PythonOperator
with DAG(
dag_id='sensor_example',
schedule='@daily',
start_date=datetime(2024, 1, 1),
catchup=False,
) as dag:
wait_for_file = S3KeySensor(
task_id='wait_for_s3_file',
bucket_name='data-lake',
bucket_key='raw/{{ ds }}/data.parquet',
aws_conn_id='aws_default',
timeout=60 * 60 * 2,
poke_interval=60 * 5,
mode='reschedule',
)
wait_for_upstream = ExternalTaskSensor(
task_id='wait_for_upstream_dag',
external_dag_id='upstream_etl',
external_task_id='final_task',
execution_date_fn=lambda dt: dt,
timeout=60 * 60 * ,
mode=,
)
() -> PokeReturnValue:
requests
response = requests.get()
is_done = response.status_code ==
PokeReturnValue(is_done=is_done, xcom_value=response.json())
api_ready = wait_for_api()
():
api_result = context[].xcom_pull(task_ids=)
()
process = PythonOperator(
task_id=,
python_callable=process_data,
)
[wait_for_file, wait_for_upstream, api_ready] >> process
Pattern 5: Error Handling and Alerts
from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.utils.trigger_rule import TriggerRule
from airflow.models import Variable
def task_failure_callback(context):
"""Callback on task failure"""
task_instance = context['task_instance']
exception = context.get('exception')
message = f"""
Task Failed!
DAG: {task_instance.dag_id}
Task: {task_instance.task_id}
Execution Date: {context['ds']}
Error: {exception}
Log URL: {task_instance.log_url}
"""
print(message)
def dag_failure_callback(context):
"""Callback on DAG failure"""
pass
with DAG(
dag_id='error_handling_example',
schedule='@daily',
start_date=datetime(2024, 1, 1),
catchup=False,
on_failure_callback=dag_failure_callback,
default_args={
'on_failure_callback': task_failure_callback,
'retries': 3,
'retry_delay': timedelta(minutes=),
},
) dag:
():
random
random.random() < :
ValueError()
risky_task = PythonOperator(
task_id=,
python_callable=might_fail,
)
():
()
cleanup_task = PythonOperator(
task_id=,
python_callable=cleanup,
trigger_rule=TriggerRule.ALL_DONE,
)
():
()
success_notification = PythonOperator(
task_id=,
python_callable=notify_success,
trigger_rule=TriggerRule.ALL_SUCCESS,
)
risky_task >> [cleanup_task, success_notification]
Pattern 6: Testing DAGs
import pytest
from datetime import datetime
from airflow.models import DagBag
@pytest.fixture
def dagbag():
return DagBag(dag_folder='dags/', include_examples=False)
def test_dag_loaded(dagbag):
"""Test that all DAGs load without errors"""
assert len(dagbag.import_errors) == 0, f"DAG import errors: {dagbag.import_errors}"
def test_dag_structure(dagbag):
"""Test specific DAG structure"""
dag = dagbag.get_dag('example_etl')
assert dag is not None
assert len(dag.tasks) == 3
assert dag.schedule_interval == '0 6 * * *'
def test_task_dependencies(dagbag):
"""Test task dependencies are correct"""
dag = dagbag.get_dag('example_etl')
extract_task = dag.get_task('extract')
assert 'start' in [t.task_id for t in extract_task.upstream_list]
assert 'end' in [t.task_id t extract_task.downstream_list]
():
dag_id, dag dagbag.dags.items():
dag.test_cycle() ,
():
dags.example_dag extract_data
result = extract_data(ds=)
result
(result[], )
Project Structure
airflow/
โโโ dags/
โ โโโ __init__.py
โ โโโ common/
โ โ โโโ __init__.py
โ โ โโโ operators.py # Custom operators
โ โ โโโ sensors.py # Custom sensors
โ โ โโโ callbacks.py # Alert callbacks
โ โโโ etl/
โ โ โโโ customers.py
โ โ โโโ orders.py
โ โโโ ml/
โ โโโ training.py
โโโ plugins/
โ โโโ custom_plugin.py
โโโ tests/
โ โโโ __init__.py
โ โโโ test_dags.py
โ โโโ test_operators.py
โโโ docker-compose.yml
โโโ requirements.txt
Best Practices
Do's
- Use TaskFlow API - Cleaner code, automatic XCom
- Set timeouts - Prevent zombie tasks
- Use
mode='reschedule' - For sensors, free up workers
- Test DAGs - Unit tests and integration tests
- Idempotent tasks - Safe to retry
Don'ts
- Don't use
depends_on_past=True - Creates bottlenecks
- Don't hardcode dates - Use
{{ ds }} macros
- Don't use global state - Tasks should be stateless
- Don't skip catchup blindly - Understand implications
- Don't put heavy logic in DAG file - Import from modules
Resources