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

jason810496 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 c24242eea52 Load the serialized Dag in ti_run only for stub tasks 
(#75795)
c24242eea52 is described below

commit c24242eea52193cd10bd2547382eae6f495ea7f8
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Wed Oct 28 15:07:54 2026 +0800

    Load the serialized Dag in ti_run only for stub tasks (#75795)
    
    * Load the serialized Dag in ti_run only for stub tasks
    
    Starting any task deserialized its whole Dag so the route could find out
    whether the task was a stub and read its arg_bindings. Denormalize the
    serialized is_stub flag onto task_instance, which ti_run already fetches
    and locks, and load the Dag only when it is set.
    
    execution_timeout is resolved from the same definition, so it is now sent
    for a lang-SDK task only; #75387 has a Python task send its own to the
    supervisor.
    
    * Hold SQLite foreign keys off while the downgrade rebuilds
    
    Dropping a column rebuilds the table on SQLite, unlike adding one, so the
    downgrade copies task_instance rows while other tables still point at it.
    
    * Pin the bulk-insert path and reword the is_stub comments
    
    create_dagrun refreshes every row from its task right after the bulk write,
    so the existing test passed even when insert_mapping dropped the key. Assert
    the mapping itself instead, and record what a row keeps when it moves to a
    new Dag version without being refreshed.
    
    * Mark a Go or Java TriggerDagRun task a stub, as TypeScript does
    
    * Inline the table name in the is_stub migration
    
    ---------
---
 airflow-core/docs/migrations-ref.rst               |  4 +-
 .../execution_api/datamodels/taskinstance.py       |  5 +-
 .../execution_api/routes/task_instances.py         |  9 +++-
 ... => 0149_3_4_0_add_is_stub_to_task_instance.py} | 32 ++++++++-----
 ..._job.py => 0150_3_5_0_add_session_id_to_job.py} |  4 +-
 ...51_3_5_0_add_dag_run_dag_id_run_after_index.py} |  0
 airflow-core/src/airflow/models/taskinstance.py    |  7 +++
 airflow-core/src/airflow/utils/db.py               |  2 +-
 .../versions/head/test_task_instances.py           | 47 +++++++++++++++++-
 .../lang_sdk_fixtures/go_native.json               |  2 +
 .../lang_sdk_fixtures/java_native.json             |  3 ++
 .../tests/unit/dag_processing/test_dagbag.py       |  9 ++++
 .../tests/unit/models/test_taskinstance.py         | 55 ++++++++++++++++++++++
 go-sdk/airflow/serialize.go                        |  9 ++--
 go-sdk/airflow/serialize_test.go                   |  3 +-
 .../org/apache/airflow/sdk/execution/Serde.kt      | 11 +++--
 .../org/apache/airflow/sdk/TriggerDagRunTest.kt    |  5 +-
 ts-sdk/src/coordinator/serde.ts                    |  8 ++--
 18 files changed, 179 insertions(+), 36 deletions(-)

diff --git a/airflow-core/docs/migrations-ref.rst 
b/airflow-core/docs/migrations-ref.rst
index af1282ee470..608170aa039 100644
--- a/airflow-core/docs/migrations-ref.rst
+++ b/airflow-core/docs/migrations-ref.rst
@@ -41,7 +41,9 @@ Here's the list of all the Database Migrations that are 
executed via when you ru
 
+=========================+==================+===================+==============================================================+
 | ``38c8df3fcb9c`` (head) | ``c71065a1b14f`` | ``3.5.0``         | Add index 
on dag_run (dag_id, run_after).                    |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
-| ``c71065a1b14f``        | ``ee86eed19e24`` | ``3.5.0``         | Add 
session_id and registration_id to Job.                   |
+| ``c71065a1b14f``        | ``b4f1a7d2c903`` | ``3.5.0``         | Add 
session_id and registration_id to Job.                   |
++-------------------------+------------------+-------------------+--------------------------------------------------------------+
+| ``b4f1a7d2c903``        | ``ee86eed19e24`` | ``3.4.0``         | Add is_stub 
to task_instance.                                |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | ``ee86eed19e24``        | ``9cffe90fa0c0`` | ``3.4.0``         | Add 
pending_partition_key to asset_partition_dag_run and     |
 |                         |                  |                   | enforce 
single pending row per key.                          |
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 f911b4ffab9..c90d64da1ec 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
@@ -558,9 +558,10 @@ class TIRunContext(BaseModel):
 
     execution_timeout: float | None = None
     """
-    The task's ``execution_timeout`` in seconds, taken from its serialized Dag.
+    The ``execution_timeout`` in seconds of a task a language SDK runs, from 
its serialized Dag.
 
-    The supervisor enforces the timeout with this value. ``None`` when no 
timeout is set.
+    The supervisor enforces the timeout with this value. A Python task runs 
its own timer in the task
+    runner instead, and gets no value here. ``None`` when no timeout is set.
     """
 
     multi_team: bool = False
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 daa65d41c87..e305a615f9d 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
@@ -197,6 +197,7 @@ def ti_run(
             TI.unixname,
             TI.pid,
             TI.dag_version_id,
+            TI.is_stub,
             # This selects the raw JSON value, bypassing the deserialization 
-- we want that to happen on the
             # client
             column("next_kwargs", JSON),
@@ -382,7 +383,10 @@ def ti_run(
         if first_task_reschedule_start_date is not None:
             context.first_task_reschedule_start_date = 
first_task_reschedule_start_date
 
-        serialized_task = get_task(dag_bag, ti, session=session)
+        # Avoid loading the serialized Dag on the normal Python task path: 
only a lang-SDK
+        # (foreign-runtime) task needs its definition here, and ti.is_stub 
carries the row's own copy
+        # of the serialized flag.
+        serialized_task = get_task(dag_bag, ti, session=session) if ti.is_stub 
else None
 
         # Only set for lang-SDK (foreign-runtime) tasks with a captured 
TaskFlow arg
         # spec; the route excludes unset fields, keeping regular responses 
lean.
@@ -404,6 +408,9 @@ def ti_run(
                     },
                 )
 
+        # Resolved here for a lang-SDK task only, since a regular task no 
longer loads its
+        # definition. #75387 has a Python task send its own execution_timeout 
to the supervisor,
+        # which is where the value for that kind of task belongs.
         if (execution_timeout := get_execution_timeout(serialized_task)) is 
not None:
             context.execution_timeout = execution_timeout
 
diff --git 
a/airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_dag_run_dag_id_run_after_index.py
 
b/airflow-core/src/airflow/migrations/versions/0149_3_4_0_add_is_stub_to_task_instance.py
similarity index 54%
copy from 
airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_dag_run_dag_id_run_after_index.py
copy to 
airflow-core/src/airflow/migrations/versions/0149_3_4_0_add_is_stub_to_task_instance.py
index 9c5ba748c5f..48cc4c5870e 100644
--- 
a/airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_dag_run_dag_id_run_after_index.py
+++ 
b/airflow-core/src/airflow/migrations/versions/0149_3_4_0_add_is_stub_to_task_instance.py
@@ -17,32 +17,38 @@
 # under the License.
 
 """
-Add index on dag_run (dag_id, run_after).
+Add is_stub to task_instance.
+
+Revision ID: b4f1a7d2c903
+Revises: ee86eed19e24
+Create Date: 2026-10-27 09:12:44.317602
 
-Revision ID: 38c8df3fcb9c
-Revises: c71065a1b14f
-Create Date: 2026-10-08 22:19:14.965383
 """
 
 from __future__ import annotations
 
+import sqlalchemy as sa
 from alembic import op
 
+from airflow.migrations.utils import sqlite_rebuilds
+
 # revision identifiers, used by Alembic.
-revision = "38c8df3fcb9c"
-down_revision = "c71065a1b14f"
+revision = "b4f1a7d2c903"
+down_revision = "ee86eed19e24"
 branch_labels = None
 depends_on = None
-airflow_version = "3.5.0"
+airflow_version = "3.4.0"
 
 
 def upgrade():
-    """Add index on dag_run (dag_id, run_after)."""
-    with op.batch_alter_table("dag_run", schema=None) as batch_op:
-        batch_op.create_index("idx_dag_run_dag_id_run_after", ["dag_id", 
"run_after"], unique=False)
+    """Add is_stub to task_instance."""
+    with op.batch_alter_table("task_instance") as batch_op:
+        batch_op.add_column(sa.Column("is_stub", sa.Boolean(), nullable=True))
 
 
 def downgrade():
-    """Remove index on dag_run (dag_id, run_after)."""
-    with op.batch_alter_table("dag_run", schema=None) as batch_op:
-        batch_op.drop_index("idx_dag_run_dag_id_run_after")
+    """Remove is_stub from task_instance."""
+    # Dropping a column rebuilds the table on SQLite, unlike adding one, so 
the foreign keys of
+    # everything that points at task_instance have to be held off while the 
rows are copied.
+    with sqlite_rebuilds(op), op.batch_alter_table("task_instance") as 
batch_op:
+        batch_op.drop_column("is_stub")
diff --git 
a/airflow-core/src/airflow/migrations/versions/0149_3_5_0_add_session_id_to_job.py
 
b/airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_session_id_to_job.py
similarity index 97%
rename from 
airflow-core/src/airflow/migrations/versions/0149_3_5_0_add_session_id_to_job.py
rename to 
airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_session_id_to_job.py
index e595a52b12a..406271582ba 100644
--- 
a/airflow-core/src/airflow/migrations/versions/0149_3_5_0_add_session_id_to_job.py
+++ 
b/airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_session_id_to_job.py
@@ -20,7 +20,7 @@
 Add session_id and registration_id to Job.
 
 Revision ID: c71065a1b14f
-Revises: ee86eed19e24
+Revises: b4f1a7d2c903
 Create Date: 2026-10-05 08:40:20.000000
 
 """
@@ -33,7 +33,7 @@ from alembic import op
 from airflow.migrations.utils import disable_sqlite_fkeys
 
 revision = "c71065a1b14f"
-down_revision = "ee86eed19e24"
+down_revision = "b4f1a7d2c903"
 branch_labels = None
 depends_on = None
 airflow_version = "3.5.0"
diff --git 
a/airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_dag_run_dag_id_run_after_index.py
 
b/airflow-core/src/airflow/migrations/versions/0151_3_5_0_add_dag_run_dag_id_run_after_index.py
similarity index 100%
rename from 
airflow-core/src/airflow/migrations/versions/0150_3_5_0_add_dag_run_dag_id_run_after_index.py
rename to 
airflow-core/src/airflow/migrations/versions/0151_3_5_0_add_dag_run_dag_id_run_after_index.py
diff --git a/airflow-core/src/airflow/models/taskinstance.py 
b/airflow-core/src/airflow/models/taskinstance.py
index 3def6f39fdc..78d1cb1acaf 100644
--- a/airflow-core/src/airflow/models/taskinstance.py
+++ b/airflow-core/src/airflow/models/taskinstance.py
@@ -1141,6 +1141,11 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload):
     priority_weight: Mapped[int | None] = mapped_column(Integer, nullable=True)
     operator: Mapped[str | None] = mapped_column(String(1000), nullable=True)
     custom_operator_name: Mapped[str | None] = mapped_column(String(1000), 
nullable=True)
+    # Denormalized from the task so ti_run can tell a stub (foreign-runtime) 
task from a regular one
+    # without loading the serialized Dag. Null on rows written before the 
column existed. A row moved
+    # to a new Dag version without being refreshed from its task keeps the 
value it was created with,
+    # which fails closed: a task that has become a stub loses its arg_bindings 
rather than running wrong.
+    is_stub: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
     queued_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, 
nullable=True)
     scheduled_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime, 
nullable=True)
     queued_by_job_id: Mapped[int | None] = mapped_column(Integer, 
nullable=True)
@@ -1385,6 +1390,7 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload):
             "executor_config": task.executor_config,
             "operator": task.task_type,
             "custom_operator_name": getattr(task, "operator_name", None),
+            "is_stub": task.is_stub,
             "region_index": region_index,
             "region_id": region_id,
             "_task_display_property_value": task.task_display_name,
@@ -1581,6 +1587,7 @@ class TaskInstance(Base, LoggingMixin, BaseWorkload):
         self.operator = task.task_type
         op_name = getattr(task, "operator_name", None)
         self.custom_operator_name = op_name if isinstance(op_name, str) else ""
+        self.is_stub = task.is_stub
         # Re-apply cluster policy here so that task default do not overload 
previous data
         task_instance_mutation_hook(self, dag_run=dag_run)
 
diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index 5fb7ac5dfff..85af474f8ab 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -117,7 +117,7 @@ _REVISION_HEADS_MAP: dict[str, str] = {
     "3.1.8": "509b94a1042d",
     "3.2.0": "1d6611b6ab7c",
     "3.3.0": "d2f4e1b3c5a7",
-    "3.4.0": "ee86eed19e24",
+    "3.4.0": "b4f1a7d2c903",
     "3.5.0": "38c8df3fcb9c",
 }
 
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 aef15aade0b..c40f69fcc60 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
@@ -1109,6 +1109,49 @@ class TestTIRunState:
         assert response.status_code == 200
         assert "arg_bindings" not in response.json()
 
+    def test_ti_run_does_not_load_the_dag_for_a_regular_task(self, client, 
dag_maker, mocker):
+        """The row rules the task out, so starting it reads no serialized Dag 
-- the point of the column."""
+        with dag_maker("test_regular_task_arg_bindings", serialized=True):
+
+            @task
+            def transform(country: str): ...
+
+            transform("uk")
+
+        dr = dag_maker.create_dagrun()
+        (ti,) = dr.get_task_instances()
+        assert ti.is_stub is False
+        ti.set_state(State.QUEUED)
+        dag_maker.session.flush()
+        get_dag = mocker.patch.object(DBDagBag, "get_dag", autospec=True)
+
+        response = client.patch(f"/execution/task-instances/{ti.id}/run", 
json=self.RUN_PAYLOAD)
+
+        assert response.status_code == 200
+        assert "arg_bindings" not in response.json()
+        get_dag.assert_not_called()
+
+    def test_ti_run_treats_an_unset_is_stub_as_a_regular_task(self, client, 
dag_maker):
+        """A row written before the column existed carries null and keeps its 
pre-upgrade behavior."""
+        with dag_maker("test_unset_is_stub", serialized=True):
+
+            @task.stub
+            def transform(country: str): ...
+
+            transform("uk")
+
+        dr = dag_maker.create_dagrun()
+        (ti,) = dr.get_task_instances()
+        ti.set_state(State.QUEUED)
+        dag_maker.session.get(TaskInstance, ti.id).is_stub = None
+        dag_maker.session.commit()
+        assert 
dag_maker.session.scalar(select(TaskInstance.is_stub).where(TaskInstance.id == 
ti.id)) is None
+
+        response = client.patch(f"/execution/task-instances/{ti.id}/run", 
json=self.RUN_PAYLOAD)
+
+        assert response.status_code == 200
+        assert "arg_bindings" not in response.json()
+
     @mock.patch(
         
"airflow.api_fastapi.execution_api.routes.task_instances.get_arg_bindings",
         autospec=True,
@@ -1160,7 +1203,9 @@ class TestTIRunState:
             pytest.param(
                 task.stub(execution_timeout=timedelta(seconds=-30)), 0.0, 
id="stub-negative-timeout"
             ),
-            pytest.param(task(execution_timeout=timedelta(seconds=10)), 10.0, 
id="python-task"),
+            # A Python task's timeout stays with the task runner, which sends 
it to the supervisor,
+            # so the server resolves one only for a task whose definition it 
loads anyway.
+            pytest.param(task(execution_timeout=timedelta(seconds=10)), None, 
id="python-task"),
         ],
     )
     def test_ti_run_returns_execution_timeout(self, client, dag_maker, 
decorator, expected):
diff --git 
a/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json 
b/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json
index 031f73bebd1..8b440945e3b 100644
--- a/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json
+++ b/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json
@@ -1621,6 +1621,7 @@
               "ratio": 0.5,
               "rows": 2
             },
+            "is_stub": true,
             "logical_date": "2026-09-30 00:00:00+00:00",
             "skip_when_already_exists": true,
             "task_id": "trigger_all",
@@ -1649,6 +1650,7 @@
               "Triggered DAG": "_link_TriggerDagRunLink"
             },
             "_task_module": 
"airflow.providers.standard.operators.trigger_dagrun",
+            "is_stub": true,
             "logical_date": "NOTSET",
             "skip_when_already_exists": false,
             "task_id": "trigger_default",
diff --git 
a/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/java_native.json 
b/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/java_native.json
index 50314784b10..53f0a7256d7 100644
--- a/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/java_native.json
+++ b/airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/java_native.json
@@ -27,6 +27,7 @@
                 "task_type": "DumpExtract",
                 "_task_module": "org.apache.airflow.sdk.execution",
                 "language": "java",
+                "is_stub": true,
                 "template_fields": [],
                 "retries": 2,
                 "queue": "java",
@@ -44,6 +45,7 @@
                 "task_type": "DumpLoad",
                 "_task_module": "org.apache.airflow.sdk.execution",
                 "language": "java",
+                "is_stub": true,
                 "template_fields": [],
                 "queue": "java",
                 "trigger_rule": "all_done"
@@ -112,6 +114,7 @@
                 "task_type": "DumpExtract",
                 "_task_module": "org.apache.airflow.sdk.execution",
                 "language": "java",
+                "is_stub": true,
                 "template_fields": []
               }
             }
diff --git a/airflow-core/tests/unit/dag_processing/test_dagbag.py 
b/airflow-core/tests/unit/dag_processing/test_dagbag.py
index d24443f709f..7bcd745c24c 100644
--- a/airflow-core/tests/unit/dag_processing/test_dagbag.py
+++ b/airflow-core/tests/unit/dag_processing/test_dagbag.py
@@ -1660,6 +1660,15 @@ def test_recorded_runtime_payloads_are_valid(data):
     DagSerialization.validate_serialized_dag(data)
 
 
[email protected]("data", _recorded_payloads())
+def test_recorded_runtime_payloads_mark_their_tasks_as_stubs(data):
+    """ti_run derives arg_bindings only for a stub task, and a language 
runtime runs every task of a
+    Dag defined in it, a TriggerDagRun one included, so each has to say so."""
+    dag = DagSerialization.from_dict(data)
+    for task_id, task in dag.task_dict.items():
+        assert task.is_stub, task_id
+
+
 def _serialize_native_dag(dag_id: str, fileloc: str, relative_fileloc: str) -> 
dict:
     with DAG(dag_id, schedule=None) as dag:
         BaseOperator(task_id="extract")
diff --git a/airflow-core/tests/unit/models/test_taskinstance.py 
b/airflow-core/tests/unit/models/test_taskinstance.py
index f5b8116038c..8087538ff8b 100644
--- a/airflow-core/tests/unit/models/test_taskinstance.py
+++ b/airflow-core/tests/unit/models/test_taskinstance.py
@@ -2865,6 +2865,7 @@ class TestTaskInstance:
             "priority_weight": 123,
             "operator": "some_custom_operator",
             "custom_operator_name": "some_custom_operator",
+            "is_stub": False,
             "queued_dttm": run_date + datetime.timedelta(hours=1),
             "scheduled_dttm": run_date + datetime.timedelta(hours=1),
             "rendered_map_index": None,
@@ -5067,6 +5068,60 @@ class TestMakeTaskCarrier:
         assert "traceparent" in carrier
 
 
[email protected]("is_stub", [True, False])
+def test_refresh_from_task_copies_is_stub(is_stub):
+    """ti_run gates on the row, so the flag has to travel from the task to the 
TaskInstance."""
+    ser_task = create_scheduler_operator(EmptyOperator(task_id="empty"))
+    ser_task.is_stub = is_stub
+
+    ti = TI(ser_task, run_id=None, dag_version_id=mock.MagicMock())
+
+    assert ti.is_stub is is_stub
+
+
[email protected]_test
[email protected](
+    ("decorator", "expected"),
+    [pytest.param(task.stub, True, id="stub"), pytest.param(task, False, 
id="regular")],
+)
+def test_scheduled_task_instances_carry_is_stub(dag_maker, decorator, 
expected):
+    """A scheduled row carries the flag, whichever of the two write paths 
filled it in."""
+    with dag_maker("test_scheduled_is_stub", serialized=True):
+
+        @decorator
+        def transform(country: str): ...
+
+        transform("uk")
+
+    (ti,) = dag_maker.create_dagrun().get_task_instances()
+
+    assert ti.is_stub is expected
+
+
[email protected]_test
[email protected]("is_stub", [True, False])
+def test_insert_mapping_carries_is_stub(dag_maker, session, is_stub):
+    """The scheduler bulk-inserts rows from this mapping, so the flag has to 
be in it."""
+    with dag_maker("test_insert_mapping_is_stub"):
+        EmptyOperator(task_id="t1")
+    session.flush()
+
+    op = create_scheduler_operator(dag_maker.dag.get_task("t1"))
+    op.is_stub = is_stub
+    dag_run = mock.MagicMock()
+    dag_run.context_carrier = new_dagrun_trace_carrier()
+
+    mapping = TaskInstance.insert_mapping(
+        run_id="test_run",
+        task=op,
+        map_index=0,
+        dag_version_id=None,
+        dag_run=dag_run,
+    )
+
+    assert mapping["is_stub"] is is_stub
+
+
 @pytest.mark.db_test
 def test_insert_mapping_includes_context_carrier(dag_maker, session):
     """insert_mapping should include a context_carrier with a traceparent 
derived from the dag run."""
diff --git a/go-sdk/airflow/serialize.go b/go-sdk/airflow/serialize.go
index bd294b85b4f..0eec1a44a3b 100644
--- a/go-sdk/airflow/serialize.go
+++ b/go-sdk/airflow/serialize.go
@@ -424,9 +424,6 @@ func (d *DagRef) serializeTaskLocked(task *TaskRef) 
map[string]any {
                // Python writes template_fields for every operator. A Go task 
has no template fields, so the list
                // is empty.
                data["template_fields"] = []string{}
-               // is_stub makes the API server send the task its arguments 
from _arg_bindings when the
-               // task runs, as it does for a @task.stub task of a Python Dag.
-               data["is_stub"] = true
                if len(task.inputs) > 0 {
                        data["_arg_bindings"] = 
serializeArgBindings(task.inputs)
                }
@@ -437,7 +434,11 @@ func (d *DagRef) serializeTaskLocked(task *TaskRef) 
map[string]any {
                        data["_can_skip_downstream"] = true
                }
        }
-       // A task from TriggerDagRun takes the queue of the Dag too, because 
the Go runtime runs it.
+       // The Go runtime runs every task of a Go Dag, a task from 
TriggerDagRun included, so every one of
+       // them is a stub. is_stub makes the API server send the task its 
arguments from _arg_bindings when
+       // the task runs, as it does for a @task.stub task of a Python Dag.
+       data["is_stub"] = true
+       // The worker picks a runtime by queue, so a task from TriggerDagRun 
takes the queue of the Dag too.
        if spec.Queue == "" {
                spec.Queue = d.spec.Queue
        }
diff --git a/go-sdk/airflow/serialize_test.go b/go-sdk/airflow/serialize_test.go
index 4f728ada5d8..4a7bcf96dd8 100644
--- a/go-sdk/airflow/serialize_test.go
+++ b/go-sdk/airflow/serialize_test.go
@@ -925,6 +925,7 @@ func 
TestSerializeWritesATriggerDagRunAsATriggerDagRunOperator(t *testing.T) {
        assertJSON(t, `{
                "task_id": "trigger",
                "task_type": "TriggerDagRunOperator",
+               "is_stub": true,
                "_task_module": 
"airflow.providers.standard.operators.trigger_dagrun",
                "ui_color": "#ffefeb",
                "template_fields": [
@@ -1012,7 +1013,7 @@ func 
TestSerializeWritesOnlyTheTriggerDagRunOptionsThatAreSet(t *testing.T) {
                "skip_when_already_exists": false
        }`, withoutKeys(serializedTask(t, got, "trigger"),
                "task_id", "task_type", "_task_module", "ui_color", 
"template_fields",
-               "template_fields_renderers", "_operator_extra_links",
+               "template_fields_renderers", "_operator_extra_links", "is_stub",
        ))
        // An empty FailedStates means that no Dag run state fails the task.
        assertJSON(t, `[]`, serializedTask(t, got, 
"trigger_never_fails")["failed_states"])
diff --git 
a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt 
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt
index c01592b8eea..8a5d470426a 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Serde.kt
@@ -145,15 +145,16 @@ private fun serializeTask(
     // value never matches the tuple default it is compared against), so it
     // is unconditional here too. Java tasks have no template fields.
     data["template_fields"] = emptyList<Any?>()
-    // What marks a task whose arguments Airflow resolves per instance for a
-    // runtime outside Python, as `@task.stub` does on the Python side.
-    // `get_arg_bindings` reads nothing without it.
-    data["is_stub"] = true
     argBindings(taskId, def)?.let { data["_arg_bindings"] = it }
   }
+  // The Java runtime runs every task of a Java Dag, a trigger task included, 
so
+  // every one of them is a stub. is_stub marks a task whose arguments Airflow
+  // resolves per instance for a runtime outside Python, as `@task.stub` does 
on
+  // the Python side. `get_arg_bindings` reads nothing without it.
+  data["is_stub"] = true
   // Lets NotPreviouslySkippedDep re-skip a cleared downstream, as Python's 
SkipMixin does.
   if (def.decider != null) data["_can_skip_downstream"] = true
-  // A trigger task takes the Dag's queue too, because the Java runtime runs 
it.
+  // The worker picks a runtime by queue, so a trigger task takes the Dag's 
queue too.
   val config =
     if (dagQueue == null || "queue" in def.configValues) def.configValues else 
def.configValues + ("queue" to dagQueue)
   // Emit only config entries that differ from their schema default, mirroring
diff --git 
a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/TriggerDagRunTest.kt 
b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/TriggerDagRunTest.kt
index 56c0bb410b7..b7bef8724f7 100644
--- a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/TriggerDagRunTest.kt
+++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/TriggerDagRunTest.kt
@@ -168,9 +168,10 @@ internal class TriggerDagRunTest {
     assertEquals("NOTSET", data["logical_date"])
     assertEquals(false, data["wait_for_completion"])
     assertEquals(false, data["skip_when_already_exists"])
-    // A Java task's markers belong to a task the Java runtime runs a body for.
+    // The Java runtime runs a trigger task too, so it carries the stub marker.
+    assertEquals(true, data["is_stub"])
+    // `language` marks a task the Java runtime runs a body for, which this is 
not.
     assertFalse("language" in data)
-    assertFalse("is_stub" in data)
     assertFalse("reset_dag_run" in data)
   }
 
diff --git a/ts-sdk/src/coordinator/serde.ts b/ts-sdk/src/coordinator/serde.ts
index c528980487c..b6938fa81d6 100644
--- a/ts-sdk/src/coordinator/serde.ts
+++ b/ts-sdk/src/coordinator/serde.ts
@@ -252,9 +252,11 @@ function serializeTask(
     // matches the tuple default it is compared against. A TypeScript task has
     // no Jinja templating, so the list is empty rather than absent.
     template_fields: [],
-    // What marks a task whose arguments the API server resolves per instance
-    // and sends to a foreign runtime, as `@task.stub` does on the Python side.
-    // `get_arg_bindings` reads nothing without it.
+    // The TypeScript runtime runs every task of a TypeScript Dag, a trigger 
task
+    // included, so every one of them is a stub. `is_stub` marks a task whose
+    // arguments the API server resolves per instance and sends to a foreign
+    // runtime, as `@task.stub` does on the Python side. `get_arg_bindings` 
reads
+    // nothing without it.
     is_stub: true,
   };
   const label = `task "${taskId}" of Dag "${dagId}"`;

Reply via email to