Skip to content

dagshift rules

Generated by tools/gen_rules_doc.py; every example below is run through dagshift when the page is built.

Severity says what happened to a finding:

  • fixed: rewritten, behaviour preserved
  • manual: breaks on the target and has no safe rewrite; a recipe is attached
  • warn: still imports, but behaves differently on the target
  • info: worth knowing before cutover

Rules only fire when they matter for the upgrade you picked. 2.7 to 2.10 gets the 2.x deprecations; 2.10 to 3.3 gets those plus the Airflow 3 changes; 3.0 to 3.3 gets what changed inside Airflow 3.

Turn a rule off with --ignore DS102, ignore = ["DS102"] in [tool.dagshift], or # dagshift: ignore[DS102] on a line.

DS1xx Import paths

DS101

import-moved: Import path removed or deprecated on the target; rewritten to where it lives on the target.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,2 +1,2 @@
-from airflow.operators.bash_operator import BashOperator
-from airflow.operators.python_operator import PythonOperator
+from airflow.providers.standard.operators.bash import BashOperator
+from airflow.providers.standard.operators.python import PythonOperator

fixed, line 1: airflow.operators.bash_operator.BashOperator -> airflow.providers.standard.operators.bash.BashOperator (removed on Airflow 3.3)

fixed, line 2: airflow.operators.python_operator.PythonOperator -> airflow.providers.standard.operators.python.PythonOperator (removed on Airflow 3.3)

DS102

import-modernized: Import still works but has a newer home on the target (for example airflow.sdk); rewritten when modernize is on.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,2 +1,2 @@
-from airflow import DAG
+from airflow.sdk import DAG
 from airflow.decorators import task

fixed, line 1: airflow.DAG -> airflow.sdk.DAG (still works, but airflow.sdk is the Airflow 3 authoring interface)

DS103

import-removed: Imported name no longer exists on the target and has no drop-in replacement.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

from airflow.utils.dates import date_range

manual, line 1: airflow.utils.dates.date_range does not exist in Airflow 3.3.

How to fix: Use pendulum.interval(...) or a Timetable.

DS104

module-family-removed: airflow.contrib / airflow.www / airflow.api.client / SubDAG modules, star imports.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

from airflow.operators.subdag import SubDagOperator

manual, line 1: from airflow.operators.subdag import SubDagOperator: SubDAGs are removed in Airflow 3. Rebuild as a TaskGroup or a separate DAG.

DS2xx Removed functions and changed signatures

DS201

days-ago: days_ago() deprecated or removed on the target; replaced with a fixed pendulum start_date.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,5 +1,5 @@
 from airflow import DAG
-from airflow.utils.dates import days_ago
+import pendulum

-with DAG("etl", start_date=days_ago(2), schedule="@daily"):
+with DAG("etl", start_date=pendulum.datetime(2025, 12, 30, tz="UTC"), schedule="@daily"):
     pass

fixed, line 4: days_ago(2) was removed in Airflow 3.0 (target 3.3); replaced with a fixed start_date 2025-12-30 (what days_ago(2) returned on 2026-01-01). A fixed start_date also stops a new DAG version being created every day.

DS202

apply-defaults: @apply_defaults deprecated or removed on the target; decorator dropped.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, plugins/ops.py:

@@ -1,9 +1,7 @@
 from airflow.models import BaseOperator
-from airflow.utils.decorators import apply_defaults


 class LoadOperator(BaseOperator):
-    @apply_defaults
     def __init__(self, table, **kwargs):
         super().__init__(**kwargs)
         self.table = table

fixed, line 7: @apply_defaults was removed; operators apply default_args automatically.

DS203

dag-arguments: DAG() arguments the target renamed or removed (schedule_interval, concurrency, ...); positional arguments converted to keywords.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.7 to 2.10, dags/etl.py:

@@ -1,5 +1,5 @@
 from datetime import datetime
 from airflow import DAG

-dag = DAG("etl", "Nightly load", schedule_interval="@daily", concurrency=4,
+dag = DAG("etl", "Nightly load", schedule="@daily", max_active_tasks=4,
           start_date=datetime(2024, 1, 1))

fixed, line 4: DAG(schedule_interval=...) is deprecated in Airflow 2.10; renamed to schedule.

fixed, line 4: DAG(concurrency=...) is deprecated in Airflow 2.10; renamed to max_active_tasks.

DS204

operator-arguments: Operator arguments the target rejects or renamed (provide_context, task_concurrency, TriggerDagRun execution_date).

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -3,4 +3,4 @@
 def load(**context):
     pass

-PythonOperator(task_id="load", python_callable=load, provide_context=True, task_concurrency=1)
+PythonOperator(task_id="load", python_callable=load, max_active_tis_per_dag=1)

fixed, line 6: provide_context is rejected by Airflow 3 ('Invalid arguments were passed'); context is always passed, so it was dropped.

fixed, line 6: Operator task_concurrency= is not accepted in Airflow 3.3; renamed to max_active_tis_per_dag=.

DS205

variable-get-default: Variable.get(default_var=) -> Variable.get(default=) where the target only accepts default.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,3 +1,3 @@
 from airflow.models import Variable

-bucket = Variable.get("bucket", default_var="raw")
+bucket = Variable.get("bucket", default="raw")

fixed, line 3: Variable.get(default_var=...) is Variable.get(default=...) in Airflow 3.3.

DS206

sla: SLAs are ignored from Airflow 3.0; Deadline Alerts replace them from 3.1.

Applies to: upgrades whose target is Airflow 3.0 or newer.

Example, Airflow 2.10 to 3.3, dags/etl.py:

from datetime import datetime, timedelta
from airflow import DAG

def alert(*args):
    pass

with DAG("etl", start_date=datetime(2024, 1, 1), schedule="@daily", sla_miss_callback=alert,
         default_args={"sla": timedelta(hours=1)}):
    pass

warn, line 7: sla_miss_callback is still accepted but SLAs were removed in Airflow 3: it has no effect.

How to fix: Replace SLAs with Deadline Alerts: DAG(deadline=DeadlineAlert(reference=..., interval=..., callback=...)). See the Airflow 'Deadline Alerts' docs for your version.

warn, line 8: sla is still accepted but SLAs were removed in Airflow 3: it has no effect.

How to fix: Replace SLAs with Deadline Alerts: DAG(deadline=DeadlineAlert(reference=..., interval=..., callback=...)). See the Airflow 'Deadline Alerts' docs for your version.

DS208

schedule-default: DAG without schedule: Airflow 2 ran it daily, Airflow 3 never schedules it.

Applies to: upgrades from Airflow 2 to Airflow 3.

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,5 +1,5 @@
-from datetime import datetime
+from datetime import timedelta, datetime
 from airflow import DAG

-with DAG("etl", start_date=datetime(2024, 1, 1)):
+with DAG("etl", start_date=datetime(2024, 1, 1), schedule=timedelta(days=1)):
     pass

fixed, line 4: DAG had no schedule: Airflow 2 ran it daily, Airflow 3 would never schedule it. Added schedule=timedelta(days=1) to keep it running. Set schedule=None if it should only run when triggered.

DS209

catchup-default: catchup default changed from True to False in Airflow 3.

Applies to: upgrades from Airflow 2 to Airflow 3.

Example, Airflow 2.10 to 3.3, dags/etl.py:

from datetime import datetime
from airflow import DAG

with DAG("etl", start_date=datetime(2024, 1, 1), schedule="@daily"):
    pass

info, line 4: No explicit catchup: Airflow 2 used [scheduler] catchup_by_default (True unless changed); Airflow 3 defaults to False, so missed intervals are no longer backfilled.

How to fix: Set catchup=True on DAGs that rely on backfilling, or run with pin_catchup=true.

DS210

dynamic-dag-kwargs: DAG(**kwargs) built at runtime; keys not visible to static analysis.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

from airflow import DAG
from config import load

for cfg in load():
    globals()[cfg["dag_id"]] = DAG(**cfg)

info, line 5: DAG arguments come from **kwargs; dagshift cannot see those keys. Check the source (often a config file) for schedule_interval, concurrency, timetable or sla_miss_callback.

DS211

replaced-operators: PostgresOperator & co -> SQLExecuteQueryOperator, SimpleHttpOperator -> HttpOperator.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,3 +1,3 @@
-from airflow.providers.postgres.operators.postgres import PostgresOperator
+from airflow.providers.common.sql.operators.sql import SQLExecuteQueryOperator

-PostgresOperator(task_id="vacuum", postgres_conn_id="dwh", sql="VACUUM")
+SQLExecuteQueryOperator(task_id="vacuum", conn_id="dwh", sql="VACUUM")

fixed, line 3: PostgresOperator no longer exists in current provider releases; replaced with SQLExecuteQueryOperator from airflow.providers.common.sql.operators.sql (postgres_conn_id= -> conn_id=).

DS3xx Task context and templates

DS301

template-variables: Jinja template variables the target removed or deprecated (execution_date, next_ds, tomorrow_ds, conf, ...).

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,3 +1,3 @@
 from airflow.operators.bash import BashOperator

-BashOperator(task_id="load", bash_command="load.sh {{ execution_date }} {{ next_ds }} {{ prev_ds }}")
+BashOperator(task_id="load", bash_command="load.sh {{ logical_date }} {{ data_interval_end | ds }} {{ prev_ds }}")

fixed, line 3: Template variable execution_date does not exist in Airflow 3.3; renamed to logical_date (same value).

fixed, line 3: Template variable next_ds does not exist in Airflow 3.3; replaced with data_interval_end | ds (same value).

manual, line 3: Template variable prev_ds does not exist in Airflow 3.3.

How to fix: prev_ds (logical date of the previous scheduled run) is removed. Closest: {{ prev_data_interval_start_success | ds }} (previous successful run), or for a fixed schedule {{ macros.ds_add(ds, -1) }} (daily). Pick the one that matches your intent.

DS302

context-keys: Task-context keys the target removed or deprecated, in Python (context['execution_date'], def f(next_ds)).

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,3 +1,3 @@
 def extract(execution_date, **context):
-    next_day = context["next_ds"]
+    next_day = context["data_interval_end"].strftime("%Y-%m-%d")
     return execution_date, next_day

fixed, line 2: context["next_ds"]: next_ds does not exist in Airflow 3.3; replaced with context["data_interval_end"].strftime("%Y-%m-%d").

DS303

xcom-pull-task-ids: xcom_pull() without task_ids reads only the current task from Airflow 3.

Applies to: upgrades from Airflow 2 to Airflow 3.

Example, Airflow 2.10 to 3.3, dags/etl.py:

def report(ti, **context):
    return ti.xcom_pull(key="rows")

warn, line 2: xcom_pull() without task_ids reads only the current task's XComs from Airflow 3.0 on (Airflow 2 searched every task in the run).

How to fix: Pass task_ids explicitly: ti.xcom_pull(task_ids='producer', key='...').

DS304

execution-date-attribute: dag_run.execution_date / ti.execution_date -> .logical_date.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

@@ -1,2 +1,2 @@
 def report(dag_run, **context):
-    return dag_run.execution_date
+    return dag_run.logical_date

fixed, line 2: .execution_date on the DAG run / task instance is gone in Airflow 3.3; now .logical_date (same value).

DS4xx Metadata database access

DS401

metadata-db-access: ORM sessions/models used from DAG or task code (blocked from Airflow 3).

Applies to: upgrades whose target is Airflow 3.0 or newer.

Example, Airflow 2.10 to 3.3, dags/etl.py:

from airflow.models import TaskInstance
from airflow.utils.session import provide_session

@provide_session
def failures(session=None, **context):
    return session.query(TaskInstance).filter(TaskInstance.state == "failed").count()

manual, line 5: failures reads or writes the metadata DB directly (TaskInstance). Airflow 3 blocks database access from task code; this fails at run time, not at parse time.

How to fix: Task-instance state: context['ti'].get_task_states(dag_id=..., run_ids=[...]) or get_ti_count(dag_id=..., states=[...]).

DS402

db-backed-methods: dag_run.get_task_instances(), ti.current_state() and similar ORM methods.

Applies to: upgrades whose target is Airflow 3.0 or newer.

Example, Airflow 2.10 to 3.3, dags/etl.py:

def check(dag_run, **context):
    return [ti.task_id for ti in dag_run.get_task_instances()]

manual, line 2: dag_run.get_task_instances() hits the metadata DB, which task code cannot reach in Airflow 3.

How to fix: Ask the Task SDK instead of the ORM. Inside a task:

    ti = context['ti']
    states = ti.get_task_states(dag_id=ti.dag_id, run_ids=[ti.run_id])[ti.run_id]
    failed = [t for t, s in states.items() if s == 'failed' and t != ti.task_id]
Signature on Airflow 3.3: get_task_states(dag_id: 'str', map_index: 'int | None' = None, task_ids: 'list[str] | None' = None, task_group_id: 'str | None' = None, logical_dates: 'list[datetime] | None' = None, run_ids: 'list[str] | None' = None) -> 'dict[str, Any]'

DS5xx Behaviour changes

DS501

guarded-imports: try/except ImportError around Airflow imports hides breakage.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, dags/etl.py:

try:
    from airflow.operators.bash import BashOperator
except ImportError:
    from airflow.operators.bash_operator import BashOperator

warn, line 1: Airflow imports are wrapped in try/except: if one of them is missing on Airflow 3 the except branch runs silently and the DAG imports with fallback objects (often a no-op decorator), failing later at run time instead of at parse time.

How to fix: Guard only the 'is Airflow installed' check (e.g. import airflow alone inside the try), or catch ImportError and re-raise when running under Airflow. dagshift has rewritten the imports inside the block; run dagshift validate to confirm they resolve.

DS502

parse-time-values: now()/time()/uuid4() evaluated at parse time create a DAG version per parse.

Applies to: upgrades whose target is Airflow 3.0 or newer.

Example, Airflow 2.10 to 3.3, dags/etl.py:

from datetime import datetime
from airflow import DAG

with DAG("etl", start_date=datetime(2024, 1, 1), schedule="@daily",
         tags=[datetime.now().strftime("%Y%m%d")]):
    pass

warn, line 5: datetime.datetime.now() is evaluated every time the DAG file is parsed. Airflow 3 versions DAGs, so a value that changes on each parse creates a new DAG version each time (and in Airflow 2 it was already non-deterministic).

How to fix: Use a fixed value, or compute it at run time: in a templated field ('{{ data_interval_end }}', '{{ macros.datetime.now() }}') or inside the task callable.

DS503

schedule-semantics: Cron/timedelta schedules use trigger timetables in Airflow 3: logical_date and ds shift.

Applies to: upgrades from Airflow 2 to Airflow 3.

Example, Airflow 2.10 to 3.3, dags/etl.py:

from datetime import datetime
from airflow import DAG
from airflow.operators.bash import BashOperator

with DAG("etl", start_date=datetime(2024, 1, 1), schedule="30 4 * * *", catchup=False):
    BashOperator(task_id="load", bash_command="load.sh {{ ds }}")

info, line 5: Schedule '30 4 * * *': Airflow 3 runs it with CronTriggerTimetable, so a run's logical_date is the trigger time instead of the start of the interval, and data_interval_start == data_interval_end. Nothing in this file reads those values.

How to fix: To keep Airflow 2 semantics: run dagshift with keep_cron_data_intervals=true (wraps the schedule in a *DataIntervalTimetable), or set [scheduler] create_cron_data_intervals = True for the whole environment (Cloud Composer: airflow_config_overrides 'scheduler-create_cron_data_intervals' = 'True') before the first Airflow 3 run.

DS504

plugins: Flask/FAB plugin views and plugin attributes the target removed.

Applies to: upgrades whose target is Airflow 3.0 or newer.

Example, Airflow 2.10 to 3.3, plugins/acme.py:

from airflow.plugins_manager import AirflowPlugin
from flask import Blueprint

bp = Blueprint("acme", __name__)

class AcmePlugin(AirflowPlugin):
    name = "acme"
    flask_blueprints = [bp]

manual, line 8: Plugin attribute flask_blueprints is a Flask/FAB UI extension. The Airflow 3 UI does not render it; it only works with the FAB provider's legacy views.

How to fix: Port it to fastapi_apps, external_views and react_apps on Airflow 3.3, or install apache-airflow-providers-fab and use FabAuthManager to keep the legacy view.

DS505

templated-file: Jinja in code positions; rendered values are not visible to static analysis.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, templates/table_dag.py.j2:

from airflow import DAG
from airflow.operators.bash_operator import BashOperator

with DAG("{{ name }}", schedule_interval="{{ schedule }}") as dag:
{% for t in tables %}
    BashOperator(task_id="load_{{ t }}", bash_command="load.sh {{ t }} {{ '{{' }} ds {{ '}}' }}")
{% endfor %}

info, line 1: Jinja template: 2 Jinja construct(s) in code positions were kept as-is. Values your generator injects there (schedules, operator classes, kwargs) are only visible after rendering.

How to fix: Render the templates and run dagshift check on the output, or use dagshift validate --render-cmd '<your build command>'.

DS6xx Code generators

DS601

embedded-code: Airflow code inside string literals (code generators) migrated recursively.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, generators/build.py:

@@ -1,4 +1,4 @@
 TEMPLATE = """
-from airflow.operators.bash_operator import BashOperator
-BashOperator(task_id="{name}", bash_command="run {{{{ execution_date }}}}")
+from airflow.providers.standard.operators.bash import BashOperator
+BashOperator(task_id="{name}", bash_command="run {{{{ logical_date }}}}")
 """

fixed, line 1: Migrated Airflow code embedded in a string literal (DS101, DS301).

DS602

legacy-names-in-strings: Dotted paths / Dataset(...) inside string literals.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, generators/registry.py:

@@ -1 +1 @@
-OPERATORS = {"bash": "airflow.operators.bash_operator.BashOperator"}
+OPERATORS = {"bash": "airflow.providers.standard.operators.bash.BashOperator"}

fixed, line 1: Updated Airflow name inside a string literal: airflow.operators.bash_operator.BashOperator -> airflow.providers.standard.operators.bash.BashOperator

DS7xx YAML and JSON configs

DS701

config-import-paths: Dotted Airflow import paths in YAML/JSON configs.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, configs/pipelines.yaml:

@@ -1,4 +1,4 @@
 pipelines:
   - name: customers
-    operator: airflow.operators.bash_operator.BashOperator
+    operator: airflow.providers.standard.operators.bash.BashOperator
     command: "load.sh {{ execution_date }}"

fixed, line 3: airflow.operators.bash_operator.BashOperator -> airflow.providers.standard.operators.bash.BashOperator

DS703

dag-factory-keys: dag-factory keys renamed or removed.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, configs/dag_factory.yml:

@@ -1,7 +1,7 @@
 default:
   default_args:
     owner: data-eng
-  schedule_interval: "@daily"
+  schedule: "@daily"
 customers:
   tasks:
     load:

fixed, line 4: schedule_interval is not accepted in Airflow 3.3; renamed to schedule.

DS704

config-airflow-keys: Airflow parameter names the target removed, used as keys in homegrown configs.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, configs/pipeline.json:

{"dag_id": "json_pipeline", "schedule_interval": "@daily", "concurrency": 2}

warn, line 1: Config key schedule_interval matches an Airflow parameter that is not accepted in Airflow 3.3 (now schedule).

How to fix: If your generator passes this key straight into DAG()/an operator (e.g. **config), rename it (or run with rename_config_keys=true). If the generator maps it in code, dagshift has already fixed the Python side.

DS8xx Environment and platform

DS801

airflow-version-pin: Airflow pinned below the target in requirements, constraints, images, compose or Helm values.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, requirements.txt:

apache-airflow==2.10.5
apache-airflow-providers-google==10.26.0

manual, line 1: Airflow is pinned to apache-airflow==2.10.5; the upgrade targets 3.3.

How to fix: Pin apache-airflow to the 3.3.x release you are moving to. Install with the constraints file of the exact 3.3.x release: https://raw.githubusercontent.com/apache/airflow/constraints-3.3./constraints-.txt

info, line 2: apache-airflow-providers-google is pinned to 10.26.0.

How to fix: Take provider versions from the constraints file of Airflow 3.3.x; older provider releases may not support it.

DS802

provider-requirements: Provider packages required by migrated imports.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

DS803

airflowignore-syntax: .airflowignore uses regexps; Airflow 3 defaults to glob.

Applies to: upgrades from Airflow 2 to Airflow 3.

Example, Airflow 2.10 to 3.3, dags/.airflowignore:

scratch_.*
.*_test\.py

warn, line 1: .airflowignore line scratch_.* looks like a regular expression; Airflow 3 reads this file as glob patterns by default.

How to fix: Rewrite it as a glob (e.g. .*_test\.py -> *_test.py), or set [core] dag_ignore_file_syntax = regexp.

warn, line 2: .airflowignore line .*_test\.py looks like a regular expression; Airflow 3 reads this file as glob patterns by default.

How to fix: Rewrite it as a glob (e.g. .*_test\.py -> *_test.py), or set [core] dag_ignore_file_syntax = regexp.

DS804

cloud-composer: Cloud Composer image below the target (no in-place upgrade from Airflow 2 to 3).

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, infra/main.tf:

resource "google_composer_environment" "prod" {
  config {
    software_config {
      image_version = "composer-2.9.7-airflow-2.10.5"
    }
  }
}

info, line 4: Cloud Composer image composer-2.9.7-airflow-2.10.5 runs Airflow 2.10.5.

How to fix: Composer does not yet support upgrading an environment from Airflow 2 to 3 in place: create a second Composer 3 environment on composer-3-airflow-3.3.x-build.N and cut over. Do not just change image_version on the existing resource.

DS805

mwaa: Amazon MWAA environment below the target.

Applies to: any upgrade (the knowledge base decides per target what is removed or deprecated).

Example, Airflow 2.10 to 3.3, infra/mwaa.tf:

resource "aws_mwaa_environment" "prod" {
  airflow_version = "2.10.3"
}

info, line 2: Amazon MWAA environment on Airflow 2.10.3.

How to fix: Check that MWAA offers Airflow 3.3 and its documented upgrade path before cutover.

DS806

airflow-config: airflow.cfg / webserver config overrides.

Applies to: upgrades from Airflow 2 to Airflow 3.

Example, Airflow 2.10 to 3.3, airflow.cfg:

[webserver]
rbac = True
[core]
dag_concurrency = 16

info, line 1: airflow.cfg found.

How to fix: Run airflow config update --fix with Airflow 3.3 installed to rename and drop changed options.

DS807

dag-factory: dag-factory configs: use dag-factory 1.0's own convert/lint for layout changes.

Applies to: upgrades whose target is Airflow 3.0 or newer.

Example, Airflow 2.10 to 3.3, configs/dag_factory.yml:

default:
  default_args:
    owner: data-eng
customers:
  schedule: "@daily"
  tasks:
    load:
      operator: airflow.providers.standard.operators.bash.BashOperator
      bash_command: load.sh

info, line 1: dag-factory config. dagshift updated import paths, template variables and renamed keys. Airflow 3 needs dag-factory 1.0+, which also changed its YAML layout.

How to fix: Run dagfactory convert and then dagfactory lint (dag-factory 1.0 CLI) for structural changes such as tasks/task_groups as lists.