{"id":23740,"plugin_id":"plugins_6ab2f25e4928819184294ebadcbe38ab","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:17:46.211Z","digest":"fe4f939c32d9f2db6df77734e5a4e6b5a841108447fdb44f34a1dcffe95d7051","against":null,"payload":{"name":"authoring-go-sdk-tasks","description":"Writes Airflow task logic in Go using the Airflow Go SDK. Use when the user wants to implement Airflow tasks in Go, asks about `BundleProvider`/`RegisterDags`, the `bundlev1` Registry/Dag interfaces, registering Go tasks (`AddTask`/`AddTaskWithName`), dependency injection by parameter type (`context.Context`, `sdk.TIRunContext`, `*slog.Logger`, `sdk.Client`), or reading connections/variables/XComs from Go. This skill covers the Go-specific native API; the shared Python-stub pattern and conceptual model live in authoring-language-sdk-tasks. For building/packing/shipping the bundle see deploying-go-sdk-bundles; for coordinator config see configuring-airflow-language-sdks.","included_files":[],"skill_md_contents":"---\nname: authoring-go-sdk-tasks\ndescription: Writes Airflow task logic in Go using the Airflow Go SDK. Use when the user wants to implement Airflow tasks in Go, asks about `BundleProvider`/`RegisterDags`, the `bundlev1` Registry/Dag interfaces, registering Go tasks (`AddTask`/`AddTaskWithName`), dependency injection by parameter type (`context.Context`, `sdk.TIRunContext`, `*slog.Logger`, `sdk.Client`), or reading connections/variables/XComs from Go. This skill covers the Go-specific native API; the shared Python-stub pattern and conceptual model live in authoring-language-sdk-tasks. For building/packing/shipping the bundle see deploying-go-sdk-bundles; for coordinator config see configuring-airflow-language-sdks.\n---\n\n# Authoring Go SDK Tasks\n\nThe Airflow Go SDK implements the language-SDK model for Go: your DAG stays in Python, and each task is a compiled Go function registered inside a **bundle** (a single native executable). This skill covers the **Go-specific** native API. The shared model (the Python `@task.stub` pattern, ID matching, the XCom-as-JSON contract) lives in **authoring-language-sdk-tasks**; read that first if you are new to language SDKs.\n\n> **Experimental.** The Go SDK is under active development and not production-ready. Module path `github.com/apache/airflow/go-sdk` (Go 1.24+). APIs may change.\n\n> **Related skills:** **authoring-language-sdk-tasks** (shared Python stub + concepts), **deploying-go-sdk-bundles** (build, pack, and ship the bundle), **configuring-airflow-language-sdks** (route the queue to the Go coordinator).\n\n---\n\n## Recap: the Python side\n\nA Go task is paired with a Python stub that carries no logic; it declares the task, its queue, and the dependency graph. IDs must match the Go registration exactly, and `queue=` routes the task to the Go runtime. Full rules are in **authoring-language-sdk-tasks**; the minimal shape:\n\n```python\nfrom airflow.sdk import dag, task\n\n\n@task.stub(queue=\"golang\")\ndef extract(): ...\n\n\n@task.stub(queue=\"golang\")\ndef transform(): ...\n\n\n@dag()\ndef simple_dag():\n    extract() >> transform()\n\n\nsimple_dag()\n```\n\nThe `queue` value (`\"golang\"` here) is an arbitrary label that must match the queue routed to the Go coordinator (`queue_to_coordinator`). See **configuring-airflow-language-sdks**.\n\n---\n\n## The bundle entry point\n\nA bundle implements `bundlev1.BundleProvider`: report its version and register your DAGs and tasks. `main` is one line; `bundlev1server.Serve` wires the bundle to the Airflow runtime for you.\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\tv1 \"github.com/apache/airflow/go-sdk/bundle/bundlev1\"\n\t\"github.com/apache/airflow/go-sdk/bundle/bundlev1/bundlev1server\"\n)\n\ntype myBundle struct{}\n\nvar _ v1.BundleProvider = (*myBundle)(nil)\n\nfunc (m *myBundle) GetBundleVersion() v1.BundleInfo {\n\treturn v1.BundleInfo{Name: bundleName, Version: &bundleVersion}\n}\n\nfunc (m *myBundle) RegisterDags(dagbag v1.Registry) error {\n\tsimpleDag := dagbag.AddDag(\"simple_dag\")      // dag_id must match the Python @dag name\n\tsimpleDag.AddTask(extract)                    // task_id is the function name; must match the stub\n\tsimpleDag.AddTaskWithName(\"transform\", transform) // or set the task_id explicitly\n\treturn nil\n}\n\nfunc main() {\n\tif err := bundlev1server.Serve(&myBundle{}); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n```\n\n`AddTask(fn)` derives the `task_id` from the Go function's name; use `AddTaskWithName(\"<task_id>\", fn)` when that name can't match the Python stub (an unexported, renamed, or reused function). `RegisterDags` is the single source of truth for task identity: the bundle's manifest (used by the packer and by the coordinator) is generated by running it, never hand-written.\n\n---\n\n## Task functions: dependency injection by parameter type\n\nA task is an ordinary Go function. The runtime inspects its signature and injects arguments **by type**; declare only what you need.\n\n| Parameter type | Injected value |\n|----------------|----------------|\n| `context.Context` | Task context for cancellation. Always available. |\n| `sdk.TIRunContext` | Richer context (embeds `context.Context`) exposing `TaskInstance()` and `DagRun()`. See [Runtime context](#runtime-context). |\n| `*slog.Logger` | Logger wired to the Airflow task log. |\n| `sdk.Client` | Full Airflow model access: Variables, Connections, XComs. |\n| `sdk.VariableClient` / `sdk.ConnectionClient` / `sdk.XComClient` | A narrower slice of `sdk.Client`. Prefer the narrowest you need; it documents intent and is trivial to fake in tests. |\n\nThe optional return signature is `(result, error)`: a non-nil `result` is pushed as the task's `return_value` XCom; a non-nil `error` fails the task (which triggers the stub's retry policy). Returning only `error`, or nothing, is also valid.\n\n```go\nfunc extract(ctx sdk.TIRunContext, client sdk.Client, log *slog.Logger) (any, error) {\n\tconn, err := client.GetConnection(ctx, \"test_http\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Info(\"connected\", \"host\", conn.Host)\n\treturn map[string]any{\"go_version\": runtime.Version()}, nil\n}\n\nfunc transform(ctx sdk.TIRunContext, client sdk.VariableClient) error {\n\tval, err := client.GetVariable(ctx, \"my_variable\")\n\tif err != nil {\n\t\treturn err // VariableNotFound (a sentinel error) if absent\n\t}\n\t_ = val\n\treturn nil\n}\n```\n\n---\n\n## The `sdk.Client` surface\n\n| Call | Returns | Notes |\n|------|---------|-------|\n| `GetVariable(ctx, key)` | `(string, error)` | `VariableNotFound` if absent. |\n| `UnmarshalJSONVariable(ctx, key, &ptr)` | `error` | Decode a JSON variable into a struct/pointer. |\n| `GetConnection(ctx, connID)` | `(Connection, error)` | `ConnectionNotFound` if absent. |\n| `GetXCom(ctx, dagID, runID, taskID, mapIndex, key, value)` | `(any, error)` | `XComNotFound` only if the key is absent; a stored null returns `(nil, nil)`. |\n| `PushXCom(ctx, ti, key, value)` | `error` | Rarely needed; a returned value is pushed for you. |\n\n`Connection` exposes `ID`, `Type`, `Host`, `Port` (`int`), `Login *string`, `Password *string` (nil when unset, distinct from empty), `Path` (schema), `Extra map[string]any`, plus `GetURI()`. Not-found cases return the sentinels `sdk.VariableNotFound`, `sdk.ConnectionNotFound`, `sdk.XComNotFound`.\n\nTo read an upstream task's result, call `GetXCom` explicitly, taking the `dag_id`/`run_id`/`task_id` you need from the runtime context (below).\n\n---\n\n## Runtime context\n\nDeclare an `sdk.TIRunContext` parameter to read metadata about the task instance and its DAG run. It is an interface that embeds `context.Context`, so it is usable anywhere a `context.Context` is expected.\n\n```go\nfunc extract(ctx sdk.TIRunContext, log *slog.Logger) error {\n\tti, dagRun := ctx.TaskInstance(), ctx.DagRun()\n\tlog.Info(\"running\",\n\t\t\"task_id\", ti.TaskID,\n\t\t\"run_id\", dagRun.RunID,\n\t\t\"logical_date\", dagRun.LogicalDate)\n\treturn nil\n}\n```\n\n- `TaskInstance()`: `DagID`, `RunID`, `TaskID`, `MapIndex *int` (nil when unmapped), `TryNumber`.\n- `DagRun()`: `DagID`, `RunID`, and the `*time.Time` timestamps `LogicalDate`, `DataIntervalStart`, `DataIntervalEnd` (nil when not sent).\n\nThe accessors are populated from the task's startup details before the body runs. Because `TIRunContext` embeds `context.Context`, pass it straight to client calls and cancellation checks (`ctx.Done()`); declare it as your context parameter by default. In tests, build the argument with `sdk.NewTIRunContext(ctx, ti, dagRun)` (it panics on a nil `ctx`).\n\n---\n\n## Go-specific pitfalls\n\n- **IDs must match the Python stub** (`dag_id` from `AddDag`, `task_id` from the registered function name), and the stub's `queue=` must route to the Go coordinator, or the task is never delivered.\n- **`RegisterDags` is authoritative.** Do not hand-write the manifest; the packer generates it by running `RegisterDags`.\n- **Ask for the narrowest client interface** you need (`sdk.VariableClient` over `sdk.Client`) for clearer intent and easier fakes.\n- **A non-nil `error` return fails the task** and applies the stub's retries; a recovered panic is also a failure.\n- See **authoring-language-sdk-tasks** for the language-agnostic pitfalls (one process per task instance, set queue and retries on the stub).\n\n---\n\n## Related Skills\n\n- **authoring-language-sdk-tasks**: Shared Python-stub pattern and concepts (read first).\n- **deploying-go-sdk-bundles**: Build and pack the bundle with `go tool airflow-go-pack`, then deploy it for the coordinator.\n- **configuring-airflow-language-sdks**: Route the queue to the Go coordinator (`ExecutableCoordinator`).\n- **authoring-dags**: General Airflow DAG authoring.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}