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 804eee944b4 Expose Dag run configuration and metadata to Go tasks 
(#74674)
804eee944b4 is described below

commit 804eee944b4e84beaa3ef4e6d572c9f76493ab44
Author: Aaron Chen <[email protected]>
AuthorDate: Sat Nov 7 04:59:36 2026 -0800

    Expose Dag run configuration and metadata to Go tasks (#74674)
    
    * Expose Dag run configuration and metadata to Go tasks
    
    * Refine documentation for DagRun configuration and XCom type mapping in Go 
SDK
    
    * Refactor DagRunType to use sdk package and improve type consistency in 
context
    
    * Adjust DagRun RunAfter handling to ensure UTC consistency in task 
execution
---
 .../authoring-and-scheduling/language-sdks/go.rst  | 20 +++--
 go-sdk/DEVELOPMENT.md                              |  7 +-
 go-sdk/adr/0008-native-dag-interface.md            |  2 +-
 go-sdk/airflow/context.go                          |  2 +-
 go-sdk/airflow/enums.go                            | 29 +++++--
 go-sdk/airflow/enums_test.go                       | 14 ++++
 go-sdk/airflow/serve_test.go                       | 89 ++++++++++++++++++++++
 go-sdk/example/bundle/main.go                      |  3 +
 go-sdk/pkg/execution/integration_test.go           | 11 +++
 go-sdk/pkg/execution/task_runner.go                | 60 +++++++++++++++
 go-sdk/pkg/execution/task_runner_test.go           | 89 ++++++++++++++++++++++
 go-sdk/sdk/context.go                              | 18 ++++-
 12 files changed, 323 insertions(+), 21 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 19bc36d9c2e..03dcb72d056 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst
@@ -467,14 +467,19 @@ Its first parameter is an ``airflow.Context``, which 
gives it what Airflow provi
      - ``DagID``, ``RunID``, ``TaskID`` (including any task group prefix), 
``MapIndex`` (``nil`` for an unmapped
        task) and ``TryNumber``.
    * - ``actx.DagRun()``
-     - ``DagID``, ``RunID``, and the ``*time.Time`` fields ``LogicalDate``, 
``DataIntervalStart`` and
-       ``DataIntervalEnd`` (``nil`` when the run has no such value, such as a 
manual run).
+     - ``DagID``, ``RunID``, ``RunAfter``, ``RunType`` (an 
``airflow.DagRunType`` such as
+       ``airflow.DagRunTypeManual``), ``Conf`` (empty when no configuration 
was supplied), and
+       the ``*time.Time`` fields ``LogicalDate``, ``DataIntervalStart`` and 
``DataIntervalEnd`` (``nil`` when the
+       run has no such value, such as a manual run).
 
 ``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``.
 
+``Conf`` is a ``map[string]any`` whose values follow the :ref:`XCom type 
mapping <go-sdk/types>`. Use a map lookup
+or ``len`` to check for configuration entries.
+
 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.
 
@@ -596,7 +601,8 @@ A namespace level applies to the longest matching dotted 
prefix, so ``example=DE
 XCom type mapping
 -----------------
 
-XComs are stored as JSON, so a value read with ``GetXCom`` arrives as the 
matching Go type:
+XComs are stored as JSON, so a value read with ``GetXCom`` arrives as the 
matching Go type. The same mapping
+applies to the values in ``actx.DagRun().Conf``:
 
 .. list-table::
    :header-rows: 1
@@ -629,9 +635,11 @@ XComs are stored as JSON, so a value read with ``GetXCom`` 
arrives as the matchi
 
 .. note::
 
-  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.
+  An integer arrives as a Go integer type whose width depends on the value. In 
``actx.DagRun().Conf``, it
+  arrives as ``int64`` instead, or as ``uint64`` when it is above 
``math.MaxInt64``, including in nested maps
+  and arrays. 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:
 
diff --git a/go-sdk/DEVELOPMENT.md b/go-sdk/DEVELOPMENT.md
index f90a25aa3c4..7419e3605c7 100644
--- a/go-sdk/DEVELOPMENT.md
+++ b/go-sdk/DEVELOPMENT.md
@@ -102,9 +102,10 @@ drift, so a missed bump is caught by `go test`. A snapshot 
can also grow a field
 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`](./airflow/enums.go) declares the `genmodels.DagRunState` 
and
+`genmodels.DagRunType` constants again, so that a Dag author does not import 
`genmodels`.
+`TestDagRunStateMatchesGenmodels` and `TestDagRunTypeMatchesGenmodels` fail 
when regenerating
+the models adds, renames or removes a constant from either enum. The tests 
keep failing until
 `airflow/enums.go` declares the same constants as `genmodels`.
 
 ## Regenerating the Dag, task and task group specs
diff --git a/go-sdk/adr/0008-native-dag-interface.md 
b/go-sdk/adr/0008-native-dag-interface.md
index 806e430644a..79b90b74ca5 100644
--- a/go-sdk/adr/0008-native-dag-interface.md
+++ b/go-sdk/adr/0008-native-dag-interface.md
@@ -184,7 +184,7 @@ const (
   Either the schema gains titles or the generate step keeps the map.
 - **The schema is the serialized shape, not the authoring shape.** It requires 
`fileloc` and `tasks` on a Dag, and `task_type`, `_task_module`, `ui_color`, 
`ui_fgcolor`, and `template_fields` on an operator. The SDK writes each of 
those or leaves it at its default instead of taking it from an author, and the 
schema carries a serialized `timetable` object where an author writes a 
schedule.
   Generation needs an exclusion list and a hand-written field or two, the same 
kind of rule 
[ADR-0009](../../airflow-core/adr/lang-sdk/0009-provider-operators-as-generated-dsl.md)
 states for provider operators.
-- **A state enum generated into `genmodels` is re-exported from `airflow` once 
a field that an author can reach holds its values.** Today the only re-exported 
enum is `DagRunState`, the type of the values in 
`TriggerDagRunSpec.AllowedStates` and `TriggerDagRunSpec.FailedStates`. 
`DagRunType` is re-exported once a field such as `allowed_run_types` on 
`DagSpec` holds its values. `TaskInstanceState` is re-exported once 
`airflow.TaskInstance` gets a state field.
+- **An enum generated into `genmodels` is re-exported from `airflow` once a 
field that an author can reach holds its values.** `DagRunState` is the type of 
the values in `TriggerDagRunSpec.AllowedStates` and 
`TriggerDagRunSpec.FailedStates`; `DagRunType` is the type of `DagRun.RunType`. 
`TaskInstanceState` is re-exported once `airflow.TaskInstance` gets a state 
field.
   `airflow/enums.go` lists every user-facing enum with the Python enum that it 
mirrors.
   `airflow.DagRunState` is a type alias of `genmodels.DagRunState`, not a new 
type. `airflow.DagRun` is an alias of `sdk.DagRun`, and package `sdk` cannot 
import `airflow`. So a state field that `sdk.DagRun` gains later has a type 
declared outside `airflow`. `actx.DagRun().State == airflow.DagRunStateFailed` 
compiles only if both sides have the same type.
   The cost is that `%T` and package reflect report `airflow.DagRunState` as 
`genmodels.DagRunState`, and package `airflow` cannot declare a method on that 
type.
diff --git a/go-sdk/airflow/context.go b/go-sdk/airflow/context.go
index 1f2d0fb438e..7bb949a327e 100644
--- a/go-sdk/airflow/context.go
+++ b/go-sdk/airflow/context.go
@@ -30,7 +30,7 @@ type (
        TaskInstance = sdk.TaskInstance
 
        // DagRun identifies the Dag run the task instance belongs to,
-       // and carries its scheduling timestamps.
+       // and carries its scheduling timestamps, run type, and configuration.
        DagRun = sdk.DagRun
 )
 
diff --git a/go-sdk/airflow/enums.go b/go-sdk/airflow/enums.go
index 49f8a490f0d..709b581a36d 100644
--- a/go-sdk/airflow/enums.go
+++ b/go-sdk/airflow/enums.go
@@ -22,21 +22,22 @@ import (
        "slices"
 
        "github.com/apache/airflow/go-sdk/pkg/execution/genmodels"
+       "github.com/apache/airflow/go-sdk/sdk"
 )
 
-// This file declares every enum type whose values a Dag author sets. Each 
item below gives
-// the Go enum, the Python file that declares the same enum, and the fields 
that hold its values:
+// This file declares every enum type whose values a Dag author sets or reads. 
Each item below
+// gives the Go enum, the Python file that declares the same enum, and the 
fields that hold its
+// values:
 //
 //   - TriggerRule: airflow-core/src/airflow/task/trigger_rule.py, for 
TaskSpec.TriggerRule
 //   - WeightRule: airflow-core/src/airflow/task/weight_rule.py, for 
TaskSpec.WeightRule
 //   - DagRunState: airflow-core/src/airflow/utils/state.py, for 
TriggerDagRunSpec.AllowedStates
 //     and TriggerDagRunSpec.FailedStates
+//   - DagRunType: airflow-core/src/airflow/utils/types.py, for DagRun.RunType
 //
-// Two more Python enums are not declared here yet, because no field that an 
author can reach
-// holds their values:
+// One more Python enum is not declared here yet, because no field that an 
author can reach
+// holds its values:
 //
-//   - DagRunType: airflow-core/src/airflow/utils/types.py. DagSpec leaves out
-//     allowed_run_types, and DagRun has no run type field.
 //   - TaskInstanceState: airflow-core/src/airflow/utils/state.py. 
TaskInstance has no state field.
 //
 // Adding such a field also means declaring its enum here, the way DagRunState 
is declared.
@@ -108,9 +109,23 @@ const (
        DagRunStateFailed  DagRunState = "failed"
 )
 
+// DagRunType says how a Dag run was created. DagRun.RunType holds it.
+//
+// Like DagRunState, %T and package reflect report its name as 
genmodels.DagRunType.
+type DagRunType = sdk.DagRunType
+
+const (
+       DagRunTypeBackfill             DagRunType = "backfill"
+       DagRunTypeScheduled            DagRunType = "scheduled"
+       DagRunTypeManual               DagRunType = "manual"
+       DagRunTypeOperatorTriggered    DagRunType = "operator_triggered"
+       DagRunTypeAssetTriggered       DagRunType = "asset_triggered"
+       DagRunTypeAssetMaterialization DagRunType = "asset_materialization"
+)
+
 // triggerRules, weightRules and dagRunStates list the valid values of 
TriggerRule, WeightRule and
 // DagRunState. Tests in this package check these lists and the constants 
above against the
-// Python enums for TriggerRule and WeightRule, and against genmodels for 
DagRunState.
+// Python enums for TriggerRule and WeightRule, and against genmodels for 
DagRunState and DagRunType.
 var (
        triggerRules = []TriggerRule{
                TriggerRuleAllSuccess,
diff --git a/go-sdk/airflow/enums_test.go b/go-sdk/airflow/enums_test.go
index 32c8cd7fb09..2cde5a791f4 100644
--- a/go-sdk/airflow/enums_test.go
+++ b/go-sdk/airflow/enums_test.go
@@ -151,6 +151,20 @@ func TestDagRunStateMatchesGenmodels(t *testing.T) {
        assert.ElementsMatch(t, slices.Collect(maps.Values(generated)), 
toStrings(dagRunStates))
 }
 
+// TestDagRunTypeMatchesGenmodels fails when regenerating genmodels adds, 
renames or removes a
+// DagRunType constant. Like DagRunState, DagRunType is an alias, so enums.go 
declares each of the
+// genmodels constants again.
+func TestDagRunTypeMatchesGenmodels(t *testing.T) {
+       generated := readPairs(
+               t,
+               "../pkg/execution/genmodels/models.gen.go",
+               `(?m)^const DagRunType(\w+) DagRunType = "([^"]*)"$`,
+       )
+       declared := readPairs(t, enumsPath, `(?m)^\tDagRunType(\w+) +DagRunType 
= "([^"]*)"$`)
+
+       assert.Equal(t, generated, declared)
+}
+
 func TestTaskRejectsAValueOutsideAnEnum(t *testing.T) {
        const (
                triggerRuleValues = `["all_success" "all_failed" "all_done" 
"all_done_min_one_success" ` +
diff --git a/go-sdk/airflow/serve_test.go b/go-sdk/airflow/serve_test.go
index bca506bc14c..e797e327251 100644
--- a/go-sdk/airflow/serve_test.go
+++ b/go-sdk/airflow/serve_test.go
@@ -22,6 +22,7 @@ import (
        "encoding/json"
        "errors"
        "io"
+       "math"
        "net"
        "testing"
        "time"
@@ -257,6 +258,94 @@ func serveForSupervisor(t *testing.T, b *BundleRef) 
(*execution.CoordinatorComm,
 
 // A fake supervisor sends StartupDetails over the comm socket, as the Python
 // ExecutableCoordinator does after it starts the bundle with --comm and 
--logs.
+func TestServeInjectsDagRunMetadata(t *testing.T) {
+       // msgpack decodes a timestamp in time.Local, so a worker outside UTC
+       // would otherwise get RunAfter in its own zone.
+       local := time.Local
+       time.Local = time.FixedZone("UTC+8", 8*60*60)
+       t.Cleanup(func() { time.Local = local })
+       runAfter := time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)
+       cases := []struct {
+               name     string
+               sendConf bool
+               conf     map[string]any
+               want     map[string]any
+       }{
+               {name: "absent"},
+               {name: "null", sendConf: true},
+               {name: "empty", sendConf: true, conf: map[string]any{}, want: 
map[string]any{}},
+               {
+                       name:     "nested numbers",
+                       sendConf: true,
+                       conf: map[string]any{
+                               "days":       7,
+                               "batch_size": 500,
+                               "big":        70000,
+                               "precise":    int64(9007199254740993),
+                               "max":        uint64(math.MaxInt64),
+                               "above_max":  uint64(math.MaxInt64) + 1,
+                               "nested":     map[string]any{"attempts": -129, 
"enabled": true, "ratio": 1.25},
+                               "list":       []any{-1, -32769, 
int64(-2147483649), "us", nil},
+                       },
+                       want: map[string]any{
+                               "days":       int64(7),
+                               "batch_size": int64(500),
+                               "big":        int64(70000),
+                               "precise":    int64(9007199254740993),
+                               "max":        int64(math.MaxInt64),
+                               "above_max":  uint64(math.MaxInt64) + 1,
+                               "nested": map[string]any{
+                                       "attempts": int64(-129),
+                                       "enabled":  true,
+                                       "ratio":    1.25,
+                               },
+                               "list": []any{int64(-1), int64(-32769), 
int64(-2147483649), "us", nil},
+                       },
+               },
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       var got DagRun
+                       b := Bundle()
+                       b.Register(TaskHandler("py_etl", "transform", func(actx 
Context) error {
+                               got = actx.DagRun()
+                               return nil
+                       }))
+                       supervisor, done := serveForSupervisor(t, b)
+                       dagRun := map[string]any{"run_after": runAfter, 
"run_type": "manual"}
+                       if tc.sendConf {
+                               dagRun["conf"] = tc.conf
+                       }
+                       require.NoError(t, supervisor.SendRequest(0, 
map[string]any{
+                               "type": "StartupDetails",
+                               "ti": map[string]any{
+                                       "id":         
"550e8400-e29b-41d4-a716-446655440000",
+                                       "dag_id":     "py_etl",
+                                       "task_id":    "transform",
+                                       "run_id":     "run1",
+                                       "try_number": 1,
+                               },
+                               "ti_context":  map[string]any{"dag_run": 
dagRun},
+                               "bundle_info": map[string]any{"name": "test", 
"version": "1.0"},
+                       }))
+                       frame, err := supervisor.ReadMessage()
+                       require.NoError(t, err)
+                       var terminal map[string]any
+                       require.NoError(t, msgpack.Unmarshal(frame.Body, 
&terminal))
+                       require.Equal(t, "SucceedTask", terminal["type"])
+                       select {
+                       case err := <-done:
+                               require.NoError(t, err)
+                       case <-time.After(2 * time.Second):
+                               t.Fatal("Serve did not return after the task 
finished")
+                       }
+                       assert.Equal(t, DagRunTypeManual, got.RunType)
+                       assert.Equal(t, runAfter, got.RunAfter)
+                       assert.Equal(t, tc.want, got.Conf)
+               })
+       }
+}
+
 func TestServeRunsTaskForSupervisor(t *testing.T) {
        ran := false
        b := Bundle()
diff --git a/go-sdk/example/bundle/main.go b/go-sdk/example/bundle/main.go
index 5bca037f7f1..08ba77e3490 100644
--- a/go-sdk/example/bundle/main.go
+++ b/go-sdk/example/bundle/main.go
@@ -124,6 +124,9 @@ func extract(actx airflow.Context) (any, error) {
                                "logical_date", dagRun.LogicalDate,
                                "data_interval_start", dagRun.DataIntervalStart,
                                "data_interval_end", dagRun.DataIntervalEnd,
+                               "run_after", dagRun.RunAfter,
+                               "run_type", dagRun.RunType,
+                               "conf", dagRun.Conf,
                        ),
                ),
        )
diff --git a/go-sdk/pkg/execution/integration_test.go 
b/go-sdk/pkg/execution/integration_test.go
index 8e4b24358c8..a93ff793a31 100644
--- a/go-sdk/pkg/execution/integration_test.go
+++ b/go-sdk/pkg/execution/integration_test.go
@@ -496,6 +496,11 @@ func TestRunTaskInjectsAirflowContext(t *testing.T) {
        logical := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC)
        start := logical.Add(-time.Hour)
        end := logical.Add(time.Hour)
+       runAfter := end.Add(30 * time.Minute)
+       conf := genmodels.Conf{
+               "export_date": "2026-06-09",
+               "output":      map[string]any{"bucket": "reports"},
+       }
 
        var got contexttest.Context
        bundle := buildBundle(t, func(r testBundle) {
@@ -523,6 +528,9 @@ func TestRunTaskInjectsAirflowContext(t *testing.T) {
                                LogicalDate:       logical,
                                DataIntervalStart: start,
                                DataIntervalEnd:   end,
+                               RunAfter:          runAfter,
+                               RunType:           genmodels.DagRunTypeManual,
+                               Conf:              &conf,
                        },
                },
        }
@@ -552,6 +560,9 @@ func TestRunTaskInjectsAirflowContext(t *testing.T) {
        assert.Equal(t, start, *dagRun.DataIntervalStart)
        require.NotNil(t, dagRun.DataIntervalEnd)
        assert.Equal(t, end, *dagRun.DataIntervalEnd)
+       assert.Equal(t, runAfter, dagRun.RunAfter)
+       assert.Equal(t, genmodels.DagRunTypeManual, dagRun.RunType)
+       assert.Equal(t, map[string]any(conf), dagRun.Conf)
 }
 
 // Guards against the runtime binding the task state store to an empty task 
instance id.
diff --git a/go-sdk/pkg/execution/task_runner.go 
b/go-sdk/pkg/execution/task_runner.go
index 16791640890..d01b770e836 100644
--- a/go-sdk/pkg/execution/task_runner.go
+++ b/go-sdk/pkg/execution/task_runner.go
@@ -21,6 +21,7 @@ import (
        "context"
        "fmt"
        "log/slog"
+       "math"
        "runtime/debug"
        "time"
 
@@ -74,6 +75,10 @@ func RunTask(
        // live task context.
        ti := taskInstanceOf(details)
        dagRun := details.TIContext.DagRun
+       var conf map[string]any
+       if dagRun.Conf != nil {
+               conf = normalizeDagRunConf(*dagRun.Conf)
+       }
        runtimeContext := sdk.NewTIRunContext(
                context.Background(),
                ti,
@@ -83,6 +88,9 @@ func RunTask(
                        LogicalDate:       ifaceTimePtr(dagRun.LogicalDate),
                        DataIntervalStart: 
ifaceTimePtr(dagRun.DataIntervalStart),
                        DataIntervalEnd:   ifaceTimePtr(dagRun.DataIntervalEnd),
+                       RunAfter:          dagRun.RunAfter.UTC(), // msgpack 
decodes it in the local zone.
+                       RunType:           dagRun.RunType,
+                       Conf:              conf,
                },
        )
 
@@ -125,6 +133,58 @@ func RunTask(
        return executeTask(ctx, task, args, details.TIContext.ShouldRetry, 
logger)
 }
 
+func normalizeDagRunConf(conf map[string]any) map[string]any {
+       if conf == nil {
+               return nil
+       }
+       normalized := make(map[string]any, len(conf))
+       for key, value := range conf {
+               normalized[key] = normalizeDagRunConfValue(value)
+       }
+       return normalized
+}
+
+func normalizeDagRunConfValue(value any) any {
+       switch v := value.(type) {
+       case map[string]any:
+               return normalizeDagRunConf(v)
+       case []any:
+               if v == nil {
+                       return v
+               }
+               normalized := make([]any, len(v))
+               for i, item := range v {
+                       normalized[i] = normalizeDagRunConfValue(item)
+               }
+               return normalized
+       case int:
+               return int64(v)
+       case int8:
+               return int64(v)
+       case int16:
+               return int64(v)
+       case int32:
+               return int64(v)
+       case uint:
+               return normalizeDagRunConfValue(uint64(v))
+       case uint8:
+               return int64(v)
+       case uint16:
+               return int64(v)
+       case uint32:
+               return int64(v)
+       case uint64:
+               if v <= math.MaxInt64 {
+                       return int64(v)
+               }
+               return v
+       case float32:
+               return float64(v)
+       default:
+               return value
+       }
+}
+
 func taskInstanceOf(details *genmodels.StartupDetails) sdk.TaskInstance {
        return sdk.TaskInstance{
                DagID:     details.TI.DagID,
diff --git a/go-sdk/pkg/execution/task_runner_test.go 
b/go-sdk/pkg/execution/task_runner_test.go
new file mode 100644
index 00000000000..ecc06a30d75
--- /dev/null
+++ b/go-sdk/pkg/execution/task_runner_test.go
@@ -0,0 +1,89 @@
+// 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.
+
+package execution
+
+import (
+       "math"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestNormalizeDagRunConfNumbers(t *testing.T) {
+       cases := []struct {
+               name  string
+               value any
+               want  any
+       }{
+               {name: "int", value: int(7), want: int64(7)},
+               {name: "int8", value: int8(-1), want: int64(-1)},
+               {name: "int16", value: int16(-129), want: int64(-129)},
+               {name: "int32", value: int32(-32769), want: int64(-32769)},
+               {name: "uint", value: uint(500), want: int64(500)},
+               {name: "uint8", value: uint8(200), want: int64(200)},
+               {name: "uint16", value: uint16(500), want: int64(500)},
+               {name: "uint32", value: uint32(70000), want: int64(70000)},
+               {
+                       name:  "large exact integer",
+                       value: uint64(9007199254740993),
+                       want:  int64(9007199254740993),
+               },
+               {name: "max int64", value: uint64(math.MaxInt64), want: 
int64(math.MaxInt64)},
+               {
+                       name:  "above max int64",
+                       value: uint64(math.MaxInt64) + 1,
+                       want:  uint64(math.MaxInt64) + 1,
+               },
+               {name: "float32", value: float32(1.25), want: float64(1.25)},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       original := map[string]any{"value": tc.value}
+                       got := normalizeDagRunConf(original)
+                       assert.Equal(t, tc.want, got["value"])
+                       assert.Equal(t, tc.value, original["value"])
+               })
+       }
+}
+
+func TestNormalizeDagRunConfCopiesContainers(t *testing.T) {
+       original := map[string]any{
+               "label":       "original",
+               "nested":      map[string]any{"bucket": "reports"},
+               "list":        []any{map[string]any{"region": "us"}, 
[]any{"eu"}},
+               "nil_map":     map[string]any(nil),
+               "empty_map":   map[string]any{},
+               "nil_slice":   []any(nil),
+               "empty_slice": []any{},
+               "flag":        true,
+               "null":        nil,
+       }
+       normalized := normalizeDagRunConf(original)
+       require.Equal(t, original, normalized)
+
+       normalized["label"] = "changed"
+       normalized["nested"].(map[string]any)["bucket"] = "changed"
+       normalized["list"].([]any)[0].(map[string]any)["region"] = "changed"
+       normalized["list"].([]any)[1].([]any)[0] = "changed"
+
+       assert.Equal(t, "original", original["label"])
+       assert.Equal(t, "reports", 
original["nested"].(map[string]any)["bucket"])
+       assert.Equal(t, "us", 
original["list"].([]any)[0].(map[string]any)["region"])
+       assert.Equal(t, "eu", original["list"].([]any)[1].([]any)[0])
+}
diff --git a/go-sdk/sdk/context.go b/go-sdk/sdk/context.go
index c6ca1dfe8c3..eb0263a10d6 100644
--- a/go-sdk/sdk/context.go
+++ b/go-sdk/sdk/context.go
@@ -20,6 +20,8 @@ package sdk
 import (
        "context"
        "time"
+
+       "github.com/apache/airflow/go-sdk/pkg/execution/genmodels"
 )
 
 // TIRunContext is a context.Context that also exposes the identifiers and
@@ -84,14 +86,24 @@ type TaskInstance struct {
        TryNumber int
 }
 
+// DagRunType identifies how a Dag run was created.
+type DagRunType = genmodels.DagRunType
+
 // DagRun identifies the Dag run the current task instance belongs to and
-// carries its scheduling timestamps. The *time.Time fields are nil when the
-// supervisor did not provide a value (for example, a manually triggered run
-// without a logical date).
+// carries its scheduling timestamps, how it was created, and its conf. The
+// *time.Time fields are nil when the supervisor did not provide a value (for
+// example, a manually triggered run without a logical date).
 type DagRun struct {
        DagID             string
        RunID             string
        LogicalDate       *time.Time
        DataIntervalStart *time.Time
        DataIntervalEnd   *time.Time
+       RunAfter          time.Time
+       // RunType says how the run was created, such as
+       // [github.com/apache/airflow/go-sdk/airflow.DagRunTypeManual].
+       RunType DagRunType
+       // Conf is the run configuration, empty when none was supplied. 
Integers are
+       // int64, or uint64 above math.MaxInt64; floating-point values are 
float64.
+       Conf map[string]any
 }

Reply via email to