/authoring-java-sdk-tasks
Writes Airflow task logic in Java, Kotlin, or any JVM language using the Airflow Java SDK. Use when the user wants to implement Airflow tasks in Java/JVM, asks about `@Builder.Dag`/`@Builder.Task`/`@Builder.XCom`, the `Task`/`BundleBuilder` interfaces, reading
$ npx -y skills add astronomer/agents --skill authoring-java-sdk-tasks --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/authoring-java-sdk-tasks
Context preview
The summary Claude sees to decide when to auto-load this skill.
Writes Airflow task logic in Java, Kotlin, or any JVM language using the Airflow Java SDK. Use when the user wants to implement Airflow tasks in Java/JVM, asks about `@Builder.Dag`/`@Builder.Task`/`@Builder.XCom`, the `Task`/`BundleBuilder` interfaces, reading
SKILL.md
authoring-java-sdk-tasks.SKILL.mdname: authoring-java-sdk-tasks
description: Writes Airflow task logic in Java, Kotlin, or any JVM language using the Airflow Java SDK. Use when the user wants to implement Airflow tasks in Java/JVM, asks about `@Builder.Dag`/`@Builder.Task`/`@Builder.XCom`, the `Task`/`BundleBuilder` interfaces, reading connections/variables/XComs from Java, the JSON-to-Java type mapping, or logging from Java tasks. This skill covers the Java-specific native API; the shared Python-stub pattern and conceptual model live in authoring-language-sdk-tasks. For building/shipping the bundle see deploying-java-sdk-bundles; for coordinator config see configuring-airflow-language-sdks.
Authoring Java SDK Tasks
The Airflow Java SDK implements the language-SDK model for the JVM: your DAG stays in Python, and each task instance runs in a short-lived JVM subprocess. This skill covers the **Java-specific** native API. The shared model — the Python `@task.stub` pattern, ID matching, and the XCom-as-JSON contract — lives in **authoring-language-sdk-tasks**; read that first if you're new to language SDKs.
> **Experimental.** The Java SDK is in preview. Artifact coordinates and APIs may change.
> **Related skills:** **authoring-language-sdk-tasks** (shared Python stub + concepts), **configuring-airflow-language-sdks** (route the queue to `JavaCoordinator`), **deploying-java-sdk-bundles** (compile and ship the JAR).
---
Recap: the Python side
Java tasks are paired with Python stubs that carry no logic — they declare the task, queue, dependency graph, and retries. IDs must match the Java annotations exactly, and an upstream argument on a stub only declares the dependency (the value is fetched in Java). Full rules are in **authoring-language-sdk-tasks**; the minimal shape:
from airflow.sdk import dag, task
@dag
def sales_pipeline(): # dag_id "sales_pipeline" -> @Builder.Dag(id="sales_pipeline")
@task.stub(queue="java")
def extract(): ... # task_id "extract" -> @Builder.Task(id="extract")
@task.stub(queue="java")
def transform(extracted): ...
transform(extract())
sales_pipeline()---
Java side: two APIs
Both APIs produce identical runtime behavior; pick by style, and you can mix them in one bundle.
Annotation-based API (recommended)
Annotate a plain class; an annotation processor generates the wiring (`<ClassName>Builder`) at compile time.
import static java.lang.System.Logger.Level.INFO;
import org.apache.airflow.sdk.*;
@Builder.Dag(id = "sales_pipeline") // must match the Python dag_id
public class SalesPipeline {
private static final System.Logger log = System.getLogger(SalesPipeline.class.getName());
@Builder.Task(id = "extract") // must match the Python @task.stub name
public long extract(Client client) {
var conn = client.getConnection("sales_db");
log.log(INFO, "connected to {0}", conn.host);
return 42L; // return value is pushed as the return_value XCom
}
@Builder.Task(id = "transform")
public long transform(
Client client,
@Builder.XCom(task = "extract") long recordCount) { // pulls extract's return_value
var threshold = (String) client.getVariable("transform_threshold");
return recordCount * 2;
}
@Builder.Task // id omitted -> the method name "load" is used
public void load(Context context, @Builder.XCom(task = "transform") long transformed) {
log.log(INFO, "attempt {0}, value {1}", context.ti.tryNumber, transformed);
}
}Annotation reference:
| Annotation | Purpose | |------------|---------| | `@Builder.Dag(id = "...")` | Marks the class as a task container. `id` must match the Python `dag_id`; if omitted, the class name is used. Optional `to = "..."` renames the generated builder (default `<ClassName>Builder`). | | `@Builder.Task(id = "...")` | Marks a method as a task. `id` must match the Python `@task.stub` function name; if omitted, the method name is used. | | `@Builder.XCom(task = "...", key = "...")` | Injects an upstream task's XCom as a parameter. `task` defaults to the parameter name; `key` defaults to the producing task's `return_value`. The parameter type must be compatible with the stored JSON value. |
A task method's return value is automatically pushed as that task's `return_value` XCom. A method may declare `throws Exception`; any uncaught exception fails the task instance (which triggers retries if the stub configured them).
Interface-based API
Implement `Task` directly when you want full control over registration and XCom handling.
import org.apache.airflow.sdk.*;
public class ExtractTask implements Task {
@Override
public void execute(Context context, Client client) throws Exception {
var conn = client.getConnection("sales_db");
// ... do work ...
client.setXCom(42L); // push return_value explicitly
}
}Register tasks manually in a `Dag` and expose it through a `BundleBuilder`:
public class MyBundle implements BundleBuilder {
@Override
public Iterable<Dag> getDags() {
var dag = new Dag("sales_pipeline"); // DAG ID matches Python
dag.addTask("extract", ExtractTask.class);
dag.addTask("transform", TransformTask.class);
return java.util.List.of(dag);
}
}Each `Task` class needs a public no-arg constructor. Task IDs must be unique within a DAG, and DAG IDs unique within a bundle.
---
The entry point
Every bundle has a `main` that hands your DAGs to the SDK server. The server connects to the coordinator, runs one task instance, and exits.
import java.util.List;
import org.apache.airflow.sdk.*;
public class Main implements BundleBuilder {
@Override
public Iterable<Dag> getDags() {
// With the annotation API, the *Builder classes are generated at compile time.
return List.of(SalesPipelineBuilder.build());
}
public static void main(String[] args) {
SeRead more
name: authoring-java-sdk-tasks description: Writes Airflow task logic in Java, Kotlin, or any JVM language using the Airflow Java SDK. Use when the user wants to implement Airflow tasks in Java/JVM, asks about `@Builder.Dag`/`@Builder.Task`/`@Builder.XCom`, the `Task`/`BundleBuilder` interfaces, reading connections/variables/XComs from Java, the JSON-to-Java type mapping, or logging from Java tasks. This skill covers the Java-specific native API; the shared Python-stub pattern and conceptual model live in authoring-language-sdk-tasks. For building/shipping the bundle see deploying-java-sdk-bundles; for coordinator config see configuring-airflow-language-sdks.
Authoring Java SDK Tasks
The Airflow Java SDK implements the language-SDK model for the JVM: your DAG stays in Python, and each task instance runs in a short-lived JVM subprocess. This skill covers the **Java-specific** native API. The shared model — the Python `@task.stub` pattern, ID matching, and the XCom-as-JSON contract — lives in **authoring-language-sdk-tasks**; read that first if you're new to language SDKs.
> **Experimental.** The Java SDK is in preview. Artifact coordinates and APIs may change.
> **Related skills:** **authoring-language-sdk-tasks** (shared Python stub + concepts), **configuring-airflow-language-sdks** (route the queue to `JavaCoordinator`), **deploying-java-sdk-bundles** (compile and ship the JAR).
---
Recap: the Python side
Java tasks are paired with Python stubs that carry no logic — they declare the task, queue, dependency graph, and retries. IDs must match the Java annotations exactly, and an upstream argument on a stub only declares the dependency (the value is fetched in Java). Full rules are in **authoring-language-sdk-tasks**; the minimal shape:
from airflow.sdk import dag, task
@dag
def sales_pipeline(): # dag_id "sales_pipeline" -> @Builder.Dag(id="sales_pipeline")
@task.stub(queue="java")
def extract(): ... # task_id "extract" -> @Builder.Task(id="extract")
@task.stub(queue="java")
def transform(extracted): ...
transform(extract())
sales_pipeline()---
Java side: two APIs
Both APIs produce identical runtime behavior; pick by style, and you can mix them in one bundle.
Annotation-based API (recommended)
Annotate a plain class; an annotation processor generates the wiring (`<ClassName>Builder`) at compile time.
import static java.lang.System.Logger.Level.INFO;
import org.apache.airflow.sdk.*;
@Builder.Dag(id = "sales_pipeline") // must match the Python dag_id
public class SalesPipeline {
private static final System.Logger log = System.getLogger(SalesPipeline.class.getName());
@Builder.Task(id = "extract") // must match the Python @task.stub name
public long extract(Client client) {
var conn = client.getConnection("sales_db");
log.log(INFO, "connected to {0}", conn.host);
return 42L; // return value is pushed as the return_value XCom
}
@Builder.Task(id = "transform")
public long transform(
Client client,
@Builder.XCom(task = "extract") long recordCount) { // pulls extract's return_value
var threshold = (String) client.getVariable("transform_threshold");
return recordCount * 2;
}
@Builder.Task // id omitted -> the method name "load" is used
public void load(Context context, @Builder.XCom(task = "transform") long transformed) {
log.log(INFO, "attempt {0}, value {1}", context.ti.tryNumber, transformed);
}
}Annotation reference:
| Annotation | Purpose | |------------|---------| | `@Builder.Dag(id = "...")` | Marks the class as a task container. `id` must match the Python `dag_id`; if omitted, the class name is used. Optional `to = "..."` renames the generated builder (default `<ClassName>Builder`). | | `@Builder.Task(id = "...")` | Marks a method as a task. `id` must match the Python `@task.stub` function name; if omitted, the method name is used. | | `@Builder.XCom(task = "...", key = "...")` | Injects an upstream task's XCom as a parameter. `task` defaults to the parameter name; `key` defaults to the producing task's `return_value`. The parameter type must be compatible with the stored JSON value. |
A task method's return value is automatically pushed as that task's `return_value` XCom. A method may declare `throws Exception`; any uncaught exception fails the task instance (which triggers retries if the stub configured them).
Interface-based API
Implement `Task` directly when you want full control over registration and XCom handling.
import org.apache.airflow.sdk.*;
public class ExtractTask implements Task {
@Override
public void execute(Context context, Client client) throws Exception {
var conn = client.getConnection("sales_db");
// ... do work ...
client.setXCom(42L); // push return_value explicitly
}
}Register tasks manually in a `Dag` and expose it through a `BundleBuilder`:
public class MyBundle implements BundleBuilder {
@Override
public Iterable<Dag> getDags() {
var dag = new Dag("sales_pipeline"); // DAG ID matches Python
dag.addTask("extract", ExtractTask.class);
dag.addTask("transform", TransformTask.class);
return java.util.List.of(dag);
}
}Each `Task` class needs a public no-arg constructor. Task IDs must be unique within a DAG, and DAG IDs unique within a bundle.
---
The entry point
Every bundle has a `main` that hands your DAGs to the SDK server. The server connects to the coordinator, runs one task instance, and exits.
import java.util.List;
import org.apache.airflow.sdk.*;
public class Main implements BundleBuilder {
@Override
public Iterable<Dag> getDags() {
// With the annotation API, the *Builder classes are generated at compile time.
return List.of(SalesPipelineBuilder.build());
}
public static void main(String[] args) {
SeAI agent tooling for data engineering workflows. Includes an MCP server for Airflow, a CLI tool (af) for interacting with Airflow from your terminal, and skills that extend AI coding agents with specialized capabilities for working with Airflow and data
Other skills on data.
- /airflow-adapter
Airflow adapter pattern for v2/v3 API compatibility. Use when working with adapters, version detection, or adding new API methods that need to work across Airflow 2.x and 3.x.
Open skill - /airflow-hitl
Builds human-in-the-loop (HITL) Airflow workflows - approval gates, form input, and human-driven branching. Use when a DAG needs a human in the loop - an approval or reject step, sign-off before a task runs, a decision or approval UI, branching on a human choice, or collecting
Open skill - /airflow-plugins
Builds Airflow 3.1+ plugins that embed FastAPI apps, custom UI pages, React components, middleware, macros, and operator links directly into the Airflow UI. Use when building anything custom inside Airflow 3.1+ that involves Python and a browser-facing interface - creating an
Open skill - /airflow-state-store
Persists task and asset state across retries and DAG runs using Airflow 3.3's AIP-103 key/value stores (`task_state_store`, `asset_state_store`) and the crash-safe `ResumableJobMixin`. Use when the user asks about task state store, checkpointing in tasks, persisting state across
Open skill - /airflow
Queries, manages, and troubleshoots Apache Airflow using the `af` CLI. Use when working with anything related to Airflow - a DAG, a DAG run, a task log, an import or parse error, a broken DAG, or any Airflow operation. Covers listing and triggering DAGs, retrying runs, reading
Open skill - /analyzing-data
Queries the data warehouse with SQL and answers business questions about data. Use when answering anything that needs warehouse data - counts, metrics, trends, aggregations, joins across tables, data lookups, or ad-hoc SQL analysis (for example "who uses X", "how many Y", "show
Open skill

