Authoring Airflow DAGs
When to use
- Creating or refactoring Airflow DAGs and tasks.
- Configuring schedules, catchup/backfill, retries, and SLAs.
- Passing data between tasks (XCom) or using connections/variables.
- Do NOT use for diagnosing a broken running DAG (use
debugging-airflow-pipelines).
Workflow
- [ ] Make each task idempotent and parameterized by the data interval
- [ ] Keep expensive/import-heavy code inside tasks, not at module top level
- [ ] Set schedule + catchup deliberately
- [ ] Configure retries, retry_delay, and SLAs
- [ ] Wire dependencies via TaskFlow return values or >> operators
- Idempotent tasks — a task for the
2026-01-15 interval must produce the
same result whether it runs once or is re-run. Use the data interval, not
datetime.now().
- No heavy top-level code — the scheduler parses every DAG file frequently;
database calls, API calls, or big imports at module level slow scheduling and
can break parsing. Put them inside tasks.
- Schedule + catchup on purpose —
catchup=True backfills every missed
interval from start_date; default to False unless you want that.
- Retries and SLAs — transient failures are normal; set
retries and
retry_delay; use SLAs/alerts for lateness.
Patterns
TaskFlow DAG, idempotent and cleanly wired:
from airflow.decorators import dag, task
import pendulum
@dag(
schedule="@daily",
start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
catchup=False,
default_args={"retries": 3, "retry_delay": pendulum.duration(minutes=5)},
tags=["orders"],
)
def orders_pipeline():
@task
def extract(data_interval_start=None):
# Use the interval, not now(), so re-runs are deterministic.
return fetch_orders(day=data_interval_start.date())
@task
def load(rows):
# Delete-insert the partition -> idempotent on retry.
overwrite_partition("fct_orders", rows)
load(extract())
orders_pipeline()
Pass small data via XCom (return values); pass large data via storage — write
to S3/GCS/warehouse and pass the path/key, never megabytes through XCom.
Use connections/variables for secrets and config (BaseHook.get_connection,
Variable.get), never hard-coded credentials.
Common pitfalls
- Top-level API/DB calls or heavy imports — slow the scheduler and can fail
DAG parsing across the whole deployment.
datetime.now() inside tasks — breaks idempotency and backfills; use
data_interval_start/_end.
catchup=True unintentionally — floods the cluster with historical runs on
first deploy.
- Large payloads through XCom — bloats the metadata DB; pass references.
- Dynamic
start_date (e.g. days_ago) — makes schedules nondeterministic;
use a fixed timestamp.
- One monster task — split extract/transform/load so retries are granular.
References
- Scheduling, catchup, and backfill reference
1---2name: authoring-airflow-dags3description: Write production-grade Apache Airflow DAGs using the TaskFlow API — idempotent tasks, correct scheduling and catchup, retries/SLAs, connections/variables, and avoiding top-level code. Use when creating or reviewing Airflow DAGs, scheduling pipelines, wiring task dependencies, configuring retries/backfills, or fixing non-idempotent tasks.4---56# Authoring Airflow DAGs78## When to use910- Creating or refactoring Airflow DAGs and tasks.11- Configuring schedules, catchup/backfill, retries, and SLAs.12- Passing data between tasks (XCom) or using connections/variables.13- Do NOT use for diagnosing a broken running DAG (use14 `debugging-airflow-pipelines`).1516## Workflow1718```19- [ ] Make each task idempotent and parameterized by the data interval20- [ ] Keep expensive/import-heavy code inside tasks, not at module top level21- [ ] Set schedule + catchup deliberately22- [ ] Configure retries, retry_delay, and SLAs23- [ ] Wire dependencies via TaskFlow return values or >> operators24```25261. **Idempotent tasks** — a task for the `2026-01-15` interval must produce the27 same result whether it runs once or is re-run. Use the data interval, not28 `datetime.now()`.292. **No heavy top-level code** — the scheduler parses every DAG file frequently;30 database calls, API calls, or big imports at module level slow scheduling and31 can break parsing. Put them inside tasks.323. **Schedule + catchup on purpose** — `catchup=True` backfills every missed33 interval from `start_date`; default to `False` unless you want that.344. **Retries and SLAs** — transient failures are normal; set `retries` and35 `retry_delay`; use SLAs/alerts for lateness.3637## Patterns3839**TaskFlow DAG, idempotent and cleanly wired:**4041```python42from airflow.decorators import dag, task43import pendulum4445@dag(46 schedule="@daily",47 start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),48 catchup=False,49 default_args={"retries": 3, "retry_delay": pendulum.duration(minutes=5)},50 tags=["orders"],51)52def orders_pipeline():5354 @task55 def extract(data_interval_start=None):56 # Use the interval, not now(), so re-runs are deterministic.57 return fetch_orders(day=data_interval_start.date())5859 @task60 def load(rows):61 # Delete-insert the partition -> idempotent on retry.62 overwrite_partition("fct_orders", rows)6364 load(extract())6566orders_pipeline()67```6869**Pass small data via XCom (return values); pass large data via storage** — write70to S3/GCS/warehouse and pass the path/key, never megabytes through XCom.7172**Use connections/variables** for secrets and config (`BaseHook.get_connection`,73`Variable.get`), never hard-coded credentials.7475## Common pitfalls7677- **Top-level API/DB calls or heavy imports** — slow the scheduler and can fail78 DAG parsing across the whole deployment.79- **`datetime.now()` inside tasks** — breaks idempotency and backfills; use80 `data_interval_start`/`_end`.81- **`catchup=True` unintentionally** — floods the cluster with historical runs on82 first deploy.83- **Large payloads through XCom** — bloats the metadata DB; pass references.84- **Dynamic `start_date`** (e.g. `days_ago`) — makes schedules nondeterministic;85 use a fixed timestamp.86- **One monster task** — split extract/transform/load so retries are granular.8788## References8990- [Scheduling, catchup, and backfill reference](references/SCHEDULING.md)