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

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


The following commit(s) were added to refs/heads/main by this push:
     new 81c2eab5595 Update Go SDK docs for the airflow.Bundle authoring 
surface (#73528)
81c2eab5595 is described below

commit 81c2eab55955d72715c615edb50cc345be5a2685
Author: Henry Chen <[email protected]>
AuthorDate: Tue Sep 22 21:27:11 2026 +0800

    Update Go SDK docs for the airflow.Bundle authoring surface (#73528)
    
    * Update Go SDK docs for the airflow.Bundle authoring surface
    
    The Go SDK's authoring API was replaced: handlers are registered with
    airflow.TaskHandler under an explicit dag_id and task_id, and every handler
    takes an airflow.Context first instead of having the logger and client
    injected by parameter type.
    
    The docs still taught the removed API, so a Dag author following them writes
    a bundle that no longer compiles and handlers the runtime now rejects at
    registration. They also stated that task_id is derived from the Go function
    name, which is no longer true and silently produces a bundle whose tasks the
    supervisor cannot find.
    
    * Call it the stub Task where the docs mean the task, not the Dag
    
    A TaskFlow call and its arguments belong to the @task.stub task, not to the
    Python Dag that contains it, so saying "stub Dag" there tells a Go author to
    look in the wrong place for what binds to their handler's parameters.
    
    The places that genuinely describe the Dag itself keep saying Dag.
    
    * Lead the Go SDK argument docs with the two binding shapes
    
    A Go author arriving at the arguments section met the positional rules with
    no sign that a second, keyword-style shape existed further down, so the
    struct form read as a special case rather than one of two choices.
    
    Also drop the cross-SDK aside from the Go SDK README, which has no reason to
    explain itself in terms of the Python and Java SDKs.
    
    * Keep the two Go SDK argument shapes side by side
    
    The note about defaulted stub parameters sat between the positional and
    struct forms, splitting the comparison a reader is making and reading as if
    it applied only to the positional one. It applies to both.
---
 .../authoring-and-scheduling/language-sdks/go.rst  | 191 ++++++++++++++-------
 go-sdk/README.md                                   | 113 ++++++------
 .../adr/0007-mixed-lang-task-handler-interface.md  |  12 +-
 .../cmd/airflow-go-pack/pack_integration_test.go   |   4 +-
 .../bundle/concurrentxcom/concurrentxcom.go        |   2 +-
 5 files changed, 206 insertions(+), 116 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 42b1ecf6213..f84803843dc 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
@@ -93,78 +93,82 @@ implementation. The ``queue`` value routes the task to the 
Go coordinator.
 Go implementation
 ~~~~~~~~~~~~~~~~~~
 
-A task is an ordinary Go function. The runtime inspects its signature and 
injects arguments by type, so each
-task declares only the parameters it needs.
+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:: go
 
     import (
-        "log/slog"
         "runtime"
 
-        "github.com/apache/airflow/go-sdk/sdk"
+        "github.com/apache/airflow/go-sdk/airflow"
     )
 
-    func extract(ctx sdk.TIRunContext, client sdk.Client, log *slog.Logger) 
(any, error) {
-        conn, err := client.GetConnection(ctx, "test_http")
+    func extract(actx airflow.Context) (any, error) {
+        conn, err := actx.Client().GetConnection(actx, "test_http")
         if err != nil {
             return nil, err
         }
-        log.Info("fetched connection", "host", conn.Host)
-        // ... do work, honour ctx cancellation ...
+        actx.Logger().InfoContext(actx, "fetched connection", "host", 
conn.Host)
+        // ... do work, honour actx cancellation ...
         return map[string]any{"go_version": runtime.Version()}, nil
     }
 
-    func transform(ctx sdk.TIRunContext, client sdk.VariableClient, log 
*slog.Logger) error {
-        val, err := client.GetVariable(ctx, "my_variable")
+    func transform(actx airflow.Context) error {
+        val, err := actx.Client().GetVariable(actx, "my_variable")
         if err != nil {
             return err
         }
-        log.Info("obtained variable", "my_variable", val)
+        actx.Logger().InfoContext(actx, "obtained variable", "my_variable", 
val)
         return nil
     }
 
 .. note::
 
   As with the other language SDKs, XCom *dependencies* are declared in the 
Python stub Dag (they define task
-  order). The value must still be read explicitly in Go via 
``client.GetXCom``, and produced either by the
-  task's ``(any, error)`` return value or by ``client.PushXCom``.
+  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``.
 
 Go entry point
 ~~~~~~~~~~~~~~~
 
-Implement ``bundlev1.BundleProvider`` to register your Dags and tasks; 
``main`` is one line. ``RegisterDags``
-is 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.
+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.
 
 .. code-block:: go
 
     import (
         "log"
 
-        v1 "github.com/apache/airflow/go-sdk/bundle/bundlev1"
-        "github.com/apache/airflow/go-sdk/bundle/bundlev1/bundlev1server"
+        "github.com/apache/airflow/go-sdk/airflow"
     )
 
-    type myBundle struct{}
-
-    var _ v1.BundleProvider = (*myBundle)(nil)
+    func main() {
+        bundle := airflow.Bundle()
 
-    func (m *myBundle) RegisterDags(dagbag v1.Registry) error {
-        simpleDag := dagbag.AddDag("simple_dag") // must match the Python 
dag_id
-        simpleDag.AddTask(extract)               // task_id is taken from the 
function name
-        simpleDag.AddTask(transform)
-        return nil
-    }
+        bundle.Register(
+            airflow.TaskHandler("simple_dag", "extract", extract),
+            airflow.TaskHandler("simple_dag", "transform", transform),
+        )
 
-    func main() {
-        if err := bundlev1server.Serve(&myBundle{}); err != nil {
+        if err := bundle.Serve(); err != nil {
             log.Fatal(err)
         }
     }
 
-The ``dag_id`` passed to ``AddDag`` must match the ``dag_id`` of the Python 
Dag, and each registered task's
-name must match a ``@task.stub`` function in that Dag.
+``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.
+
+``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, or does 
not return an ``error``.
+Because ``main`` registers every handler before ``Serve``, a mistake stops the 
executable as soon as it
+starts rather than when the task first runs.
+
+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()...)``.
 
 Coordinator configuration
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -200,37 +204,58 @@ There is no separate Go worker to run: the Airflow worker 
forks the bundle binar
 Writing tasks
 -------------
 
-The runtime inspects a task function's signature and injects arguments by 
type, so you only declare the
-parameters your task actually needs:
+Every task function takes an ``airflow.Context`` as its first parameter, and 
reaches what Airflow provides
+through its methods:
 
 .. list-table::
    :header-rows: 1
    :widths: 35 65
 
-   * - Parameter type
-     - Injected value
-   * - ``sdk.TIRunContext``
-     - The task's execution context: the cancellation/deadline signal plus the 
task instance identifiers and
-       Dag run timestamps. Respect it for long-running work. See 
:ref:`go-sdk/runtime-context`.
-   * - ``*slog.Logger``
-     - A logger whose output is routed back to the Airflow task log.
-   * - ``sdk.Client`` (or a narrower interface)
-     - A client for Airflow Variables, Connections, and XCom.
+   * - Method
+     - What it returns
+   * - ``actx.Logger()``
+     - An ``*slog.Logger`` whose output is routed back to the Airflow task log.
+   * - ``actx.Client()``
+     - A client for Airflow Variables, Connections, and XCom. See 
:ref:`go-sdk/client`.
+   * - ``actx.TaskInstance()``
+     - The identifiers of the running task instance. See 
:ref:`go-sdk/runtime-context`.
+   * - ``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``.
+
+Every parameter after the Context is data, filled from the stub Task's 
TaskFlow call; see
+:ref:`go-sdk/arguments`.
 
 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.
 
-Requesting the narrowest interface you need (for example 
``sdk.VariableClient`` instead of the full
-``sdk.Client``) documents which Airflow features the task touches and makes 
unit testing easier, because you
-can pass a fake in tests.
+``airflow.NewContext`` builds a Context, so a task is an ordinary function 
call in a unit test:
+
+.. code-block:: go
+
+    actx := airflow.NewContext(
+        t.Context(), slog.Default(), fakeClient,
+        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.
 
 .. _go-sdk/client:
 
 The ``sdk.Client`` surface
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-``sdk.Client`` composes three smaller interfaces, so a task can depend on just 
one:
+``actx.Client()`` returns an ``sdk.Client``, which composes three 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``.
@@ -247,10 +272,11 @@ before storing it.
 
 .. code-block:: go
 
-    if err := client.SetVariable(ctx, "process_threshold", "42", "Rows above 
this count take the slow path"); err != nil {
+    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(ctx, "legacy_threshold"); err != nil {
+    if err := client.DeleteVariable(actx, "legacy_threshold"); err != nil {
         return err
     }
 
@@ -268,30 +294,79 @@ so you can branch on a missing value with ``errors.Is`` 
rather than parsing an e
 Reading the task runtime context
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Declare an ``sdk.TIRunContext`` parameter on a task to read 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. It is an interface that embeds ``context.Context``, so the same 
``ctx`` drives cancellation and
-client calls. The runtime binds it by type, just like the other injected 
parameters:
+``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:
 
 .. code-block:: go
 
-    func extract(ctx sdk.TIRunContext, log *slog.Logger) (any, error) {
-        ti := ctx.TaskInstance()
-        log.Info("running",
+    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", ctx.DagRun().LogicalDate,
+            "logical_date", actx.DagRun().LogicalDate,
         )
         return nil, nil
     }
 
-``ctx.TaskInstance()`` returns ``DagID``, ``RunID``, ``TaskID``, ``MapIndex`` 
(nil for an unmapped task),
-and ``TryNumber``; ``ctx.DagRun()`` returns ``DagID``, ``RunID``, and the 
``*time.Time`` fields
+``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
+    }
+
+    // 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.
+
 .. _go-sdk/types:
 
 XCom type mapping
diff --git a/go-sdk/README.md b/go-sdk/README.md
index a97303b6c96..8b2e7f490f0 100644
--- a/go-sdk/README.md
+++ b/go-sdk/README.md
@@ -74,67 +74,65 @@ known limitation.
 
 ## Authoring a bundle
 
-Implement `bundlev1.BundleProvider`, register your Dags and tasks, and `main` 
is one line. From
+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):
 
 ```go
-type myBundle struct{}
-
-var _ v1.BundleProvider = (*myBundle)(nil)
+func main() {
+    bundle := airflow.Bundle()
 
-func (m *myBundle) RegisterDags(dagbag v1.Registry) error {
-    simpleDag := dagbag.AddDag("simple_dag")
-    simpleDag.AddTask(extract)
-    simpleDag.AddTask(transform)
-    return nil
-}
+    bundle.Register(
+        airflow.TaskHandler("simple_dag", "extract", extract),
+        airflow.TaskHandler("simple_dag", "transform", transform),
+    )
 
-func main() {
-    if err := bundlev1server.Serve(&myBundle{}); err != nil {
+    if err := bundle.Serve(); err != nil {
         log.Fatal(err)
     }
 }
 ```
 
-A task is an ordinary Go function. The runtime inspects its signature and 
injects arguments by type:
-`sdk.TIRunContext`, `*slog.Logger`, and an `sdk.Client` (or a narrower 
interface such as
-`sdk.VariableClient`). An optional `(any, error)` return becomes the task's 
XCom; an `error` return marks
-the task failed.
+`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, which stops the executable as it starts rather than 
when the task first runs.
+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()...)`.
 
-Any other parameter is a **data parameter**, filled in declaration order from 
the arguments of the
-Python stub Dag'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.
+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.
+
+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.
 
 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.
 
 ```go
-func extract(ctx sdk.TIRunContext, client sdk.Client, log *slog.Logger) (any, 
error) {
-    conn, err := client.GetConnection(ctx, "test_http")
-    // ... do work, honour ctx cancellation ...
+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
 }
 
 // The stub's literal and XCom arguments bind to country and extracted.
-func transform(
-    ctx sdk.TIRunContext, client sdk.VariableClient, log *slog.Logger,
-    country string, extracted map[string]any,
-) error {
-    val, err := client.GetVariable(ctx, "my_variable")
+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
     }
-    log.Info("Obtained variable", "my_variable", val, "country", country)
+    actx.Logger().InfoContext(actx, "Obtained variable", "my_variable", val, 
"country", country)
     return nil
 }
 ```
 
-Asking for the narrowest interface a task needs (e.g. `sdk.VariableClient` 
instead of `sdk.Client`) makes
-unit testing easier and documents which Airflow features the task touches. 
`RegisterDags` is the single
-source of truth for which `dag_id`s and `task_id`s a bundle can run.
+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
 
@@ -148,8 +146,8 @@ type CombineInput struct {
     Threshold float64
 }
 
-// The stub Dag calls combine(region_code="uk", threshold=0.5).
-func Combine(ctx sdk.TIRunContext, log *slog.Logger, input CombineInput) (any, 
error) {
+// The Python stub Task calls combine(region_code="uk", threshold=0.5).
+func Combine(actx airflow.Context, input CombineInput) (any, error) {
     return nil, nil
 }
 ```
@@ -169,37 +167,54 @@ data parameters is rejected at registration. A sole 
struct parameter also falls
 decoding when it gets exactly one passed argument no field claims, so a task 
can still take an
 upstream object as a single argument.
 
-### Reading the task runtime context
+### 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) |
 
-Declare an `sdk.TIRunContext` parameter on a task to read 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. It is an interface that embeds `context.Context`, so the same `ctx` 
drives cancellation and client
-calls. The runtime binds it by type, just like the other injected parameters:
+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`.
 
 ```go
-func extract(ctx sdk.TIRunContext, log *slog.Logger) (any, error) {
-    ti := ctx.TaskInstance()
-    log.Info("running",
+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", ctx.DagRun().LogicalDate,
+        "logical_date", actx.DagRun().LogicalDate,
     )
     return nil, nil
 }
 ```
 
-`ctx.TaskInstance()` returns `DagID`, `RunID`, `TaskID`, `MapIndex` (nil for 
an unmapped task), and
-`TryNumber`; `ctx.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).
+`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):
+
+```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)
+```
 
 ### Task logging
 
-In coordinator mode, the injected 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:
+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:
 
 ```go
-databaseLog := log.WithGroup("example.database")
+databaseLog := actx.Logger().WithGroup("example.database")
 databaseLog.Debug("query complete", "rows", 42)
 ```
 
diff --git a/go-sdk/adr/0007-mixed-lang-task-handler-interface.md 
b/go-sdk/adr/0007-mixed-lang-task-handler-interface.md
index 6150ca4c2fc..f3739149248 100644
--- a/go-sdk/adr/0007-mixed-lang-task-handler-interface.md
+++ b/go-sdk/adr/0007-mixed-lang-task-handler-interface.md
@@ -30,7 +30,7 @@ Proposed.
 1. **A "bundle" is a value the author builds.** `airflow.Bundle()` returns a 
`*airflow.BundleRef`;
    `main` reads build, register, serve, with `bundle.Serve()` as its last 
statement.
    It replaces `BundleProvider` and `Registry`, the callback and the write 
half of the same bundle.
-2. **`bundle.Register(items ...airflow.Registraterable)`** is the single 
registration verb, taking native Dags and task handlers.
+2. **`bundle.Register(items ...airflow.Registerable)`** is the single 
registration verb, taking native Dags and task handlers.
 3. **A Go bundle registers task handlers, not Dags**: 
`airflow.TaskHandler(dagId, taskId, fn)`, the Go body for a task Python 
declares with `@task.stub`.
 4. **Both dag_id and task_id are written out on TaskHandler definition**, 
because Python owns them; nothing is derived from the Go function name.
 5. **Every handler must take an `airflow.Context` first**: a struct embedding 
`context.Context`, exposing `Logger()`, `Client()`, `TaskInstance()`, and 
`DagRun()`.
@@ -75,7 +75,7 @@ func main() {
 }
 ```
 
-Registration can be spread across packages, either by passing the bundle along 
or by returning `[]airflow.Registraterable` for the caller: 
`bundle.Register(taskflowbinding.Handlers()...)`.
+Registration can be spread across packages, either by passing the bundle along 
or by returning `[]airflow.Registerable` for the caller: 
`bundle.Register(taskflowbinding.Handlers()...)`.
 
 Three ways a Go function receives a stub task's data, all live in 
`go-sdk/example/bundle/`.
 
@@ -126,15 +126,15 @@ package airflow
 
 func Bundle() *BundleRef
 
-func (b *BundleRef) Register(items ...Registraterable)
+func (b *BundleRef) Register(items ...Registerable)
 func (b *BundleRef) Serve() error
 
-// Registraterable is sealed: its only method is unexported, so the set of 
things a bundle
+// Registerable is sealed: its only method is unexported, so the set of things 
a bundle
 // accepts stays closed to the SDK's own types — task handlers today, a Dag 
authored in Go
 // once there is one.
-type Registraterable interface{ registraterable() }
+type Registerable interface{ registerable() }
 
-func TaskHandler(dagId, taskId string, fn any) Registraterable
+func TaskHandler(dagId, taskId string, fn any) Registerable
 
 // Context is what every handler takes first. It is a context, so it passes 
straight to the
 // logger and the client rather than being stored inside either of them.
diff --git a/go-sdk/cmd/airflow-go-pack/pack_integration_test.go 
b/go-sdk/cmd/airflow-go-pack/pack_integration_test.go
index f064935d6a5..f0b735ebf2c 100644
--- a/go-sdk/cmd/airflow-go-pack/pack_integration_test.go
+++ b/go-sdk/cmd/airflow-go-pack/pack_integration_test.go
@@ -67,8 +67,8 @@ func TestPack_CrossArchExecutableWithMetadataFile(t 
*testing.T) {
                t.Skipf("no cross-arch mapping for host arch %q", 
runtime.GOARCH)
        }
 
-       // The example bundle is a real BundleProvider that answers
-       // --airflow-metadata, so it exercises the genuine metadata path.
+       // The example bundle is a real bundle that answers --airflow-metadata,
+       // so it exercises the genuine metadata path.
        exampleDir, err := filepath.Abs(filepath.Join("..", "..", "example", 
"bundle"))
        require.NoError(t, err)
        sourceFile := filepath.Join(exampleDir, "main.go")
diff --git a/go-sdk/example/bundle/concurrentxcom/concurrentxcom.go 
b/go-sdk/example/bundle/concurrentxcom/concurrentxcom.go
index bb810ab65a2..e85111421ec 100644
--- a/go-sdk/example/bundle/concurrentxcom/concurrentxcom.go
+++ b/go-sdk/example/bundle/concurrentxcom/concurrentxcom.go
@@ -17,7 +17,7 @@
 
 // Package concurrentxcom holds the pull_xcoms_concurrently task in its own
 // package, so main.go can register tasks defined across packages with one
-// RegisterDags.
+// bundle.Register call.
 package concurrentxcom
 
 import (

Reply via email to