This is an automated email from the ASF dual-hosted git repository.

jscheffl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 477b1482e7e Add prek hook enforcing the "example" tag on example DAGs 
(#67354)
477b1482e7e is described below

commit 477b1482e7e242f3717aa7ffc490af4592ada3d3
Author: André Ahlert <[email protected]>
AuthorDate: Sat May 23 07:49:45 2026 -0300

    Add prek hook enforcing the "example" tag on example DAGs (#67354)
    
    Example DAGs are filtered and grouped by tag across the docs and the UI,
    but the "example" tag was applied inconsistently. This adds an AST-based
    prek hook, check-example-dag-tags, that fails when a DAG defined under an
    example_dags/ folder does not carry the "example" tag, and retags every
    current violator so the hook passes on a clean checkout.
    
    Part of #52477
    
    Signed-off-by: André Ahlert <[email protected]>
---
 .pre-commit-config.yaml                            |   6 +
 .../airflow/example_dags/example_asset_alias.py    |   8 +-
 .../example_asset_alias_with_no_taskflow.py        |   8 +-
 .../example_dags/example_asset_allow_teams.py      |   4 +-
 .../example_dags/example_asset_decorator.py        |   2 +-
 .../example_dags/example_asset_partition.py        |  14 +--
 .../example_dags/example_asset_with_watchers.py    |   1 +
 .../src/airflow/example_dags/example_assets.py     |  17 +--
 .../airflow/example_dags/example_branch_labels.py  |   1 +
 .../example_dags/example_dynamic_task_mapping.py   |   6 +-
 ...amic_task_mapping_with_no_taskflow_operators.py |   1 +
 .../example_dags/example_inlet_event_extra.py      |   4 +-
 .../example_dags/example_kubernetes_executor.py    |   2 +-
 .../example_latest_only_with_trigger.py            |   2 +-
 .../example_local_kubernetes_executor.py           |   2 +-
 .../example_dags/example_outlet_event_extra.py     |   6 +-
 .../airflow/example_dags/example_simplest_dag.py   |   2 +-
 .../common/ai/example_dags/example_agent.py        |  12 +-
 .../ai/example_dags/example_agent_durable.py       |   4 +-
 .../ai/example_dags/example_document_loader.py     |  10 +-
 .../ai/example_dags/example_langchain_hook.py      |   8 +-
 .../example_dags/example_langchain_tool_agent.py   |   2 +-
 .../ai/example_dags/example_llamaindex_hook.py     |   8 +-
 .../ai/example_dags/example_llamaindex_rag.py      |   7 +-
 .../common/ai/example_dags/example_llm.py          |  14 +--
 .../example_dags/example_llm_analysis_pipeline.py  |   2 +-
 .../common/ai/example_dags/example_llm_branch.py   |   8 +-
 .../ai/example_dags/example_llm_classification.py  |   2 +-
 .../ai/example_dags/example_llm_file_analysis.py   |  10 +-
 .../ai/example_dags/example_llm_schema_compare.py  |  10 +-
 .../common/ai/example_dags/example_llm_sql.py      |  12 +-
 .../ai/example_dags/example_llm_survey_agentic.py  |   2 +-
 .../ai/example_dags/example_llm_survey_analysis.py |   4 +-
 .../common/ai/example_dags/example_mcp.py          |   4 +-
 .../ai/example_dags/example_pydantic_ai_hook.py    |   6 +-
 .../common/sql/example_dags/example_analytics.py   |   2 +-
 .../providers/edge3/example_dags/win_notepad.py    |   2 +-
 .../providers/edge3/example_dags/win_test.py       |   2 +-
 .../oracle/example_dags/example_oracle.py          |   1 +
 .../example_dags/example_bash_decorator.py         |   2 +-
 .../example_external_task_marker_dag.py            |   4 +-
 .../standard/example_dags/example_latest_only.py   |   2 +-
 scripts/ci/prek/check_example_dag_tags.py          | 137 +++++++++++++++++++++
 .../tests/ci/prek/test_check_example_dag_tags.py   | 109 ++++++++++++++++
 44 files changed, 368 insertions(+), 104 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aa728188737..6b3635df810 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -812,6 +812,12 @@ repos:
         entry: 
"default_args\\s*=\\s*{\\s*(\"|')start_date(\"|')|(\"|')start_date(\"|'):"
         files: \.*example_dags.*\.py$
         pass_filenames: true
+      - id: check-example-dag-tags
+        name: Check example DAGs carry the "example" tag
+        entry: ./scripts/ci/prek/check_example_dag_tags.py
+        language: python
+        files: ^.*/example_dags/.*\.py$
+        pass_filenames: true
       - id: check-apache-license-rat
         name: Check if licenses are OK for Apache
         entry: ./scripts/ci/prek/check_license.py
diff --git a/airflow-core/src/airflow/example_dags/example_asset_alias.py 
b/airflow-core/src/airflow/example_dags/example_asset_alias.py
index 5c4df1aa09c..810c0b76071 100644
--- a/airflow-core/src/airflow/example_dags/example_asset_alias.py
+++ b/airflow-core/src/airflow/example_dags/example_asset_alias.py
@@ -42,7 +42,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=None,
     catchup=False,
-    tags=["producer", "asset"],
+    tags=["example", "producer", "asset"],
 ):
 
     @task(outlets=[Asset("s3://bucket/my-task")])
@@ -56,7 +56,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=None,
     catchup=False,
-    tags=["producer", "asset-alias"],
+    tags=["example", "producer", "asset-alias"],
 ):
 
     @task(outlets=[AssetAlias("example-alias")])
@@ -72,7 +72,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=[Asset("s3://bucket/my-task")],
     catchup=False,
-    tags=["consumer", "asset"],
+    tags=["example", "consumer", "asset"],
 ):
 
     @task
@@ -86,7 +86,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=[AssetAlias("example-alias")],
     catchup=False,
-    tags=["consumer", "asset-alias"],
+    tags=["example", "consumer", "asset-alias"],
 ):
 
     @task(inlets=[AssetAlias("example-alias")])
diff --git 
a/airflow-core/src/airflow/example_dags/example_asset_alias_with_no_taskflow.py 
b/airflow-core/src/airflow/example_dags/example_asset_alias_with_no_taskflow.py
index d9de5e06d9f..13868b62927 100644
--- 
a/airflow-core/src/airflow/example_dags/example_asset_alias_with_no_taskflow.py
+++ 
b/airflow-core/src/airflow/example_dags/example_asset_alias_with_no_taskflow.py
@@ -43,7 +43,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=None,
     catchup=False,
-    tags=["producer", "asset"],
+    tags=["example", "producer", "asset"],
 ):
 
     def produce_asset_events():
@@ -61,7 +61,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=None,
     catchup=False,
-    tags=["producer", "asset-alias"],
+    tags=["example", "producer", "asset-alias"],
 ):
 
     def produce_asset_events_through_asset_alias_with_no_taskflow(*, 
outlet_events=None):
@@ -80,7 +80,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=[Asset("s3://bucket/my-task-with-no-taskflow")],
     catchup=False,
-    tags=["consumer", "asset"],
+    tags=["example", "consumer", "asset"],
 ):
 
     def consume_asset_event():
@@ -93,7 +93,7 @@ with DAG(
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=[AssetAlias("example-alias-no-taskflow")],
     catchup=False,
-    tags=["consumer", "asset-alias"],
+    tags=["example", "consumer", "asset-alias"],
 ):
 
     def consume_asset_event_from_asset_alias(*, inlet_events=None):
diff --git a/airflow-core/src/airflow/example_dags/example_asset_allow_teams.py 
b/airflow-core/src/airflow/example_dags/example_asset_allow_teams.py
index d77eeb9d2b7..ae593e62a51 100644
--- a/airflow-core/src/airflow/example_dags/example_asset_allow_teams.py
+++ b/airflow-core/src/airflow/example_dags/example_asset_allow_teams.py
@@ -54,7 +54,7 @@ with DAG(
     start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
     schedule="@daily",
     catchup=False,
-    tags=["team_analytics", "produces", "asset-scheduled", "allow-teams"],
+    tags=["example", "team_analytics", "produces", "asset-scheduled", 
"allow-teams"],
 ) as producer_dag:
     BashOperator(
         task_id="produce_shared_data",
@@ -70,7 +70,7 @@ with DAG(
     start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
     schedule=[shared_data],
     catchup=False,
-    tags=["team_ml", "consumes", "asset-scheduled", "allow-teams"],
+    tags=["example", "team_ml", "consumes", "asset-scheduled", "allow-teams"],
 ) as consumer_dag:
     BashOperator(
         task_id="consume_shared_data",
diff --git a/airflow-core/src/airflow/example_dags/example_asset_decorator.py 
b/airflow-core/src/airflow/example_dags/example_asset_decorator.py
index 55f09417808..e6dd452a95a 100644
--- a/airflow-core/src/airflow/example_dags/example_asset_decorator.py
+++ b/airflow-core/src/airflow/example_dags/example_asset_decorator.py
@@ -37,7 +37,7 @@ def asset2_producer(self, context, asset1_producer):
     | Asset(uri="s3://bucket/object", name="asset2_producer"),
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     catchup=False,
-    tags=["consumes", "asset-scheduled"],
+    tags=["example", "consumes", "asset-scheduled"],
 )
 def consumes_asset_decorator():
     @task(outlets=[Asset(name="process_nothing")])
diff --git a/airflow-core/src/airflow/example_dags/example_asset_partition.py 
b/airflow-core/src/airflow/example_dags/example_asset_partition.py
index f9c8258f951..4edff9127ce 100644
--- a/airflow-core/src/airflow/example_dags/example_asset_partition.py
+++ b/airflow-core/src/airflow/example_dags/example_asset_partition.py
@@ -42,7 +42,7 @@ combined_player_stats = 
Asset(uri="file://curated/player-stats/combined.csv", na
 with DAG(
     dag_id="ingest_team_a_player_stats",
     schedule=CronPartitionTimetable("0 * * * *", timezone="UTC"),
-    tags=["player-stats", "ingestion"],
+    tags=["example", "player-stats", "ingestion"],
 ):
     """Produce hourly partitioned stats for Team A."""
 
@@ -81,7 +81,7 @@ with DAG(
         default_partition_mapper=StartOfHourMapper(),
     ),
     catchup=False,
-    tags=["player-stats", "cleanup"],
+    tags=["example", "player-stats", "cleanup"],
 ):
     """
     Combine hourly partitions from Team A, B and C into a single curated 
dataset.
@@ -128,7 +128,7 @@ with DAG(
         },
     ),
     catchup=False,
-    tags=["player-stats", "odds"],
+    tags=["example", "player-stats", "odds"],
 ):
     """
     Demonstrate a partition mapper mismatch scenario.
@@ -149,7 +149,7 @@ regional_sales = 
Asset(uri="file://incoming/sales/regional.csv", name="regional_
 with DAG(
     dag_id="ingest_regional_sales",
     schedule=CronPartitionTimetable("0 * * * *", timezone="UTC"),
-    tags=["sales", "ingestion"],
+    tags=["example", "sales", "ingestion"],
 ):
     """Produce hourly regional sales data with composite partition keys."""
 
@@ -168,7 +168,7 @@ with DAG(
         default_partition_mapper=ProductMapper(IdentityMapper(), 
StartOfDayMapper()),
     ),
     catchup=False,
-    tags=["sales", "aggregation"],
+    tags=["example", "sales", "aggregation"],
 ):
     """
     Aggregate regional sales using ProductMapper.
@@ -194,7 +194,7 @@ region_raw_stats = 
Asset(uri="file://incoming/player-stats/by-region.csv", name=
 with DAG(
     dag_id="ingest_region_stats",
     schedule=None,
-    tags=["player-stats", "regional"],
+    tags=["example", "player-stats", "regional"],
 ):
     """
     Ingest player statistics per region.
@@ -247,7 +247,7 @@ with DAG(
     dag_id="summarize_live_region_stats",
     
schedule=PartitionedAssetTimetable(assets=Asset.ref(name="live_region_player_stats")),
     catchup=False,
-    tags=["player-stats", "runtime"],
+    tags=["example", "player-stats", "runtime"],
 ):
     """
     Summarize the live region statistics for each runtime-emitted partition.
diff --git 
a/airflow-core/src/airflow/example_dags/example_asset_with_watchers.py 
b/airflow-core/src/airflow/example_dags/example_asset_with_watchers.py
index 673be86a796..228b437dc53 100644
--- a/airflow-core/src/airflow/example_dags/example_asset_with_watchers.py
+++ b/airflow-core/src/airflow/example_dags/example_asset_with_watchers.py
@@ -33,6 +33,7 @@ with DAG(
     dag_id="example_asset_with_watchers",
     schedule=[asset],
     catchup=False,
+    tags=["example"],
 ):
 
     @task
diff --git a/airflow-core/src/airflow/example_dags/example_assets.py 
b/airflow-core/src/airflow/example_dags/example_assets.py
index ed1e61c4891..26ac857a79c 100644
--- a/airflow-core/src/airflow/example_dags/example_assets.py
+++ b/airflow-core/src/airflow/example_dags/example_assets.py
@@ -67,7 +67,7 @@ with DAG(
     catchup=False,
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule="@daily",
-    tags=["produces", "asset-scheduled"],
+    tags=["example", "produces", "asset-scheduled"],
 ) as dag1:
     # [START task_outlet]
     BashOperator(outlets=[dag1_asset], task_id="producing_task_1", 
bash_command="sleep 5")
@@ -78,7 +78,7 @@ with DAG(
     catchup=False,
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=None,
-    tags=["produces", "asset-scheduled"],
+    tags=["example", "produces", "asset-scheduled"],
 ) as dag2:
     BashOperator(outlets=[dag2_asset], task_id="producing_task_2", 
bash_command="sleep 5")
 
@@ -88,7 +88,7 @@ with DAG(
     catchup=False,
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=[dag1_asset],
-    tags=["consumes", "asset-scheduled"],
+    tags=["example", "consumes", "asset-scheduled"],
 ) as dag3:
     # [END dag_dep]
     BashOperator(
@@ -102,7 +102,7 @@ with DAG(
     catchup=False,
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=[dag1_asset, dag2_asset],
-    tags=["consumes", "asset-scheduled"],
+    tags=["example", "consumes", "asset-scheduled"],
 ) as dag4:
     BashOperator(
         outlets=[Asset("s3://consuming_2_task/asset_other_unknown.txt")],
@@ -118,7 +118,7 @@ with DAG(
         dag1_asset,
         Asset("s3://unrelated/this-asset-doesnt-get-triggered"),
     ],
-    tags=["consumes", "asset-scheduled"],
+    tags=["example", "consumes", "asset-scheduled"],
 ) as dag5:
     BashOperator(
         outlets=[Asset("s3://consuming_2_task/asset_other_unknown.txt")],
@@ -134,7 +134,7 @@ with DAG(
         Asset("s3://unrelated/asset3.txt"),
         Asset("s3://unrelated/asset_other_unknown.txt"),
     ],
-    tags=["asset-scheduled"],
+    tags=["example", "asset-scheduled"],
 ) as dag6:
     BashOperator(
         task_id="unrelated_task",
@@ -146,6 +146,7 @@ with DAG(
     dag_id="consume_1_and_2_with_asset_expressions",
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=(dag1_asset & dag2_asset),
+    tags=["example"],
 ) as dag5:
     BashOperator(
         outlets=[Asset("s3://consuming_2_task/asset_other_unknown.txt")],
@@ -156,6 +157,7 @@ with DAG(
     dag_id="consume_1_or_2_with_asset_expressions",
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=(dag1_asset | dag2_asset),
+    tags=["example"],
 ) as dag6:
     BashOperator(
         outlets=[Asset("s3://consuming_2_task/asset_other_unknown.txt")],
@@ -166,6 +168,7 @@ with DAG(
     dag_id="consume_1_or_both_2_and_3_with_asset_expressions",
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     schedule=(dag1_asset | (dag2_asset & dag3_asset)),
+    tags=["example"],
 ) as dag7:
     BashOperator(
         outlets=[Asset("s3://consuming_2_task/asset_other_unknown.txt")],
@@ -179,7 +182,7 @@ with DAG(
     schedule=AssetOrTimeSchedule(
         timetable=CronTriggerTimetable("0 1 * * 3", timezone="UTC"), 
assets=(dag1_asset & dag2_asset)
     ),
-    tags=["asset-time-based-timetable"],
+    tags=["example", "asset-time-based-timetable"],
 ) as dag8:
     BashOperator(
         outlets=[Asset("s3://asset_time_based/asset_other_unknown.txt")],
diff --git a/airflow-core/src/airflow/example_dags/example_branch_labels.py 
b/airflow-core/src/airflow/example_dags/example_branch_labels.py
index fcff3d5a3a1..3e08bd36592 100644
--- a/airflow-core/src/airflow/example_dags/example_branch_labels.py
+++ b/airflow-core/src/airflow/example_dags/example_branch_labels.py
@@ -31,6 +31,7 @@ with DAG(
     schedule="@daily",
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     catchup=False,
+    tags=["example"],
 ) as dag:
     ingest = EmptyOperator(task_id="ingest")
     analyse = EmptyOperator(task_id="analyze")
diff --git 
a/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py 
b/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py
index c7b3a02301d..dd0362e338c 100644
--- a/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py
+++ b/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py
@@ -24,7 +24,9 @@ from datetime import datetime
 
 from airflow.sdk import DAG, task, task_group
 
-with DAG(dag_id="example_dynamic_task_mapping", schedule=None, 
start_date=datetime(2022, 3, 4)):
+with DAG(
+    dag_id="example_dynamic_task_mapping", schedule=None, 
start_date=datetime(2022, 3, 4), tags=["example"]
+):
 
     @task
     def add_one(x: int):
@@ -43,6 +45,7 @@ with DAG(
     schedule=None,
     catchup=False,
     start_date=datetime(2022, 3, 4),
+    tags=["example"],
 ):
 
     @task
@@ -66,6 +69,7 @@ with DAG(
     schedule=None,
     catchup=False,
     start_date=datetime(2022, 3, 4),
+    tags=["example"],
 ):
 
     @task_group
diff --git 
a/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping_with_no_taskflow_operators.py
 
b/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping_with_no_taskflow_operators.py
index 07cd653d29b..3308c3c73c7 100644
--- 
a/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping_with_no_taskflow_operators.py
+++ 
b/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping_with_no_taskflow_operators.py
@@ -55,6 +55,7 @@ with DAG(
     schedule=None,
     start_date=datetime(2022, 3, 4),
     catchup=False,
+    tags=["example"],
 ):
     # map the task to a list of values
     add_one_task = AddOneOperator.partial(task_id="add_one").expand(value=[1, 
2, 3])
diff --git a/airflow-core/src/airflow/example_dags/example_inlet_event_extra.py 
b/airflow-core/src/airflow/example_dags/example_inlet_event_extra.py
index eb61ed443f6..f48cb6ac10a 100644
--- a/airflow-core/src/airflow/example_dags/example_inlet_event_extra.py
+++ b/airflow-core/src/airflow/example_dags/example_inlet_event_extra.py
@@ -35,7 +35,7 @@ with DAG(
     catchup=False,
     start_date=datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
     schedule="@daily",
-    tags=["consumes"],
+    tags=["example", "consumes"],
 ):
 
     @task(inlets=[asset])
@@ -50,7 +50,7 @@ with DAG(
     catchup=False,
     start_date=datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
     schedule="@daily",
-    tags=["consumes"],
+    tags=["example", "consumes"],
 ):
     BashOperator(
         task_id="read_asset_event_from_classic",
diff --git 
a/airflow-core/src/airflow/example_dags/example_kubernetes_executor.py 
b/airflow-core/src/airflow/example_dags/example_kubernetes_executor.py
index 937c174b0df..d3102c089f2 100644
--- a/airflow-core/src/airflow/example_dags/example_kubernetes_executor.py
+++ b/airflow-core/src/airflow/example_dags/example_kubernetes_executor.py
@@ -48,7 +48,7 @@ if k8s:
         schedule=None,
         start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
         catchup=False,
-        tags=["example3"],
+        tags=["example", "example3"],
     ) as dag:
         # You can use annotations on your kubernetes pods!
         start_task_executor_config = {
diff --git 
a/airflow-core/src/airflow/example_dags/example_latest_only_with_trigger.py 
b/airflow-core/src/airflow/example_dags/example_latest_only_with_trigger.py
index f90f01a644a..e8d9e762ed3 100644
--- a/airflow-core/src/airflow/example_dags/example_latest_only_with_trigger.py
+++ b/airflow-core/src/airflow/example_dags/example_latest_only_with_trigger.py
@@ -35,7 +35,7 @@ with DAG(
     schedule=datetime.timedelta(hours=4),
     start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
     catchup=False,
-    tags=["example3"],
+    tags=["example", "example3"],
 ) as dag:
     latest_only = LatestOnlyOperator(task_id="latest_only")
     task1 = EmptyOperator(task_id="task1")
diff --git 
a/airflow-core/src/airflow/example_dags/example_local_kubernetes_executor.py 
b/airflow-core/src/airflow/example_dags/example_local_kubernetes_executor.py
index 752c2bc29ca..6634dbe94bd 100644
--- a/airflow-core/src/airflow/example_dags/example_local_kubernetes_executor.py
+++ b/airflow-core/src/airflow/example_dags/example_local_kubernetes_executor.py
@@ -46,7 +46,7 @@ if k8s:
         schedule=None,
         start_date=datetime(2021, 1, 1),
         catchup=False,
-        tags=["example3"],
+        tags=["example", "example3"],
     ) as dag:
         # You can use annotations on your kubernetes pods!
         start_task_executor_config = {
diff --git 
a/airflow-core/src/airflow/example_dags/example_outlet_event_extra.py 
b/airflow-core/src/airflow/example_dags/example_outlet_event_extra.py
index 7baab90625d..f423597bb26 100644
--- a/airflow-core/src/airflow/example_dags/example_outlet_event_extra.py
+++ b/airflow-core/src/airflow/example_dags/example_outlet_event_extra.py
@@ -35,7 +35,7 @@ with DAG(
     catchup=False,
     start_date=datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
     schedule="@daily",
-    tags=["produces"],
+    tags=["example", "produces"],
 ):
 
     @task(outlets=[asset])
@@ -49,7 +49,7 @@ with DAG(
     catchup=False,
     start_date=datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
     schedule="@daily",
-    tags=["produces"],
+    tags=["example", "produces"],
 ):
 
     @task(outlets=[asset])
@@ -63,7 +63,7 @@ with DAG(
     catchup=False,
     start_date=datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
     schedule="@daily",
-    tags=["produces"],
+    tags=["example", "produces"],
 ):
 
     def _asset_with_extra_from_classic_operator_post_execute(context, result):
diff --git a/airflow-core/src/airflow/example_dags/example_simplest_dag.py 
b/airflow-core/src/airflow/example_dags/example_simplest_dag.py
index 660f38c2e00..59271bf27b8 100644
--- a/airflow-core/src/airflow/example_dags/example_simplest_dag.py
+++ b/airflow-core/src/airflow/example_dags/example_simplest_dag.py
@@ -22,7 +22,7 @@ from __future__ import annotations
 from airflow.sdk import dag, task
 
 
-@dag
+@dag(tags=["example"])
 def example_simplest_dag():
     @task
     def my_task():
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
index d36461e5a0c..699386e9042 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
@@ -31,7 +31,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_operator_agent_sql]
-@dag
+@dag(tags=["example"])
 def example_agent_operator_sql():
     AgentOperator(
         task_id="analyst",
@@ -62,7 +62,7 @@ example_agent_operator_sql()
 
 
 # [START howto_operator_agent_hook]
-@dag
+@dag(tags=["example"])
 def example_agent_operator_hook():
     from airflow.providers.http.hooks.http import HttpHook
 
@@ -94,7 +94,7 @@ example_agent_operator_hook()
 
 
 # [START howto_decorator_agent]
-@dag
+@dag(tags=["example"])
 def example_agent_decorator():
     @task.agent(
         llm_conn_id="pydanticai_default",
@@ -123,7 +123,7 @@ example_agent_decorator()
 
 
 # [START howto_decorator_agent_structured]
-@dag
+@dag(tags=["example"])
 def example_agent_structured_output():
     from pydantic import BaseModel
 
@@ -155,7 +155,7 @@ example_agent_structured_output()
 
 
 # [START howto_agent_chain]
-@dag
+@dag(tags=["example"])
 def example_agent_chain():
     @task.agent(
         llm_conn_id="pydanticai_default",
@@ -186,7 +186,7 @@ example_agent_chain()
 
 
 # [START howto_operator_agent_hitl_review]
-@dag
+@dag(tags=["example"])
 def example_agent_operator_hitl_review():
     """AgentOperator with HITL review — a human approves output via 
hitl-review plugin UI."""
     AgentOperator(
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_durable.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_durable.py
index e2d3ec6e3fc..6385c005918 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_durable.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_durable.py
@@ -30,7 +30,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_operator_agent_durable]
-@dag(default_args={"retries": 3, "retry_delay": timedelta(seconds=30)})
+@dag(default_args={"retries": 3, "retry_delay": timedelta(seconds=30)}, 
tags=["example"])
 def example_agent_durable_operator():
     """Agent with durable execution -- resumes from the last model call on 
retry."""
     AgentOperator(
@@ -63,7 +63,7 @@ example_agent_durable_operator()
 
 
 # [START howto_decorator_agent_durable]
-@dag(default_args={"retries": 3, "retry_delay": timedelta(seconds=30)})
+@dag(default_args={"retries": 3, "retry_delay": timedelta(seconds=30)}, 
tags=["example"])
 def example_agent_durable_decorator():
     @task.agent(
         llm_conn_id="pydanticai_default",
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_document_loader.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_document_loader.py
index aa80c77d32d..9fa63c12323 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_document_loader.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_document_loader.py
@@ -27,7 +27,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_operator_document_loader_basic]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_document_loader_basic():
     """Parse a single local file -- the operator infers the format from the 
suffix."""
 
@@ -49,7 +49,7 @@ example_document_loader_basic()
 
 
 # [START howto_operator_document_loader_directory]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_document_loader_directory():
     """Walk a directory recursively, only picking up PDFs and Markdown."""
 
@@ -77,7 +77,7 @@ example_document_loader_directory()
 
 
 # [START howto_operator_document_loader_bytes]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_document_loader_bytes():
     """Feed raw bytes from an upstream hook (e.g. an S3 download) into the 
parser."""
 
@@ -103,7 +103,7 @@ example_document_loader_bytes()
 
 
 # [START howto_operator_document_loader_json_field]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_document_loader_json_field():
     """Read an array of records, embedding only the ``body`` field per item.
 
@@ -126,7 +126,7 @@ example_document_loader_json_field()
 
 
 # [START howto_operator_document_loader_cloud_uri]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_document_loader_cloud_uri():
     """Read PDFs directly from S3 -- no separate download step."""
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_hook.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_hook.py
index 49d6b790b87..60ed33263ef 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_hook.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_hook.py
@@ -29,7 +29,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_hook_langchain_chat]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_langchain_chat():
     @task
     def summarize(text: str) -> str:
@@ -51,7 +51,7 @@ example_langchain_chat()
 
 
 # [START howto_hook_langchain_embedding]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_langchain_embedding():
     @task
     def embed_documents(texts: list[str]) -> int:
@@ -77,7 +77,7 @@ example_langchain_embedding()
 
 
 # [START howto_hook_langchain_chat_and_embedding]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_langchain_chat_and_embedding():
     """One hook instance serves both chat and embeddings when both models are 
set."""
 
@@ -104,7 +104,7 @@ example_langchain_chat_and_embedding()
 
 
 # [START howto_hook_langchain_different_conns]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_langchain_different_conns():
     """Use separate connections when chat and embeddings live on different API 
keys."""
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_tool_agent.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_tool_agent.py
index be84c5f78a0..47acdff3f95 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_tool_agent.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_langchain_tool_agent.py
@@ -382,7 +382,7 @@ def _build_tools(hook, index_dir: str, survey_csv_path: 
str) -> list:
 
 
 # [START example_langchain_tool_agent]
-@dag
+@dag(tags=["example"])
 def example_langchain_tool_agent():
     """
     Research agent with LangChain tools and human review.
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_hook.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_hook.py
index f089ee02b4d..31898a330be 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_hook.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_hook.py
@@ -29,7 +29,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_hook_llamaindex_embed]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_llamaindex_embed():
     """Chunk + embed a directory of documents and persist the index locally."""
 
@@ -58,7 +58,7 @@ example_llamaindex_embed()
 
 
 # [START howto_hook_llamaindex_retrieve]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_llamaindex_retrieve():
     """Load a persisted index and run similarity search."""
 
@@ -80,7 +80,7 @@ example_llamaindex_retrieve()
 
 
 # [START howto_hook_llamaindex_cloud_persist]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_llamaindex_cloud_persist():
     """Persist the index directly to S3 -- no separate upload step."""
 
@@ -109,7 +109,7 @@ example_llamaindex_cloud_persist()
 
 
 # [START howto_hook_llamaindex_byo_embed_model]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_llamaindex_byo_embed_model():
     """Use a non-OpenAI embedding by instantiating the LlamaIndex class 
directly.
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_rag.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_rag.py
index 6c044b965f4..1f7c0238457 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_rag.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llamaindex_rag.py
@@ -43,7 +43,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_llamaindex_rag_pipeline]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_llamaindex_rag_pipeline():
     """End-to-end RAG pipeline in a single DAG.
 
@@ -109,7 +109,7 @@ example_llamaindex_rag_pipeline()
 
 
 # [START howto_llamaindex_index_dag]
-@dag(schedule="@weekly")
+@dag(schedule="@weekly", tags=["example"])
 def example_llamaindex_index_pdf():
     """Weekly indexing DAG -- keep the vector index fresh as PDFs arrive.
 
@@ -142,6 +142,7 @@ example_llamaindex_index_pdf()
 @dag(
     schedule=None,
     params={"question": "Summarize the key findings from the latest quarterly 
report."},
+    tags=["example"],
 )
 def example_llamaindex_query():
     """On-demand query DAG -- retrieve from a pre-built index and synthesize.
@@ -193,7 +194,7 @@ example_llamaindex_query()
 
 
 # [START howto_llamaindex_multi_source]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_llamaindex_multi_source():
     """Combine multiple loaders with source-tagging metadata.
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
index 3ff8509285e..860cb7f7f57 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
@@ -28,7 +28,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_operator_llm_basic]
-@dag
+@dag(tags=["example"])
 def example_llm_operator():
     LLMOperator(
         task_id="summarize",
@@ -44,7 +44,7 @@ example_llm_operator()
 
 
 # [START howto_operator_llm_structured]
-@dag
+@dag(tags=["example"])
 def example_llm_operator_structured():
     class Entities(BaseModel):
         names: list[str]
@@ -65,7 +65,7 @@ example_llm_operator_structured()
 
 
 # [START howto_operator_llm_agent_params]
-@dag
+@dag(tags=["example"])
 def example_llm_operator_agent_params():
     LLMOperator(
         task_id="creative_writing",
@@ -82,7 +82,7 @@ example_llm_operator_agent_params()
 
 
 # [START howto_decorator_llm]
-@dag
+@dag(tags=["example"])
 def example_llm_decorator():
     @task.llm(llm_conn_id="pydanticai_default", system_prompt="Summarize 
concisely.")
     def summarize(text: str):
@@ -97,7 +97,7 @@ example_llm_decorator()
 
 
 # [START howto_decorator_llm_structured]
-@dag
+@dag(tags=["example"])
 def example_llm_decorator_structured():
     class Entities(BaseModel):
         names: list[str]
@@ -120,7 +120,7 @@ example_llm_decorator_structured()
 
 
 # [START howto_operator_llm_usage_limits]
-@dag
+@dag(tags=["example"])
 def example_llm_operator_usage_limits():
     LLMOperator(
         task_id="capped_summary",
@@ -144,7 +144,7 @@ example_llm_operator_usage_limits()
 
 
 # [START howto_operator_llm_approval]
-@dag
+@dag(tags=["example"])
 def example_llm_operator_approval():
 
     LLMOperator(
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_analysis_pipeline.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_analysis_pipeline.py
index c391beee1c6..ac1a6e4d8ec 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_analysis_pipeline.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_analysis_pipeline.py
@@ -24,7 +24,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_decorator_llm_pipeline]
-@dag
+@dag(tags=["example"])
 def example_llm_analysis_pipeline():
     class TicketAnalysis(BaseModel):
         priority: str
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_branch.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_branch.py
index 086b866ca18..9dceb685071 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_branch.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_branch.py
@@ -23,7 +23,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_operator_llm_branch_basic]
-@dag
+@dag(tags=["example"])
 def example_llm_branch_operator():
     route = LLMBranchOperator(
         task_id="route_ticket",
@@ -53,7 +53,7 @@ example_llm_branch_operator()
 
 
 # [START howto_operator_llm_branch_multi]
-@dag
+@dag(tags=["example"])
 def example_llm_branch_multi():
     route = LLMBranchOperator(
         task_id="classify",
@@ -84,7 +84,7 @@ example_llm_branch_multi()
 
 
 # [START howto_decorator_llm_branch]
-@dag
+@dag(tags=["example"])
 def example_llm_branch_decorator():
     @task.llm_branch(
         llm_conn_id="pydanticai_default",
@@ -118,7 +118,7 @@ example_llm_branch_decorator()
 
 
 # [START howto_decorator_llm_branch_multi]
-@dag
+@dag(tags=["example"])
 def example_llm_branch_decorator_multi():
     @task.llm_branch(
         llm_conn_id="pydanticai_default",
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_classification.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_classification.py
index cda2a3aef64..ce6ca6257a6 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_classification.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_classification.py
@@ -24,7 +24,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_decorator_llm_classification]
-@dag
+@dag(tags=["example"])
 def example_llm_classification():
     @task.llm(
         llm_conn_id="pydanticai_default",
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_file_analysis.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_file_analysis.py
index 7fe2bb89a64..a9d8d59f4af 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_file_analysis.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_file_analysis.py
@@ -25,7 +25,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_operator_llm_file_analysis_basic]
-@dag
+@dag(tags=["example"])
 def example_llm_file_analysis_basic():
     LLMFileAnalysisOperator(
         task_id="analyze_error_logs",
@@ -42,7 +42,7 @@ example_llm_file_analysis_basic()
 
 
 # [START howto_operator_llm_file_analysis_prefix]
-@dag
+@dag(tags=["example"])
 def example_llm_file_analysis_prefix():
     LLMFileAnalysisOperator(
         task_id="summarize_partitioned_logs",
@@ -65,7 +65,7 @@ example_llm_file_analysis_prefix()
 
 
 # [START howto_operator_llm_file_analysis_multimodal]
-@dag
+@dag(tags=["example"])
 def example_llm_file_analysis_multimodal():
     LLMFileAnalysisOperator(
         task_id="validate_dashboards",
@@ -83,7 +83,7 @@ example_llm_file_analysis_multimodal()
 
 
 # [START howto_operator_llm_file_analysis_structured]
-@dag
+@dag(tags=["example"])
 def example_llm_file_analysis_structured():
 
     class FileAnalysisSummary(BaseModel):
@@ -114,7 +114,7 @@ example_llm_file_analysis_structured()
 
 
 # [START howto_decorator_llm_file_analysis]
-@dag
+@dag(tags=["example"])
 def example_llm_file_analysis_decorator():
     @task.llm_file_analysis(
         llm_conn_id="pydanticai_default",
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
index 88d3891ead1..e884b74a5f8 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
@@ -24,7 +24,7 @@ from airflow.providers.common.sql.config import 
DataSourceConfig
 
 
 # [START howto_operator_llm_schema_compare_basic]
-@dag
+@dag(tags=["example"])
 def example_llm_schema_compare_basic():
     LLMSchemaCompareOperator(
         task_id="detect_schema_drift",
@@ -41,7 +41,7 @@ example_llm_schema_compare_basic()
 
 
 # [START howto_operator_llm_schema_compare_full]
-@dag
+@dag(tags=["example"])
 def example_llm_schema_compare_full_context():
     LLMSchemaCompareOperator(
         task_id="detect_schema_drift",
@@ -62,7 +62,7 @@ example_llm_schema_compare_full_context()
 
 
 # [START howto_operator_llm_schema_compare_datasource]
-@dag
+@dag(tags=["example"])
 def example_llm_schema_compare_with_object_storage():
     s3_source = DataSourceConfig(
         conn_id="aws_default",
@@ -87,7 +87,7 @@ example_llm_schema_compare_with_object_storage()
 
 
 # [START howto_decorator_llm_schema_compare]
-@dag
+@dag(tags=["example"])
 def example_llm_schema_compare_decorator():
     @task.llm_schema_compare(
         llm_conn_id="pydanticai_default",
@@ -106,7 +106,7 @@ example_llm_schema_compare_decorator()
 
 
 # [START howto_operator_llm_schema_compare_conditional]
-@dag
+@dag(tags=["example"])
 def example_llm_schema_compare_conditional():
     @task.llm_schema_compare(
         llm_conn_id="pydanticai_default",
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_sql.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_sql.py
index ed710ec6ed8..4f5261fe7a7 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_sql.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_sql.py
@@ -24,7 +24,7 @@ from airflow.providers.common.sql.config import 
DataSourceConfig
 
 
 # [START howto_operator_llm_sql_basic]
-@dag
+@dag(tags=["example"])
 def example_llm_sql_basic():
     LLMSQLQueryOperator(
         task_id="generate_sql",
@@ -45,7 +45,7 @@ example_llm_sql_basic()
 
 
 # [START howto_operator_llm_sql_schema]
-@dag
+@dag(tags=["example"])
 def example_llm_sql_schema_introspection():
     LLMSQLQueryOperator(
         task_id="generate_sql",
@@ -63,7 +63,7 @@ example_llm_sql_schema_introspection()
 
 
 # [START howto_decorator_llm_sql]
-@dag
+@dag(tags=["example"])
 def example_llm_sql_decorator():
     @task.llm_sql(
         llm_conn_id="pydanticai_default",
@@ -81,7 +81,7 @@ example_llm_sql_decorator()
 
 
 # [START howto_operator_llm_sql_expand]
-@dag
+@dag(tags=["example"])
 def example_llm_sql_expand():
     LLMSQLQueryOperator.partial(
         task_id="generate_sql",
@@ -104,7 +104,7 @@ example_llm_sql_expand()
 
 
 # [START howto_operator_llm_sql_with_object_storage]
-@dag
+@dag(tags=["example"])
 def example_llm_sql_with_object_storage():
     datasource_config = DataSourceConfig(
         conn_id="aws_default",
@@ -127,7 +127,7 @@ example_llm_sql_with_object_storage()
 
 
 # [START howto_operator_llm_sql_approval]
-@dag
+@dag(tags=["example"])
 def example_llm_sql_approval():
     from datetime import timedelta
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_agentic.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_agentic.py
index 88f2113eada..9ac48bceb2c 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_agentic.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_agentic.py
@@ -136,7 +136,7 @@ Focus on patterns and proportions rather than raw counts."""
 
 
 # [START example_llm_survey_agentic]
-@dag
+@dag(tags=["example"])
 def example_llm_survey_agentic():
     """
     Fan-out across four survey dimensions, then synthesize into a single 
narrative.
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_analysis.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_analysis.py
index c4c2af93aef..df28bb04516 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_analysis.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_survey_analysis.py
@@ -145,7 +145,7 @@ reference_datasource = DataSourceConfig(
 
 
 # [START example_llm_survey_interactive]
-@dag
+@dag(tags=["example"])
 def example_llm_survey_interactive():
     """
     Ask a natural language question about the survey with human review at each 
end.
@@ -237,7 +237,7 @@ example_llm_survey_interactive()
 
 
 # [START example_llm_survey_scheduled]
-@dag(schedule="@monthly", start_date=datetime.datetime(2025, 1, 1), 
catchup=False)
+@dag(schedule="@monthly", start_date=datetime.datetime(2025, 1, 1), 
catchup=False, tags=["example"])
 def example_llm_survey_scheduled():
     """
     Download, validate, query, and report on the survey CSV on a schedule.
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_mcp.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_mcp.py
index 320fe8428ce..80233ea7c75 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_mcp.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_mcp.py
@@ -28,7 +28,7 @@ from airflow.providers.common.compat.sdk import dag
 
 
 # [START howto_toolset_mcp_connection]
-@dag
+@dag(tags=["example"])
 def example_mcp_toolset():
     """Use an MCP server configured via an Airflow connection."""
     AgentOperator(
@@ -53,7 +53,7 @@ example_mcp_toolset()
 
 
 # [START howto_toolset_mcp_multiple]
-@dag
+@dag(tags=["example"])
 def example_mcp_multiple_servers():
     """Combine multiple MCP servers with prefixes to avoid tool name 
collisions."""
     AgentOperator(
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_pydantic_ai_hook.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_pydantic_ai_hook.py
index 16b961438bc..d1790dcaba6 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_pydantic_ai_hook.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_pydantic_ai_hook.py
@@ -25,7 +25,7 @@ from airflow.providers.common.compat.sdk import dag, task
 
 
 # [START howto_hook_pydantic_ai_basic]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_pydantic_ai_hook():
     @task
     def generate_summary(text: str) -> str:
@@ -43,7 +43,7 @@ example_pydantic_ai_hook()
 
 
 # [START howto_hook_pydantic_ai_structured_output]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_pydantic_ai_structured_output():
     @task
     def generate_sql(prompt: str) -> dict:
@@ -68,7 +68,7 @@ example_pydantic_ai_structured_output()
 
 
 # [START howto_task_with_toolsets]
-@dag(schedule=None)
+@dag(schedule=None, tags=["example"])
 def example_task_with_toolsets():
     """Use toolsets directly in a @task function without AgentOperator."""
 
diff --git 
a/providers/common/sql/src/airflow/providers/common/sql/example_dags/example_analytics.py
 
b/providers/common/sql/src/airflow/providers/common/sql/example_dags/example_analytics.py
index 89a08bc1c26..07a23d41e81 100644
--- 
a/providers/common/sql/src/airflow/providers/common/sql/example_dags/example_analytics.py
+++ 
b/providers/common/sql/src/airflow/providers/common/sql/example_dags/example_analytics.py
@@ -61,7 +61,7 @@ with DAG(
     schedule=datetime.timedelta(hours=4),
     start_date=datetime.datetime(2021, 1, 1),
     catchup=False,
-    tags=["analytics", "common-sql"],
+    tags=["example", "analytics", "common-sql"],
 ) as dag:
     # [START howto_analytics_operator_with_s3]
     analytics_with_s3 = AnalyticsOperator(
diff --git 
a/providers/edge3/src/airflow/providers/edge3/example_dags/win_notepad.py 
b/providers/edge3/src/airflow/providers/edge3/example_dags/win_notepad.py
index ee85dded3c4..4f2f0005de4 100644
--- a/providers/edge3/src/airflow/providers/edge3/example_dags/win_notepad.py
+++ b/providers/edge3/src/airflow/providers/edge3/example_dags/win_notepad.py
@@ -65,7 +65,7 @@ with DAG(
     doc_md=__doc__,
     schedule=None,
     start_date=datetime(2024, 7, 1),
-    tags=["edge", "Windows"],
+    tags=["example", "edge", "Windows"],
     default_args={"queue": "windows"},
     params={
         "notepad_text": Param(
diff --git 
a/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py 
b/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py
index 0f021bfebc0..df59265de50 100644
--- a/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py
+++ b/providers/edge3/src/airflow/providers/edge3/example_dags/win_test.py
@@ -252,7 +252,7 @@ with DAG(
     doc_md=__doc__,
     schedule=None,
     start_date=datetime(2025, 1, 1),
-    tags=["edge", "Windows"],
+    tags=["example", "edge", "Windows"],
     default_args={"queue": "windows"},
     params={
         "mapping_count": Param(
diff --git 
a/providers/oracle/src/airflow/providers/oracle/example_dags/example_oracle.py 
b/providers/oracle/src/airflow/providers/oracle/example_dags/example_oracle.py
index 5501831e946..6eab1f4de1b 100644
--- 
a/providers/oracle/src/airflow/providers/oracle/example_dags/example_oracle.py
+++ 
b/providers/oracle/src/airflow/providers/oracle/example_dags/example_oracle.py
@@ -27,6 +27,7 @@ with DAG(
     catchup=False,
     start_date=datetime(2023, 1, 1),
     dag_id="example_oracle",
+    tags=["example"],
 ) as dag:
     # [START howto_oracle_stored_procedure_operator_with_list_inout]
 
diff --git 
a/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py
 
b/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py
index 1cd8d5b61ee..edf694d061f 100644
--- 
a/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py
+++ 
b/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py
@@ -26,7 +26,7 @@ from airflow.sdk import chain, dag, task
 from airflow.sdk.exceptions import AirflowSkipException
 
 
-@dag(schedule=None, start_date=pendulum.datetime(2023, 1, 1, tz="UTC"), 
catchup=False)
+@dag(schedule=None, start_date=pendulum.datetime(2023, 1, 1, tz="UTC"), 
catchup=False, tags=["example"])
 def example_bash_decorator():
     """
     ### Bash TaskFlow Decorator Example
diff --git 
a/providers/standard/src/airflow/providers/standard/example_dags/example_external_task_marker_dag.py
 
b/providers/standard/src/airflow/providers/standard/example_dags/example_external_task_marker_dag.py
index b8fad182549..50ab3e806cf 100644
--- 
a/providers/standard/src/airflow/providers/standard/example_dags/example_external_task_marker_dag.py
+++ 
b/providers/standard/src/airflow/providers/standard/example_dags/example_external_task_marker_dag.py
@@ -53,7 +53,7 @@ with DAG(
     start_date=start_date,
     catchup=False,
     schedule=None,
-    tags=["example2"],
+    tags=["example", "example2"],
 ) as parent_dag:
     # [START howto_operator_external_task_marker]
     parent_task = ExternalTaskMarker(
@@ -68,7 +68,7 @@ with DAG(
     start_date=start_date,
     schedule=None,
     catchup=False,
-    tags=["example2"],
+    tags=["example", "example2"],
 ) as child_dag:
     # [START howto_operator_external_task_sensor]
     child_task1 = ExternalTaskSensor(
diff --git 
a/providers/standard/src/airflow/providers/standard/example_dags/example_latest_only.py
 
b/providers/standard/src/airflow/providers/standard/example_dags/example_latest_only.py
index 4484c9f7111..7868bc11a1d 100644
--- 
a/providers/standard/src/airflow/providers/standard/example_dags/example_latest_only.py
+++ 
b/providers/standard/src/airflow/providers/standard/example_dags/example_latest_only.py
@@ -41,7 +41,7 @@ with DAG(
     schedule=datetime.timedelta(hours=4),
     start_date=datetime.datetime(2021, 1, 1),
     catchup=False,
-    tags=["example2", "example3"],
+    tags=["example", "example2", "example3"],
     doc_md=__doc__,
 ) as dag:
     # [START howto_operator_latest_only]
diff --git a/scripts/ci/prek/check_example_dag_tags.py 
b/scripts/ci/prek/check_example_dag_tags.py
new file mode 100755
index 00000000000..484348dc96a
--- /dev/null
+++ b/scripts/ci/prek/check_example_dag_tags.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# /// script
+# requires-python = ">=3.10,<3.11"
+# dependencies = [
+#   "rich>=13.6.0",
+# ]
+# ///
+"""Check that every example DAG carries the ``example`` tag.
+
+Example DAGs are filtered and grouped by tag across the docs and the UI, so a
+consistent ``example`` tag must be present on every DAG defined under an
+``example_dags`` folder. This is enforced statically: the ``tags`` argument
+must be an inline list literal that contains the string ``"example"``.
+"""
+
+from __future__ import annotations
+
+import ast
+import sys
+from pathlib import Path
+
+from rich.console import Console
+
+console = Console(color_system="standard", width=200)
+
+REQUIRED_TAG = "example"
+
+
+def _resolves_to_dag(node: ast.AST) -> bool:
+    """Return ``True`` when ``node`` references the ``DAG`` class or the 
``dag`` decorator.
+
+    The ``dag`` decorator is always imported and used as a bare name 
(``@dag``),
+    so it is only matched as an ``ast.Name``. ``DAG`` is additionally matched 
as
+    an attribute (``models.DAG``). This keeps an unrelated 
``something.dag(...)``
+    method call from being mistaken for a DAG definition.
+    """
+    if isinstance(node, ast.Name):
+        return node.id in ("DAG", "dag")
+    if isinstance(node, ast.Attribute):
+        return node.attr == "DAG"
+    return False
+
+
+def _tags_keyword(call: ast.Call) -> ast.keyword | None:
+    for keyword in call.keywords:
+        if keyword.arg == "tags":
+            return keyword
+    return None
+
+
+def _dag_call_error(location: str, call: ast.Call) -> str | None:
+    """Return an error message when a ``DAG(...)`` / ``@dag(...)`` call lacks 
the tag."""
+    tags_keyword = _tags_keyword(call)
+    if tags_keyword is None:
+        return (
+            f"[red]{location}: example DAG is missing the '{REQUIRED_TAG}' 
tag.[/]\n"
+            f'Add [yellow]tags=["{REQUIRED_TAG}"][/] to the DAG definition.\n'
+        )
+    value = tags_keyword.value
+    if not isinstance(value, (ast.List, ast.Tuple)):
+        return (
+            f"[red]{location}: 'tags' must be an inline list literal so the "
+            f"'{REQUIRED_TAG}' tag can be verified statically.[/]\n"
+        )
+    literal_tags = [element.value for element in value.elts if 
isinstance(element, ast.Constant)]
+    if REQUIRED_TAG not in literal_tags:
+        return (
+            f"[red]{location}: example DAG is missing the '{REQUIRED_TAG}' 
tag.[/]\n"
+            f"Add [yellow]\"{REQUIRED_TAG}\"[/] to the existing 'tags' list.\n"
+        )
+    return None
+
+
+def check_file(file: Path) -> list[str]:
+    """Return the list of tagging errors found in a single example DAG file."""
+    try:
+        tree = ast.parse(file.read_text(), filename=str(file))
+    except SyntaxError as exc:
+        return [f"[red]{file}: could not be parsed: {exc}[/]\n"]
+    errors: list[str] = []
+    for node in ast.walk(tree):
+        # ``DAG(...)``, ``with DAG(...) as ...:`` and ``@dag(...)`` are all 
calls.
+        if isinstance(node, ast.Call) and _resolves_to_dag(node.func):
+            error = _dag_call_error(f"{file}:{node.lineno}", node)
+            if error:
+                errors.append(error)
+            continue
+        # A bare ``@dag`` decorator (no parentheses) takes no arguments at all,
+        # so it can never carry the required tag.
+        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+            for decorator in node.decorator_list:
+                # ``@dag(...)`` is an ast.Call already handled by the branch
+                # above; only a bare ``@dag`` needs to be flagged here.
+                if isinstance(decorator, ast.Call) or not 
_resolves_to_dag(decorator):
+                    continue
+                errors.append(
+                    f"[red]{file}:{decorator.lineno}: example DAG is missing 
the "
+                    f"'{REQUIRED_TAG}' tag.[/]\n"
+                    f'Add [yellow]tags=["{REQUIRED_TAG}"][/] to the 
[yellow]@dag[/] decorator.\n'
+                )
+    return errors
+
+
+def main(argv: list[str]) -> int:
+    errors: list[str] = []
+    for argument in argv:
+        errors.extend(check_file(Path(argument)))
+    if not errors:
+        return 0
+    console.print(f"[red]Found example DAGs without the '{REQUIRED_TAG}' 
tag:[/]\n")
+    for error in errors:
+        console.print(error)
+    console.print(
+        f'Every example DAG must be tagged with [yellow]"{REQUIRED_TAG}"[/] '
+        "so it can be filtered consistently.\n"
+    )
+    return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/scripts/tests/ci/prek/test_check_example_dag_tags.py 
b/scripts/tests/ci/prek/test_check_example_dag_tags.py
new file mode 100644
index 00000000000..e356080fc86
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_example_dag_tags.py
@@ -0,0 +1,109 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+from check_example_dag_tags import check_file
+
+
+class TestCheckFilePasses:
+    """A correctly tagged example DAG produces no errors."""
+
+    @pytest.mark.parametrize(
+        "code",
+        [
+            pytest.param(
+                'from airflow import DAG\nDAG("d", tags=["example"])\n',
+                id="dag-call-with-example-tag",
+            ),
+            pytest.param(
+                'from airflow import DAG\nwith DAG("d", tags=["example"]) as 
dag:\n    pass\n',
+                id="dag-context-manager-with-example-tag",
+            ),
+            pytest.param(
+                'from airflow import DAG\nDAG("d", tags=["other", 
"example"])\n',
+                id="example-tag-among-others",
+            ),
+            pytest.param(
+                'from airflow import DAG\nDAG("d", tags=("example",))\n',
+                id="tags-as-tuple-literal",
+            ),
+            pytest.param(
+                'from airflow import models\nmodels.DAG("d", 
tags=["example"])\n',
+                id="dag-as-attribute",
+            ),
+            pytest.param(
+                'from airflow.sdk import dag\n\n\n@dag(tags=["example"])\ndef 
my_dag():\n    pass\n',
+                id="dag-decorator-with-example-tag",
+            ),
+            pytest.param(
+                'from airflow.sdk import dag\n\n\n@dag(tags=("example",))\ndef 
my_dag():\n    pass\n',
+                id="dag-decorator-with-tuple-tags",
+            ),
+            pytest.param(
+                "x = 1\n",
+                id="file-defines-no-dag",
+            ),
+            pytest.param(
+                'obj.dag(tags=["other"])\n',
+                id="unrelated-dag-method-call-ignored",
+            ),
+        ],
+    )
+    def test_no_errors(self, write_python_file, code: str):
+        assert check_file(write_python_file(code)) == []
+
+
+class TestCheckFileFails:
+    """A missing or unverifiable ``example`` tag produces exactly one error."""
+
+    @pytest.mark.parametrize(
+        "code",
+        [
+            pytest.param(
+                'from airflow import DAG\nDAG("d")\n',
+                id="dag-call-without-tags",
+            ),
+            pytest.param(
+                'from airflow import DAG\nDAG("d", tags=["other"])\n',
+                id="dag-call-with-wrong-tag",
+            ),
+            pytest.param(
+                'from airflow import DAG\nDAG("d", tags=TAGS)\n',
+                id="tags-not-an-inline-literal",
+            ),
+            pytest.param(
+                "from airflow.sdk import dag\n\n\n@dag\ndef my_dag():\n    
pass\n",
+                id="bare-dag-decorator",
+            ),
+            pytest.param(
+                "from airflow.sdk import dag\n\n\n@dag()\ndef my_dag():\n    
pass\n",
+                id="dag-decorator-without-tags",
+            ),
+            pytest.param(
+                'from airflow.sdk import dag\n\n\n@dag(tags=["other"])\ndef 
my_dag():\n    pass\n',
+                id="dag-decorator-with-wrong-tag",
+            ),
+        ],
+    )
+    def test_single_error(self, write_python_file, code: str):
+        assert len(check_file(write_python_file(code))) == 1
+
+    def test_syntax_error_is_reported(self, write_python_file):
+        errors = check_file(write_python_file('from airflow import 
DAG\nDAG("d"\n'))
+        assert len(errors) == 1
+        assert "could not be parsed" in errors[0]

Reply via email to