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

vincbeck 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 84a211b5d22 UI: Show owning team on Dag, Dag Run, and Task Instance 
detail pages (#70312)
84a211b5d22 is described below

commit 84a211b5d22a280fa37a03cd93a9234e4c22dd43
Author: Vincent <[email protected]>
AuthorDate: Tue Aug 11 08:47:01 2026 -0400

    UI: Show owning team on Dag, Dag Run, and Task Instance detail pages 
(#70312)
    
    Multi-team deployments need to see which team owns a Dag directly from the
    detail pages, not just from the list views. This surfaces the owning team on
    the Dag, Dag Run, and Task Instance detail pages as a linked row that 
filters
    the Dag list by that team, mirroring how the Owner field already behaves.
    
    The team row is only shown when multi-team support is enabled and a team is
    resolved for the object, so single-team deployments see no change.
---
 .../src/airflow/api_fastapi/common/db/dag_runs.py  |   6 +-
 .../src/airflow/api_fastapi/common/db/dags.py      |  37 +++---
 .../api_fastapi/common/db/task_instances.py        |   5 +-
 .../api_fastapi/core_api/datamodels/dags.py        |   1 +
 .../core_api/openapi/v2-rest-api-generated.yaml    |   5 +
 .../api_fastapi/core_api/routes/public/dag_run.py  |   8 +-
 .../api_fastapi/core_api/routes/public/dags.py     |   4 +-
 .../core_api/routes/public/task_instances.py       |   7 +-
 .../execution_api/datamodels/taskinstance.py       |   4 +
 .../execution_api/routes/task_instances.py         |   5 +-
 airflow-core/src/airflow/models/dag.py             |  24 ++++
 airflow-core/src/airflow/models/dagbundle.py       |   6 +
 airflow-core/src/airflow/models/dagrun.py          |   8 ++
 airflow-core/src/airflow/models/taskinstance.py    |   1 +
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |  11 ++
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |   1 +
 .../airflow/ui/src/components/TeamName.test.tsx    |  60 ++++++++++
 .../src/airflow/ui/src/components/TeamName.tsx     |  40 +++++++
 .../src/airflow/ui/src/hooks/useShowTeam.ts        |  26 +++++
 .../src/airflow/ui/src/pages/Dag/Details.tsx       |  12 ++
 .../src/airflow/ui/src/pages/Dag/Header.test.tsx   |  37 +++++-
 .../src/airflow/ui/src/pages/Dag/Header.tsx        |  11 ++
 .../src/airflow/ui/src/pages/DagRuns/DagRuns.tsx   |   8 +-
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  |   7 +-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |   8 +-
 .../src/airflow/ui/src/pages/Run/Details.tsx       |  12 ++
 .../src/airflow/ui/src/pages/Run/Header.test.tsx   |  21 ++++
 .../src/airflow/ui/src/pages/Run/Header.tsx        |  11 ++
 .../airflow/ui/src/pages/TaskInstance/Details.tsx  |  12 ++
 .../ui/src/pages/TaskInstance/Header.test.tsx      |  78 +++++++++++++
 .../airflow/ui/src/pages/TaskInstance/Header.tsx   |  11 ++
 .../ui/src/pages/TaskInstances/TaskInstances.tsx   |   8 +-
 .../core_api/routes/public/test_dag_run.py         |  19 +++
 .../core_api/routes/public/test_dags.py            |  36 +++++-
 .../core_api/routes/public/test_task_instances.py  |  45 +++++++
 .../versions/head/test_task_instances.py           |  59 ++++++++++
 airflow-core/tests/unit/models/test_team.py        | 130 +++++++++++++++++++++
 .../src/airflowctl/api/datamodels/generated.py     |   1 +
 .../airflow/providers/openlineage/utils/utils.py   |   7 +-
 scripts/ci/prek/check_ti_vs_tis_attributes.py      |   2 +
 40 files changed, 736 insertions(+), 58 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py 
b/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py
index 1508ea8528e..09779b17dd0 100644
--- a/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py
+++ b/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py
@@ -26,6 +26,7 @@ from sqlalchemy import func, select, tuple_, union_all
 from sqlalchemy.orm import joinedload
 from sqlalchemy.orm.interfaces import LoaderOption
 
+from airflow.api_fastapi.common.db.dags import eager_load_teams
 from airflow.models.dag import DagModel
 from airflow.models.dag_version import DagVersion
 from airflow.models.dagrun import DagRun
@@ -56,14 +57,15 @@ def eager_load_dag_run_for_list() -> tuple[LoaderOption, 
...]:
     Lightweight eager loading for the DagRun list endpoint.
 
     Only loads the direct relationships needed for serialization (dag_model,
-    dag_run_note, created_dag_version).  The dag_versions property — which
-    requires iterating every TI and TIH — is populated separately by
+    dag_run_note, created_dag_version, and the owning team).  The dag_versions
+    property — which requires iterating every TI and TIH — is populated 
separately by
     :func:`attach_dag_versions_to_runs` using a single DISTINCT query.
     """
     return (
         joinedload(DagRun.dag_model),
         joinedload(DagRun.dag_run_note),
         joinedload(DagRun.created_dag_version).joinedload(DagVersion.bundle),
+        *eager_load_teams(DagRun.dag_model),
     )
 
 
diff --git a/airflow-core/src/airflow/api_fastapi/common/db/dags.py 
b/airflow-core/src/airflow/api_fastapi/common/db/dags.py
index 7113e104666..c0a7c7cbcdf 100644
--- a/airflow-core/src/airflow/api_fastapi/common/db/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/common/db/dags.py
@@ -17,11 +17,10 @@
 
 from __future__ import annotations
 
-from collections.abc import Sequence
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 from sqlalchemy import func, select
-from sqlalchemy.orm import selectinload
+from sqlalchemy.orm import joinedload, selectinload
 
 from airflow.api_fastapi.common.db.common import (
     apply_filters_to_select,
@@ -29,10 +28,11 @@ from airflow.api_fastapi.common.db.common import (
 from airflow.api_fastapi.common.parameters import BaseParam, RangeFilter, 
SortParam
 from airflow.configuration import conf
 from airflow.models import DagModel
+from airflow.models.dagbundle import DagBundleModel
 from airflow.models.dagrun import DagRun
 
 if TYPE_CHECKING:
-    from sqlalchemy.orm import Session
+    from sqlalchemy.orm.strategy_options import _AbstractLoad
     from sqlalchemy.sql import Select
 
 
@@ -98,18 +98,23 @@ def generate_dag_with_latest_run_query(
     return query
 
 
-def attach_team_names(objects: Sequence, *, session: Session) -> None:
+def eager_load_teams(*path: Any) -> tuple[_AbstractLoad, ...]:
     """
-    Attach the owning team name to each object exposing a ``dag_id``.
+    Eager loading options for the team owning a Dag, for serializing 
``team_name``.
 
-    Only performs a database lookup when multi-team mode is enabled; otherwise 
every
-    object keeps its default ``team_name`` of ``None``. The resolved name is 
set as the
-    ``team_name`` attribute on each object so the response serializer can read 
it.
-    """
-    if not objects or not conf.getboolean("core", "multi_team"):
-        return
+    ``DagModel.bundle`` is ``lazy="raise"``, so any endpoint whose response 
exposes
+    ``team_name`` must apply these options. Nothing is loaded when multi-team 
mode is
+    off: :attr:`DagModel.team_name` short-circuits to ``None`` without 
touching the
+    relationship, so single-team deployments pay for no extra join.
 
-    dag_ids = list({obj.dag_id for obj in objects})
-    team_names_by_dag_id = DagModel.get_dag_id_to_team_name_mapping(dag_ids, 
session=session)
-    for obj in objects:
-        obj.team_name = team_names_by_dag_id.get(obj.dag_id)
+    :param path: relationship attributes leading to ``DagModel``, empty when 
selecting it
+        directly. For example, ``eager_load_teams(DagRun.dag_model)`` for a 
Dag run query.
+    """
+    if not conf.getboolean("core", "multi_team"):
+        return ()
+
+    loader: Any = None
+    for attribute in path:
+        loader = joinedload(attribute) if loader is None else 
loader.joinedload(attribute)
+    bundle_loader = joinedload(DagModel.bundle) if loader is None else 
loader.joinedload(DagModel.bundle)
+    return (bundle_loader.selectinload(DagBundleModel.teams),)
diff --git a/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py 
b/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py
index 37ff83d406c..9d93baf2530 100644
--- a/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py
+++ b/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 from sqlalchemy import Select
 from sqlalchemy.orm import contains_eager, joinedload
 
+from airflow.api_fastapi.common.db.dags import eager_load_teams
 from airflow.models import Base
 from airflow.models.dag_version import DagVersion
 from airflow.models.dagrun import DagRun
@@ -48,7 +49,9 @@ def eager_load_TI_and_TIH_for_validation(
 
     query = query.join(orm_model.dag_run).outerjoin(orm_model.dag_version)
     query = query.options(
-        
contains_eager(orm_model.dag_run).options(joinedload(DagRun.dag_model)),
+        contains_eager(orm_model.dag_run).options(
+            joinedload(DagRun.dag_model).options(*eager_load_teams()),
+        ),
         
contains_eager(orm_model.dag_version).options(joinedload(DagVersion.bundle)),
     )
     if orm_model is TaskInstance:
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
index 2151a8599cc..e67777d6ffa 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
@@ -207,6 +207,7 @@ class DAGDetailsResponse(DAGResponse):
     owner_links: dict[str, str] | None = None
     is_favorite: bool = False
     active_runs_count: int = 0
+    team_name: str | None = None
 
     @field_validator("timezone", mode="before")
     @classmethod
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 92edb82d7f9..81b0a710c9e 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -13105,6 +13105,11 @@ components:
           type: integer
           title: Active Runs Count
           default: 0
+        team_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Team Name
         is_backfillable:
           type: boolean
           title: Is Backfillable
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
index e5d06587cb5..f2310f092d5 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
@@ -42,7 +42,7 @@ from airflow.api_fastapi.common.db.dag_runs import (
     attach_dag_versions_to_runs,
     eager_load_dag_run_for_list,
 )
-from airflow.api_fastapi.common.db.dags import attach_team_names
+from airflow.api_fastapi.common.db.dags import eager_load_teams
 from airflow.api_fastapi.common.parameters import (
     FilterOptionEnum,
     FilterParam,
@@ -134,7 +134,9 @@ dag_run_at_dag_router = AirflowRouter(tags=["DagRun"], 
prefix="/dags/{dag_id}")
 )
 def get_dag_run(dag_id: str, dag_run_id: str, session: SessionDep) -> 
DAGRunResponse:
     dag_run = session.scalar(
-        select(DagRun).filter_by(dag_id=dag_id, 
run_id=dag_run_id).options(joinedload(DagRun.dag_model))
+        select(DagRun)
+        .filter_by(dag_id=dag_id, run_id=dag_run_id)
+        .options(joinedload(DagRun.dag_model), 
*eager_load_teams(DagRun.dag_model))
     )
     if dag_run is None:
         raise HTTPException(
@@ -693,7 +695,6 @@ def get_dag_runs(
             has_next = has_more
 
         attach_dag_versions_to_runs(dag_runs, session=session)
-        attach_team_names(dag_runs, session=session)
 
         return DAGRunCollectionResponse(
             dag_runs=dag_runs,
@@ -713,7 +714,6 @@ def get_dag_runs(
     )
     dag_runs = list(session.scalars(dag_run_select))
     attach_dag_versions_to_runs(dag_runs, session=session)
-    attach_team_names(dag_runs, session=session)
 
     return DAGRunCollectionResponse(
         dag_runs=dag_runs,
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
index 69b6b2932f0..112e6c98391 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
@@ -27,7 +27,7 @@ from sqlalchemy import delete, func, insert, select, update
 from airflow.api.common import delete_dag as delete_dag_module
 from airflow.api_fastapi.common.dagbag import DagBagDep, 
get_latest_version_of_dag
 from airflow.api_fastapi.common.db.common import SessionDep, 
apply_filters_to_select, paginated_select
-from airflow.api_fastapi.common.db.dags import 
generate_dag_with_latest_run_query
+from airflow.api_fastapi.common.db.dags import eager_load_teams, 
generate_dag_with_latest_run_query
 from airflow.api_fastapi.common.parameters import (
     FilterOptionEnum,
     FilterParam,
@@ -228,7 +228,7 @@ def get_dag_details(
     """Get details of Dag."""
     dag = get_latest_version_of_dag(dag_bag, dag_id, session)
 
-    dag_model = session.get(DagModel, dag_id)
+    dag_model = session.scalar(select(DagModel).where(DagModel.dag_id == 
dag_id).options(*eager_load_teams()))
     if not dag_model:
         raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unable to obtain dag 
with id {dag_id} from session")
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
index 214179cdca9..b5558746ff0 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
@@ -41,7 +41,7 @@ from airflow.api_fastapi.common.dagbag import (
     resolve_run_on_latest_version,
 )
 from airflow.api_fastapi.common.db.common import SessionDep, 
apply_filters_to_select, paginated_select
-from airflow.api_fastapi.common.db.dags import attach_team_names
+from airflow.api_fastapi.common.db.dags import eager_load_teams
 from airflow.api_fastapi.common.db.task_instances import 
eager_load_TI_and_TIH_for_validation
 from airflow.api_fastapi.common.parameters import (
     FilterOptionEnum,
@@ -144,6 +144,7 @@ def get_task_instance(
         .options(joinedload(TI.rendered_task_instance_fields))
         .options(joinedload(TI.dag_version))
         .options(joinedload(TI.dag_run).options(joinedload(DagRun.dag_model)))
+        .options(*eager_load_teams(TI.dag_run, DagRun.dag_model))
     )
     task_instance = session.scalar(query)
 
@@ -434,6 +435,7 @@ def get_mapped_task_instance(
         .options(joinedload(TI.rendered_task_instance_fields))
         .options(joinedload(TI.dag_version))
         .options(joinedload(TI.dag_run).options(joinedload(DagRun.dag_model)))
+        .options(*eager_load_teams(TI.dag_run, DagRun.dag_model))
     )
     task_instance = session.scalar(query)
 
@@ -640,8 +642,6 @@ def get_task_instances(
             has_prev = bool(cursor)
             has_next = has_more
 
-        attach_team_names(task_instances, session=session)
-
         return TaskInstanceCollectionResponse(
             task_instances=task_instances,
             next_cursor=(
@@ -663,7 +663,6 @@ def get_task_instances(
         session=session,
     )
     task_instances = list(session.scalars(task_instance_select))
-    attach_team_names(task_instances, session=session)
     return TaskInstanceCollectionResponse(
         task_instances=task_instances,
         total_entries=total_entries,
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
index ad051b3e6d3..0064bdf1e73 100644
--- 
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
+++ 
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
@@ -390,6 +390,10 @@ class DagRun(StrictBaseModel):
             else:
                 values["note"] = None
 
+        # A property rather than a column, so the loop above never picks it up.
+        if not insp.detached:
+            values["team_name"] = data.team_name
+
         return values
 
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py
index 41ecf49b053..8f79c89808b 100644
--- 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py
+++ 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py
@@ -46,6 +46,7 @@ from airflow._shared.timezones import timezone
 from airflow.api_fastapi.auth.tokens import JWTGenerator
 from airflow.api_fastapi.common.dagbag import DagBagDep, 
get_latest_version_of_dag
 from airflow.api_fastapi.common.db.common import SessionDep
+from airflow.api_fastapi.common.db.dags import eager_load_teams
 from airflow.api_fastapi.common.types import UtcDateTime
 from airflow.api_fastapi.compat import HTTP_422_UNPROCESSABLE_CONTENT
 from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
@@ -262,7 +263,7 @@ def ti_run(
             session.scalars(
                 select(DR)
                 .filter_by(dag_id=ti.dag_id, run_id=ti.run_id)
-                .options(joinedload(DR.consumed_asset_events))
+                .options(joinedload(DR.consumed_asset_events), 
*eager_load_teams(DR.dag_model))
             )
             .unique()
             .one_or_none()
@@ -297,8 +298,6 @@ def ti_run(
             or 0
         )
 
-        dr.team_name = get_team_name_for_ti(task_instance_id, session)
-
         context = TIRunContext(
             dag_run=dr,
             task_reschedule_count=task_reschedule_count,
diff --git a/airflow-core/src/airflow/models/dag.py 
b/airflow-core/src/airflow/models/dag.py
index d6dfab06a27..4c8d4d3e680 100644
--- a/airflow-core/src/airflow/models/dag.py
+++ b/airflow-core/src/airflow/models/dag.py
@@ -38,6 +38,7 @@ from sqlalchemy import (
     Text,
     case,
     func,
+    inspect as sa_inspect,
     or_,
     select,
 )
@@ -79,6 +80,7 @@ if TYPE_CHECKING:
     from typing import TypeAlias
 
     from dateutil.relativedelta import relativedelta
+    from sqlalchemy.orm.state import InstanceState
 
     from airflow.sdk import Context
     from airflow.serialization.definitions.assets import (
@@ -440,6 +442,28 @@ class DagModel(Base):
     dag_versions = relationship(
         "DagVersion", back_populates="dag_model", cascade="all, delete, 
delete-orphan"
     )
+    # Path from a Dag to its owning team, used by ``team_name`` below. 
``lazy="raise"`` keeps the
+    # traversal opt-in so a caller that forgets eager_load_teams() cannot emit 
a silent N+1.
+    bundle = relationship("DagBundleModel", viewonly=True, lazy="raise")
+
+    @property
+    def team_name(self) -> str | None:
+        """Name of the team owning this Dag, or ``None`` when it is not 
team-owned."""
+        if not airflow_conf.getboolean("core", "multi_team"):
+            return None
+
+        state: InstanceState = sa_inspect(self)
+        if "bundle" in state.unloaded:
+            # Serialization paths that fetch a Dag by primary key cannot apply 
loader options
+            # (e.g. Deadline.handle_miss, asset materialization), so fall back 
to the cached
+            # resolver rather than tripping ``lazy="raise"``. Reuse this 
instance's own session:
+            # ``get_team_name`` is ``@provide_session``, and the session it 
would otherwise open
+            # is the *same* scoped session the caller holds, so closing it on 
exit would detach
+            # every object still in use.
+            if state.session is not None:
+                return DagModel.get_team_name(self.dag_id, 
session=state.session)
+            return DagModel.get_team_name(self.dag_id)
+        return self.bundle.team_name if self.bundle else None
 
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
diff --git a/airflow-core/src/airflow/models/dagbundle.py 
b/airflow-core/src/airflow/models/dagbundle.py
index f18c548a019..a881d3f9a4a 100644
--- a/airflow-core/src/airflow/models/dagbundle.py
+++ b/airflow-core/src/airflow/models/dagbundle.py
@@ -59,6 +59,12 @@ class DagBundleModel(Base, LoggingMixin):
     template_params: Mapped[dict | None] = mapped_column(sa.JSON(), 
nullable=True)
     teams = relationship("Team", secondary=dag_bundle_team_association_table, 
back_populates="dag_bundles")
 
+    @property
+    def team_name(self) -> str | None:
+        """Name of the team owning this bundle, if any."""
+        # unique index on dag_bundle_team.dag_bundle_name -> at most one team
+        return self.teams[0].name if self.teams else None
+
     def __init__(self, *, name: str, version: str | None = None):
         super().__init__()
         self.name = name
diff --git a/airflow-core/src/airflow/models/dagrun.py 
b/airflow-core/src/airflow/models/dagrun.py
index 3da601ef69c..46fbebb0800 100644
--- a/airflow-core/src/airflow/models/dagrun.py
+++ b/airflow-core/src/airflow/models/dagrun.py
@@ -394,6 +394,14 @@ class DagRun(Base, LoggingMixin):
     backfill_max_active_runs = association_proxy("backfill", "max_active_runs")
     max_active_runs = association_proxy("dag_model", "max_active_runs")
 
+    @property
+    def team_name(self) -> str | None:
+        """Name of the team owning this run's Dag, or ``None`` when it is not 
team-owned."""
+        # Gate before touching ``dag_model``: single-team deployments must not 
pay for the load.
+        if not airflow_conf.getboolean("core", "multi_team"):
+            return None
+        return self.dag_model.team_name if self.dag_model else None
+
     note = association_proxy("dag_run_note", "content", creator=_creator_note)
 
     DEFAULT_DAGRUNS_TO_EXAMINE = airflow_conf.getint(
diff --git a/airflow-core/src/airflow/models/taskinstance.py 
b/airflow-core/src/airflow/models/taskinstance.py
index dbcb311afe4..6a91d18223e 100644
--- a/airflow-core/src/airflow/models/taskinstance.py
+++ b/airflow-core/src/airflow/models/taskinstance.py
@@ -695,6 +695,7 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload):
 
     run_after = association_proxy("dag_run", "run_after")
     logical_date = association_proxy("dag_run", "logical_date")
+    team_name = association_proxy("dag_run", "team_name")
     task_instance_note = relationship(
         "TaskInstanceNote",
         back_populates="task_instance",
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index e1f10b5e91e..1a1f29b0951 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -3247,6 +3247,17 @@ export const $DAGDetailsResponse = {
             title: 'Active Runs Count',
             default: 0
         },
+        team_name: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Team Name'
+        },
         is_backfillable: {
             type: 'boolean',
             title: 'Is Backfillable',
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 6d6256f88db..13252e1126f 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -901,6 +901,7 @@ export type DAGDetailsResponse = {
 } | null;
     is_favorite?: boolean;
     active_runs_count?: number;
+    team_name?: string | null;
     /**
      * Whether this Dag's schedule supports backfilling.
      */
diff --git a/airflow-core/src/airflow/ui/src/components/TeamName.test.tsx 
b/airflow-core/src/airflow/ui/src/components/TeamName.test.tsx
new file mode 100644
index 00000000000..de684894ad3
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/TeamName.test.tsx
@@ -0,0 +1,60 @@
+/*!
+ * 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.
+ */
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { TeamName } from "./TeamName";
+
+const mockConfig: Record<string, unknown> = { multi_team: false };
+
+vi.mock("src/queries/useConfig", () => ({
+  useConfig: (key: string) => mockConfig[key],
+}));
+
+describe("TeamName", () => {
+  beforeEach(() => {
+    mockConfig.multi_team = false;
+  });
+
+  it("links to the Dags list filtered on the team when multi-team is enabled", 
() => {
+    mockConfig.multi_team = true;
+    render(<TeamName teamName="team a" />, { wrapper: Wrapper });
+
+    expect(screen.getByRole("link", { name: "team a" 
})).toHaveAttribute("href", "/dags?teams=team%20a");
+  });
+
+  it.each([{ teamName: null }, { teamName: undefined }])(
+    "renders nothing when the team is $teamName",
+    ({ teamName }) => {
+      mockConfig.multi_team = true;
+      render(<TeamName teamName={teamName} />, { wrapper: Wrapper });
+
+      expect(screen.queryByRole("link")).not.toBeInTheDocument();
+    },
+  );
+
+  it("renders nothing when multi-team is disabled", () => {
+    render(<TeamName teamName="team-a" />, { wrapper: Wrapper });
+
+    expect(screen.queryByRole("link")).not.toBeInTheDocument();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/TeamName.tsx 
b/airflow-core/src/airflow/ui/src/components/TeamName.tsx
new file mode 100644
index 00000000000..a60be035ea9
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/TeamName.tsx
@@ -0,0 +1,40 @@
+/*!
+ * 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.
+ */
+import { SearchParamsKeys } from "src/constants/searchParams";
+import { useShowTeam } from "src/hooks/useShowTeam";
+
+import { RouterLink } from "./ui";
+
+type Props = {
+  readonly teamName?: string | null;
+};
+
+export const TeamName = ({ teamName }: Props) => {
+  const showTeam = useShowTeam(teamName);
+
+  if (!showTeam) {
+    return undefined;
+  }
+
+  return (
+    <RouterLink 
to={`/dags?${SearchParamsKeys.TEAMS}=${encodeURIComponent(teamName as 
string)}`}>
+      {teamName}
+    </RouterLink>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/hooks/useShowTeam.ts 
b/airflow-core/src/airflow/ui/src/hooks/useShowTeam.ts
new file mode 100644
index 00000000000..3970d4f4012
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/hooks/useShowTeam.ts
@@ -0,0 +1,26 @@
+/*!
+ * 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.
+ */
+import { useConfig } from "src/queries/useConfig";
+
+/**
+ * Whether a team should be surfaced at all: teams only exist in multi-team
+ * deployments, and an entity may not be owned by any team.
+ */
+export const useShowTeam = (teamName?: string | null) =>
+  Boolean(useConfig("multi_team")) && teamName !== undefined && teamName !== 
null;
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx
index 9aed9814726..3d3905e03a7 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx
@@ -23,8 +23,10 @@ import { useParams } from "react-router-dom";
 import { useDagServiceGetDagDetails } from "openapi/queries";
 import { DagVersionDetails } from "src/components/DagVersionDetails";
 import RenderedJsonField from "src/components/RenderedJsonField";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
+import { useShowTeam } from "src/hooks/useShowTeam";
 import { renderDuration } from "src/utils";
 
 export const Details = () => {
@@ -35,6 +37,8 @@ export const Details = () => {
     dagId,
   });
 
+  const showTeam = useShowTeam(dag?.team_name);
+
   return (
     <Box p={2}>
       {dag === undefined ? (
@@ -53,6 +57,14 @@ export const Details = () => {
                 </HStack>
               </Table.Cell>
             </Table.Row>
+            {showTeam ? (
+              <Table.Row data-testid="team-row">
+                <Table.Cell>{translate("dagDetails.team")}</Table.Cell>
+                <Table.Cell>
+                  <TeamName teamName={dag.team_name} />
+                </Table.Cell>
+              </Table.Row>
+            ) : undefined}
             <Table.Row data-testid="description-row">
               <Table.Cell>{translate("dagDetails.description")}</Table.Cell>
               <Table.Cell>{dag.description}</Table.Cell>
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
index 4be93388cca..dc79e170ba2 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
@@ -19,7 +19,7 @@
 import "@testing-library/jest-dom";
 import { render, screen } from "@testing-library/react";
 import type { DAGDetailsResponse } from "openapi-gen/requests/types.gen";
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
 
 import i18n from "src/i18n/config";
 import { MOCK_DAG } from "src/mocks/handlers/dag";
@@ -27,6 +27,12 @@ import { Wrapper } from "src/utils/Wrapper";
 
 import { Header } from "./Header";
 
+const mockConfig: Record<string, unknown> = { multi_team: false };
+
+vi.mock("src/queries/useConfig", () => ({
+  useConfig: (key: string) => mockConfig[key],
+}));
+
 const mockDag = {
   ...MOCK_DAG,
   active_runs_count: 0,
@@ -50,6 +56,10 @@ const mockDag = {
 } as unknown as DAGDetailsResponse;
 
 describe("Header", () => {
+  afterEach(() => {
+    mockConfig.multi_team = false;
+  });
+
   it("shows a deactivated badge and hides stale-only next actions for stale 
dags", () => {
     render(
       <Wrapper>
@@ -71,4 +81,29 @@ describe("Header", () => {
     
expect(screen.getByText(i18n.t("dag:dagDetails.nextRun"))).toBeInTheDocument();
     expect(screen.queryByText("2024-08-22 19:00:00")).not.toBeInTheDocument();
   });
+
+  it("shows the team alongside the owner when multi-team is enabled", () => {
+    mockConfig.multi_team = true;
+    render(
+      <Wrapper>
+        <Header dag={{ ...mockDag, team_name: "team-a" }} />
+      </Wrapper>,
+    );
+
+    
expect(screen.getByText(i18n.t("common:dagDetails.team"))).toBeInTheDocument();
+    expect(screen.getByRole("link", { name: "team-a" 
})).toHaveAttribute("href", "/dags?teams=team-a");
+    
expect(screen.getByText(i18n.t("common:dagDetails.owner"))).toBeInTheDocument();
+  });
+
+  it("shows the owner stat when multi-team is enabled but no team is 
resolved", () => {
+    mockConfig.multi_team = true;
+    render(
+      <Wrapper>
+        <Header dag={{ ...mockDag, team_name: null }} />
+      </Wrapper>,
+    );
+
+    
expect(screen.getByText(i18n.t("common:dagDetails.owner"))).toBeInTheDocument();
+    
expect(screen.queryByText(i18n.t("common:dagDetails.team"))).not.toBeInTheDocument();
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
index 419db668c81..a5b39a8effb 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
@@ -30,8 +30,10 @@ import { DagVersion } from "src/components/DagVersion";
 import DisplayMarkdownButton from "src/components/DisplayMarkdownButton";
 import { HeaderCard } from "src/components/HeaderCard";
 import { NeedsReviewButtonWithModal } from "src/components/NeedsReviewButton";
+import { TeamName } from "src/components/TeamName";
 import { TogglePause } from "src/components/TogglePause";
 import { RouterLink } from "src/components/ui";
+import { useShowTeam } from "src/hooks/useShowTeam";
 
 import { DagOwners } from "../DagsList/DagOwners";
 import { DagTags } from "../DagsList/DagTags";
@@ -58,6 +60,7 @@ export const Header = ({
   const { t: translate } = useTranslation(["common", "dag"]);
   // We would still like to show the dagId even if the dag object hasn't 
loaded yet
   const { dagId } = useParams();
+  const showTeam = useShowTeam(dag?.team_name);
   const isStale = dag?.is_stale;
 
   const nextRunStat = isStale
@@ -116,6 +119,14 @@ export const Header = ({
       label: translate("dagDetails.owner"),
       value: <DagOwners ownerLinks={dag?.owner_links ?? undefined} 
owners={dag?.owners} />,
     },
+    ...(showTeam
+      ? [
+          {
+            label: translate("dagDetails.team"),
+            value: <TeamName teamName={dag?.team_name} />,
+          },
+        ]
+      : []),
     {
       label: translate("dagDetails.tags"),
       value: <DagTags tags={dag?.tags ?? []} />,
diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
index 183306a396c..26a5fd0ae93 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
@@ -42,6 +42,7 @@ import { MarkRunAsButton } from "src/components/MarkAs";
 import RenderedJsonField from "src/components/RenderedJsonField";
 import { RunTypeIcon } from "src/components/RunTypeIcon";
 import { StateBadge } from "src/components/StateBadge";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { TruncatedText } from "src/components/TruncatedText";
 import { RouterLink } from "src/components/ui";
@@ -160,12 +161,7 @@ const runColumns = ({ dagId, multiTeam, open, translate }: 
ColumnProps): Array<C
     ? [
         {
           accessorKey: "team_name",
-          cell: ({ row: { original } }: DagRunRow) =>
-            original.team_name !== undefined && original.team_name !== null ? (
-              <RouterLink 
to={`/dags?teams=${encodeURIComponent(original.team_name)}`}>
-                {original.team_name}
-              </RouterLink>
-            ) : undefined,
+          cell: ({ row: { original } }: DagRunRow) => <TeamName 
teamName={original.team_name} />,
           enableSorting: false,
           header: translate("dagDetails.team"),
         },
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index 188b484fe8c..6f4f9e93220 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next";
 import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
 import DagRunInfo from "src/components/DagRunInfo";
 import { Stat } from "src/components/Stat";
+import { TeamName } from "src/components/TeamName";
 import { RouterLink, Tooltip } from "src/components/ui";
 import { useNearViewport } from "src/hooks/useNearViewport";
 import { useConfig } from "src/queries/useConfig";
@@ -125,11 +126,7 @@ export const DagCard = ({ dag, runStateCounts, 
runStateCountsLoading, stateCount
         {multiTeamEnabled ? (
           <GridItem gridColumn={4} gridRow={1}>
             <Stat label={translate("dagDetails.team")}>
-              {dag.team_name === undefined || dag.team_name === null ? 
undefined : (
-                <RouterLink 
to={`/dags?teams=${encodeURIComponent(dag.team_name)}`}>
-                  {dag.team_name}
-                </RouterLink>
-              )}
+              <TeamName teamName={dag.team_name} />
             </Stat>
           </GridItem>
         ) : undefined}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index 5faaa5321ae..a3a621f41ff 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -32,6 +32,7 @@ import { useTableURLState } from 
"src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { NeedsReviewBadge } from "src/components/NeedsReviewBadge";
 import { SearchBar } from "src/components/SearchBar";
+import { TeamName } from "src/components/TeamName";
 import { TogglePause } from "src/components/TogglePause";
 import { TriggerDAGButton } from "src/components/TriggerDag/TriggerDAGButton";
 import { RouterLink } from "src/components/ui";
@@ -164,10 +165,9 @@ const createColumns = (
     ? [
         {
           accessorKey: "team_name",
-          cell: ({ row: { original } }: { row: { original: 
DAGWithLatestDagRunsResponse } }) =>
-            original.team_name !== undefined && original.team_name !== null ? (
-              <RouterLink 
to={`/dags?teams=${original.team_name}`}>{original.team_name}</RouterLink>
-            ) : undefined,
+          cell: ({ row: { original } }: { row: { original: 
DAGWithLatestDagRunsResponse } }) => (
+            <TeamName teamName={original.team_name} />
+          ),
           enableSorting: false,
           header: () => translate("dagDetails.team"),
         },
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
index e7ec9fa8364..8a793147bbd 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
@@ -25,8 +25,10 @@ import { DagVersionDetails } from 
"src/components/DagVersionDetails";
 import RenderedJsonField from "src/components/RenderedJsonField";
 import { RunTypeIcon } from "src/components/RunTypeIcon";
 import { StateBadge } from "src/components/StateBadge";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
+import { useShowTeam } from "src/hooks/useShowTeam";
 import { getDuration, isStatePending, renderDuration, useAutoRefresh } from 
"src/utils";
 
 export const Details = () => {
@@ -46,6 +48,8 @@ export const Details = () => {
 
   const { data: dagRunStats } = useDagRunServiceGetDagRunStats({ dagId, 
dagRunId: runId });
 
+  const showTeam = useShowTeam(dagRun?.team_name);
+
   if (!dagRun) {
     return undefined;
   }
@@ -82,6 +86,14 @@ export const Details = () => {
             </HStack>
           </Table.Cell>
         </Table.Row>
+        {showTeam ? (
+          <Table.Row>
+            <Table.Cell>{translate("dagDetails.team")}</Table.Cell>
+            <Table.Cell>
+              <TeamName teamName={dagRun.team_name} />
+            </Table.Cell>
+          </Table.Row>
+        ) : undefined}
         <Table.Row>
           <Table.Cell>{translate("duration")}</Table.Cell>
           <Table.Cell>{getDuration(dagRun.start_date, 
dagRun.end_date)}</Table.Cell>
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Header.test.tsx
index a9ba55247b7..881758834f8 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Header.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.test.tsx
@@ -45,6 +45,12 @@ vi.mock("openapi/queries", async (importOriginal) => {
   };
 });
 
+const mockConfig: Record<string, unknown> = { multi_team: false };
+
+vi.mock("src/queries/useConfig", () => ({
+  useConfig: (key: string) => mockConfig[key],
+}));
+
 const { useDeadlinesServiceGetDagDeadlineAlerts } = await 
import("openapi/queries");
 
 const baseDagRun = {
@@ -73,6 +79,7 @@ const baseDagRun = {
 
 describe("Header", () => {
   beforeEach(() => {
+    mockConfig.multi_team = false;
     vi.mocked(useDeadlinesServiceGetDagDeadlineAlerts).mockReturnValue({
       data: undefined,
     } as ReturnType<typeof useDeadlinesServiceGetDagDeadlineAlerts>);
@@ -91,4 +98,18 @@ describe("Header", () => {
 
     
expect(screen.queryByText(i18n.t("dagRun.partitionKey"))).not.toBeInTheDocument();
   });
+
+  it("shows the team stat when multi-team is enabled and a team is resolved", 
() => {
+    mockConfig.multi_team = true;
+    render(<Header dagRun={{ ...baseDagRun, team_name: "team-a" }} />, { 
wrapper: Wrapper });
+
+    
expect(screen.getByText(i18n.t("common:dagDetails.team"))).toBeInTheDocument();
+    expect(screen.getByRole("link", { name: "team-a" 
})).toHaveAttribute("href", "/dags?teams=team-a");
+  });
+
+  it("hides the team stat when multi-team is disabled", () => {
+    render(<Header dagRun={{ ...baseDagRun, team_name: "team-a" }} />, { 
wrapper: Wrapper });
+
+    
expect(screen.queryByText(i18n.t("common:dagDetails.team"))).not.toBeInTheDocument();
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
index c31db0e9ac0..a4505195548 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
@@ -30,9 +30,11 @@ import { MarkRunAsButton } from "src/components/MarkAs";
 import { NeedsReviewButtonWithModal } from "src/components/NeedsReviewButton";
 import NotePreview from "src/components/NotePreview";
 import { RunTypeIcon } from "src/components/RunTypeIcon";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { RouterLink } from "src/components/ui";
 import { SearchParamsKeys } from "src/constants/searchParams";
+import { useShowTeam } from "src/hooks/useShowTeam";
 import DeleteRunButton from "src/pages/DagRuns/DeleteRunButton";
 import { useDagRunNote } from "src/queries/useDagRunNote";
 import { getDuration } from "src/utils";
@@ -42,6 +44,7 @@ import { DeadlineStatus } from "./DeadlineStatus";
 export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => {
   const { t: translate } = useTranslation();
   const { isPending, note, onOpen, onSave, setNote } = useDagRunNote(dagRun);
+  const showTeam = useShowTeam(dagRun.team_name);
 
   const dagId = dagRun.dag_id;
   const dagRunId = dagRun.dag_run_id;
@@ -105,6 +108,14 @@ export const Header = ({ dagRun }: { readonly dagRun: 
DAGRunResponse }) => {
                   ),
                 },
               ]),
+          ...(showTeam
+            ? [
+                {
+                  label: translate("dagDetails.team"),
+                  value: <TeamName teamName={dagRun.team_name} />,
+                },
+              ]
+            : []),
           {
             label: translate("dagRun.dagVersions"),
             value: (
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
index 6b5eccf7f5d..279a4ac860a 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
@@ -29,9 +29,11 @@ import { DagVersionDetails } from 
"src/components/DagVersionDetails";
 import RenderedJsonField from "src/components/RenderedJsonField";
 import { StateBadge } from "src/components/StateBadge";
 import { TaskTrySelect } from "src/components/TaskTrySelect";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
 import { SearchParamsKeys } from "src/constants/searchParams";
+import { useShowTeam } from "src/hooks/useShowTeam";
 import { useAutoRefresh, isStatePending, renderDuration } from "src/utils";
 
 import { BlockingDeps } from "./BlockingDeps";
@@ -59,6 +61,8 @@ export const Details = () => {
     },
   );
 
+  const showTeam = useShowTeam(taskInstance?.team_name);
+
   const onSelectTryNumber = (newTryNumber: number) => {
     if (newTryNumber === taskInstance?.try_number) {
       searchParams.delete(SearchParamsKeys.TRY_NUMBER);
@@ -179,6 +183,14 @@ export const Details = () => {
             <Table.Cell>{translate("mapIndex")}</Table.Cell>
             <Table.Cell>{tryInstance?.map_index}</Table.Cell>
           </Table.Row>
+          {showTeam ? (
+            <Table.Row>
+              <Table.Cell>{translate("dagDetails.team")}</Table.Cell>
+              <Table.Cell>
+                <TeamName teamName={taskInstance?.team_name} />
+              </Table.Cell>
+            </Table.Row>
+          ) : undefined}
           <Table.Row>
             <Table.Cell>{translate("task.operator")}</Table.Cell>
             <Table.Cell>{tryInstance?.operator_name}</Table.Cell>
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.test.tsx
new file mode 100644
index 00000000000..b9b263e7dcd
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.test.tsx
@@ -0,0 +1,78 @@
+/*!
+ * 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.
+ */
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import i18n from "src/i18n/config";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { Header } from "./Header";
+
+// Action buttons and note preview pull in mutation/permission wiring that is
+// unrelated to the team stat under test; stub them out so the test only
+// depends on the stats rendered by Header itself.
+vi.mock("src/components/Clear", () => ({ ClearTaskInstanceButton: () => 
undefined }));
+vi.mock("src/components/Clear/TaskInstance/ClearTaskInstanceDialog", () => ({ 
default: () => undefined }));
+vi.mock("src/components/MarkAs", () => ({ MarkTaskInstanceAsButton: () => 
undefined }));
+vi.mock("src/components/NotePreview", () => ({ default: () => undefined }));
+
+const mockConfig: Record<string, unknown> = { multi_team: false };
+
+vi.mock("src/queries/useConfig", () => ({
+  useConfig: (key: string) => mockConfig[key],
+}));
+
+const baseTaskInstance = {
+  dag_id: "test_dag",
+  dag_run_id: "run_1",
+  dag_version: null,
+  duration: null,
+  end_date: null,
+  map_index: -1,
+  note: null,
+  operator_name: "PythonOperator",
+  rendered_map_index: null,
+  start_date: null,
+  state: "success",
+  task_display_name: "test_task",
+  task_id: "test_task",
+  try_number: 1,
+} satisfies Partial<TaskInstanceResponse> as unknown as TaskInstanceResponse;
+
+describe("Header", () => {
+  beforeEach(() => {
+    mockConfig.multi_team = false;
+  });
+
+  it("shows the team stat when multi-team is enabled and a team is resolved", 
() => {
+    mockConfig.multi_team = true;
+    render(<Header taskInstance={{ ...baseTaskInstance, team_name: "team-a" }} 
/>, { wrapper: Wrapper });
+
+    
expect(screen.getByText(i18n.t("common:dagDetails.team"))).toBeInTheDocument();
+    expect(screen.getByRole("link", { name: "team-a" 
})).toHaveAttribute("href", "/dags?teams=team-a");
+  });
+
+  it("hides the team stat when multi-team is disabled", () => {
+    render(<Header taskInstance={{ ...baseTaskInstance, team_name: "team-a" }} 
/>, { wrapper: Wrapper });
+
+    
expect(screen.queryByText(i18n.t("common:dagDetails.team"))).not.toBeInTheDocument();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
index 48364a9e9a6..624751e6dd0 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
@@ -28,13 +28,16 @@ import { DagVersion } from "src/components/DagVersion";
 import { HeaderCard } from "src/components/HeaderCard";
 import { MarkTaskInstanceAsButton } from "src/components/MarkAs";
 import NotePreview from "src/components/NotePreview";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
+import { useShowTeam } from "src/hooks/useShowTeam";
 import { useTaskInstanceNote } from "src/queries/useTaskInstanceNote";
 import { getDuration, renderDuration } from "src/utils";
 
 export const Header = ({ taskInstance }: { readonly taskInstance: 
TaskInstanceResponse }) => {
   const { t: translate } = useTranslation();
   const { isPending, note, onOpen, onSave, setNote } = 
useTaskInstanceNote(taskInstance);
+  const showTeam = useShowTeam(taskInstance.team_name);
 
   const stats = [
     { label: translate("task.operator"), value: taskInstance.operator_name },
@@ -56,6 +59,14 @@ export const Header = ({ taskInstance }: { readonly 
taskInstance: TaskInstanceRe
           },
         ]
       : []),
+    ...(showTeam
+      ? [
+          {
+            label: translate("dagDetails.team"),
+            value: <TeamName teamName={taskInstance.team_name} />,
+          },
+        ]
+      : []),
     {
       label: translate("taskInstance.dagVersion"),
       value: <DagVersion version={taskInstance.dag_version} />,
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
index c1b5f583467..7f6a899adf6 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -38,6 +38,7 @@ import { useTableURLState } from 
"src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { MarkTaskInstanceAsButton } from "src/components/MarkAs";
 import { StateBadge } from "src/components/StateBadge";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { TruncatedText } from "src/components/TruncatedText";
 import { RouterLink } from "src/components/ui";
@@ -181,12 +182,7 @@ const taskInstanceColumns = ({
     ? [
         {
           accessorKey: "team_name",
-          cell: ({ row: { original } }: TaskInstanceRow) =>
-            original.team_name !== undefined && original.team_name !== null ? (
-              <RouterLink 
to={`/dags?teams=${encodeURIComponent(original.team_name)}`}>
-                {original.team_name}
-              </RouterLink>
-            ) : undefined,
+          cell: ({ row: { original } }: TaskInstanceRow) => <TeamName 
teamName={original.team_name} />,
           enableSorting: false,
           header: translate("dagDetails.team"),
         },
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
index 0a14f69bcbe..974ebf82d3e 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
@@ -413,6 +413,25 @@ class TestGetDagRun:
         assert body["triggered_by"] == triggered_by.value
         assert body["note"] == dag_run_note
 
+    @conf_vars({("core", "multi_team"): "True"})
+    @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
+    def test_get_dag_run_includes_team_name(self, test_client, session):
+        original_bundle_name = _attach_dag_to_team(
+            session, DAG1_ID, bundle_name="team-bundle-run", 
team_name="team-run"
+        )
+        try:
+            response = 
test_client.get(f"/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}")
+            assert response.status_code == 200
+            assert response.json()["team_name"] == "team-run"
+        finally:
+            _detach_dag_from_team(
+                session,
+                DAG1_ID,
+                bundle_name="team-bundle-run",
+                team_name="team-run",
+                original_bundle_name=original_bundle_name,
+            )
+
     def test_get_dag_run_not_found(self, test_client):
         response = test_client.get(f"/dags/{DAG1_ID}/dagRuns/invalid")
         assert response.status_code == 404
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
index f47d6f54c2a..8873c82e273 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
@@ -21,17 +21,20 @@ from unittest import mock
 
 import pendulum
 import pytest
-from sqlalchemy import insert, select
+from sqlalchemy import delete, insert, select, update
 
 from airflow.models.asset import AssetModel, DagScheduleAssetReference
 from airflow.models.dag import DagModel, DagTag
 from airflow.models.dag_favorite import DagFavorite
+from airflow.models.dagbundle import DagBundleModel
 from airflow.models.dagrun import DagRun
+from airflow.models.team import Team
 from airflow.providers.standard.operators.empty import EmptyOperator
 from airflow.utils.state import DagRunState, TaskInstanceState
 from airflow.utils.types import DagRunTriggeredByType, DagRunType
 
 from tests_common.test_utils.asserts import assert_queries_count, count_queries
+from tests_common.test_utils.config import conf_vars
 from tests_common.test_utils.db import (
     clear_db_assets,
     clear_db_connections,
@@ -1082,6 +1085,7 @@ class TestDagDetails(TestDagEndpoint):
             "timetable_periodic": False,
             "timetable_summary": None,
             "timezone": UTC_JSON_REPR,
+            "team_name": None,
         }
         assert res_json == expected
 
@@ -1184,6 +1188,7 @@ class TestDagDetails(TestDagEndpoint):
             "timetable_partitioned": False,
             "timetable_periodic": False,
             "timezone": UTC_JSON_REPR,
+            "team_name": None,
         }
         assert res_json == expected
 
@@ -1289,6 +1294,35 @@ class TestDagDetails(TestDagEndpoint):
         assert isinstance(body["active_runs_count"], int)
         assert body["active_runs_count"] == 0
 
+    def test_dag_details_team_name_none_without_multi_team(self, test_client):
+        """Without multi-team enabled, ``team_name`` stays ``None`` and no 
lookup happens."""
+        response = test_client.get(f"/dags/{DAG1_ID}/details")
+        assert response.status_code == 200
+        assert response.json()["team_name"] is None
+
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_dag_details_includes_team_name(self, session, test_client):
+        original_bundle_name = 
session.scalar(select(DagModel.bundle_name).where(DagModel.dag_id == DAG1_ID))
+        bundle = DagBundleModel(name="team-bundle-details")
+        bundle.teams.append(Team(name="team-details"))
+        session.add(bundle)
+        session.flush()
+        session.execute(
+            update(DagModel).where(DagModel.dag_id == 
DAG1_ID).values(bundle_name="team-bundle-details")
+        )
+        session.commit()
+        try:
+            response = test_client.get(f"/dags/{DAG1_ID}/details")
+            assert response.status_code == 200
+            assert response.json()["team_name"] == "team-details"
+        finally:
+            session.execute(
+                update(DagModel).where(DagModel.dag_id == 
DAG1_ID).values(bundle_name=original_bundle_name)
+            )
+            session.execute(delete(DagBundleModel).where(DagBundleModel.name 
== "team-bundle-details"))
+            session.execute(delete(Team).where(Team.name == "team-details"))
+            session.commit()
+
 
 class TestGetDag(TestDagEndpoint):
     """Unit tests for Get DAG."""
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
index f68acf75d60..6dc6ecac582 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
@@ -274,6 +274,27 @@ class TestGetTaskInstance(TestTaskInstanceEndpoint):
             "team_name": None,
         }
 
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_should_include_team_name(self, test_client, session):
+        self.create_task_instances(session)
+        original_bundle_name = _attach_dag_to_team(
+            session, "example_python_operator", bundle_name="team-bundle-ti", 
team_name="team-ti"
+        )
+        try:
+            response = test_client.get(
+                
"/dags/example_python_operator/dagRuns/TEST_DAG_RUN_ID/taskInstances/print_the_context"
+            )
+            assert response.status_code == 200
+            assert response.json()["team_name"] == "team-ti"
+        finally:
+            _detach_dag_from_team(
+                session,
+                "example_python_operator",
+                bundle_name="team-bundle-ti",
+                team_name="team-ti",
+                original_bundle_name=original_bundle_name,
+            )
+
     def test_should_respond_200_with_decorator(self, test_client, session):
         self.create_task_instances(session, "example_python_decorator")
         response = test_client.get(
@@ -701,6 +722,30 @@ class TestGetMappedTaskInstance(TestTaskInstanceEndpoint):
             "detail": "The Mapped Task Instance with dag_id: 
`example_python_operator`, run_id: `TEST_DAG_RUN_ID`, task_id: 
`print_the_context`, and map_index: `10` was not found"
         }
 
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_should_include_team_name(self, test_client, session):
+        self.create_task_instances(session)
+        original_bundle_name = _attach_dag_to_team(
+            session,
+            "example_python_operator",
+            bundle_name="team-bundle-mapped-ti",
+            team_name="team-mapped-ti",
+        )
+        try:
+            response = test_client.get(
+                
"/dags/example_python_operator/dagRuns/TEST_DAG_RUN_ID/taskInstances/print_the_context/-1",
+            )
+            assert response.status_code == 200
+            assert response.json()["team_name"] == "team-mapped-ti"
+        finally:
+            _detach_dag_from_team(
+                session,
+                "example_python_operator",
+                bundle_name="team-bundle-mapped-ti",
+                team_name="team-mapped-ti",
+                original_bundle_name=original_bundle_name,
+            )
+
 
 class TestGetMappedTaskInstances:
     @pytest.fixture(autouse=True)
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
index bb3c0f7e5a7..36ed804e647 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
@@ -1032,6 +1032,65 @@ class TestTIRunState:
         assert response.status_code == 200
         assert response.json()["dag_run"]["team_name"] == (team_name if 
expect_team else None)
 
+    def test_ti_run_team_name_is_not_served_from_a_stale_cache(
+        self, client, session, dag_maker, time_machine
+    ):
+        """
+        ``ti_run`` must eager load the team rather than fall back to the 
cached resolver.
+
+        The fallback caches per dag_id for ``team_name_cache_ttl`` seconds, so 
a Dag whose team
+        was looked up before it moved bundles would keep reporting the old 
team to the worker.
+        """
+        from airflow.models.dag import clear_team_name_cache
+        from airflow.models.dagbundle import DagBundleModel
+        from airflow.models.team import Team
+
+        instant = timezone.parse("2024-09-30T12:00:00Z")
+        time_machine.move_to(instant, tick=False)
+
+        dag_id = str(uuid4())
+        with dag_maker(dag_id=dag_id, session=session):
+            EmptyOperator(task_id="task")
+        dr = dag_maker.create_dagrun(
+            run_id="test", logical_date=instant, state=DagRunState.RUNNING, 
start_date=instant
+        )
+        ti = dr.get_task_instance(task_id="task")
+        ti.set_state(State.QUEUED)
+
+        for suffix in ("old", "new"):
+            bundle = DagBundleModel(name=f"bundle-{suffix}-{dag_id}")
+            bundle.teams.append(Team(name=f"team-{suffix}-{dag_id[:8]}"))
+            session.add(bundle)
+        session.flush()
+        session.execute(
+            update(DagModel).where(DagModel.dag_id == 
dag_id).values(bundle_name=f"bundle-old-{dag_id}")
+        )
+        session.commit()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            clear_team_name_cache()
+            # Warm the cache with the old team, then move the Dag to the other 
bundle.
+            assert DagModel.get_team_name(dag_id, session=session) == 
f"team-old-{dag_id[:8]}"
+            session.execute(
+                update(DagModel).where(DagModel.dag_id == 
dag_id).values(bundle_name=f"bundle-new-{dag_id}")
+            )
+            session.commit()
+
+            response = client.patch(
+                f"/execution/task-instances/{ti.id}/run",
+                json={
+                    "state": "running",
+                    "hostname": "h",
+                    "unixname": "u",
+                    "pid": 1,
+                    "start_date": "2024-09-30T12:00:00Z",
+                },
+            )
+            clear_team_name_cache()
+
+        assert response.status_code == 200
+        assert response.json()["dag_run"]["team_name"] == 
f"team-new-{dag_id[:8]}"
+
     def test_ti_run_creates_audit_log(self, client, session, 
create_task_instance, time_machine):
         """Test that transitioning to RUNNING creates an audit log record."""
         instant_str = "2024-09-30T12:00:00Z"
diff --git a/airflow-core/tests/unit/models/test_team.py 
b/airflow-core/tests/unit/models/test_team.py
index dcc4c85cc2c..c98f0d0e3f6 100644
--- a/airflow-core/tests/unit/models/test_team.py
+++ b/airflow-core/tests/unit/models/test_team.py
@@ -17,9 +17,24 @@
 from __future__ import annotations
 
 import pytest
+from sqlalchemy import delete, inspect as sa_inspect, select, update
 
+from airflow.api_fastapi.common.db.dags import eager_load_teams
+from airflow.models.dag import DagModel, clear_team_name_cache
+from airflow.models.dagbundle import DagBundleModel
+from airflow.models.dagrun import DagRun
+from airflow.models.taskinstance import TaskInstance
 from airflow.models.team import Team
 
+from tests_common.test_utils.asserts import assert_queries_count
+from tests_common.test_utils.config import conf_vars
+
+pytestmark = pytest.mark.db_test
+
+DAG_ID = "team_owned_dag"
+BUNDLE_NAME = "team-owned-bundle"
+TEAM_NAME = "owning-team"
+
 
 class TestTeam:
     """Unit tests for Team model class methods."""
@@ -38,3 +53,118 @@ class TestTeam:
 
         assert result == {"testing"}
         assert isinstance(result, set)
+
+
+class TestTeamName:
+    """``team_name`` resolution on the models that reach a team through a 
relationship."""
+
+    @pytest.fixture
+    def team_owned_run(self, dag_maker, session):
+        """A Dag run and task instance whose Dag is owned by ``TEAM_NAME`` via 
its bundle."""
+        with dag_maker(DAG_ID, session=session):
+            from airflow.providers.standard.operators.empty import 
EmptyOperator
+
+            EmptyOperator(task_id="task")
+        dag_run = dag_maker.create_dagrun()
+        original_bundle_name = 
session.scalar(select(DagModel.bundle_name).where(DagModel.dag_id == DAG_ID))
+
+        session.execute(delete(DagBundleModel).where(DagBundleModel.name == 
BUNDLE_NAME))
+        session.execute(delete(Team).where(Team.name == TEAM_NAME))
+        session.flush()
+        bundle = DagBundleModel(name=BUNDLE_NAME)
+        bundle.teams.append(Team(name=TEAM_NAME))
+        session.add(bundle)
+        session.flush()
+        session.execute(update(DagModel).where(DagModel.dag_id == 
DAG_ID).values(bundle_name=BUNDLE_NAME))
+        session.commit()
+        clear_team_name_cache()
+
+        yield dag_run
+
+        # bundle_name is a foreign key with no ON DELETE action, so restore it 
before
+        # dropping the bundle this fixture introduced.
+        session.execute(
+            update(DagModel).where(DagModel.dag_id == 
DAG_ID).values(bundle_name=original_bundle_name)
+        )
+        session.execute(delete(DagBundleModel).where(DagBundleModel.name == 
BUNDLE_NAME))
+        session.execute(delete(Team).where(Team.name == TEAM_NAME))
+        session.commit()
+        clear_team_name_cache()
+
+    def get_run(self, session, *, eager_load: bool) -> DagRun:
+        """Re-fetch the run, with or without the team eager loading options."""
+        session.expunge_all()
+        options = eager_load_teams(DagRun.dag_model) if eager_load else ()
+        return session.scalar(select(DagRun).where(DagRun.dag_id == 
DAG_ID).options(*options))
+
+    def get_dag_model(self, session, *, eager_load: bool) -> DagModel:
+        """Re-fetch the Dag, with or without the team eager loading options."""
+        session.expunge_all()
+        options = eager_load_teams() if eager_load else ()
+        return session.scalar(select(DagModel).where(DagModel.dag_id == 
DAG_ID).options(*options))
+
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_team_name_uses_eager_loaded_relationships(self, team_owned_run, 
session):
+        dag_run = self.get_run(session, eager_load=True)
+
+        with assert_queries_count(0, session=session):
+            assert dag_run.team_name == TEAM_NAME
+
+    @conf_vars({("core", "multi_team"): "True"})
+    @pytest.mark.parametrize("entity", ["dag", "dag_run"])
+    def test_team_name_falls_back_when_not_eager_loaded(self, team_owned_run, 
session, entity):
+        """Paths that cannot eager load resolve via the cached resolver, not 
``lazy="raise"``."""
+        if entity == "dag":
+            assert self.get_dag_model(session, eager_load=False).team_name == 
TEAM_NAME
+        else:
+            assert self.get_run(session, eager_load=False).team_name == 
TEAM_NAME
+
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_team_name_fallback_keeps_caller_objects_attached(self, 
team_owned_run, session):
+        """The fallback must not close the caller's scoped session out from 
under it."""
+        dag_model = self.get_dag_model(session, eager_load=False)
+
+        assert dag_model.team_name == TEAM_NAME
+        assert not sa_inspect(dag_model).detached
+        # Would raise DetachedInstanceError if the resolver had closed the 
shared session.
+        assert dag_model.dag_versions is not None
+
+    @conf_vars({("core", "multi_team"): "False"})
+    def test_team_name_is_none_without_multi_team(self, team_owned_run, 
session):
+        """Single-team deployments answer ``None`` without loading anything."""
+        dag_run = self.get_run(session, eager_load=False)
+
+        with assert_queries_count(0, session=session):
+            assert dag_run.team_name is None
+
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_team_name_on_task_instance(self, team_owned_run, session):
+        task_instance = 
session.scalar(select(TaskInstance).where(TaskInstance.dag_id == DAG_ID))
+
+        assert task_instance.team_name == TEAM_NAME
+
+    @conf_vars({("core", "multi_team"): "True"})
+    def test_team_name_is_none_for_bundle_without_team(self, dag_maker, 
session):
+        with dag_maker("unteamed_dag", session=session):
+            from airflow.providers.standard.operators.empty import 
EmptyOperator
+
+            EmptyOperator(task_id="task")
+        dag_maker.create_dagrun()
+        session.commit()
+        clear_team_name_cache()
+
+        dag_run = session.scalar(
+            select(DagRun).where(DagRun.dag_id == 
"unteamed_dag").options(*eager_load_teams(DagRun.dag_model))
+        )
+
+        assert dag_run.team_name is None
+        clear_team_name_cache()
+
+    def test_eager_load_teams_is_a_no_op_without_multi_team(self):
+        with conf_vars({("core", "multi_team"): "False"}):
+            assert eager_load_teams() == ()
+            assert eager_load_teams(DagRun.dag_model) == ()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            assert eager_load_teams() != ()
+            assert eager_load_teams(DagRun.dag_model) != ()
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py 
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index a4b7f60fdaf..ca3c63d6168 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -2640,6 +2640,7 @@ class DAGDetailsResponse(BaseModel):
     owner_links: Annotated[dict[str, str] | None, Field(title="Owner Links")] 
= None
     is_favorite: Annotated[bool | None, Field(title="Is Favorite")] = False
     active_runs_count: Annotated[int | None, Field(title="Active Runs Count")] 
= 0
+    team_name: Annotated[str | None, Field(title="Team Name")] = None
     is_backfillable: Annotated[
         bool, Field(description="Whether this Dag's schedule supports 
backfilling.", title="Is Backfillable")
     ]
diff --git 
a/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py 
b/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py
index 0afb3b347df..dace509b880 100644
--- a/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py
+++ b/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py
@@ -1075,9 +1075,10 @@ class DagRunInfo(InfoJsonEncodable):
         # The Execution API delivers the team name on the DagRun payload it 
sends to the task
         # runner, so task events resolve it from there rather than through the 
bundle lookup below,
         # which needs a metadata DB session the task runner does not have.
-        # `hasattr` rather than a None check -- a team-less run legitimately 
carries None, while
-        # the scheduler's ORM DagRun has no such attribute at all.
-        if hasattr(dagrun, "team_name"):
+        # `hasattr` rather than a None check -- a team-less run legitimately 
carries None. The ORM
+        # DagRun exposes `team_name` too, but reading it lazy-loads the 
bundle, so scheduler-side
+        # runs stay on the guarded lookup below instead of risking a load on a 
detached instance.
+        if not isinstance(dagrun, DagRun) and hasattr(dagrun, "team_name"):
             return dagrun.team_name
 
         # Best-effort: the scheduler stamps `_team_name` on ORM DagRun objects 
before
diff --git a/scripts/ci/prek/check_ti_vs_tis_attributes.py 
b/scripts/ci/prek/check_ti_vs_tis_attributes.py
index 3ae595c7398..49de566fcba 100755
--- a/scripts/ci/prek/check_ti_vs_tis_attributes.py
+++ b/scripts/ci/prek/check_ti_vs_tis_attributes.py
@@ -63,6 +63,8 @@ def compare_attributes(path1, path2):
         # Storing last heartbeat for historic TIs is not interesting/useful
         "last_heartbeat_at",
         "id",
+        # Resolved through the dag_run relationship, not stored on the TI row
+        "team_name",
     }  # exclude attrs not necessary to be in TaskInstanceHistory
     if not diff:
         return

Reply via email to