$npx -y skills add astronomer/agents --skill debugging-dagsComprehensive DAG failure diagnosis and root-cause analysis with structured investigation and prevention recommendations. Use when deep failure investigation is needed, a DAG fails to import/parse or 'airflow dags list' errors on a file; a task or run is failing and must be diag
| 1 | # DAG Diagnosis |
| 2 | |
| 3 | You are a data engineer debugging a failed Airflow DAG. Follow this systematic approach to identify the root cause and provide actionable remediation. |
| 4 | |
| 5 | ## Running the CLI |
| 6 | |
| 7 | These commands assume `af` is on PATH. Run via `astro otto` to get it automatically, or install standalone with `uv tool install astro-airflow-mcp`. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Step 1: Identify the Failure |
| 12 | |
| 13 | If a specific DAG was mentioned: |
| 14 | - Run `af runs diagnose <dag_id> <dag_run_id>` (if run_id is provided) |
| 15 | - If no run_id specified, run `af dags stats` to find recent failures |
| 16 | |
| 17 | If no DAG was specified: |
| 18 | - Run `af health` to find recent failures across all DAGs |
| 19 | - Check for import errors with `af dags errors` |
| 20 | - Show DAGs with recent failures |
| 21 | - Ask which DAG to investigate further |
| 22 | |
| 23 | ## Step 2: Get the Error Details |
| 24 | |
| 25 | Once you have identified a failed task: |
| 26 | |
| 27 | 1. **Get task logs** using `af tasks logs <dag_id> <dag_run_id> <task_id>` |
| 28 | 2. **Look for the actual exception** - scroll past the Airflow boilerplate to find the real error |
| 29 | 3. **Categorize the failure type**: |
| 30 | - **Data issue**: Missing data, schema change, null values, constraint violation |
| 31 | - **Code issue**: Bug, syntax error, import failure, type error |
| 32 | - **Infrastructure issue**: Connection timeout, resource exhaustion, permission denied |
| 33 | - **Dependency issue**: Upstream failure, external API down, rate limiting |
| 34 | |
| 35 | ## Step 3: Check Context |
| 36 | |
| 37 | Gather additional context to understand WHY this happened: |
| 38 | |
| 39 | 1. **Recent changes**: Was there a code deploy? Check git history if available |
| 40 | 2. **Package version changes**: Was a package upgraded — in the image, in a venv-style operator, or at the index? See [Package version changes](#package-version-changes) below. |
| 41 | 3. **Data volume**: Did data volume spike? Run a quick count on source tables |
| 42 | 4. **Upstream health**: Did upstream tasks succeed but produce unexpected data? |
| 43 | 5. **Historical pattern**: Is this a recurring failure? Check if same task failed before |
| 44 | 6. **Timing**: Did this fail at an unusual time? (resource contention, maintenance windows) |
| 45 | |
| 46 | Use `af runs get <dag_id> <dag_run_id>` to compare the failed run against recent successful runs. |
| 47 | |
| 48 | ### Package version changes |
| 49 | |
| 50 | A common cause of failures with no git activity is dependency drift — the user's code didn't change, but a package they depend on did. Check in this order: |
| 51 | |
| 52 | 1. **Worker image diff** (preferred when available). Every Astro deploy = new image tag, so the registry has a "before" and "after". Diff `pip freeze` between current and previous image — that's ground truth for what changed: |
| 53 | ``` |
| 54 | docker run --rm <current_image> pip freeze > /tmp/now.txt |
| 55 | docker run --rm <previous_image> pip freeze > /tmp/prev.txt |
| 56 | diff /tmp/prev.txt /tmp/now.txt |
| 57 | ``` |
| 58 | Also compare `docker run --rm <image> python --version` between the two — a Python minor-version bump (3.11 → 3.12, or even a patch) can break wheel compatibility even when `pip freeze` looks identical. `af config providers` lists currently installed provider versions, useful for cross-checking against modules named in the traceback. |
| 59 | |
| 60 | 2. **Venv-style operators bypass the worker image.** `@task.virtualenv`, `PythonVirtualenvOperator`, `ExternalPythonOperator`, and `KubernetesPodOperator` build their environment per task run, so an image diff won't catch failures inside them. If the failed task is one of these, read its `requirements` / `image` / `python_version` / `python` args directly: |
| 61 | - Unbounded specifier (e.g. `pandas>=2.0.0` with no upper bound, or no specifier at all) → a new upstream release is the prime suspect. |
| 62 | - `image="foo:latest"` or no tag → the image moved underneath you. |
| 63 | - `python_version="3.11"` (on `@task.virtualenv` / `PythonVirtualenvOperator`) or a `python` path (on `ExternalPythonOperator`) resolving to a different interpreter than it used to — a Python minor-version change can break wheel compatibility for unchanged `requirements`. Same vector applies to the worker image itself if the base Python changed there. |
| 64 | |
| 65 | Fix is to pin: `pandas>=2.0.0,<3.0.0`, a lockfile, a specific image SHA, or a fully-qualified Python version (`python_version="3.11.7"` instead of `"3.11"`). |
| 66 | |
| 67 | 3. **Index lookup** when image diff isn't conclusive (no image history, or a venv-style operator). Identify the configured index first — it may not be PyPI: |
| 68 | - Env vars: `UV_INDEX_URL`, `PIP_INDEX_URL`, `PIP_EXTRA_INDEX_URL` |
| 69 | - `pyproject.toml` → `[[tool.uv.index]]` |
| 70 | - `~/.pip/pip.conf`, `/etc/pip.co |