How to check DAGs for Airflow 3 with Ruff AIR rules before upgrading
Ruff's AIR rules find many Airflow 3 breaking changes in DAG code before any cluster is touched, so a team still on Airflow 2 should run them on the repository first. The Airflow 3 upgrade guide requires Airflow 2.7 or later before upgrading and recommends moving to the latest 2.x line first, which is currently 2.11.2. Airflow 2 entered limited maintenance on October 22, 2025 and reaches end of life on April 22, 2026, after which it gets no fixes, so the scan should be scheduled with that date in mind. Use Ruff 0.13.1 or later, since the upgrade guide recommends that version for the AIR checks.
Run AIR301 and AIR302 first. These flag breaking changes that will fail or change behavior on Airflow 3. AIR311 and AIR312 flag recommended updates that are not breaking, such as moving imports to their new locations. Review the output before using --fix, because the safe fixes rewrite only what the rule can change with confidence, while --unsafe-fixes applies rewrites that need a human to confirm intent. Expect import moves from airflow.decorators.dag and airflow.datasets.Dataset to airflow.sdk, which still work on 3.1 with warnings but should be cleared before the cutover. Confirm the exact rule set for your Ruff version against the upgrade guide, because rule coverage changes between releases.
The scan does not catch everything. It reads code, so it will not find runtime behavior such as a task querying the metadata database through a hook, a plugin that depends on Flask AppBuilder, a Helm values file, or a .env setting that changes at deploy time. Add the scan to CI so it runs on every merge, then pair it with a staging run of the DAGs on Airflow 3 and a review of the provider packages each DAG imports. Ask whether the scan results will be stored as upgrade evidence for your change records.
Which AIR rules flag which Airflow 3 changes
This table maps the main Airflow 3 changes to the Ruff AIR rule families that report them. Use the rule family column to see what to expect, then confirm the exact code in your Ruff version's output before planning fixes. The main takeaway is that breaking changes need code changes before the upgrade, while recommended updates can be scheduled but should not be ignored.
| Change | Rule family | Impact | Action needed |
|---|---|---|---|
| SubDAGs (SubDagOperator) | Breaking (AIR301 or AIR302) | Removed in Airflow 3, so affected DAGs fail to load | Rewrite as TaskGroups and keep task IDs stable where run history matters |
| Metadata database session and model access in task code | Breaking (AIR301 or AIR302, confirm in output) | Task code can no longer use database sessions or models directly | Move reads and writes to the REST API or Task SDK; search manually if no finding appears |
| airflow.decorators.dag and airflow.datasets.Dataset | Recommended update (AIR311 or AIR312, confirm in output) | Still imports in 3.1 with deprecation warnings | Switch to airflow.sdk.dag and airflow.sdk.Asset before the legacy path is removed |
| BashOperator and PythonOperator | Breaking (AIR301 or AIR302, confirm in output) | Moved to apache-airflow-providers-standard, so core imports fail | Install the standard provider, which also works on Airflow 2.x, and update imports |
| ExternalTaskSensor and FileSensor | Breaking (AIR301 or AIR302, confirm in output) | Moved to apache-airflow-providers-standard, so core imports fail | Install the standard provider and update imports in every DAG that uses them |
| Automatic rewrites | Applied by --fix and --unsafe-fixes | Unsafe fixes can change DAG behavior, not only imports | Review each diff, run DAG tests, and commit fixes in separate changes |
Safe fixes versus unsafe fixes: what ruff check --fix changes
Running ruff check --select AIR --fix applies the fixes Ruff classifies as safe. These are rewrites where the replacement is expected to keep the same behavior, so they make a sensible first pass across a DAG repository. Commit them as a separate change so reviewers can see exactly what the tool touched.
- Import path rewrites: Legacy paths such as
airflow.decorators.dagcan be rewritten to theirairflow.sdkequivalents where Ruff provides a safe mapping. - Operator import moves: Where Ruff offers a safe fix for operators that moved to the standard provider package, the import is updated for you.
- Whole-repository scope: Run the pass against every DAG folder and shared module, then re-run the check without fixes to list what remains.
- Version pin: Use Ruff 0.13.1 or later so the AIR rule set matches the guidance in the upgrade documentation.
Confirm that the changed files still parse and that your test suite imports every DAG before you move on to the next pass.
The --unsafe-fixes flag widens the pass to rewrites that Ruff marks as unsafe, which means the new code may not behave identically in every case. Ruff requires the flag to be stated explicitly, so these changes are never applied by accident. Treat each unsafe rewrite as a code change that needs the same scrutiny as a feature.
- Behavior change risk: An unsafe rewrite can alter argument handling, return values or task timing even when the code still parses and imports.
- Diff review: Read every changed file, paying particular attention to templated fields, sensors, custom operators and any code that reads run context.
- One rule at a time: Narrow the run with
--selectto a single rule code so each change has a clear owner and a simple rollback. - Staging run: Trigger the affected DAGs in a staging environment and compare task states, XCom values and output with the version before the change.
Confirm each rule's fix classification in Ruff's rule documentation before you accept its diff.
Some AIR findings have no automatic rewrite because the correct change depends on what the DAG is meant to do. These need engineers to redesign or rewrite code, and the Ruff output serves as the starting list. Plan these items separately from the fix passes so they do not get lost in a large generated commit.
- SubDAG conversion: SubDAGs are removed in Airflow 3, so each
SubDagOperatorneeds a TaskGroup or an asset-based design, with task IDs kept stable where history matters. - Metadata database access: Task code can no longer use Airflow database sessions or models directly, so those queries move to the REST API or the Task SDK.
- Removed context keys: Keys such as
execution_datemay need a deliberate choice between the logical date and data interval equivalents, based on what the task expects. - Shared helpers outside DAG folders: Common modules and custom plugins that import removed symbols are often easy to miss and need their own tickets.
Track each manual finding as its own change with an owner and a test, and keep the AIR check open until these items are closed.
Automation fits well for detection and for the safe fix pass, while unsafe fixes should stay under human review. An unattended job that rewrites the whole repository can hide behavior changes inside a large commit that nobody reads closely. The goal is to make every automated change visible, reviewable and reversible.
- Gate on new findings: Fail the pipeline when new AIR301 or AIR302 findings appear, so regressions are caught before merge.
- Keep unsafe fixes out of pipelines: Do not run
--unsafe-fixesin scheduled jobs or merge jobs across the whole repository. - Open fixes as pull requests: Let the safe fix job propose a change that a person reviews and merges, instead of pushing directly to protected branches.
- Record decisions: Keep the list of accepted unsafe rewrites and the staging results with the change ticket, which supports later change management reviews.
Confirm with your change management owner how automated pull requests are approved before you enable them on protected branches.
Running the Ruff AIR check across a DAG repository
This sequence moves a DAG repository from an unknown Airflow 3 readiness state to a clean Ruff AIR check, with each change isolated in its own commit so any step can be reverted.
Pin Ruff 0.13.1 or later
Add Ruff at version 0.13.1 or later to the repository's dev dependencies or pre-commit configuration, so every developer and CI runner uses the same rule set. The Airflow 3 upgrade guide recommends this minimum version for the AIR rules. Done means
ruff --versionreturns the pinned version on a clean checkout and the pin is committed as its own change, which gives you a clean rollback point.Run AIR rules in report-only mode
Enable the AIR301 and AIR302 rules (breaking changes) and the AIR311 and AIR312 rules (recommended non-breaking updates) in the Ruff configuration, then run a check across the whole DAG repository without any fix flags. Nothing in the code changes at this stage. Done means you have a saved report listing every finding by file, line and rule ID, and the report is stored as an artifact your team can reference.
Triage findings by rule and owner
Group the report by rule ID and by the team that owns each DAG folder or pipeline, then mark each group as safe to automate, needs review, or needs a manual rewrite. Import path changes, such as moving from legacy airflow.decorators imports to airflow.sdk, are typically automatable. Changes to database access or removed features usually need a developer to decide the replacement. Done means every finding has an owner and a disposition in a tracked list.
Apply safe fixes on a branch
Create a feature branch and run Ruff with the fix flag so it applies only the automatic rewrites it considers safe. Review the diff for DAG files, keeping the change limited to the AIR rules. Commit the result separately from any manual edits. Done means the branch contains one commit of automatic rewrites that a reviewer can read, and each commit can be reverted independently.
Review unsafe fixes before applying them
Run Ruff again with the unsafe-fixes option to see which rewrites it marks as possibly changing behavior. Inspect each proposed change in a diff, since these rewrites can alter runtime semantics in ways the tool cannot confirm. Apply only the changes the owning team approves, and record rejected ones with a reason. Done means the unsafe-fix decisions are documented and approved changes are in their own commit.
Complete manual rewrites by owner
Work through the findings that automation cannot resolve, such as code that depended on removed features or direct metadata database access. Each owning team makes its changes in a separate commit with a reference to the finding ID. Where a replacement needs a design decision, record it in the pull request rather than applying a workaround silently. Done means every manual item in the triage list is closed or has an approved exception.
Verify that DAGs parse and import
Before accepting any commit, run a parse and import check across every DAG file, for example by loading the DAG bundle in a test environment with Airflow 3 installed, and fail the build on any import error. Confirm that DAG IDs and task IDs match the pre-change inventory so scheduling history is not disrupted. Done means the import check passes on the branch and the list of DAG and task IDs is unchanged or the differences are approved.
Rerun the check until it is clean
Run the full Ruff AIR check again on the branch and repeat the triage for any remaining findings until the report is empty for AIR301 and AIR302, and the recommended AIR311 and AIR312 items are either resolved or accepted by their owners. Keep the final clean report as evidence that the repository was checked before the upgrade. Done means the check returns no unresolved findings and the report is attached to the change record.
What to inventory before the first Ruff run
Ruff can only flag what it can read, and the AIR rules only see imports and call patterns in Python files. Before the first run, map every place Airflow code lives, including code that is not in the main DAG folder, so the findings reflect the whole estate and not just the files that happen to sit in one repository.
DAG repository layout and shared libraries
Start with the directory tree: DAG folders, a dags/ package, shared utility modules and any monorepo that ships DAGs alongside application code. Shared libraries imported by DAGs carry Airflow imports too, so a clean DAG folder can still fail at runtime. Record which directories Ruff will scan and which are excluded, and confirm that the same configuration runs locally and in CI.
Custom operators and hooks
Custom operators, sensors and hooks often import Airflow internals such as models, sessions or helpers that Airflow 3 moved or removed. Task code that touches the metadata database directly needs a separate rewrite plan, because Ruff flags the import but not the design. List every custom operator, its base class and the Airflow modules it imports.
Plugins and webserver_config.py
Plugins that add UI views, menu items or blueprints, and any webserver_config.py security manager, follow an Airflow 2 pattern that Airflow 3 changes. Ruff AIR rules do not replace a review of these files. Inventory each plugin, its UI surface and any Flask AppBuilder dependency, so the port is scoped before the upgrade window opens.
Dynamically generated DAGs
DAGs built in loops, from YAML or JSON config, or from database queries can contain removed imports in templates that Ruff never sees as literal code. Identify the generators, the config sources they read and whether they run at parse time against external systems. Test generated output in a staging environment, since static analysis of the generator may miss the final DAG shape.
Provider package pins
Provider packages carry their own minimum Airflow versions and their own operator signatures, so a pinned provider can block the upgrade regardless of how clean the DAG code is. Record the installed provider versions, the constraint file in use and any packages pinned outside it. Ask the platform owner which pins can move independently of the core upgrade.
DAGs without an active owner
Some DAGs belong to teams that have reorganized or left, and nobody will approve changes to them. Assign an owner to each DAG before the first run, or decide explicitly to retire it. Unowned DAGs that still run on a schedule are the most likely to fail after the upgrade with no one available to fix them.
Wiring the AIR check into CI before the upgrade
Add the AIR check as its own CI job that runs on every merge request against the DAG repository, so that new breaking code is caught before it reaches the main branch. The job should run ruff check with the AIR rules selected and fail the pipeline on AIR301 and AIR302, which flag breaking changes. Configure AIR311 and AIR312 as warnings rather than as failures, so that recommended non-breaking updates stay visible in the job log without blocking unrelated work. Pin the Ruff version in your lock file or CI image instead of installing the latest release on each run, because rule sets and their behavior change between Ruff versions. The upgrade guide recommends Ruff 0.13.1 or later, and the version you pin should be confirmed against that guidance and tested on your own DAGs before you rely on its results.
Scope the job to the directories that hold DAG files and the shared modules they import, not the whole monorepo. A path filter in the pipeline definition keeps run times short and avoids noise from application code that is not Airflow. Run the job against the branch that will become your upgrade target, and keep the same configuration after the upgrade is complete. New DAGs written in Airflow 2 style will otherwise reintroduce removed imports long after the migration project has closed. Record the pinned Ruff version, the selected rule codes and the path scope in the repository so that auditors and future maintainers can see what the gate checked and when it changed.
Treat a passing job as evidence about source code only. Ruff reads the syntax and imports of each file, so it cannot show that a DAG parses under the Airflow 3 scheduler, that a connection or variable exists, or that a task behaves the same at runtime. Keep the AIR job alongside DAG import tests, a staging run of the DAG processor and a review of any warnings the team chose to accept. Confirm the exact rule behavior, including which fixes are safe, against the upgrade guide and your Ruff release notes before you make the gate mandatory.
What makes a DAG repository take longer to clear
How does the number of DAGs and custom operators change Ruff review time?
The more DAG files and custom operators a repository holds, the more findings Ruff reports and the more code a team must read, triage and test. Each AIR finding needs a decision: apply the automatic rewrite, change the code by hand, or confirm the finding does not apply. A repository with a few dozen DAGs and little custom code usually clears in a short cycle, while one with hundreds of DAGs and many custom operators spreads the same triage across more files and more owners. Count your DAG files, operator classes and hook subclasses first, since that count sets the size of the review queue. Ask any partner quoting the work to base its estimate on that inventory from your own repository.
Why does dynamic DAG generation slow down an Airflow 3 check?
Dynamic DAG generation builds tasks from configuration files, loops, database queries or external APIs, so Ruff sees only the code that builds the DAG and not every task it produces. A rule can pass on the source file while the generated tasks still import removed modules or use removed context keys. Reviewers then have to run the parser and inspect generated output, which adds test time. Teams with heavy generation should plan to render DAGs in a test environment and compare the task lists before and after the upgrade.
Why does custom code that touches the metadata database take longer to fix?
Task code that opens Airflow database sessions or imports Airflow models cannot keep working unchanged in Airflow 3, and the upgrade guide directs that work to the REST API or Task SDK. Ruff can flag some of these imports, but it cannot tell you what the code was trying to achieve. Each case needs a design decision, a rewrite and a test against a real run, which usually takes longer than a mechanical import change. Search your repository for airflow.models, airflow.settings and Session usage, and group the results by purpose, such as reading run state, writing audit rows or cleaning up history.
How do SubDAGs add time to an Airflow 3 review?
SubDAGs are removed in Airflow 3 in favor of TaskGroups, so every SubDagOperator needs a rewrite rather than a rename. Ruff can point to the usage, but the decision about whether a TaskGroup, an asset-based dependency or a separate DAG fits the job belongs to the team that owns the pipeline. Task IDs can change during the rewrite, which affects run history and any downstream references, so each rewritten DAG needs a comparison test. Count the SubDAG usages early, because each one adds a design decision and a regression check.
How do plugins and UI code affect the time needed to clear findings?
Plugins and UI code are reviewed differently from DAG files because they depend on the Airflow web layer, which changed in Airflow 3. Flask AppBuilder views, menu items, blueprints and custom security manager imports may not be covered by the DAG rules at all, so they need a separate inventory and review. Ruff can help with DAG code, but plugin work often requires framework changes, UI tests and access checks. Count plugins, custom views and any webserver_config.py overrides before planning the review, since each one can require work outside the DAG repository.
How does the number of owning teams affect DAG review and sign-off?
Each owning team reviews its own DAGs, decides how to handle its findings and signs off on its tests, so the total time grows with the number of teams even when the code volume is modest. Shared operators and common libraries add another layer, because a fix in one shared module can change behavior for every team that imports it. Map DAG directories to owners, agree who approves automatic rewrites, and set a single date for shared code to be reviewed first. Ownership gaps are a common cause of stalled findings.
Where should I get the figures that drive these estimates?
The figures come from your own repository, not from a general rule of thumb. Run Ruff with the AIR rules against a copy of the repository and record the number of findings by rule, by directory and by owner. Then count DAG files, dynamically generated DAGs, custom operators, metadata database calls, SubDAG usages and plugins. Those counts give a planning baseline that a partner or internal team can use to scope the work in writing, and they show which drivers dominate your effort.
What Ruff cannot tell you about an Airflow 3 upgrade
A clean Ruff run over your DAG repository means the Python code in your DAG files no longer matches the AIR301 and AIR302 patterns that the Airflow 3 upgrade guide flags. It does not mean the platform underneath those DAGs is ready. Ruff reads source files and nothing else. It cannot see your Helm chart values, your database migration state, your executor configuration or the versions of the provider packages installed on your workers and schedulers. Those are the parts of the upgrade that most often fail after the DAG code already passes.
Several changes sit outside the code Ruff inspects. The webserver is replaced by an API server started with airflow api-server, the DAG processor must run as its own component, and the metadata database must be migrated with airflow db migrate. Executor settings and the hybrid executors that Airflow 3 no longer supports need a manual review of your configuration files and deployment manifests. Provider compatibility is a separate question: confirm the provider versions you pin against the Airflow version you target, using the provider documentation and your constraint files. Each of these needs its own plan, its own owner and its own verification, covered by the upgrade work for the Helm chart, the database and the providers.
Treat the Ruff run as the first gate and a staging run of real DAGs as the second. Deploy a staging environment on the target Airflow 3 version with a copy of your production configuration and metadata, then trigger representative DAG runs across every executor and queue you use. Check task logs, scheduler behavior, connections and secrets retrieval, and any sensor or operator that depends on a provider you changed. The Airflow 3 upgrade guide is the reference for what each change affects. Ask whoever runs the upgrade to write down which checks passed in staging and which were not run, so the production cutover decision rests on evidence rather than on a green lint job.
Questions teams ask before checking DAGs for Airflow 3
Can our own team run the Ruff AIR check without a partner?
Yes, in most cases. Ruff is a standalone linter, and the AIR rules run against your DAG files without a running Airflow environment, so a developer or platform engineer can run it from a workstation or a CI job. The work that usually needs outside help is not the scan itself but deciding what to do with the findings: which fixes to accept, which DAGs depend on removed behavior, and how to sequence changes across several teams. Confirm your Ruff version with the upgrade guide first, since the guide recommends Ruff 0.13.1 or later for these rules.
How long does it take to set up the check, and what drives that time?
Setup for a single repository is usually short, because it means installing a pinned Ruff version, selecting the AIR rules and running the check. The effort grows with the number of repositories, the amount of custom code that imports Airflow internals, and how many DAGs depend on removed features such as SubDAGs or legacy context keys. Ask your team to count DAG repositories, plugins and shared libraries before estimating, and ask any partner for a quote built from that written inventory rather than from a generic figure.
Are the unsafe fixes safe to apply automatically?
Not without review. Ruff applies safe fixes with the --fix option, and the --unsafe-fixes option applies rewrites that can change behavior in edge cases. Treat every unsafe fix as a code change that needs a test run and a diff review. Keep them in a separate commit, run the DAG test suite, and confirm that task IDs, schedules and imports still behave as before. Automated rewrites do not cover behavior that depends on runtime state, so manual review is still required for those cases.
Do we have to be on Airflow 2.11 before running these checks?
No. The AIR301 and AIR302 checks are designed to find breaking changes in your DAG code regardless of the Airflow 2 patch you run. The Airflow 3 upgrade guide requires being on Airflow 2.7 or later before the upgrade itself, and it recommends moving to the latest 2.x line first. Running Ruff on your current code is useful early because it shows the work size. Whether you stop at 2.11 as an intermediate step is a planning decision that depends on your provider packages and maintenance window.
What do our developers and platform team need to provide?
Developers need to own the DAG code, know which operators and imports each pipeline uses, and review the changes the check proposes. The platform team needs to provide a CI runner with network access to the package index, a pinned Ruff version, a branch or environment where the changes can be tested, and access to the repositories. Both groups should agree on who approves unsafe fixes and who signs off that a DAG is ready for the Airflow 3 scheduler.
Does Airflow 2 end of life change the timing of this work?
It makes the timing more pressing. Airflow 2 entered limited maintenance on October 22, 2025 and reached end of life on April 22, 2026, after which it gets no fixes. Limited maintenance covers only security and critical bug fixes, so teams still on 2.x should plan a move rather than wait. Running the Ruff check early gives you a measured list of changes that feeds into that plan. Confirm with your security team how long your current 2.x deployment can remain in service under your own policies.
Will the Ruff check catch every problem that breaks an Airflow 3 upgrade?
No. The AIR rules catch known import paths, removed features and renamed APIs that can be detected statically. They cannot see runtime behavior, such as a task that reads the metadata database through a session, a custom plugin that depends on the Flask AppBuilder UI, or a provider operator whose parameters changed. Use the check as the first gate in CI, then run DAGs in a staging environment with a representative Airflow 3 deployment before cutover.
How DeployOpen works on this
DeployOpen approaches a DAG compatibility check as a planned piece of upgrade work inside your own repository and CI system. We scope the check against your DAG code, build the Ruff configuration and pipeline step, and hand over findings with a remediation list your team can act on.
How we scope
- Inventory the DAG repositories, plugins and custom operators that the Ruff AIR check will read
- Agree which AIR301 and AIR302 breaking-change findings must be cleared before the Airflow 3 upgrade
- Record in the written scope whether AIR311 and AIR312 recommendations are in or out of the work
- Confirm the Ruff version, the target Airflow 3 release and the CI platform in use
How we build
- Configure Ruff with AIR rules in a shared settings file so every DAG repository gets the same checks
- Review safe and unsafe fixes before applying them, and keep unsafe rewrites in separate, tested changes
- Rewrite airflow.sdk import moves and test the changed DAGs against your staging Airflow environment
- Add the Ruff step to CI so each pull request reports AIR findings before merge
How we hand over
- Deliver a findings report that lists each remaining AIR finding, its file, and the owner for the fix
- Provide the Ruff configuration, the CI job definition and a short runbook for DAG authors
- Document which findings were auto-fixed, which were fixed by hand, and which need a decision
- Walk your engineers through the check so they can extend it when new AIR rules are added
How we support
- Support after handover is available as a retainer covering rule updates and new DAG reviews
- Ask in the quote who maintains the Ruff version pin and how changes to it are tested
- Agree in writing how ongoing findings from new DAG code will be reviewed and by whom
- Confirm the response path for questions raised during the Airflow 3 upgrade window

