Repository navigation
Cyclic TaskGroup dependencies break Grid/Graph since 3.3.1; proposed parse-time rejection is a breaking change #73678
Description
Activity
Another case, found by @shubhamraj-git while reviewing #73087 (#73087 (comment)): a Dag whose group-level dependencies have no cycle can still get a 500 from Grid/Graph when the view is filtered.
with DAG("filter_cycle", schedule=None): with TaskGroup("g"): first = EmptyOperator(task_id="first") second = EmptyOperator(task_id="second") guard = EmptyOperator(task_id="guard") guard >> second bridge = EmptyOperator(task_id="bridge") first >> bridge >> second
Unfiltered Grid/Graph return 200. With
root=g.first&include_downstream=true,partial_subset()dropsg.guard, sog.secondbecomes a root ofg. The group-level edges then becomeg ↔ bridge, and both endpoints return 500. The same failure happens without #73087.So filtering can create group-level cycles even for Dags that pass any parse-time check. Whatever we decide on rejecting these Dags, the Grid/Graph ordering needs to tolerate group-level cycles rather than error out. I'll handle that in a separate PR.
Drafted-by: Claude Code (Opus 5.5); reviewed by @dheerajturaga before posting
- addedkind:bugThis is a clearly a bugThis is a clearly a bugarea:APIAirflow's REST/HTTP APIAirflow's REST/HTTP APIand removedarea:APIAirflow's REST/HTTP APIAirflow's REST/HTTP API
on Sep 25, 2026 We use task groups as a visual aid for grouping tasks not to create dependencies.
Disallowing these type of dags needs to come with an alternative to organize tasks in "folders".Reacted by Jitendra, IgorBobic, Oscar Ligthart, vbukhonov-mercuryo and Michiel De MuynckThanks @rloredo. Many people use TaskGroups as folders, and you're not the only one affected. The same came up on #73087 (one group per database schema) and #73724 (dbt models in a group with a Spark task between two of them).
Here's what the dev list agreed (discussion, result):
- 3.4.0: Grid and Graph render these Dags again (Fix Grid and Graph 500 errors for cyclic TaskGroup dependencies #73724). Parsing them shows a Dag warning in the UI and issues a
TaskGroupCycleDeprecationWarning(Deprecate cyclic TaskGroup dependencies #73746). They keep parsing and running. - 3.5.0 (planned): these Dags fail to parse (Reject cyclic TaskGroup dependencies during Dag parsing #73087).
Until 3.4.0, only Grid and Graph are affected. Tasks keep running and the REST API still works.
The reason for the change is that planned TaskGroup features, such as loops, dynamic TaskGroups, retrying a group and waiting for a group to finish, need each group to come either before or after its neighbours. A cycle makes that impossible.
The rule: a dependency into or out of any task in a group counts as a dependency of the whole group. So a path that leaves a group and comes back into it (
g.a >> x >> g.b) is a cycle.How to restructure. One of these usually works without changing any task dependencies:
- Move the task between the two group tasks into the group, or move the downstream task out of it.
- Split the group along the direction of the data flow. If you use one group per schema and have dependencies across schemas, group by stage first and by schema second:
with TaskGroup("load"): with TaskGroup("sales"): load_sales = EmptyOperator(task_id="load") with TaskGroup("customers"): load_customers = EmptyOperator(task_id="load") with TaskGroup("report"): with TaskGroup("sales"): report_sales = EmptyOperator(task_id="report") with TaskGroup("customers"): report_customers = EmptyOperator(task_id="report") [load_sales, load_customers] >> report_sales [load_sales, load_customers] >> report_customers
This changes task IDs (for example,
sales.loadbecomesload.sales.load). Check anything that refers to tasks by ID, such as sensors orxcom_pullcalls.Once 3.4.0 is out, you can find affected Dags in CI by running your Dag tests with
-W error::airflow.sdk.exceptions.TaskGroupCycleDeprecationWarning.A folder-only alternative. That's a fair ask, and Airflow doesn't have one today. Rejection in 3.5 is the plan. The dev list discussion also said we should check how common this pattern is and reconsider if it would break many Dags. Options include letting a Dag opt out of group-level features, or a grouping that's only for display. The 3.4 deprecation period is the time to collect that information. If you're affected, please reply here with:
- an example of your Dag's shape, with any sensitive names removed (or which of the two shapes in the issue description it matches),
- roughly how many of your Dags are affected,
- whether the restructuring above works for you, and if it doesn't, why not.
If enough users can't restructure, I'll raise it on the dev list again before 3.5.
Drafted-by: Claude Code (Opus 5.5); reviewed by @dheerajturaga before posting
- 3.4.0: Grid and Graph render these Dags again (Fix Grid and Graph 500 errors for cyclic TaskGroup dependencies #73724). Parsing them shows a Dag warning in the UI and issues a
Scope note from #73746: the deprecation warning, and the parse-time rejection as currently planned, live in the Python SDK's
DAG.check_cycle(). Go, Java and TypeScript SDK Dags reach the Dag processor already serialized, andlang_sdk_processor.pyonly validates them, so they get neither the warning nor the rejection. Before the rejection lands in 3.5, the check should run onSerializedTaskGroupin core (for example fromlang_sdk_processor), so it covers every SDK. If that doesn't make 3.5, the rejection should be documented as Python-only.
Drafted-by: Claude Code (Opus 5.5); reviewed by @dheerajturaga before posting
The main unresolved issue I see here still is the conflict between:
- The Airflow deprecation policy promising not to make breaking changes in minor versions
- The current plan of doing exactly that, for a change that actually breaks real-world code (examples: 1, 2, 3)
There are multiple possible solutions to this:
- Update the deprecation policy to reflect reality, that semantic versioning is no longer used
- Rename the planned version 3.5 to 4.0
- Make the DAG only invalid if the new features for which these DAG structures are problematic are actually used
Just ignoring the discrepancy seems like the worst choice.
@michiel-de-muynck The dev list treated this as a bug fix, not as removing a feature. The deprecation policy covers supported behavior. Airflow never defined how TaskGroups that depend on each other in a cycle should behave: before 3.3.1, Grid and Graph ignored the edges between groups and showed them in an arbitrary order (#65291), and the planned features that act on a TaskGroup as a whole (loops, dynamic TaskGroups, retrying a group, waiting for a group to finish) have no defined behavior for them. The release process docs also note that SemVer isn't a promise of 100% compatibility, because a bug to one person can be a feature another person depends on.
We still didn't want these Dags to start failing without notice. So 3.4 first warns at parse time and in the UI, with guidance on how to restructure, and the rejection comes in 3.5. The lazy consensus agreed on this as an explicit exception, so I don't plan to propose a change to the deprecation policy for it.
If you think the decision is wrong, the dev list is the place to raise it. As mentioned above, if the 3.4 period shows that many users can't restructure their Dags, I'll take it back to the list before 3.5.
Starting with Airflow 3.3.1, the Grid and Graph views return HTTP 500 for Dags whose TaskGroups have dependencies that form a cycle when each group is treated as a single unit, even though the task-level graph is acyclic. These Dags parse, schedule and run normally, and they rendered in the UI up to and including 3.3.0.
One proposed change (#73087) would reject these Dags at parse time. That is also a breaking change for existing Dags, this time with an import error. Before choosing a direction, I'd like community input on:
I'm affected by this on my own Dags as well.
How we got here
v3-3-testin [v3-3-test] Fix grid/graph view topological sort for group-level and cross-group dependencies (#69933) #70591, released in 3.3.1) fixed the group-level topological sort used by Grid/Graph (closes Tasks in the Grid view are not in topological order #65291). Before that change, the sort ignored group-to-group and cross-group edges and silently rendered an arbitrary order. It now takes those edges into account, so a pre-existing group-level cycle raisesValueError("A cyclic dependency occurred in dag: ..."), which surfaces as a 500 from the Grid and Graph endpoints.Affected Dag shapes
A task with no upstream task inside its own group counts as a root of that group, even when a task outside the group provides its only upstream. Group-level edges are built from those roots.
1. Sibling groups with dependencies in both directions:
2. Two tasks in one group bridged by a task outside it:
Both are acyclic at the task level. This pattern is common when TaskGroups are used for logical or visual grouping (for example one group per database schema) rather than as execution units. Users report hitting it when upgrading from 2.x to 3.3.1.
Impact by version
Open questions for the community
significantnewsfragment exists in Reject cyclic TaskGroup dependencies during Dag parsing #73087. Should there also be a known-issue note in the 3.3.1/3.3.2 release notes and/or an announcement on the dev/users mailing list?Related
Are you willing to submit PR?
Drafted-by: Claude Code (Opus 5.5); reviewed by @dheerajturaga before posting