Author, debug and configure Apache Airflow 3 DAGs: TaskFlow API, scheduling and assets, XCom, sensors, dynamic task mapping, and multi-layer test suites
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Automatic in Airflow 3. No setup needed. A new version is created when a structural change occurs.
| Scenario | Unversioned (LocalDagBundle) | Versioned (GitDagBundle) |
|---|---|---|
| Clear & rerun task | Uses latest code | Uses original version's code |
| Rerun single task | Uses latest code | Uses original version's code |
| Code change mid-run | Uses new code immediately | Finishes with original version |
| Each structural change | DAG version created | DAG version + bundle version |
| Action | LocalDagBundle (unversioned) | GitDagBundle (versioned) |
|---|---|---|
| New DAG run | Uses current code | Uses current code |
| View previous run | Shows original version | Shows original version |
| Rerun entire DAG run | Uses current code | Uses original bundle code |
| Rerun single task | Uses current code | Uses original bundle code |
| Code changes mid-run | Running tasks use new code | Running tasks use bundle code |
Key difference: With GitDagBundle, reruns reproduce original behavior. With LocalDagBundle, reruns use whatever code is deployed now. Use GitDagBundle in production for reproducibility.
A bundle is a collection of DAG code + supporting files. Two types:
git to packages.txtapache-airflow-providers-gitAIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST='[
{
"name": "my_dags",
"classpath": "airflow.providers.git.bundles.git.GitDagBundle",
"kwargs": {
"tracking_ref": "main",
"git_conn_id": "my_git_conn"
}
}
]'| Setting | Purpose | Default |
|---|---|---|
AIRFLOW__CORE__PARALLELISM | Max concurrent tasks globally | 32 |
AIRFLOW__CORE__MAX_ACTIVE_TASKS_PER_DAG | Per-DAG concurrent task limit | 16 |
AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG | Concurrent DAG runs per DAG | 16 |
AIRFLOW__CORE__DAGBAG_IMPORT_TIMEOUT | DAG file parse timeout | 30s |
AIRFLOW__SCHEDULER__MIN_FILE_PROCESS_INTERVAL | Min seconds between file parses | 30s |
AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC | Scheduler loop frequency | 5s |
AIRFLOW__DAG_PROCESSOR__DAG_FILE_PROCESSOR_TIMEOUT | Timeout per DAG file parse | 50s |
AIRFLOW__DAG_PROCESSOR__PARSING_PROCESSES | Parallel DAG parsing processes | 2 |
@dag(
max_active_runs=3, # Max concurrent runs of this DAG
max_active_tasks=8, # Max parallel tasks per run
max_consecutive_failed_dag_runs=5, # Auto-disable after 5 consecutive failures
dagrun_timeout=timedelta(hours=2), # Fail entire run if exceeds 2 hours
)@task(
pool="database_pool", # Limit concurrent DB tasks
max_active_tis_per_dag=2, # Max 2 instances of this task across all runs
execution_timeout=timedelta(minutes=30),
)Limit concurrent access to shared resources (databases, APIs):
# Create pool via UI: Admin → Pools
# Or via CLI:
# airflow pools set database_pool 5 "Max 5 concurrent DB connections"
@task(pool="database_pool")
def query_db():
pass # Only 5 of these run concurrently across all DAGs| Callback | Level | When |
|---|---|---|
on_success_callback | DAG + Task | Succeeded |
on_failure_callback | DAG + Task | Failed |
on_skipped_callback | Task only | Skipped |
on_execute_callback | Task only | Before execution starts |
on_retry_callback | Task only | Being retried |
from airflow.providers.slack.notifications.slack import SlackNotifier
def alert_on_failure(context):
"""Custom failure callback."""
dag_id = context["dag"].dag_id
task_id = context["task_instance"].task_id
log_url = context["task_instance"].log_url
print(f"ALERT: {dag_id}.{task_id} failed! Logs: {log_url}")
@dag(
on_failure_callback=alert_on_failure,
# Or use built-in notifiers:
# on_failure_callback=SlackNotifier(
# slack_conn_id="slack_default",
# text="DAG {{ dag.dag_id }} failed!",
# channel="#alerts",
# ),
)
def my_dag():
...| Key | Type | Description |
|---|---|---|
dag | DAG | DAG object |
dag_run | DagRun | Current DAG run |
task_instance / ti | TaskInstance | Task instance |
logical_date | datetime | Scheduled datetime |
ds | str | Date string (YYYY-MM-DD) |
exception | Exception | Error (failure callbacks) |
@dag(
default_args={
"on_failure_callback": alert_on_failure,
"on_retry_callback": log_retry,
},
)The executor determines how and where tasks run. Your choice affects how you write DAG code.
| Executor | Task Isolation | When to Use | DAG Code Impact |
|---|---|---|---|
LocalExecutor | Process-level | Development, small production | Default — no special config needed |
CeleryExecutor | Process on shared workers | Production, steady workloads | Pool management critical for shared resources |
KubernetesExecutor | Pod-level (full isolation) | Production, heterogeneous tasks | Use executor_config for resource requests |
EdgeExecutor | Remote worker | Hybrid/edge deployments | Consider network latency for data-heavy tasks |
Each task runs in its own Kubernetes pod. Specify resources per task:
@task(
executor_config={
"pod_override": k8s.V1Pod(
spec=k8s.V1PodSpec(
containers=[
k8s.V1Container(
name="base",
resources=k8s.V1ResourceRequirements(
requests={"memory": "512Mi", "cpu": "250m"},
limits={"memory": "1Gi", "cpu": "500m"},
),
)
]
)
)
}
)
def heavy_transform():
...Tasks share worker processes. Use pools to prevent resource contention:
# Limit concurrent database connections across all DAGs
@task(pool="database_pool")
def query_warehouse():
...
# Limit concurrent API calls
@task(pool="api_rate_limit", pool_slots=2) # Uses 2 slots per instance
def call_external_api():
...CeleryExecutor — good balance of isolation and simplicityKubernetesExecutor — different tasks need different CPU/memoryKubernetesExecutor for heavy tasks and CeleryExecutor for lightweight onesastro dev run dags list-import-errors or airflow dags list-import-errorsdag_id is unique across all files@dag function is called at module levelstart_date must be in the past (never datetime.now())astro dev logs -sastro dev dags test <dag_id>start_date < current datedepends_on_past — if True, previous run must have succeededmax_active_tasks, pool slots, PARALLELISM@dag functionlog_fetch_timeout_secairflow connections listairflow connections test <conn_id>extra JSON is validastro dev startAirflow exposes metrics at /metrics endpoint. Key metrics:
| Metric | What It Tells You |
|---|---|
scheduler_heartbeat | Scheduler is alive |
dag_processing.total_parse_time | DAG parsing performance |
task_instance_created | Task creation rate |
task_instance_successes | Success rate |
task_instance_failures | Failure rate |
executor.queued_tasks | Queue depth |
executor.running_tasks | Active tasks |
Use the community Airflow Grafana dashboard or build custom dashboards tracking:
| Indicator | Healthy | Warning |
|---|---|---|
| Scheduler heartbeat | Regular interval | Missing beats |
| Parse time per DAG | < 30s | > 30s |
| Task queue depth | Stable | Growing |
| Failure rate | < 5% | > 10% |
| DAG run duration | Within SLA | Exceeding dagrun_timeout |