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

jason810496 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 40cc196f740 [v3-4-test] Go SDK: Rewrite the user docs around Dag 
definition (#74698) (#75383)
40cc196f740 is described below

commit 40cc196f7404ea1381f43742e65d6bf5f11d0f44
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 22 18:31:25 2026 +0800

    [v3-4-test] Go SDK: Rewrite the user docs around Dag definition (#74698) 
(#75383)
    
    * Go SDK: Rewrite the user docs around Dag definition
    
    Rewrite the Go SDK guide to cover declaring a Dag in Go first, then stub
    TaskHandlers. Trim the README to a user-facing landing page, move the
    contributor sections to DEVELOPMENT.md, and add a README for the examples.
    
    * Drop the beta status from the Go SDK docs
    
    
    
    * Explain the trigger rule a task after both If sides needs in the Go SDK 
guide
    
    * Apply suggestion from @pierrejeambrun
    
    
    
    * Tighten the If wording, the LocalExecutor note and the duplicate config 
block
    
    ---------
    (cherry picked from commit b3db4d5201a25031fcb5b524936b471120d892ef)
    
    Co-authored-by: Jason(Zhe-You) Liu 
<[email protected]>
    Co-authored-by: Pierre Jeambrun <[email protected]>
---
 .../authoring-and-scheduling/language-sdks/go.rst  | 997 ++++++++++++---------
 go-sdk/DEVELOPMENT.md                              | 215 +++++
 go-sdk/README.md                                   | 507 ++---------
 go-sdk/example/README.md                           | 129 +++
 4 files changed, 953 insertions(+), 895 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst 
b/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
index fac17348007..8e87a094aa4 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
@@ -20,270 +20,464 @@
 Go SDK
 ======
 
-|experimental|
+The Go SDK lets you write Airflow Dags and tasks in Go. There are two ways to 
use it:
 
-The Go SDK lets you implement Airflow task logic in Go, with native access to 
the Airflow "model"
-(Variables, Connections, and XCom). The Dag and its scheduling remain in 
Python; individual tasks delegate
-to a compiled Go *bundle* that is launched by
-:class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator` for each 
task instance.
+* **Dag definition.** The schedule, the tasks, their options and the 
dependencies between them are all written
+  in Go, with no Python file involved. See :ref:`go-sdk/dag-definition`.
+* **Python Dag with stub TaskHandler.** A Python Dag declares the tasks with 
``@task.stub`` and wires them
+  together, and a Go ``TaskHandler`` implements each one. See 
:ref:`go-sdk/stub-tasks`.
 
-Because Go is a compiled language, every task must be compiled ahead of time 
and registered inside a single,
-self-contained native executable called a **bundle**. The bundle also embeds 
the source file of each Dag built with ``airflow.Dag``
-plus the entrypoint file (the one with ``func main``), and a metadata
-manifest (the ``dag_id`` and ``task_id`` map) in a footer appended to the 
executable, so the executable *is*
-the bundle: one runnable file to ship, with no separate manifest or archive. 
The
-:ref:`airflow-go-pack <go-sdk/build>` tool builds and packs that bundle.
+Both use the same task API and the same build tool, and one bundle can serve 
both.
+
+Go is compiled, so your Dags and tasks are built into a single self-contained 
executable, the **bundle**, with
+the :ref:`airflow-go-pack <go-sdk/build>` tool. The bundle is the only file 
you deploy.
+
+The SDK is the ``github.com/apache/airflow/go-sdk`` Go module.
+
+.. seealso::
+
+  For the full API reference and the released versions, see
+  `pkg.go.dev <https://pkg.go.dev/github.com/apache/airflow/go-sdk>`__.
 
 .. contents:: Contents
    :local:
    :depth: 2
 
-API reference
--------------
-
-The generated API reference for the Go SDK module, and the list of its 
released versions, is available on
-`pkg.go.dev <https://pkg.go.dev/github.com/apache/airflow/go-sdk>`__.
-
 Prerequisites
 -------------
 
-* Go 1.24 or later to build and pack bundles. This is a build-time requirement 
only; the worker that runs a
-  packed bundle needs no Go toolchain, because the bundle is a self-contained 
native executable.
-* The packed bundle must be accessible from the Airflow worker and the Dag 
processor, in the Dag bundle the
-  coordinator scans, and built for the operating system and CPU architecture 
of both.
-* The ``apache-airflow-task-sdk`` package (installed with Airflow) provides 
the coordinator; no additional
-  Python packages are needed.
+* Go 1.25 or later to build bundles. Airflow needs no Go toolchain to run them.
+* A bundle runs on the Airflow workers and on the Dag processor, so build it 
for their operating system and CPU
+  architecture (see :ref:`go-sdk/cross-platform`).
+* The ``apache-airflow-task-sdk`` package, installed with Airflow, provides
+  :class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator`, which 
runs the bundle. No other Python
+  package is needed.
+* In your Go module, add the SDK, and ``airflow-go-pack`` as a tool:
 
-Execution architecture
-----------------------
+  .. code-block:: bash
 
-A Python task runner launches the Go bundle directly, with no separate Go 
worker process on the host. This
-is the same coordinator mechanism the Java SDK uses. Because the mature Python 
supervisor handles the
-Airflow-facing concerns, Go tasks inherit remote task logs (S3/GCS), the full 
range of task states, and
-alternate XCom backends rather than implementing them again in Go.
+      go get github.com/apache/airflow/go-sdk
+      go get -tool github.com/apache/airflow/go-sdk/cmd/airflow-go-pack
 
 .. _go-sdk/quick-start:
 
 Quick start
 -----------
 
-The following example shows the minimal moving parts: a Python Dag with two 
stub tasks, and a Go
-implementation of those tasks.
+This example declares a whole Dag in Go, builds it into a bundle, and deploys 
it, with no Python file involved.
 
-Python Dag (the scheduling side)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Declare a Dag in ``main.go``:
 
-.. code-block:: python
+.. code-block:: go
 
-    from airflow.sdk import dag, task
+    package main
 
+    import (
+        "log"
 
-    @dag
-    def simple_dag():
-        @task.stub(queue="golang")
-        def extract(): ...
+        "github.com/apache/airflow/go-sdk/airflow"
+    )
 
-        @task.stub(queue="golang")
-        def transform(): ...
+    func extract(airflow.Context) (int, error) {
+        return 42, nil
+    }
 
-        extract() >> transform()
+    func report(actx airflow.Context, rows int) error {
+        actx.Logger().InfoContext(actx, "extracted rows", "rows", rows)
+        return nil
+    }
 
+    func main() {
+        dag := airflow.Dag("go_hello", airflow.DagSpec{Schedule: "@daily", 
Queue: "golang"})
 
-    simple_dag()
+        extracted := dag.Task(extract)
+        dag.Task(report, airflow.Inputs(extracted))
 
-``@task.stub`` declares the *shape* of the Go tasks (their names and 
dependencies) without any Python
-implementation. The ``queue`` value routes the task to the Go coordinator.
+        bundle := airflow.Bundle()
+        bundle.Register(dag)
+        if err := bundle.Serve(); err != nil {
+            log.Fatal(err)
+        }
+    }
 
-Go implementation
-~~~~~~~~~~~~~~~~~~
+Build it into a bundle:
 
-A task is an ordinary Go function whose first parameter is an 
``airflow.Context``. Everything Airflow gives
-the task is a method on it, so the signature stays the same whatever the task 
uses.
+.. code-block:: bash
 
-.. code-block:: go
+    go tool airflow-go-pack --output dist/go_hello .
 
-    import (
-        "runtime"
+Put ``dist/go_hello`` in a Dag bundle, for example the default ``dags-folder`` 
bundle, which reads the
+``[core] dags_folder`` directory (see :doc:`Dag bundles 
</administration-and-deployment/dag-bundles>`). Then send
+the ``golang`` queue to the Go coordinator in ``airflow.cfg``:
 
-        "github.com/apache/airflow/go-sdk/airflow"
-    )
+.. code-block:: ini
 
-    func extract(actx airflow.Context) (any, error) {
-        conn, err := actx.Client().GetConnection(actx, "test_http")
-        if err != nil {
-            return nil, err
-        }
-        actx.Logger().InfoContext(actx, "fetched connection", "host", 
conn.Host)
-        // ... do work, honour actx cancellation ...
-        return map[string]any{"go_version": runtime.Version()}, nil
+    [sdk]
+    coordinators = {"go": {"classpath": 
"airflow.sdk.coordinators.executable.ExecutableCoordinator"}}
+    queue_to_coordinator = {"golang": "go"}
+
+Airflow reads the bundle like any other Dag file: ``go_hello`` shows up in the 
UI, runs on its schedule, and
+``report`` receives the value ``extract`` returned.
+
+.. _go-sdk/dag-definition:
+
+Declaring a Dag in Go
+---------------------
+
+Tasks and their inputs
+~~~~~~~~~~~~~~~~~~~~~~
+
+``airflow.Dag(dagID, spec)`` creates a Dag, and ``dag.Task(fn, opts...)`` adds 
a task to it and returns a
+reference to the task. ``airflow.Inputs`` gives each parameter after the 
``airflow.Context`` one input:
+
+* A task reference passes that task's return value, and makes this task wait 
for it.
+* ``airflow.Literal(value)`` passes a fixed JSON value, and draws no 
dependency.
+
+.. code-block:: go
+
+    type Summary struct {
+        Total   int `json:"total"`
+        Regions int `json:"regions"`
     }
 
-    func transform(actx airflow.Context) error {
-        val, err := actx.Client().GetVariable(actx, "my_variable")
-        if err != nil {
-            return err
-        }
-        actx.Logger().InfoContext(actx, "obtained variable", "my_variable", 
val)
+    func summarize(airflow.Context) (Summary, error) {
+        return Summary{Total: 42, Regions: 2}, nil
+    }
+
+    func publish(actx airflow.Context, summary Summary, label string) error {
+        actx.Logger().InfoContext(actx, label, "total", summary.Total)
         return nil
     }
 
-.. note::
+    summarized := dag.Task(summarize)
+    dag.Task(publish, airflow.Inputs(summarized, airflow.Literal("nightly")))
+
+``publish`` receives the ``Summary`` that ``summarize`` returned, and the 
literal ``"nightly"`` as ``label``.
+
+A task function takes an ``airflow.Context`` first, and returns ``error`` or 
``(T, error)``. The ``T`` becomes the
+task's ``return_value`` XCom and what a downstream task receives. Values move 
between tasks as JSON (see
+:ref:`go-sdk/types`), so return types JSON can hold.
+
+The inputs are checked when the Dag is built. Each parameter needs exactly one 
input, an upstream task's
+result type must be assignable to the parameter type, and a literal must 
decode into it. A mismatch panics,
+which the Dag processor reports as an import error.
+
+Task ids
+~~~~~~~~
 
-  As with the other language SDKs, XCom *dependencies* are declared in the 
Python stub Dag (they define task
-  order). An upstream task's value reaches a downstream task either through a 
parameter, when the stub Task
-  passes it in the TaskFlow call (see :ref:`go-sdk/arguments`), or by reading 
it explicitly with
-  ``actx.Client().GetXCom``.
+The task id is the function's name, so ``dag.Task(summarize)`` adds the task 
``summarize``. A method value such
+as ``svc.Extract`` takes the method's name, ``Extract``. A function literal 
has no usable name, so give it one
+with ``airflow.TaskSpec{TaskID: "..."}``.
 
-Go entry point
-~~~~~~~~~~~~~~~
+A task id is made of letters, digits, dashes, underscores and dots, and is at 
most 250 characters with the
+prefixes of its task groups. Two tasks of one Dag cannot share an id.
 
-Build a bundle with ``airflow.Bundle()``, register a handler for each task, 
and call ``Serve`` as the last
-statement of ``main``. The ``Register`` calls are the single source of truth 
for which ``dag_id`` and task
-names this bundle can run, so the generated manifest can never drift from what 
the binary actually executes.
+Dag and task options
+~~~~~~~~~~~~~~~~~~~~
+
+``airflow.DagSpec`` and ``airflow.TaskSpec`` hold Airflow's own Dag and task 
arguments, spelled in Go:
 
 .. code-block:: go
 
-    import (
-        "log"
+    dag := airflow.Dag("go_etl", airflow.DagSpec{
+        Schedule: "0 6 * * *",
+        Tags:     []string{"etl"},
+        Queue:    "golang",
+    })
 
-        "github.com/apache/airflow/go-sdk/airflow"
-    )
+    loaded := dag.Task(loadRows, airflow.Inputs(summarized), 
airflow.TaskSpec{Retries: 2})
 
-    func main() {
-        bundle := airflow.Bundle()
+``Schedule`` accepts ``@once``, ``@continuous``, a cron preset such as 
``@daily``, or a cron expression. Leave it
+empty for a Dag that only runs when triggered. A cron schedule runs in UTC.
 
-        bundle.Register(
-            airflow.TaskHandler("simple_dag", "extract", extract),
-            airflow.TaskHandler("simple_dag", "transform", transform),
-        )
+Every task of the Dag runs in the Go bundle, so it needs a queue that
+:ref:`queue_to_coordinator <go-sdk/coordinator-config>` sends to the Go 
coordinator. Set ``Queue`` once on the
+``DagSpec`` and each task inherits it. A task's own ``Queue`` wins over the 
Dag's.
 
-        if err := bundle.Serve(); err != nil {
-            log.Fatal(err)
-        }
+Order-only dependencies
+~~~~~~~~~~~~~~~~~~~~~~~
+
+When a task only has to run after another, without receiving its value, draw 
the dependency with ``Before`` and
+``After``, the Go spelling of Python's ``>>`` and ``<<``:
+
+.. code-block:: go
+
+    cleaned := dag.Task(cleanup)
+
+    loaded.Before(cleaned)                        // loaded >> cleaned
+    cleaned.After(loaded, summarized)             // [loaded, summarized] >> 
cleaned
+    loaded.Before(airflow.Label(cleaned, "done")) // a labelled edge
+
+Both take any number of tasks or task groups, and drawing a dependency that 
already exists changes nothing.
+``airflow.Label`` puts a label on the edge, which the Airflow UI shows on the 
graph.
+
+Pass a value through ``airflow.Inputs`` when the downstream task needs it, and 
use ``Before`` or ``After`` when it
+only needs to run in order.
+
+Task groups
+~~~~~~~~~~~
+
+``dag.TaskGroup(groupID)`` opens a group with the same ``Task``, 
``TaskGroup``, ``If`` and ``Switch`` methods as
+the Dag. Every id declared in it is prefixed with the group's, as in Python:
+
+.. code-block:: go
+
+    staging := dag.TaskGroup("staging")
+    staging.Task(stageRows)                    // task id "staging.stageRows"
+    staging.TaskGroup("checks").Task(checkNulls) // task id 
"staging.checks.checkNulls"
+
+    staging.Before(loaded)                     // staging >> loaded
+
+A group takes ``Before`` and ``After`` too, which orders the whole group 
against a task or another group.
+
+Tasks and groups share one id namespace, so a Dag cannot hold both a task and 
a group called ``staging``. Set
+``PrefixGroupID`` in an ``airflow.TaskGroupSpec`` to a pointer to ``false`` to 
keep the ids declared in a group
+as written. A group id is made of letters, digits, dashes and underscores, and 
is at most 200 characters.
+
+Conditional branching
+~~~~~~~~~~~~~~~~~~~~~
+
+``dag.If`` declares a task from a function that returns ``(bool, error)``, and 
names the task each outcome runs:
+
+.. code-block:: go
+
+    func hasRows(_ airflow.Context, summary Summary) (bool, error) {
+        return summary.Total > 0, nil
     }
 
-``TaskHandler`` names the ``dag_id`` and the ``task_id`` explicitly: the 
``dag_id`` must match the Python
-Dag, and the ``task_id`` must match a ``@task.stub`` function in that Dag. 
Neither is derived from the Go
-function name, so a handler can be named whatever reads best in Go.
+    dag.If(hasRows, 
airflow.Inputs(summarized)).Then(loaded).Else(reportedEmpty)
 
-``TaskHandler`` also checks the signature of the function it is given and 
panics if the check fails -- for
-instance when the function does not take an ``airflow.Context`` first, does 
not return an ``error``, or
-declares a variadic ``...`` parameter, which no stub argument can fill. 
Because ``main`` registers every
-handler before ``Serve``, a mistake stops the executable as soon as it starts 
rather than when the task
-first runs.
+``If`` takes inputs and task options as ``Task`` does. ``Then`` is required, 
and ``Else`` is optional: without it,
+the ``Then`` task is skipped when the condition is false.
 
-``Serve`` closes registration: a ``Register`` left below it in ``main`` panics 
rather than adding a handler
-to the map the runtime is already answering from, so what a bundle can run 
never depends on how far
-``main`` has got.
+The condition skips only the side not taken, which stays skipped if it is 
cleared later. Tasks after it follow their
+own trigger rule, so with the default ``all_success``, a task after both sides 
is skipped too. To run such a task when
+at least one side succeeds and none fails, give it 
``airflow.TriggerRuleNoneFailedMinOneSuccess``:
 
-A package that defines task handlers of its own can export them as a 
``[]airflow.Registerable`` for ``main``
-to pass on with ``bundle.Register(reports.Handlers()...)``.
+.. code-block:: go
 
-Coordinator configuration
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    dag.Task(finish, airflow.TaskSpec{
+        TriggerRule: airflow.TriggerRuleNoneFailedMinOneSuccess,
+    }).After(loaded, reportedEmpty)
 
-Register a Dag bundle for the packed bundles, register the coordinator, and 
route the queue to it in
-``airflow.cfg`` (or the equivalent ``AIRFLOW__*`` environment variables):
+Multi-way branching
+~~~~~~~~~~~~~~~~~~~
 
-.. code-block:: ini
+``dag.Switch`` declares a task from a function that returns the reference of 
the task to run, and lists the
+candidates with ``Case``:
 
-    [dag_processor]
-    dag_bundle_config_list = [
-        {"name": "dags-folder", "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle", "kwargs": {}},
-        {
-          "name": "go-task-handlers",
-          "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
-          "kwargs": {"path": "/opt/airflow/go-task-handlers"}
+.. code-block:: go
+
+    daily := dag.Task(publishDaily)
+    weekly := dag.Task(publishWeekly)
+
+    pickCadence := func(actx airflow.Context) (*airflow.TaskRef, error) {
+        cadence, err := actx.Client().GetVariable(actx, "cadence")
+        if err != nil {
+            return nil, err
         }
-      ]
+        if cadence == "weekly" {
+            return weekly, nil
+        }
+        return daily, nil
+    }
 
-    [sdk]
-    coordinators = {
-      "go": {
-        "classpath": 
"airflow.sdk.coordinators.executable.ExecutableCoordinator",
-        "kwargs": {"task_handler_bundle_name": "go-task-handlers"}
-      }
+    dag.Switch(pickCadence, airflow.TaskSpec{TaskID: 
"pick_cadence"}).Case(daily).Case(weekly)
+
+Exactly one case runs, and the others are skipped. There is no default: a 
decider that returns anything other
+than a case fails, naming what it returned and the cases it could have 
returned. The deciding task's value is
+the id of the task it chose.
+
+Triggering another Dag
+~~~~~~~~~~~~~~~~~~~~~~
+
+``airflow.TriggerDagRun`` makes a task that starts a run of another Dag. Pass 
it to ``dag.Task`` in place of a
+function:
+
+.. code-block:: go
+
+    triggered := dag.Task(
+        airflow.TriggerDagRun(airflow.TriggerDagRunSpec{
+            DagID:             "downstream_etl",
+            Conf:              map[string]any{"source": "go_etl"},
+            WaitForCompletion: true,
+        }),
+        airflow.TaskSpec{TaskID: "trigger_downstream"},
+    )
+    triggered.After(loaded)
+
+The task has no function to take an id from, so it needs a ``TaskID``, and it 
takes no inputs.
+``TriggerDagRunSpec`` takes the options of Python's ``TriggerDagRunOperator``, 
such as ``Conf``,
+``WaitForCompletion``, ``Deferrable``, ``PokeInterval``, ``AllowedStates``, 
``FailedStates`` and
+``SkipWhenAlreadyExists``. Values are sent as written, so Jinja in ``Conf`` is 
not rendered.
+
+When ``Deferrable`` is unset, it follows ``[operators] default_deferrable``. A 
deferred task waits in the
+triggerer, which needs the standard provider installed. Without it, 
``WaitForCompletion`` polls from the task
+and holds its worker slot.
+
+Generating Dags from data
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A Dag is an ordinary Go value, so ``main`` can build any number of them, for 
example from a loop over a
+configuration file. ``go-sdk/example/dynamic`` builds one Dag per entry of a 
``dags.yaml`` file deployed next to
+the bundle, so changing the file changes the Dags with no rebuild.
+
+Registering Dags
+~~~~~~~~~~~~~~~~
+
+``bundle.Register`` takes the Dags, and ``Serve`` runs the bundle, so call it 
last in ``main``. ``Register``
+checks each Dag as a whole: a cycle, an ``If`` with no ``Then``, a ``Switch`` 
with no ``Case``, or two Dags with
+the same ``dag_id`` panic, and the Dag processor reports an import error.
+
+A complete example
+~~~~~~~~~~~~~~~~~~
+
+``go-sdk/example/native`` uses all of the above: a task group, a fan-in, a 
condition, a multi-way branch,
+order-only dependencies and a triggered Dag run.
+
+.. _go-sdk/stub-tasks:
+
+Implementing stub tasks of a Python Dag
+---------------------------------------
+
+Here a Python Dag declares the tasks and their dependencies, and Go implements 
some of them. Use it when the Dag
+needs something a Go Dag cannot express yet (see :ref:`go-sdk/limitations`), 
or to mix Python and Go tasks in
+one Dag.
+
+The Python Dag
+~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    from airflow.sdk import dag, task
+
+
+    @dag
+    def simple_dag():
+        @task.stub(queue="golang")
+        def extract(): ...
+
+        @task.stub(queue="golang")
+        def transform(country: str, extracted: dict): ...
+
+        transform("uk", extract())
+
+
+    simple_dag()
+
+``@task.stub`` declares a task that is implemented elsewhere, and ``queue`` 
sends it to the Go coordinator.
+
+The Go side
+~~~~~~~~~~~
+
+Create a ``TaskHandler`` per stub task, naming the ``dag_id`` and ``task_id`` 
it implements, and register them on
+the bundle:
+
+.. code-block:: go
+
+    func extract(actx airflow.Context) (any, error) {
+        return map[string]any{"rows": 42}, nil
     }
-    queue_to_coordinator = {"golang": "go"}
 
-``task_handler_bundle_name`` names the Dag bundle the coordinator scans for 
packed bundles;
-``queue_to_coordinator`` routes stub tasks with ``queue="golang"`` to this Go 
coordinator. See
-:ref:`go-sdk/coordinator-config` for the full list of accepted ``kwargs`` and 
how bundles are located.
+    func transform(actx airflow.Context, country string, extracted 
map[string]any) error {
+        actx.Logger().InfoContext(actx, "transforming", "country", country, 
"rows", extracted["rows"])
+        return nil
+    }
 
-There is no separate Go worker to run: the Airflow worker forks the bundle 
binary once per task instance.
+    func main() {
+        bundle := airflow.Bundle()
+        bundle.Register(
+            airflow.TaskHandler("simple_dag", "extract", extract),
+            airflow.TaskHandler("simple_dag", "transform", transform),
+        )
+        if err := bundle.Serve(); err != nil {
+            log.Fatal(err)
+        }
+    }
 
-.. note::
+The ``task_id`` must match the stub's task id, including any task group 
prefix. Neither id comes from the Go
+function's name, so a handler can be named whatever reads best in Go.
 
-  The ``[sdk]`` config and the packed bundle files must be present wherever 
tasks execute and on the Dag
-  processor. With ``CeleryExecutor``, tasks execute on the Celery workers; 
with ``LocalExecutor``, they run
-  inside the scheduler process. The Dag processor checks the stub tasks of 
each Python Dag against the task
-  handlers the packed bundles register, so it runs them too and needs bundles 
built for its operating
-  system and CPU architecture. The API server does not need any of it. 
Register the Dag bundle in
-  ``[dag_processor] dag_bundle_config_list`` on every component, like your 
other Dag bundles: the worker
-  and the Dag processor resolve ``task_handler_bundle_name`` through it, and 
wherever the ``[sdk]`` config
-  is read it is rejected if the name is missing there.
+``TaskHandler`` checks the function's signature and panics on a bad one, so a 
mistake stops the bundle as soon
+as it starts rather than when the task first runs.
 
-A Dag processor with this ``[sdk]`` configuration also parses the bundle 
binaries of every Dag bundle (see
-:ref:`go-sdk/native-dag-parsing`). Keep it from parsing the binaries that only 
register task handlers with
-that Dag bundle's ``.airflowignore``, whose pattern syntax follows ``[core] 
dag_ignore_file_syntax``. Tasks do
-not read ``.airflowignore``, so the binaries still run.
+``Register`` takes any number of task handlers and Dags, so one bundle can 
implement the tasks of several Python
+Dags and declare Dags of its own. A package can export its handlers as a 
``[]airflow.Registerable`` for
+``main`` to pass on with ``bundle.Register(reports.Handlers()...)``.
 
-With ``task_handler_bundle_name`` set to its own Dag bundle, as above, 
``go-task-handlers`` holds only handler
-binaries, so ignore everything in it:
+.. _go-sdk/arguments:
 
-.. code-block:: bash
+TaskFlow arguments
+~~~~~~~~~~~~~~~~~~
 
-    # [core] dag_ignore_file_syntax = glob (the default)
-    echo '*' > /opt/airflow/go-task-handlers/.airflowignore
+A Python Dag that calls a stub task TaskFlow-style passes those arguments to 
the handler. They bind in one of two
+ways.
 
-    # [core] dag_ignore_file_syntax = regexp; a bare * is not a valid pattern 
and is dropped
-    echo '.' > /opt/airflow/go-task-handlers/.airflowignore
+**By position.** Each parameter after the ``airflow.Context`` takes the 
argument in the same position, as
+``transform("uk", extract())`` fills ``country`` and ``extracted`` above.
 
-With ``task_handler_bundle_name`` unset, the binaries sit in the same Dag 
bundle as the Python Dag file. Go
-binaries have no extension, so put them in a folder such as ``bin/`` and 
ignore that folder:
+**By name.** When the handler's only parameter after the Context is a struct, 
its fields take the arguments by
+name:
 
-.. code-block:: bash
+.. code-block:: go
 
-    # [core] dag_ignore_file_syntax = glob (the default)
-    echo 'bin/*' >> /opt/airflow/dags/.airflowignore
+    type CombineInput struct {
+        Region    string `arg:"region_code"`
+        Threshold float64
+    }
 
-    # [core] dag_ignore_file_syntax = regexp
-    echo '^bin/' >> /opt/airflow/dags/.airflowignore
+    // The Python Dag calls combine(region_code="uk", threshold=0.5).
+    func combine(actx airflow.Context, input CombineInput) (any, error) {
+        return nil, nil
+    }
+
+A field takes the argument whose name matches its own, ignoring case and 
underscores, so ``Threshold`` takes
+``threshold``. An ``arg:"<name>"`` tag maps a field to a different name. A 
field the call does not pass keeps its
+zero value, and an argument no field takes is ignored. Both are logged as a 
warning.
+
+Either way, an argument filled from another task, as ``extract()`` above, 
arrives as that task's value. An
+argument the call leaves at its default needs no Go parameter, so adding a 
defaulted parameter to a stub does
+not break the handler. An argument that cannot fill its Go type fails the task 
before the handler runs.
+
+.. note::
+
+  A dependency drawn with ``>>`` in the Python Dag orders the tasks and passes 
nothing. To read a value the
+  call did not pass, use ``actx.Client().GetXCom``.
 
 Writing tasks
 -------------
 
-Every task function takes an ``airflow.Context`` as its first parameter, and 
reaches what Airflow provides
-through its methods:
+A task is an ordinary Go function. It works the same in both ways of using the 
SDK.
+
+Its first parameter is an ``airflow.Context``, which gives it what Airflow 
provides:
 
 .. list-table::
    :header-rows: 1
-   :widths: 35 65
+   :widths: 25 75
 
    * - Method
-     - What it returns
+     - Returns
    * - ``actx.Logger()``
-     - An ``*slog.Logger`` whose output is routed back to the Airflow task log.
+     - An ``*slog.Logger`` that writes to the Airflow task log. See 
:ref:`go-sdk/logging`.
    * - ``actx.Client()``
-     - A client for Airflow Variables, Connections, and XCom. See 
:ref:`go-sdk/client`.
+     - An ``sdk.Client`` for Airflow Variables, Connections, XComs and the 
task state store. See
+       :ref:`go-sdk/client`.
    * - ``actx.TaskInstance()``
-     - The identifiers of the running task instance. See 
:ref:`go-sdk/runtime-context`.
+     - ``DagID``, ``RunID``, ``TaskID`` (including any task group prefix), 
``MapIndex`` (``nil`` for an unmapped
+       task) and ``TryNumber``.
    * - ``actx.DagRun()``
-     - The identifiers and scheduling timestamps of its Dag run. See 
:ref:`go-sdk/runtime-context`.
-
-``airflow.Context`` is itself a ``context.Context``, so pass it straight to a 
client call or to
-``http.NewRequestWithContext``, and select on ``actx.Done()``, which fires 
when the supervisor asks the task
-to stop. Respect it for long-running work. Cleanup that must outlive that 
cancellation runs under ``context.WithoutCancel(actx)``.
-A helper typed as a plain ``context.Context`` recovers the same surface with 
``airflow.FromContext``.
+     - ``DagID``, ``RunID``, and the ``*time.Time`` fields ``LogicalDate``, 
``DataIntervalStart`` and
+       ``DataIntervalEnd`` (``nil`` when the run has no such value, such as a 
manual run).
 
-Every parameter after the Context is data, filled from the stub Task's 
TaskFlow call; see
-:ref:`go-sdk/arguments`.
+``airflow.Context`` is a ``context.Context``, so pass it to a client call or 
to ``http.NewRequestWithContext``. Its
+``Done()`` fires when Airflow stops the task, so long-running work can stop 
cleanly. Cleanup that must outlive
+that runs under ``context.WithoutCancel(actx)``. A helper that takes a plain 
``context.Context`` gets the
+``airflow.Context`` back with ``airflow.FromContext``.
 
-An optional ``(any, error)`` return value becomes the task's ``return_value`` 
XCom. A non-nil ``error`` (or a
-panic, which the runtime recovers) marks the task instance failed in Airflow, 
triggering retries if
-configured on the stub.
+A value the task returns becomes its ``return_value`` XCom. A non-nil 
``error``, or a panic, fails the task, and
+retries it if the task has retries.
 
-``airflow.NewContext`` builds a Context, so a task is an ordinary function 
call in a unit test:
+``airflow.NewContext`` builds a Context, so a unit test calls a task as an 
ordinary function:
 
 .. code-block:: go
 
@@ -292,83 +486,62 @@ configured on the stub.
         airflow.TaskInstance{DagID: "simple_dag", TaskID: "transform", 
TryNumber: 1},
         airflow.DagRun{DagID: "simple_dag", RunID: "run1"},
     )
-    require.NoError(t, transform(actx))
-
-A helper the task calls can still ask for the narrowest interface it needs 
(for example
-``sdk.VariableClient`` instead of the full ``sdk.Client``), which documents 
the Airflow features it touches
-and lets a test pass a fake.
+    require.NoError(t, transform(actx, "uk", map[string]any{"rows": 42}))
 
 .. _go-sdk/client:
 
-The ``sdk.Client`` surface
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``actx.Client()`` returns an ``sdk.Client``, which composes four smaller 
interfaces, so a helper can depend
-on just one:
-
-* ``VariableClient`` - ``GetVariable`` (returns the Variable as a string), 
``UnmarshalJSONVariable``
-  (decodes a JSON Variable into a pointer you provide), ``SetVariable``, and 
``DeleteVariable``.
-* ``ConnectionClient`` - ``GetConnection``, returning a ``Connection`` with 
fields ``ID``, ``Type``,
-  ``Host``, ``Port``, ``Login``, ``Password``, ``Path``, ``Extra`` (a 
``map[string]any``), plus a
-  ``GetURI()`` helper.
-* ``XComClient`` - ``GetXCom`` to read an upstream task's XCom and 
``PushXCom`` to publish one.
-* ``TaskStateStoreClient`` - ``TaskStateStore``, returning the store for this 
task instance:
-  ``Get``, ``UnmarshalJSONValue`` (decodes a JSON value into a pointer you 
provide), ``Set``,
-  ``Delete``, and ``Clear``. See :ref:`go-sdk/task-state-store`.
-
-``GetXCom`` returns the stored value as an ``any``; see :ref:`go-sdk/types` 
for how the stored JSON maps to
-Go types.
-
-``SetVariable`` stores the value as a string, so encode structured data (for 
example with ``json.Marshal``)
-before storing it.
+The ``sdk.Client``
+~~~~~~~~~~~~~~~~~~
 
-.. code-block:: go
+.. list-table::
+   :header-rows: 1
+   :widths: 40 60
 
-    client := actx.Client()
-    if err := client.SetVariable(actx, "process_threshold", "42", "Rows above 
this count take the slow path"); err != nil {
-        return err
-    }
-    if err := client.DeleteVariable(actx, "legacy_threshold"); err != nil {
-        return err
-    }
+   * - Method
+     - Description
+   * - ``GetVariable(ctx, key)``
+     - The Variable as a string.
+   * - ``UnmarshalJSONVariable(ctx, key, pointer)``
+     - Decodes a JSON Variable into the pointer.
+   * - ``SetVariable(ctx, key, value, description)``
+     - Stores a Variable, replacing any existing value. Values are strings, so 
encode structured data, for
+       example with ``json.Marshal``.
+   * - ``DeleteVariable(ctx, key)``
+     - Removes a Variable.
+   * - ``GetConnection(ctx, connID)``
+     - A ``Connection`` with ``ID``, ``Type``, ``Host``, ``Port``, ``Login``, 
``Password``, ``Path`` and ``Extra``,
+       and a ``GetURI()`` helper.
+   * - ``GetXCom(ctx, dagID, runID, taskID, mapIndex, key, nil)``
+     - An XCom value, as an ``any``. See :ref:`go-sdk/types`.
+   * - ``PushXCom(ctx, ti, key, value)``
+     - Publishes an XCom value.
+   * - ``TaskStateStore()``
+     - This task instance's key/value store. See 
:ref:`go-sdk/task-state-store`.
+
+A missing value returns an error that wraps ``sdk.VariableNotFound``, 
``sdk.ConnectionNotFound``,
+``sdk.XComNotFound`` or ``sdk.TaskStateNotFound``, so test for it with 
``errors.Is``.
+
+``sdk.Client`` is made of ``VariableClient``, ``ConnectionClient``, 
``XComClient`` and ``TaskStateStoreClient``,
+so a helper can take only the one it needs, and a test can pass a fake.
 
 .. note::
 
-  A value supplied by a secrets backend (for example an ``AIRFLOW_VAR_*`` 
environment variable) still takes
-  precedence over the stored value when the Variable is read back. Calling 
``SetVariable`` with an empty
-  description clears any existing description.
-
-Not-found lookups return sentinel errors - ``VariableNotFound``, 
``ConnectionNotFound``, ``XComNotFound``,
-``TaskStateNotFound`` - so you can branch on a missing value with 
``errors.Is`` rather than parsing an error
-string.
+   A value from a secrets backend, such as an ``AIRFLOW_VAR_*`` environment 
variable, still wins over the
+   stored value when the Variable is read back. ``SetVariable`` with an empty 
description clears the existing
+   description.
 
 .. _go-sdk/task-state-store:
 
-The task state store
-~~~~~~~~~~~~~~~~~~~~~~
-
-``actx.Client().TaskStateStore()`` returns a persistent key/value store 
private to one task instance,
-and the Go SDK's entry point to durable execution. It is the same store the 
Python SDK exposes as
-``context["task_state_store"]``; see :doc:`/core-concepts/task-state-store` 
for the concept and its
-configuration.
+Task state store
+~~~~~~~~~~~~~~~~
 
-The store is scoped to ``dag_id``, ``run_id``, ``task_id``, and ``map_index``. 
It deliberately does *not*
-include ``try_number``, so a value written by one attempt is still readable by 
the next one: a task that
-records an external job ID or its own progress can resume after a worker crash 
or a retry instead of
-redoing the work. The Execution API confines every call to the task instance 
the caller is running as, so
-there is no way to address another task's store - pass results between tasks 
with XCom instead.
-
-The usual shape is to look for a checkpoint first and only do the expensive 
work when it is missing:
+The task state store is a per-task-instance key/value store, scoped to the 
Dag, run, task and map index, but not
+the try number, so a value written by one attempt is still readable by the 
next. It survives worker crashes and
+retries, which makes it a good place to record an external job id or a 
checkpoint. See
+:doc:`/core-concepts/task-state-store` for the concept.
 
 .. code-block:: go
 
-    import (
-        "errors"
-
-        "github.com/apache/airflow/go-sdk/airflow"
-        "github.com/apache/airflow/go-sdk/sdk"
-    )
-
     func runSparkJob(actx airflow.Context) error {
         store := actx.Client().TaskStateStore()
 
@@ -376,8 +549,7 @@ The usual shape is to look for a checkpoint first and only 
do the expensive work
         stored, err := store.Get(actx, "job_id")
         switch {
         case errors.Is(err, sdk.TaskStateNotFound):
-            // First attempt: submit the job and remember its ID before doing 
anything else.
-            if jobID, err = sparkClient.SubmitJob(actx); err != nil {
+            if jobID, err = submitSparkJob(actx); err != nil {
                 return err
             }
             if err := store.Set(actx, "job_id", jobID, 
sdk.WithRetention(sdk.NeverExpire)); err != nil {
@@ -386,136 +558,44 @@ The usual shape is to look for a checkpoint first and 
only do the expensive work
         case err != nil:
             return err
         default:
-            // Get returns an any; the value was stored by this task as a 
string.
             jobID = stored.(string)
-            actx.Logger().InfoContext(actx, "reattaching to job submitted by 
an earlier attempt", "job_id", jobID)
         }
 
-        return sparkClient.WaitForCompletion(actx, jobID)
+        return waitForSparkJob(actx, jobID)
     }
 
-``value`` must not be nil and must be JSON-representable - a string, number, 
bool, slice, map, or a struct,
-which is stored as an object built from its exported fields and their ``json`` 
tags. A custom
-``MarshalJSON`` is not called, so a type that relies on one is stored as the 
shape of its fields, and a
-struct with no exported fields is stored as ``{}``. Read a scalar back with 
``Get``, which returns it as an ``any`` (the
-numeric caveat in :ref:`go-sdk/types` applies here too); for an object or 
array,
-``UnmarshalJSONValue`` decodes it straight into a pointer you provide.
-
-A value the store cannot hold is rejected before it is sent, so you get an 
error naming the problem rather
-than a round trip that fails on the server. The one that catches people out is 
``time.Time``, which JSON has
-no spelling for - store ``value.Format(time.RFC3339)`` and parse it back with 
``time.Parse``. Non-finite
-floats and ``[]byte`` are refused for the same reason. This mirrors the Python 
SDK, where the same values
-fail Pydantic validation before the write leaves the worker.
-
-Keys expire, so retention is part of writing a value:
-
-* ``Set`` without options uses the deployment's ``[state_store] 
default_retention_days`` (30 days by
-  default). The Go runtime cannot read Airflow's config, so the supervisor 
resolves that value and passes
-  it in the environment when it launches the bundle. A deployment that sets it 
to something unusable - a
-  negative number, or a value that is not a whole number of days - fails the 
write, exactly as it does for
-  a Python task, rather than quietly substituting a different lifetime.
-* ``sdk.WithRetention`` takes an explicit, positive ``time.Duration``, or 
``sdk.NeverExpire`` for a key
-  that is skipped by garbage collection entirely. A zero or negative retention 
is rejected rather than
-  given a meaning of its own: to follow the deployment default omit the 
option, and to drop a key call
-  ``Delete``.
-
-``Delete`` removes one key (deleting a key that does not exist is not an 
error) and ``Clear`` removes
-every key stored for this task instance.
-
-.. note::
+The store has ``Get``, ``UnmarshalJSONValue`` (decodes an object or array into 
a pointer), ``Set``, ``Delete``
+and ``Clear``.
 
-  The Go SDK does not implement the worker-side state backend (``[workers] 
state_store_backend``), which
-  offloads large values to external storage and records only a reference 
marker in the database. If a
-  deployment configures one, a Go task reading a key that was written through 
that backend receives the raw
-  reference marker rather than the original value, and a Go task writing a key 
stores the whole value in the
-  database instead of offloading it. This is the same behaviour as a Python 
worker that does not have the
-  backend configured.
+* A value must be JSON, and not ``nil``. A ``time.Time``, a non-finite float 
or a ``[]byte`` is refused before it
+  is sent, so store a time as ``value.Format(time.RFC3339)``.
+* ``Set`` without options keeps the key for ``[state_store] 
default_retention_days``.
+  ``sdk.WithRetention(d)`` keeps it for a positive ``time.Duration``, and 
``sdk.NeverExpire`` keeps it forever.
+* ``[workers] state_store_backend`` is not used from Go tasks, so values are 
always stored in the database. A
+  key a Python task wrote through such a backend reads back as the backend's 
reference marker.
 
-.. _go-sdk/runtime-context:
+.. _go-sdk/logging:
 
-Reading the task runtime context
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Logging
+~~~~~~~
 
-``airflow.Context`` carries the identifiers and scheduling timestamps of the 
running task instance and its
-Dag run -- the Go equivalent of the execution context the Python and Java SDKs 
expose:
+``actx.Logger()`` writes to the Airflow task log, at the level ``[logging] 
logging_level`` sets.
+``[logging] namespace_levels`` applies to a logger named with ``WithGroup``:
 
 .. code-block:: go
 
-    func extract(actx airflow.Context) (any, error) {
-        ti := actx.TaskInstance()
-        actx.Logger().InfoContext(actx, "running",
-            "dag_id", ti.DagID,
-            "run_id", ti.RunID,
-            "task_id", ti.TaskID,
-            "try_number", ti.TryNumber,
-            "logical_date", actx.DagRun().LogicalDate,
-        )
-        return nil, nil
-    }
-
-``actx.TaskInstance()`` returns ``DagID``, ``RunID``, ``TaskID``, ``MapIndex`` 
(nil for an unmapped task),
-and ``TryNumber``; ``actx.DagRun()`` returns ``DagID``, ``RunID``, and the 
``*time.Time`` fields
-``LogicalDate``, ``DataIntervalStart``, and ``DataIntervalEnd`` (nil when the 
run has no such value, e.g. a
-manual trigger).
-
-.. _go-sdk/arguments:
-
-Receiving arguments from the stub Task
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A stub Task's arguments reach a Go handler in one of two ways:
-
-1. **Positional binding** -- each data parameter takes the argument in the 
same position.
-2. **Struct-based (keyword) binding** -- a sole struct parameter takes the 
arguments by field name.
-
-Every parameter after the ``airflow.Context`` is a **data parameter**, filled 
in declaration order from the
-arguments of the Python stub Task's TaskFlow call. A literal in the Dag file 
(``transform("uk", ...)``)
-decodes straight into the parameter; an upstream task's output 
(``transform(..., extract())``) is pulled
-from that task's XCom in the current Dag run. If the argument count does not 
match, or an argument's
-declared type cannot fill the Go type, the task fails before its body runs.
-
-.. code-block:: go
-
-    // The Python stub Task calls transform("uk", extract()).
-    func transform(actx airflow.Context, country string, extracted 
map[string]any) error {
-        actx.Logger().InfoContext(actx, "transforming", "country", country)
-        return nil
-    }
-
-When a task's **sole** data parameter is a struct, its fields bind **by name** 
instead of by position --
-keyword arguments rather than positional ones. Being the only data parameter 
is the opt-in; there is no
-marker to add.
-
-.. code-block:: go
-
-    type CombineInput struct {
-        Region    string `arg:"region_code"` // renamed
-        Threshold float64
-    }
+    databaseLog := actx.Logger().WithGroup("example.database")
+    databaseLog.Debug("query complete", "rows", 42)
 
-    // The Python stub Task calls combine(region_code="uk", threshold=0.5).
-    func Combine(actx airflow.Context, input CombineInput) (any, error) {
-        return nil, nil
-    }
-
-An exported field binds the argument matching its own Go name, folding case 
and underscores, so
-``Threshold`` takes ``threshold``; reach for an ``arg:"<name>"`` tag when the 
names genuinely differ, as
-``Region`` does above. The `Go SDK README
-<https://github.com/apache/airflow/blob/main/go-sdk/README.md>`__ has the full 
binding rules, including
-how unmatched fields and arguments are treated and when an untagged struct is 
decoded whole from a single
-argument instead.
-
-Stub parameters the Dag author left at their Python defaults are the exception 
to both shapes: they reach
-the wire but need no Go parameter, so adding a defaulted parameter to a stub 
does not break the Go
-functions already bound to it.
+A namespace level applies to the longest matching dotted prefix, so 
``example=DEBUG`` also applies to
+``example.database``.
 
 .. _go-sdk/types:
 
 XCom type mapping
 -----------------
 
-XCom values are stored as JSON in Airflow's metadata database. The table below 
shows how those JSON types
-surface as Go values when read back via ``GetXCom``.
+XComs are stored as JSON, so a value read with ``GetXCom`` arrives as the 
matching Go type:
 
 .. list-table::
    :header-rows: 1
@@ -523,10 +603,10 @@ surface as Go values when read back via ``GetXCom``.
 
    * - Python type
      - JSON
-     - Go type (from ``GetXCom``)
+     - Go type
    * - ``int``
      - number (integer)
-     - numeric (see note)
+     - an integer type (see note)
    * - ``float``
      - number (decimal)
      - ``float64``
@@ -548,123 +628,148 @@ surface as Go values when read back via ``GetXCom``.
 
 .. note::
 
-  ``GetXCom`` returns the value exactly as decoded from the transport; there 
is no typed XCom
-  deserialization layer yet. The Python supervisor encodes values as 
``msgpack``, so a whole number arrives
-  as a Go integer type (whose width depends on the value) and only a 
non-integer arrives as ``float64``. Do
-  not assume a fixed integer width: type-switch over the numeric types you 
expect, or round-trip the value
-  through ``json.Marshal`` / ``json.Unmarshal`` into a typed Go value.
+  An integer arrives as a Go integer type whose width depends on the value. 
Type-switch over the integer types
+  you expect, or round-trip the value through ``json.Marshal`` and 
``json.Unmarshal`` into a typed Go value. A
+  task input from ``airflow.Inputs`` or a TaskFlow argument is already decoded 
into its parameter type.
 
 .. _go-sdk/build:
 
-Building and packaging
-----------------------
-
-A plain ``go build`` produces a runnable binary, but a *deployable* bundle 
(binary + embedded source files +
-manifest) must be produced with ``airflow-go-pack``. The packer compiles the 
bundle and appends the embedded
-metadata footer, so the coordinator can read its ``dag_id``\ s without 
executing the binary, producing a
-single runnable file. The on-disk format the packer emits (the ``AFBNDL01`` 
footer and the
-``airflow-metadata.yaml`` manifest) is the bundle format shared by all 
native-executable SDKs, specified in
-:doc:`task-sdk:executable-bundle-spec`.
-
-``airflow-go-pack`` ships via the Go 1.24 ``tool`` directive, so there is no 
global install: add
-
-.. code-block:: text
-
-    tool github.com/apache/airflow/go-sdk/cmd/airflow-go-pack
-
-to your bundle module's ``go.mod`` and run it with ``go tool 
airflow-go-pack``. This pins the packer version
-per project.
+Building a bundle
+-----------------
 
-Build and pack in one step; any flags after ``--`` are forwarded verbatim to 
``go build``:
+``airflow-go-pack`` builds your ``main`` package into a single executable, the 
bundle. It also embeds the
+entrypoint file and the file that declares each Dag, which the Airflow UI's 
**Code** view shows, and a manifest
+of the bundle's Dags and task handlers. That file is all you deploy, and 
Airflow refuses to run a bundle whose
+content was changed after it was built.
 
 .. code-block:: bash
 
-    go tool airflow-go-pack ./example/bundle -- -trimpath -tags=prod
+    go tool airflow-go-pack --output dist/my_dags ./cmd/my_dags -- -trimpath
 
-Use ``--output <path>`` to write the packed bundle straight into the directory 
of the Dag bundle the
-coordinator scans (see `Deploying`_):
+* ``--output <path>`` sets the output file. By default, the bundle is written 
to the current directory, named
+  after the package's directory.
+* Flags after ``--`` are passed to ``go build``.
+* ``go tool airflow-go-pack inspect <bundle>`` prints a bundle's manifest.
 
-.. code-block:: bash
+The Dag processor reads a bundle only when its file name has no extension, 
such as ``dags/orders``, not
+``dags/orders.bin``.
 
-    go tool airflow-go-pack --output 
/opt/airflow/go-task-handlers/sample-dag-bundle ./example/bundle
+.. _go-sdk/cross-platform:
 
 Cross-platform builds
-~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~
 
-The worker and the Dag processor that run a bundle often use a different 
operating system or CPU
-architecture than your build machine (for example, deploying to a Linux host 
from an Apple-silicon
-``darwin/arm64`` laptop). Pass ``--goos`` / ``--goarch`` and the packer 
cross-builds for you:
+The workers and the Dag processor often run a different operating system or 
CPU architecture than your build
+machine. Pass ``--goos`` and ``--goarch`` to build for them:
 
 .. code-block:: bash
 
-    go tool airflow-go-pack --goos linux --goarch amd64 \
-      --output /opt/airflow/go-task-handlers/sample-dag-bundle \
-      ./example/bundle
+    go tool airflow-go-pack --goos linux --goarch amd64 --output dist/my_dags 
./cmd/my_dags
 
-Alternatively, pack a pre-built binary with ``--executable`` / ``--source``. 
The packer normally execs the
-binary with ``--airflow-metadata`` to read its manifest, but a cross-compiled 
binary cannot run on the build
-host. In that case, generate the manifest on a machine that *can* run the 
binary and feed it to the packer
-with ``--airflow-metadata``:
+``airflow-go-pack`` runs the built binary to read its manifest. When it cannot 
do that for a cross-compiled
+binary, build and read the manifest on a machine of the target platform, then 
pack the binary with
+``--executable``:
 
 .. code-block:: bash
 
     # On a linux/amd64 machine:
-    go build -o my-bundle ./example/bundle
-    ./my-bundle --airflow-metadata > airflow-metadata.yaml
+    go build -o my_dags ./cmd/my_dags
+    ./my_dags --airflow-metadata > airflow-metadata.yaml
 
-    # Back on the darwin/arm64 machine:
-    go tool airflow-go-pack --executable ./my-bundle --source main.go \
-      --airflow-metadata airflow-metadata.yaml
-
-(``--executable`` is mutually exclusive with ``--goos`` / ``--goarch`` and 
with ``go build`` flags after
-``--``, since it packs an already-built binary instead of building one.)
+    # On the build machine:
+    go tool airflow-go-pack --executable ./my_dags --source 
cmd/my_dags/main.go \
+      --airflow-metadata airflow-metadata.yaml --output dist/my_dags
 
 Deploying
-~~~~~~~~~
+---------
 
-Copy or mount the packed bundle into the Dag bundle named by the coordinator's 
``task_handler_bundle_name``.
-The :class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator` scans 
that Dag bundle recursively,
-matches the incoming ``dag_id`` against each bundle's manifest, verifies the 
bundle's integrity hash, and
-launches the matching bundle. This scan identifies bundles by the trailer 
magic, not by file name. The Dag
-processor also requires a file name without an extension, see 
:ref:`go-sdk/native-dag-parsing`.
+Where the bundle goes depends on what it contains.
 
-The matching bundle is marked executable before it is launched, so any Dag 
bundle works, including an
-object-store one such as ``S3DagBundle`` that has no concept of file 
permissions and so cannot preserve
-the execute bit the build produced.
+Dag definition
+~~~~~~~~~~~~~~
 
-.. _go-sdk/native-dag-parsing:
+Put the bundle in a Dag bundle, such as the default ``dags-folder`` bundle. 
The Dag processor runs each bundle it
+finds to read its Dags, as it reads any other Dag file, so it needs the same 
``[sdk]`` configuration as the workers,
+shown in the :ref:`quick start <go-sdk/quick-start>`, and a bundle built for 
its platform.
 
-Parsing native Dags
-~~~~~~~~~~~~~~~~~~~
+With more than one ``ExecutableCoordinator``, map each Dag bundle that holds 
Go Dags to one of them in
+``[sdk] dag_bundle_to_coordinator``, such as ``{"dags-folder": "go"}``, or its 
bundles fail to parse.
 
-Once an :class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator` is 
configured, the Dag processor
-parses every bundle binary in each Dag bundle and runs it to collect the Dags 
it defines. The Dag processor
-recognizes a bundle binary only when its file name has no extension, such as 
``bin/orders``, not
-``bin/orders.bin``. A binary that only registers task handlers is parsed too: 
each parse runs it and finds
-no Dags, and a binary built with a Go SDK that cannot answer the Dag-parse 
request records an import error.
-Keep those out of the Dag processor with ``.airflowignore``, as described in 
:ref:`Quick start <go-sdk/quick-start>`.
+Their tasks run from the same Dag bundle, at the version their Dag run was 
created with, so a Dag declared in Go
+needs no coordinator option to find its bundle.
 
-With one :class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator`, 
it parses the binaries of every Dag
-bundle. With several, for example a second one for another 
``task_handler_bundle_name``, map each Dag bundle that
-holds native Go Dags to one of them in ``[sdk] dag_bundle_to_coordinator``. A 
binary in a Dag bundle that has no
-entry, or whose entry names no 
:class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator`, fails to 
parse
-with an import error:
+A bundle that cannot be read, or whose Dags fail a check, shows up as an 
import error, and the other Dags keep
+working.
+
+Python Dag with stub TaskHandler
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default, the coordinator loads the bundle that implements a stub task from 
the stub task's own Dag bundle, at
+the version its Dag run was created with, so the bundle goes next to the 
Python Dag that declares the stub.
+
+To keep such bundles in a Dag bundle of their own, name it with 
``task_handler_bundle_name``. A task then uses the
+version that bundle is on when the task starts:
 
 .. code-block:: ini
 
+    [dag_processor]
+    dag_bundle_config_list = [
+        {"name": "dags-folder", "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle", "kwargs": {}},
+        {
+          "name": "go-task-handlers",
+          "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
+          "kwargs": {"path": "/opt/airflow/go-task-handlers"}
+        }
+      ]
+
     [sdk]
-    dag_bundle_to_coordinator = {"dags-folder": "go"}
+    coordinators = {
+      "go": {
+        "classpath": 
"airflow.sdk.coordinators.executable.ExecutableCoordinator",
+        "kwargs": {"task_handler_bundle_name": "go-task-handlers"}
+      }
+    }
+    queue_to_coordinator = {"golang": "go"}
+
+The coordinator finds the bundle for a task by the ``dag_id`` in each bundle's 
manifest, not by file name, so one
+Dag bundle can hold several bundles. Any Dag bundle type works, including an 
object-store one such as
+``S3DagBundle``.
+
+The Dag processor checks the stub tasks of each Python Dag against the task 
handlers the bundles register, so
+it needs the bundles too, built for its platform.
+
+The Dag processor also parses a bundle that only implements stub tasks, which 
runs it once per parse and finds
+no Dags. Skip that with the Dag bundle's ``.airflowignore``; the coordinator 
still finds the bundles. For a Dag
+bundle that holds only such bundles, as ``go-task-handlers`` above:
+
+.. code-block:: bash
+
+    # [core] dag_ignore_file_syntax = glob (the default)
+    echo '*' > /opt/airflow/go-task-handlers/.airflowignore
+
+    # [core] dag_ignore_file_syntax = regexp
+    echo '.' > /opt/airflow/go-task-handlers/.airflowignore
+
+For bundles next to a Python Dag, put them in a folder such as ``bin/`` and 
ignore it with ``bin/*`` (glob) or
+``^bin/`` (regexp).
 
-* The Code view shows each native Dag's own source file, taken from the 
sources the bundle embeds.
-* A task of a native Go Dag runs the bundle binary its Dag was parsed from.
+Where the configuration is needed
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The coordinator runs where tasks run, so the ``[sdk]`` configuration and the 
bundles are needed on the workers.
+With ``CeleryExecutor``, set them on the Celery workers. With 
``LocalExecutor``, tasks run in subprocesses on the
+scheduler's host, so set them there. The Dag processor needs them too, as 
described above. Register every Dag bundle in
+``[dag_processor] dag_bundle_config_list`` on each of these components. The 
API server never needs them.
+
+There is no separate Go service to run: the worker starts the bundle once per 
task instance.
 
 .. _go-sdk/coordinator-config:
 
 :class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator` 
configuration
----------------------------------------------------------------------------------------------
+---------------------------------------------------------------------------------
 
-All ``kwargs`` in the ``coordinators`` config entry are passed to the
-:class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator` 
constructor:
+The ``kwargs`` of a ``coordinators`` entry are passed to
+:class:`~airflow.sdk.coordinators.executable.ExecutableCoordinator`, and all 
of them are optional:
 
 .. list-table::
    :header-rows: 1
@@ -674,33 +779,29 @@ All ``kwargs`` in the ``coordinators`` config entry are 
passed to the
      - Default
      - Description
    * - ``task_handler_bundle_name``
-     - *(task's own Dag bundle)*
-     - Name of the Dag bundle scanned recursively for executable bundles. It 
is used only by
-       mixed-language Dags, to locate the task handlers for the ``@task.stub`` 
tasks of a Python Dag;
-       Dags defined natively in a language SDK do not use it. It must be 
registered in
-       ``[dag_processor] dag_bundle_config_list``. It is checked when the 
``[sdk]`` configuration is
-       loaded, so a typo fails there rather than on the first task.
+     - *(unset)*
+     - The Dag bundle to load the bundles that implement stub tasks from. When 
unset, a stub task loads its
+       bundle from its own Dag bundle. It must be registered in 
``[dag_processor] dag_bundle_config_list``.
    * - ``task_startup_timeout``
      - ``10.0``
-     - Seconds to wait for the bundle subprocess to connect after launch. 
Increase this if your
-       bundle startup is slow (e.g. on constrained hardware).
-
-.. note::
+     - Seconds to wait for a task's bundle to start. Increase it if your 
bundle starts slowly, for example on
+       constrained hardware.
 
-  **Locating bundles.** The packed bundles for the ``@task.stub`` tasks of a 
Python Dag are read from a Dag
-  bundle, so they are delivered, refreshed and versioned by the same machinery 
as your Dags.
+``task_handler_bundle_name`` only concerns stub tasks: a Dag declared in Go 
always runs from the Dag bundle it was
+read from.
 
-  * The expected layout is a separate Dag bundle for the packed bundles, named 
by
-    ``task_handler_bundle_name``, rather than the Dag bundle that holds your 
``.py`` files. The task
-    uses the version that Dag bundle is on when it starts, pinned for the 
whole task.
-  * If ``task_handler_bundle_name`` is unset, packed bundles are read from the 
**task's own** Dag
-    bundle, pinned to the version the run was created with.
+``queue_to_coordinator`` maps a queue to a coordinator entry, so every task on 
that queue runs in a Go bundle.
 
 .. _go-sdk/limitations:
 
 Limitations
 -----------
 
-* **A Python stub Dag is still required.** The Execution API does not yet 
carry Dag structure for non-Python
-  languages, so task names and dependencies are declared in Python with
-  :func:`@task.stub <airflow.sdk.task.stub>`. This is a documented known 
limitation.
+* **A Dag declared in Go cannot express everything yet.** Assets, Dag params, 
custom timetables, dynamic task
+  mapping, setup and teardown tasks, and human-in-the-loop tasks have no Go 
form. Declare such a Dag in Python,
+  and implement its tasks in Go with :func:`@task.stub 
<airflow.sdk.task.stub>`.
+* **Cluster policies do not apply to Dags declared in Go.** ``dag_policy`` and 
``task_policy`` do not run when the
+  Dag processor reads them.
+* **Some CLI commands do not take a Dag declared in Go.** ``airflow dags 
test``, ``tasks test`` and
+  ``tasks render`` refuse it, and ``airflow dags reserialize`` skips it.
+* **One process per task instance.** Tasks cannot share in-process state; use 
XComs or an external store.
diff --git a/go-sdk/DEVELOPMENT.md b/go-sdk/DEVELOPMENT.md
new file mode 100644
index 00000000000..b6511e8ec99
--- /dev/null
+++ b/go-sdk/DEVELOPMENT.md
@@ -0,0 +1,215 @@
+<!--
+ 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.
+ -->
+
+# Developing the Go SDK
+
+This guide is for contributors to the Go SDK. To use the SDK, see the 
[README](./README.md).
+
+## Building and testing
+
+The [`Justfile`](./Justfile) has the common tasks:
+
+```bash
+just test              # run the unit tests
+just build             # pack the example bundles
+just docs              # serve the package docs locally
+just generate-models   # regenerate the coordinator-protocol models
+just generate-specs    # regenerate the Dag, task and task group specs
+```
+
+## How it works
+
+The bundle binary speaks the coordinator protocol when Airflow launches it 
with `--comm` and `--logs`.
+
+```
+Python supervisor / task runner
+        │  finds + validates the bundle, then forks it:
+        ▼
+  <bundle binary> --comm=127.0.0.1:P1 --logs=127.0.0.1:P2
+        │  binary dials BACK over TCP loopback (msgpack-over-IPC)
+        ▼
+  GetConnection / GetVariable / GetXCom / SetXCom ... → SucceedTask / TaskState
+```
+
+- The Python `ExecutableCoordinator` forks the bundle binary with 
`--comm`/`--logs` addresses it is already
+  listening on. The binary dials back (it never listens) and speaks a 
length-prefixed msgpack-over-IPC wire
+  protocol on the comm socket, with structured JSON-line logs on the logs 
socket.
+- The Python runtime is the worker. It proxies every `GetConnection` / 
`GetVariable` / `GetXCom` /
+  `SetXCom` call through to the Execution API. The Go binary just runs the 
task function.
+- When the first frame on the comm socket is a `DagFileParseRequest` from the 
Dag processor, the
+  binary runs no task. It answers with one `DagFileParsingResult` that holds 
the Dags from
+  `airflow.Dag` that the binary registered. The Dags are serialized as
+  [Serializing a native Dag](#serializing-a-native-dag) describes. The binary 
then exits.
+
+The Go side of the protocol is implemented in `pkg/execution/`. On the Python 
side it is the
+`ExecutableCoordinator` in 
`task-sdk/src/airflow/sdk/coordinators/executable/coordinator.py`.
+
+## The vendored schemas
+
+`schema/dag-schema.json` and `schema/supervisor-schema.json` are byte-for-byte 
copies of schemas
+airflow-core and task-sdk own. The generators read the copies, not the 
originals, so a generated file
+is explainable from a file inside this module and a standalone checkout — a 
published Go module, an
+ASF source release — can regenerate and test without the monorepo around it. 
`ts-sdk` and `java-sdk`
+vendor theirs the same way.
+
+Two prek hooks split the work that keeps the copies honest. 
`sync-go-sdk-schemas` copies an original
+over its copy when the two differ and fails, since copying is mechanical; it 
is a **manual** hook, so
+re-vendoring is a deliberate step here rather than something a change to a 
Python schema forces on
+whoever made it — which also means nothing announces on its own that a copy 
has gone stale.
+`check-go-sdk-generated-drift` runs on every commit and regenerates from the 
copies, failing when the
+committed Go differs, since what to do about a new schema construct — a 
generator rule, an authoring
+exclusion — is a decision.
+
+## Regenerating the coordinator-protocol models
+
+The types in `pkg/execution/genmodels/` are generated from 
`schema/supervisor-schema.json`, this
+module's vendored copy of the supervisor schema snapshot the Python Task SDK 
owns
+(`task-sdk/src/airflow/sdk/execution_time/schema/schema.json`); do not edit 
either by hand. To move
+the SDK to a newer schema version:
+
+1. Refresh the copy: `prek run sync-go-sdk-schemas --hook-stage manual`, which 
overwrites
+   `schema/supervisor-schema.json` from task-sdk's and fails so the change 
lands in review.
+2. Set `SupervisorSchemaVersion` in 
[`pkg/execution/messages.go`](./pkg/execution/messages.go) to the
+   copy's `api_version` date.
+3. Run `just generate-models`, and commit the copy with what it generated.
+
+`TestSupervisorSchemaVersionMatchesSnapshot` fails when the constant and the 
copy's `api_version`
+drift, so a missed bump is caught by `go test`. A snapshot can also grow a 
field without the
+`api_version` moving, which leaves the models behind with nothing failing — 
msgpack drops a field the
+struct does not declare — so the `check-go-sdk-generated-drift` prek hook 
regenerates them and fails
+when the committed files differ.
+
+[`airflow/enums.go`](./airflow/enums.go) declares the `genmodels.DagRunState` 
constants again, so that
+a Dag author does not import `genmodels`. `TestDagRunStateMatchesGenmodels` 
fails when regenerating
+the models adds, renames or removes a `DagRunState` constant. The test keeps 
failing until
+`airflow/enums.go` declares the same constants as `genmodels`.
+
+## Regenerating the Dag, task and task group specs
+
+`airflow.DagSpec`, `airflow.TaskSpec` and `airflow.TaskGroupSpec` in
+[`airflow/spec.gen.go`](./airflow/spec.gen.go) are generated from 
`schema/dag-schema.json`, this
+module's vendored copy of airflow-core's Dag serialization schema
+(`airflow-core/src/airflow/serialization/schema.json`), which Python owns; do 
not edit any of them
+by hand. Refresh the copy with `prek run sync-go-sdk-schemas --hook-stage 
manual`,
+then run `just generate-specs` after changing the schema or the generator.
+
+The schema is the serialized shape rather than the authoring one, so
+[`internal/genspec/authoring.go`](./internal/genspec/authoring.go) holds the 
three tables that turn
+it into the authoring shape, each entry carrying the reason it exists:
+
+- **exclusions** — properties an author never sets, such as the paths the 
bundle fills in and the
+  template fields of a Python operator class. A property *not* excluded 
generates, so one added on
+  the Python side surfaces in review instead of vanishing.
+- **type overrides** — the schema types a moment in time and a duration as a 
number of seconds, and
+  an integral count as a JSON number.
+- **injections** — `Schedule`, which stands in for the serialized `timetable`, 
and the `Queue` of
+  `DagSpec`, for which the schema has no Dag-level property. Injecting into 
the schema rather than
+  hand-writing the field keeps every field in one struct declaration, which is 
what lets
+  `TaskSpec` implement the sealed `TaskOption`.
+
+The schema types `trigger_rule` and `weight_rule` as plain strings and does 
not list their values.
+`TriggerRule`, `WeightRule` and their constants are therefore hand-written in
+[`airflow/enums.go`](./airflow/enums.go). `TestRuleConstantsMatchPython` 
checks those constants
+against the Python enums in airflow-core.
+
+`just generate-specs` also writes 
[`airflow/spec_fields.gen.go`](./airflow/spec_fields.gen.go), which
+holds the schema key and the schema default of each field, keyed by the name 
of the Go field.
+
+The `check-go-sdk-generated-drift` prek hook regenerates both files and fails 
when a committed one
+differs, so a schema change that never reached Go cannot merge.
+
+## Serializing a native Dag
+
+[`airflow/serialize.go`](./airflow/serialize.go) writes a registered 
`airflow.Dag` as the Dag JSON
+that Airflow stores. Python's `DagSerialization` writes the same shape for a 
Python Dag. Each field
+of `DagSpec`, `TaskSpec` and `TaskGroupSpec` has an entry in a generated 
table, which names the
+schema property that the field sets and its schema default. The serializer 
leaves out a value equal to
+the default. `serialize.go` lists only the fields that the generated tables 
cannot describe: the ones
+it writes in its own way, and the lists that Python keeps in a set.
+
+`airflow.Inputs` takes the results of tasks and `airflow.Literal` values, one 
for each parameter after
+the `Context`. A task becomes an `xcom` argument binding and a literal becomes 
a `literal` one:
+
+```go
+dag.Task(load, airflow.Inputs(transformed, airflow.Literal("s3://bucket/out")))
+```
+
+```json
+[
+  {"name": "arg0", "kind": "xcom", "task_id": "transform"},
+  {"name": "arg1", "kind": "literal", "value": "s3://bucket/out"}
+]
+```
+
+A literal must be JSON that decodes into its parameter type, and it adds no 
edge to the Dag.
+
+The `check-go-sdk-serialization-conformance` prek hook builds the Dags of
+[`scripts/ci/lang_sdk_serialization/test_dags.yaml`](../scripts/ci/lang_sdk_serialization/test_dags.yaml)
+twice: with this SDK, and with Python and Airflow's own serializer. It loads 
the Go output with
+Airflow's deserializer and compares the two serializations field by field. Run 
it from the
+repository root:
+
+```bash
+prek run check-go-sdk-serialization-conformance --all-files
+```
+
+The hook runs every Dag of the file, including the ones that `requires` a 
feature that only some SDKs
+have. 
[`go_native.json`](../airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json)
+holds the Go output for the same Dags, so that the Airflow core tests check 
that Airflow loads it.
+`TestSerializeConformanceDags` says how to rewrite it.
+
+## Architectural decisions
+
+The [`adr/`](./adr) directory records the design decisions behind the SDK:
+
+- [ADR 0001](./adr/0001-bundle-packing-options.md): bundle-packing options.
+- [ADR 0002](./adr/0002-use-go-tool-directive-for-bundle-packer.md): deliver 
the bundle packer via the
+  Go 1.24 `tool` directive.
+- [ADR 0003](./adr/0003-coordinator-protocol-msgpack-ipc.md): the 
msgpack-over-IPC coordinator protocol.
+- [ADR 0004](./adr/0004-self-contained-executable-bundle.md): the 
self-contained executable bundle, where
+  the executable *is* the bundle.
+- [ADR 0005](./adr/0005-retire-go-edge-worker.md): retire the standalone Go 
Edge Worker and make the
+  coordinator the only execution path.
+- [ADR 0006](./adr/0006-cross-language-argument-binding.md): cross-language 
TaskFlow argument binding,
+  from serialized literals and XCom references to typed Go parameters.
+- [ADR 0007](./adr/0007-mixed-lang-task-handler-interface.md): bundle 
registration and the Mixed Lang
+  task handler interface — `airflow.Bundle`/`Register`/`Serve`; Cross language 
TaskFlow: flat positional binding, `arg:` tagged structs, and the untagged 
folded-name fallback.
+- [ADR 0008](./adr/0008-native-dag-interface.md): the proposed Native Dag 
interface (`airflow.Dag`/
+  `dag.Task`/`airflow.Inputs`/`Before`-`After`)
+
+Cross-cutting Lang-SDK decisions — the coordinator architecture and how 
non-Python tasks integrate with
+Airflow core surfaces — are recorded in 
[`airflow-core/adr/lang-sdk/`](../airflow-core/adr/lang-sdk).
+Two of them shape the interfaces above:
+[ADR-0008](../airflow-core/adr/lang-sdk/0008-control-flow-constructs.md) for 
grouping, conditions,
+branching, and triggering a Dag run, and
+[ADR-0009](../airflow-core/adr/lang-sdk/0009-provider-operators-as-generated-dsl.md)
 for reaching
+Python provider operators from a native Dag.
+
+The normative, language-agnostic on-disk bundle format (the footer layout, 
manifest fields, and what the
+`ExecutableCoordinator` reads) is specified in
+[`executable-bundle-spec.rst`](https://github.com/apache/airflow/blob/main/task-sdk/docs/executable-bundle-spec.rst).
+`airflow-go-pack` produces bundles conforming to that spec.
+
+## Future Direction
+
+This is more of an "it would be nice to have" than any plan or commitment, and 
a place to record ideas.
+
+- The ability to run Airflow tasks "in" an existing code base, i.e. defining 
an Airflow task function that
+  runs (in a goroutine) inside an existing app.
+- Doing the task function reflection ahead of time, rather than for each 
Execute call.
diff --git a/go-sdk/README.md b/go-sdk/README.md
index 5ca2568faaa..4091be2599b 100644
--- a/go-sdk/README.md
+++ b/go-sdk/README.md
@@ -17,305 +17,93 @@
  under the License.
  -->
 
-# Apache Airflow Go Task SDK
+# Airflow Go SDK
 
-The Go SDK is a Go implementation of the Airflow Task SDK. It lets you write 
task functions in Go that
-have native access to the Airflow "model" (Variables, Connections, and XCom), 
instead of writing them in
-Python.
+Write Apache Airflow Dags and tasks in Go.
 
-It is built on the Task Execution Interface (TEI, a.k.a. the Task API) 
introduced by AIP-72 in Airflow
-3.0.0. AIP-72 standardised how a task runtime talks to Airflow over an HTTP 
Execution API, which decoupled
-the language a task is written in from the Airflow core. The Go SDK is one 
such runtime; the Java SDK is
-another.
+Requires Go 1.25 or later.
 
-> [!WARNING]
-> This is an **experimental** feature. The SDK is under active development and 
its APIs, wire protocols,
-> and tooling may change between releases without notice.
+## Installation
 
-## The compiled-language constraint
-
-Python tasks are imported and run in-process. Go is compiled, so the model is 
different.
-
-A single binary that bundles one or more Dags' task functions is called a 
**bundle**. You build one with
-the SDK's packer, `airflow-go-pack`, which compiles your code and appends a 
metadata footer (the manifest
-of `dag_id`s and `task_id`s, plus the source files) to the executable. The 
source files are the
-entrypoint (the file with `func main`) and the file that declares each Dag 
built with `airflow.Dag`.
-A Dag declared outside your module, such as in a dependency, uses the 
entrypoint as its source. The result is a **self-contained
-executable bundle**: a single runnable file that *is* the bundle, with no 
separate manifest or archive to
-ship alongside it.
-
-## You still need a Python stub Dag (for now)
-
-The Task API does not yet carry Dag *structure* for non-Python languages, so 
the Dag's shape and task
-dependencies are still declared in a small Python file using `@task.stub`:
-
-```python
-from airflow.sdk import dag, task
-
-
[email protected](queue="golang")
-def extract(): ...
-
-
[email protected](queue="golang")
-def transform(): ...
-
-
-@dag()
-def simple_dag():
-    extract() >> transform()
-
-
-simple_dag()
+```bash
+go get github.com/apache/airflow/go-sdk
+go get -tool github.com/apache/airflow/go-sdk/cmd/airflow-go-pack
 ```
 
-`@task.stub` tells the Dag parser the "shape" of the Go tasks (their names and 
dependencies) without any
-Python implementation. The `queue=` value routes the task to the Go runtime. 
This Python requirement is a
-known limitation.
+## Quick start
 
-
-## Authoring a bundle
-
-Build a bundle with `airflow.Bundle()`, register a handler for each task, and 
call `Serve` last. From
-[`example/bundle/main.go`](./example/bundle/main.go):
+Declare a Dag in `main.go`:
 
 ```go
-func main() {
-    bundle := airflow.Bundle()
-
-    bundle.Register(
-        airflow.TaskHandler("simple_dag", "extract", extract),
-        airflow.TaskHandler("simple_dag", "transform", transform),
-    )
-
-    if err := bundle.Serve(); err != nil {
-        log.Fatal(err)
-    }
-}
-```
-
-`TaskHandler` names the `dag_id` and the `task_id` explicitly; neither is 
derived from the Go function
-name, so a handler can be called whatever reads best in Go. It checks the 
signature of the function and
-panics if the check fails -- a variadic `...` parameter, which no stub 
argument can fill, is one such
-failure -- so the executable stops as it starts rather than when the task 
first runs. `Serve` then closes
-registration, so a `Register` left below it in `main` panics instead of 
changing what the running bundle
-answers for.
-
-A package that defines handlers of its own can export them as a 
`[]airflow.Registerable` for `main` to
-pass on with `bundle.Register(reports.Handlers()...)`.
-
-A task is an ordinary Go function whose first parameter is an 
[`airflow.Context`](#the-task-context).
-An optional `(any, error)` return becomes the task's XCom; an `error` return 
marks the task failed.
+package main
 
-Every parameter after the Context is a **data parameter**, filled in 
declaration order from the
-arguments of the Python stub Task's TaskFlow call. A literal in the Dag file 
(`transform("uk", ...)`)
-decodes straight into the parameter; an upstream task's output 
(`transform(..., extract())`) is
-pulled from that task's XCom in the current Dag run. If the argument count 
doesn't match, or an
-argument's declared type can't fill the Go type, the task fails before its 
body runs.
+import (
+    "log"
 
-Stub parameters the Dag author left at their Python defaults are the 
exception: they reach the wire
-but need no Go parameter, so adding a defaulted parameter to a stub doesn't 
break the Go functions
-already bound to it.
+    "github.com/apache/airflow/go-sdk/airflow"
+)
 
-```go
-func extract(actx airflow.Context) (any, error) {
-    conn, err := actx.Client().GetConnection(actx, "test_http")
-    // ... do work, honour actx cancellation ...
-    return map[string]any{"go_version": runtime.Version()}, nil
+func extract(airflow.Context) (int, error) {
+    return 42, nil
 }
 
-// The stub's literal and XCom arguments bind to country and extracted.
-func transform(actx airflow.Context, country string, extracted map[string]any) 
error {
-    val, err := actx.Client().GetVariable(actx, "my_variable")
-    if err != nil {
-        return err
-    }
-    actx.Logger().InfoContext(actx, "Obtained variable", "my_variable", val, 
"country", country)
+func report(actx airflow.Context, rows int) error {
+    actx.Logger().InfoContext(actx, "extracted rows", "rows", rows)
     return nil
 }
-```
-
-The `Register` calls are the single source of truth for which `dag_id`s and 
`task_id`s a bundle can run.
-A helper a task calls can still ask for the narrowest interface it needs (e.g. 
`sdk.VariableClient`
-instead of `sdk.Client`), which documents the Airflow features it touches and 
lets a test pass a fake;
-a handler itself takes the Context and reaches the client through it.
-
-### Name-based struct binding
-
-When a task's **sole data parameter** is a struct, its fields bind **by name** 
instead of by
-position — keyword arguments rather than positional ones, and a friendlier 
alternative to a long
-flat parameter list. Being the only data parameter is the opt-in; there is no 
marker to add.
-
-```go
-type CombineInput struct {
-    Region    string `arg:"region_code"` // renamed
-    Threshold float64
-}
-
-// The Python stub Task calls combine(region_code="uk", threshold=0.5).
-func Combine(actx airflow.Context, input CombineInput) (any, error) {
-    return nil, nil
-}
-```
-
-An exported field binds the argument matching its own Go name, folding case 
and underscores — so
-`Threshold` takes `threshold` and `RegionCode` would take `region_code`. Reach 
for an `arg:"<name>"`
-tag when the names genuinely differ, as `Region` does above. Declaration order 
is irrelevant on both
-sides, and embedded structs contribute their fields just as they do to 
`encoding/json`.
-
-Taking **more or fewer arguments** than the Python side passes does not fail 
the task. The runtime
-logs a warning before the task runs and carries on, one message per direction, 
so a call that does
-both at once says so twice:
-
-- `Dag's call passed argument(s) the task handler does not declare`
-- `Task handler declares argument(s) the Dag's call did not pass`
 
-Name-based struct binding is what keeps a mixed-language task working while 
the two sides drift:
-adding a parameter to the stub, or dropping a field from the struct, is a 
warning rather than a
-broken Dag. A field nothing matches keeps its Go zero value. Arguments the 
call left at their stub
-default are not reported.
-
-A struct that is **not** the sole data parameter is decoded whole from its one 
positional argument
-instead, so `arg:` tags only apply to the sole-parameter form; pairing a 
tagged struct with other
-data parameters is rejected at registration. A sole struct parameter also 
falls back to whole-value
-decoding when it gets exactly one passed argument no field claims, so a task 
can still take an
-upstream object as a single argument.
-
-### The task context
-
-`airflow.Context` is the first parameter of every task handler. Everything 
Airflow gives the task is a method on it:
-
-| Method | What it returns |
-| --- | --- |
-| `actx.Logger()` | the `*slog.Logger` that writes to the task's Airflow log |
-| `actx.Client()` | the `sdk.Client` for Variables, Connections and XCom |
-| `actx.TaskInstance()` | `DagID`, `RunID`, `TaskID`, `MapIndex` (nil for an 
unmapped task), `TryNumber` |
-| `actx.DagRun()` | `DagID`, `RunID`, and the `*time.Time` fields 
`LogicalDate`, `DataIntervalStart`, `DataIntervalEnd` (nil when the run has no 
such value, e.g. a manual trigger) |
+func main() {
+    dag := airflow.Dag("go_hello", airflow.DagSpec{Schedule: "@daily", Queue: 
"golang"})
 
-It is itself a `context.Context`, so pass it straight to a client call or to
-`http.NewRequestWithContext`, and select on `actx.Done()`, which fires when 
the supervisor asks the
-task to stop. Cleanup that must outlive that cancellation runs under 
`context.WithoutCancel(actx)`.
-A helper typed as a plain `context.Context` recovers the same surface with 
`airflow.FromContext`.
+    extracted := dag.Task(extract)
+    dag.Task(report, airflow.Inputs(extracted))
 
-```go
-func extract(actx airflow.Context) (any, error) {
-    ti := actx.TaskInstance()
-    actx.Logger().InfoContext(actx, "running",
-        "dag_id", ti.DagID,
-        "run_id", ti.RunID,
-        "task_id", ti.TaskID,
-        "try_number", ti.TryNumber,
-        "logical_date", actx.DagRun().LogicalDate,
-    )
-    return nil, nil
+    bundle := airflow.Bundle()
+    bundle.Register(dag)
+    if err := bundle.Serve(); err != nil {
+        log.Fatal(err)
+    }
 }
 ```
 
-`airflow.NewContext` builds one, so a handler is an ordinary function call in 
a unit test -- see
-[`example/bundle/main_test.go`](./example/bundle/main_test.go):
+Build it into a bundle, a single executable:
 
-```go
-actx := airflow.NewContext(
-    context.Background(), slog.Default(), &mockVars{},
-    airflow.TaskInstance{}, airflow.DagRun{},
-)
-err := transform(actx, "uk", map[string]any{"go_version": "go1.24"})
-assert.NoError(t, err)
+```bash
+go tool airflow-go-pack --output dist/go_hello .
 ```
 
-### Task logging
-
-In coordinator mode, the logger from `actx.Logger()` filters records using 
Airflow's configured `[logging] logging_level` before sending them to the 
supervisor. Airflow also propagates `[logging] namespace_levels`; use a 
group-scoped logger to set the namespace:
+Put `dist/go_hello` in a Dag bundle, such as the default `dags-folder` bundle, 
and send the `golang` queue to the
+Go coordinator in `airflow.cfg`:
 
-```go
-databaseLog := actx.Logger().WithGroup("example.database")
-databaseLog.Debug("query complete", "rows", 42)
+```ini
+[sdk]
+coordinators = {"go": {"classpath": 
"airflow.sdk.coordinators.executable.ExecutableCoordinator"}}
+queue_to_coordinator = {"golang": "go"}
 ```
 
-Namespace levels use longest dotted-prefix matching, so an `example=DEBUG` 
override also applies to `example.database` unless a more specific override 
takes precedence.
-
-## Deployment
-
-A Python task runner executes the Go task directly, with no separate Go worker 
process to run on the host.
-This is the same coordinator mechanism the Java SDK uses.
-
-The mature Python supervisor handles the Airflow-facing concerns, so Go tasks 
inherit remote task logs,
-the full range of task states, and alternate XCom backends without 
implementing those capabilities again.
-
-### Quickstart
-
-- Build and pack your bundle with `airflow-go-pack`. The packer compiles the 
bundle and appends an
-  embedded metadata footer so the coordinator can read its `dag_id`s without 
executing the binary,
-  producing a single runnable file:
-
-  ```bash
-  go tool airflow-go-pack ./example/bundle -- -trimpath -tags=prod
-  ```
-
-  Use `--output <path>` to write the packed bundle straight into the directory 
of the Dag bundle the
-  coordinator scans (`task_handler_bundle_name`), and pass extra `go build` 
flags after `--`.
+Airflow reads the bundle like any other Dag file: `go_hello` shows up in the 
UI, runs on its schedule, and
+`report` receives the value `extract` returned.
 
-  For cross-compiling (e.g. deploy to a Linux host from an Apple-silicon 
(darwin/arm64) machine), pass `--goos`/`--goarch` and the
-  packer cross-builds for you:
+## Learn more
 
-  ```bash
-  go tool airflow-go-pack --goos linux --goarch amd64 \
-    --output /opt/airflow/go-task-handlers/sample-dag-bundle \
-    ./example/bundle
-  ```
+The [Go SDK guide][guide] covers:
 
-  Alternatively, use `--executable`/`--source`. The packer normally execs the 
binary to read
-  its metadata; a cross-compiled binary cannot run on the host, so generate 
the metadata on a machine that
-  can run it and pass the file with `--airflow-metadata`:
+| Topic                                                                        
                 | Guide section                                  |
+| 
---------------------------------------------------------------------------------------------
 | ---------------------------------------------- |
+| Task inputs, options, order-only dependencies, task groups, branching, 
triggering another Dag | [Declaring a Dag in Go][declaring-a-dag-in-go] |
+| The `airflow.Context`, the `sdk.Client` and the task state store             
                 | [Writing tasks][writing-tasks]                 |
+| Building a bundle with `airflow-go-pack`, and cross-platform builds          
                 | [Building a bundle][building-a-bundle]         |
+| Where to deploy a bundle, and the `ExecutableCoordinator` options            
                 | [Deploying][deploying]                         |
+| What a Dag declared in Go cannot do yet                                      
                 | [Limitations][limitations]                     |
 
-  ```bash
-  # on linux/amd64 machine:
-  go build -o my-bundle ./example/bundle
-  ./my-bundle --airflow-metadata > airflow-metadata.yaml
+See [`example/`](./example) for working projects.
 
-  # on darwin/arm64 machine:
-  go tool airflow-go-pack --executable ./my-bundle --source main.go 
--airflow-metadata airflow-metadata.yaml
-  ```
-
-  > [!NOTE]
-  > The packer ships via the Go 1.24 `tool` directive, so there is no global 
install: add
-  > `tool github.com/apache/airflow/go-sdk/cmd/airflow-go-pack` to your bundle 
module's `go.mod` and run
-  > it with `go tool airflow-go-pack`. This pins the packer version per 
project.
-
-- Register a Dag bundle for the packed bundles, register the coordinator, and 
route the queue to it, in
-  `airflow.cfg` (or the equivalent `AIRFLOW__*` env vars):
-
-  ```ini
-  [dag_processor]
-  dag_bundle_config_list = [
-      {"name": "dags-folder", "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle", "kwargs": {}},
-      {"name": "go-task-handlers", "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle", "kwargs": {"path": 
"/opt/airflow/go-task-handlers"}}
-    ]
-
-  [sdk]
-  coordinators = {"go": {"classpath": 
"airflow.sdk.coordinators.executable.ExecutableCoordinator", "kwargs": 
{"task_handler_bundle_name": "go-task-handlers"}}}
-  queue_to_coordinator = {"golang": "go"}
-  ```
-
-  `task_handler_bundle_name` names the Dag bundle the coordinator scans for 
packed bundles (the task's own
-  Dag bundle when unset). It is used only by mixed-language Dags, to locate 
the task handlers for the
-  `@task.stub` tasks of a Python Dag; Dags defined natively in a language SDK 
do not use it.
-  `queue_to_coordinator` routes stub tasks with `queue="golang"` to this Go 
coordinator. The matching bundle
-  is marked executable before it is launched, so any Dag bundle works, 
including an object-store one such as
-  `S3DagBundle`.
-
-  > [!IMPORTANT]
-  > The `[sdk]` config and the packed bundle files must be present wherever 
tasks execute and on the Dag
-  > processor. With `CeleryExecutor`, tasks execute on the Celery workers; 
with `LocalExecutor`, they run
-  > inside the scheduler process. The Dag processor checks the stub tasks of 
each Python Dag against the
-  > task handlers the packed bundles register, so it runs them too and needs 
bundles built for its
-  > operating system and CPU architecture. The API server does not need any of 
it. Register the Dag bundle
-  > in `[dag_processor] dag_bundle_config_list` on every component, like your 
other Dag bundles: the worker
-  > and the Dag processor resolve `task_handler_bundle_name` through it, and 
wherever the `[sdk]` config is
-  > read it is rejected if the name is missing there.
-
-- Deploy the matching Python stub Dag (above) into Airflow. There is no 
separate Go worker to run: the
-  Airflow worker forks the bundle binary once per task instance.
+[guide]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html
+[declaring-a-dag-in-go]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html#declaring-a-dag-in-go
+[writing-tasks]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html#writing-tasks
+[building-a-bundle]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html#building-a-bundle
+[deploying]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html#deploying
+[limitations]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html#limitations
 
 ## Compatibility matrix
 
@@ -372,185 +160,10 @@ prek hook regenerate it.
 
 <!-- END AUTO-GENERATED LANG-SDK COMPAT MATRIX -->
 
-## How it works
-
-The bundle binary speaks the coordinator protocol when Airflow launches it 
with `--comm` and `--logs`.
-
-```
-Python supervisor / task runner
-        │  finds + validates the bundle, then forks it:
-        ▼
-  <bundle binary> --comm=127.0.0.1:P1 --logs=127.0.0.1:P2
-        │  binary dials BACK over TCP loopback (msgpack-over-IPC)
-        ▼
-  GetConnection / GetVariable / GetXCom / SetXCom ... → SucceedTask / TaskState
-```
-
-- The Python `ExecutableCoordinator` forks the bundle binary with 
`--comm`/`--logs` addresses it is already
-  listening on. The binary dials back (it never listens) and speaks a 
length-prefixed msgpack-over-IPC wire
-  protocol on the comm socket, with structured JSON-line logs on the logs 
socket.
-- The Python runtime is the worker. It proxies every `GetConnection` / 
`GetVariable` / `GetXCom` /
-  `SetXCom` call through to the Execution API. The Go binary just runs the 
task function.
-- When the first frame on the comm socket is a `DagFileParseRequest` from the 
Dag processor, the
-  binary runs no task. It answers with one `DagFileParsingResult` that holds 
the Dags from
-  `airflow.Dag` that the binary registered. The Dags are serialized as
-  [Serializing a native Dag](#serializing-a-native-dag) describes. The binary 
then exits.
-
-The Go side of the protocol is implemented in `pkg/execution/`. On the Python 
side it is the
-`ExecutableCoordinator` in 
`task-sdk/src/airflow/sdk/coordinators/executable/coordinator.py`.
-
-## The vendored schemas
-
-`schema/dag-schema.json` and `schema/supervisor-schema.json` are byte-for-byte 
copies of schemas
-airflow-core and task-sdk own. The generators read the copies, not the 
originals, so a generated file
-is explainable from a file inside this module and a standalone checkout — a 
published Go module, an
-ASF source release — can regenerate and test without the monorepo around it. 
`ts-sdk` and `java-sdk`
-vendor theirs the same way.
-
-Two prek hooks split the work that keeps the copies honest. 
`sync-go-sdk-schemas` copies an original
-over its copy when the two differ and fails, since copying is mechanical; it 
is a **manual** hook, so
-re-vendoring is a deliberate step here rather than something a change to a 
Python schema forces on
-whoever made it — which also means nothing announces on its own that a copy 
has gone stale.
-`check-go-sdk-generated-drift` runs on every commit and regenerates from the 
copies, failing when the
-committed Go differs, since what to do about a new schema construct — a 
generator rule, an authoring
-exclusion — is a decision.
-
-## Regenerating the coordinator-protocol models
-
-The types in `pkg/execution/genmodels/` are generated from 
`schema/supervisor-schema.json`, this
-module's vendored copy of the supervisor schema snapshot the Python Task SDK 
owns
-(`task-sdk/src/airflow/sdk/execution_time/schema/schema.json`); do not edit 
either by hand. To move
-the SDK to a newer schema version:
-
-1. Refresh the copy: `prek run sync-go-sdk-schemas --hook-stage manual`, which 
overwrites
-   `schema/supervisor-schema.json` from task-sdk's and fails so the change 
lands in review.
-2. Set `SupervisorSchemaVersion` in 
[`pkg/execution/messages.go`](./pkg/execution/messages.go) to the
-   copy's `api_version` date.
-3. Run `just generate-models`, and commit the copy with what it generated.
-
-`TestSupervisorSchemaVersionMatchesSnapshot` fails when the constant and the 
copy's `api_version`
-drift, so a missed bump is caught by `go test`. A snapshot can also grow a 
field without the
-`api_version` moving, which leaves the models behind with nothing failing — 
msgpack drops a field the
-struct does not declare — so the `check-go-sdk-generated-drift` prek hook 
regenerates them and fails
-when the committed files differ.
-
-[`airflow/enums.go`](./airflow/enums.go) declares the `genmodels.DagRunState` 
constants again, so that
-a Dag author does not import `genmodels`. `TestDagRunStateMatchesGenmodels` 
fails when regenerating
-the models adds, renames or removes a `DagRunState` constant. The test keeps 
failing until
-`airflow/enums.go` declares the same constants as `genmodels`.
-
-## Regenerating the Dag, task and task group specs
-
-`airflow.DagSpec`, `airflow.TaskSpec` and `airflow.TaskGroupSpec` in
-[`airflow/spec.gen.go`](./airflow/spec.gen.go) are generated from 
`schema/dag-schema.json`, this
-module's vendored copy of airflow-core's Dag serialization schema
-(`airflow-core/src/airflow/serialization/schema.json`), which Python owns; do 
not edit any of them
-by hand. Refresh the copy with `prek run sync-go-sdk-schemas --hook-stage 
manual`,
-then run `just generate-specs` after changing the schema or the generator.
-
-The schema is the serialized shape rather than the authoring one, so
-[`internal/genspec/authoring.go`](./internal/genspec/authoring.go) holds the 
three tables that turn
-it into the authoring shape, each entry carrying the reason it exists:
-
-- **exclusions** — properties an author never sets, such as the paths the 
bundle fills in and the
-  template fields of a Python operator class. A property *not* excluded 
generates, so one added on
-  the Python side surfaces in review instead of vanishing.
-- **type overrides** — the schema types a moment in time and a duration as a 
number of seconds, and
-  an integral count as a JSON number.
-- **injections** — `Schedule`, which stands in for the serialized `timetable`, 
and the `Queue` of
-  `DagSpec`, for which the schema has no Dag-level property. Injecting into 
the schema rather than
-  hand-writing the field keeps every field in one struct declaration, which is 
what lets
-  `TaskSpec` implement the sealed `TaskOption`.
-
-The schema types `trigger_rule` and `weight_rule` as plain strings and does 
not list their values.
-`TriggerRule`, `WeightRule` and their constants are therefore hand-written in
-[`airflow/enums.go`](./airflow/enums.go). `TestRuleConstantsMatchPython` 
checks those constants
-against the Python enums in airflow-core.
-
-`just generate-specs` also writes 
[`airflow/spec_fields.gen.go`](./airflow/spec_fields.gen.go), which
-holds the schema key and the schema default of each field, keyed by the name 
of the Go field.
-
-The `check-go-sdk-generated-drift` prek hook regenerates both files and fails 
when a committed one
-differs, so a schema change that never reached Go cannot merge.
-
-## Serializing a native Dag
-
-[`airflow/serialize.go`](./airflow/serialize.go) writes a registered 
`airflow.Dag` as the Dag JSON
-that Airflow stores. Python's `DagSerialization` writes the same shape for a 
Python Dag. Each field
-of `DagSpec`, `TaskSpec` and `TaskGroupSpec` has an entry in a generated 
table, which names the
-schema property that the field sets and its schema default. The serializer 
leaves out a value equal to
-the default. `serialize.go` lists only the fields that the generated tables 
cannot describe: the ones
-it writes in its own way, and the lists that Python keeps in a set.
-
-`airflow.Inputs` takes the results of tasks and `airflow.Literal` values, one 
for each parameter after
-the `Context`. A task becomes an `xcom` argument binding and a literal becomes 
a `literal` one:
-
-```go
-dag.Task(load, airflow.Inputs(transformed, airflow.Literal("s3://bucket/out")))
-```
-
-```json
-[
-  {"name": "arg0", "kind": "xcom", "task_id": "transform"},
-  {"name": "arg1", "kind": "literal", "value": "s3://bucket/out"}
-]
-```
-
-A literal must be JSON that decodes into its parameter type, and it adds no 
edge to the Dag.
-
-The `check-go-sdk-serialization-conformance` prek hook builds the Dags of
-[`scripts/ci/lang_sdk_serialization/test_dags.yaml`](../scripts/ci/lang_sdk_serialization/test_dags.yaml)
-twice: with this SDK, and with Python and Airflow's own serializer. It loads 
the Go output with
-Airflow's deserializer and compares the two serializations field by field. Run 
it from the
-repository root:
-
-```bash
-prek run check-go-sdk-serialization-conformance --all-files
-```
+## Links
 
-The hook runs every Dag of the file, including the ones that `requires` a 
feature that only some SDKs
-have. 
[`go_native.json`](../airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json)
-holds the Go output for the same Dags, so that the Airflow core tests check 
that Airflow loads it.
-`TestSerializeConformanceDags` says how to rewrite it.
-
-## Architectural decisions
-
-The [`adr/`](./adr) directory records the design decisions behind the SDK:
-
-- [ADR 0001](./adr/0001-bundle-packing-options.md): bundle-packing options.
-- [ADR 0002](./adr/0002-use-go-tool-directive-for-bundle-packer.md): deliver 
the bundle packer via the
-  Go 1.24 `tool` directive.
-- [ADR 0003](./adr/0003-coordinator-protocol-msgpack-ipc.md): the 
msgpack-over-IPC coordinator protocol.
-- [ADR 0004](./adr/0004-self-contained-executable-bundle.md): the 
self-contained executable bundle, where
-  the executable *is* the bundle.
-- [ADR 0005](./adr/0005-retire-go-edge-worker.md): retire the standalone Go 
Edge Worker and make the
-  coordinator the only execution path.
-- [ADR 0006](./adr/0006-cross-language-argument-binding.md): cross-language 
TaskFlow argument binding,
-  from serialized literals and XCom references to typed Go parameters.
-- [ADR 0007](./adr/0007-mixed-lang-task-handler-interface.md): bundle 
registration and the Mixed Lang
-  task handler interface — `airflow.Bundle`/`Register`/`Serve`; Cross language 
TaskFlow: flat positional binding, `arg:` tagged structs, and the untagged 
folded-name fallback.
-- [ADR 0008](./adr/0008-native-dag-interface.md): the proposed Native Dag 
interface (`airflow.Dag`/
-  `dag.Task`/`airflow.Inputs`/`Before`-`After`)
-
-Cross-cutting Lang-SDK decisions — the coordinator architecture and how 
non-Python tasks integrate with
-Airflow core surfaces — are recorded in 
[`airflow-core/adr/lang-sdk/`](../airflow-core/adr/lang-sdk).
-Two of them shape the interfaces above:
-[ADR-0008](../airflow-core/adr/lang-sdk/0008-control-flow-constructs.md) for 
grouping, conditions,
-branching, and triggering a Dag run, and
-[ADR-0009](../airflow-core/adr/lang-sdk/0009-provider-operators-as-generated-dsl.md)
 for reaching
-Python provider operators from a native Dag.
-
-The normative, language-agnostic on-disk bundle format (the footer layout, 
manifest fields, and what the
-`ExecutableCoordinator` reads) is specified in
-[`executable-bundle-spec.rst`](https://github.com/apache/airflow/blob/main/task-sdk/docs/executable-bundle-spec.rst).
-`airflow-go-pack` produces bundles conforming to that spec.
-
-## Future Direction
-
-This is more of an "it would be nice to have" than any plan or commitment, and 
a place to record ideas.
-
-- Defining the whole Dag in the Go SDK, so the Python stub Dag is no longer 
required and a bundle's
-  structure and task dependencies can be declared natively in Go.
-- The ability to run Airflow tasks "in" an existing code base, i.e. defining 
an Airflow task function that
-  runs (in a goroutine) inside an existing app.
-- Doing the task function reflection ahead of time, rather than for each 
Execute call.
+- [Go SDK guide][guide]
+- [API reference](https://pkg.go.dev/github.com/apache/airflow/go-sdk) on 
pkg.go.dev
+- [Source](https://github.com/apache/airflow/tree/main/go-sdk): the `go-sdk/` 
directory of the Apache Airflow monorepo
+- [Issues](https://github.com/apache/airflow/issues): bug reports and feature 
requests
+- [Developing this module](./DEVELOPMENT.md): building, testing, code 
generation and design decisions
diff --git a/go-sdk/example/README.md b/go-sdk/example/README.md
new file mode 100644
index 00000000000..bcaea1bc8f3
--- /dev/null
+++ b/go-sdk/example/README.md
@@ -0,0 +1,129 @@
+<!--
+ 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.
+ -->
+
+# Go SDK examples
+
+Example Dags for the Apache Airflow Go SDK. See the [Go SDK guide][guide] for 
the full documentation.
+
+[guide]: 
https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/language-sdks/go.html
+
+## Build
+
+Run these from `go-sdk/`. Each command writes one bundle into `bin/`:
+
+```bash
+# Dag definition
+go tool airflow-go-pack --output bin/go_native ./example/native
+
+go build -o bin/dynamic/go_dynamic_dags.raw ./example/dynamic
+cp example/dynamic/dags.yaml bin/dynamic/dags.yaml
+go tool airflow-go-pack --executable bin/dynamic/go_dynamic_dags.raw \
+  --source example/dynamic/main.go --output bin/go_dynamic_dags
+
+# Python Dag with stub TaskHandler
+go tool airflow-go-pack --output bin/example_dags ./example/bundle
+```
+
+The dynamic example reads `dags.yaml` next to its binary, including when 
`airflow-go-pack` reads its Dags, so it
+is built first and packed in place.
+
+## Configure Airflow
+
+Lay out the bundles like this:
+
+```text
+<dags_folder>/
+  go/go_native
+  go-dynamic/go_dynamic_dags
+  go-dynamic/dags.yaml
+  go_examples.py              # copied from go-sdk/dags/
+/opt/airflow/go-task-handlers/
+  example_dags
+  .airflowignore              # containing "*"
+```
+
+Register `go-task-handlers` as a Dag bundle, and route the `golang` queue to 
the Go coordinator:
+
+```bash
+export AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST='[
+  {"name": "dags-folder", "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle", "kwargs": {}},
+  {
+    "name": "go-task-handlers",
+    "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
+    "kwargs": {"path": "/opt/airflow/go-task-handlers"}
+  }
+]'
+export AIRFLOW__SDK__COORDINATORS='{
+  "go": {
+    "classpath": "airflow.sdk.coordinators.executable.ExecutableCoordinator",
+    "kwargs": {"task_handler_bundle_name": "go-task-handlers"}
+  }
+}'
+export AIRFLOW__SDK__QUEUE_TO_COORDINATOR='{"golang": "go"}'
+```
+
+The bundles must be built for the platform of the workers and the Dag 
processor.
+
+The examples read this Connection:
+
+```bash
+airflow connections add test_http --conn-type http --conn-host example.com
+```
+
+## Dag definition
+
+These Dags are declared entirely in Go:
+
+| Dag                                            | Source                     
| Shows                                                                         
         |
+| ---------------------------------------------- | -------------------------- 
| 
--------------------------------------------------------------------------------------
 |
+| `go_native_pipeline`                           | `native/main.go`           
| Task inputs, literals, a task group, an edge label, `If`, `Switch`, and a 
trigger rule |
+| `go_native_trigger`                            | `native/main.go`           
| `TriggerDagRun`, which starts `go_native_report`                              
         |
+| `go_native_report`                             | `native/report.go`         
| A Dag declared in its own file, which the Code view shows                     
         |
+| `go_dynamic_a`, `go_dynamic_b`, `go_dynamic_c` | `dynamic/` and `dags.yaml` 
| One Dag per entry of `dags.yaml`, which changes the Dags with no rebuild      
         |
+
+`go_native_pipeline` reads two Variables. Set `go_native_cadence` to `weekly` 
to take the other branch of
+`Switch`:
+
+```bash
+airflow variables set go_native_greeting "hello from Airflow"
+airflow variables set go_native_cadence daily
+
+airflow dags trigger go_native_pipeline
+airflow dags trigger go_native_trigger
+```
+
+## Python Dag with stub TaskHandler
+
+The Python Dags in `go-sdk/dags/go_examples.py` declare stub tasks, and 
`bundle/` implements them:
+
+| Dag                    | Shows                                               
                           |
+| ---------------------- | 
------------------------------------------------------------------------------ |
+| `simple_dag`           | Python and Go tasks in one Dag, TaskFlow arguments, 
Connections, and a retry   |
+| `taskflow_binding_dag` | Each way a stub task's arguments bind to a Go 
handler: by position and by name |
+| `concurrent_xcom_dag`  | Reading XComs from goroutines                       
                           |
+| `variable_write_dag`   | Writing and deleting Variables                      
                           |
+| `task_state_dag`       | The task state store                                
                           |
+
+`simple_dag` reads one Variable:
+
+```bash
+airflow variables set my_variable "hello from Airflow"
+
+airflow dags trigger simple_dag
+```

Reply via email to