/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
$ npx -y skills add astronomer/agents --skill airflow-plugins --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
/airflow-plugins
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
airflow-plugins.SKILL.mdname: airflow-plugins
description: 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 Airflow plugin, adding a custom UI page or nav entry, building FastAPI-backed endpoints inside Airflow, serving static assets from a plugin, embedding a React app, adding middleware to the API server, creating custom operator extra links, or calling the Airflow REST API from inside a plugin; also when AirflowPlugin, fastapi_apps, external_views, react_apps, or plugin registration come up.
Airflow 3 Plugins
Airflow 3 plugins let you embed FastAPI apps, React UIs, middleware, macros, operator buttons, and custom timetables directly into the Airflow process. No sidecar, no extra server.
> **CRITICAL**: Plugin components (fastapi_apps, react_apps, external_views) require **Airflow 3.1+**. **NEVER import `flask`, `flask_appbuilder`, or use `appbuilder_views` / `flask_blueprints`** — these are Airflow 2 patterns and will not work in Airflow 3. If existing code uses them, rewrite the entire registration block using FastAPI. > > **Security**: FastAPI plugin endpoints are **not automatically protected** by Airflow auth. If your endpoints need to be private, implement authentication explicitly using FastAPI's security utilities. > > **Restart required**: Changes to Python plugin files require restarting the API server. Static file changes (HTML, JS, CSS) are picked up immediately. Set `AIRFLOW__CORE__LAZY_LOAD_PLUGINS=False` during development to load plugins at startup rather than lazily. > > **Relative paths always**: In `external_views`, `href` must have no leading slash. In HTML and JavaScript, use relative paths for all assets and `fetch()` calls. Absolute paths break behind reverse proxies.
Before writing any code, verify
1. Am I using `fastapi_apps` / FastAPI — not `appbuilder_views` / Flask? 2. Are all HTML/JS asset paths and `fetch()` calls relative (no leading slash)? 3. Are all synchronous SDK or SQLAlchemy calls wrapped in `asyncio.to_thread()`? 4. Do the `static/` and `assets/` directories exist before the FastAPI app mounts them? 5. If the endpoint must be private, did I add explicit FastAPI authentication?
---
Step 1: Choose plugin components
A single plugin class can register multiple component types at once.
| Component | What it does | Field | |-----------|-------------|-------| | Custom API endpoints | FastAPI app mounted in Airflow process | `fastapi_apps` | | Nav / page link | Embeds a URL as an iframe or links out | `external_views` | | React component | Custom React app embedded in Airflow UI | `react_apps` | | API middleware | Intercepts all Airflow API requests/responses | `fastapi_root_middlewares` | | Jinja macros | Reusable Python functions in DAG templates | `macros` | | Task instance button | Extra link button in task Detail view | `operator_extra_links` / `global_operator_extra_links` | | Custom timetable | Custom scheduling logic | `timetables` | | Event hooks | Listener callbacks for Airflow events | `listeners` |
---
Step 2: Plugin registration skeleton
Project file structure
Give each plugin its own subdirectory under `plugins/` — this keeps the Python file, static assets, and templates together and makes multi-plugin projects manageable:
plugins/
my-plugin/
plugin.py # AirflowPlugin subclass — auto-discovered by Airflow
static/
index.html
app.js
assets/
icon.svg`BASE_DIR = Path(__file__).parent` in `plugin.py` resolves to `plugins/my-plugin/` — static and asset paths will be correct relative to that. Create the subdirectory and any static/assets folders before starting Airflow, or `StaticFiles` will raise on import.
from pathlib import Path
from airflow.plugins_manager import AirflowPlugin
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
BASE_DIR = Path(__file__).parent
app = FastAPI(title="My Plugin")
# Both directories must exist before Airflow starts or FastAPI raises on import
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
app.mount("/assets", StaticFiles(directory=BASE_DIR / "assets"), name="assets")
class MyPlugin(AirflowPlugin):
name = "my_plugin"
fastapi_apps = [
{
"app": app,
"url_prefix": "/my-plugin", # plugin available at {AIRFLOW_HOST}/my-plugin/
"name": "My Plugin",
}
]
external_views = [
{
"name": "My Plugin",
"href": "my-plugin/ui", # NO leading slash — breaks on Astro and reverse proxies
"destination": "nav", # see locations table below
"category": "browse", # nav bar category (nav destination only)
"url_route": "my-plugin", # unique route name (required for React apps)
"icon": "/my-plugin/static/icon.svg" # DOES use a leading slash — served by FastAPI
}
]External view locations
| `destination` | Where it appears | |--------------|-----------------| | `"nav"` | Left navigation bar (also set `category`) | | `"dag"` | Extra tab on every Dag page | | `"dag_run"` | Extra tab on every Dag run page | | `"task"` | Extra tab on every task page | | `"task_instance"` | Extra tab on every task instance page |
Nav bar categories (`destination: "nav"`)
Set `"category"` to place the link under a specific nav group: `"browse"`, `"admin"`, or omit for top-level.
External URLs and minimal plugins
`href` can be a relative path to an internal endpoint (`"my-plugin/ui"`) or a full external URL. A plugin with only `external_views` and no `fastapi_apps` is valid — no backend needed for a simple link or tab:
from airf
Read more
name: airflow-plugins description: 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 Airflow plugin, adding a custom UI page or nav entry, building FastAPI-backed endpoints inside Airflow, serving static assets from a plugin, embedding a React app, adding middleware to the API server, creating custom operator extra links, or calling the Airflow REST API from inside a plugin; also when AirflowPlugin, fastapi_apps, external_views, react_apps, or plugin registration come up.
Airflow 3 Plugins
Airflow 3 plugins let you embed FastAPI apps, React UIs, middleware, macros, operator buttons, and custom timetables directly into the Airflow process. No sidecar, no extra server.
> **CRITICAL**: Plugin components (fastapi_apps, react_apps, external_views) require **Airflow 3.1+**. **NEVER import `flask`, `flask_appbuilder`, or use `appbuilder_views` / `flask_blueprints`** — these are Airflow 2 patterns and will not work in Airflow 3. If existing code uses them, rewrite the entire registration block using FastAPI. > > **Security**: FastAPI plugin endpoints are **not automatically protected** by Airflow auth. If your endpoints need to be private, implement authentication explicitly using FastAPI's security utilities. > > **Restart required**: Changes to Python plugin files require restarting the API server. Static file changes (HTML, JS, CSS) are picked up immediately. Set `AIRFLOW__CORE__LAZY_LOAD_PLUGINS=False` during development to load plugins at startup rather than lazily. > > **Relative paths always**: In `external_views`, `href` must have no leading slash. In HTML and JavaScript, use relative paths for all assets and `fetch()` calls. Absolute paths break behind reverse proxies.
Before writing any code, verify
1. Am I using `fastapi_apps` / FastAPI — not `appbuilder_views` / Flask? 2. Are all HTML/JS asset paths and `fetch()` calls relative (no leading slash)? 3. Are all synchronous SDK or SQLAlchemy calls wrapped in `asyncio.to_thread()`? 4. Do the `static/` and `assets/` directories exist before the FastAPI app mounts them? 5. If the endpoint must be private, did I add explicit FastAPI authentication?
---
Step 1: Choose plugin components
A single plugin class can register multiple component types at once.
| Component | What it does | Field | |-----------|-------------|-------| | Custom API endpoints | FastAPI app mounted in Airflow process | `fastapi_apps` | | Nav / page link | Embeds a URL as an iframe or links out | `external_views` | | React component | Custom React app embedded in Airflow UI | `react_apps` | | API middleware | Intercepts all Airflow API requests/responses | `fastapi_root_middlewares` | | Jinja macros | Reusable Python functions in DAG templates | `macros` | | Task instance button | Extra link button in task Detail view | `operator_extra_links` / `global_operator_extra_links` | | Custom timetable | Custom scheduling logic | `timetables` | | Event hooks | Listener callbacks for Airflow events | `listeners` |
---
Step 2: Plugin registration skeleton
Project file structure
Give each plugin its own subdirectory under `plugins/` — this keeps the Python file, static assets, and templates together and makes multi-plugin projects manageable:
plugins/
my-plugin/
plugin.py # AirflowPlugin subclass — auto-discovered by Airflow
static/
index.html
app.js
assets/
icon.svg`BASE_DIR = Path(__file__).parent` in `plugin.py` resolves to `plugins/my-plugin/` — static and asset paths will be correct relative to that. Create the subdirectory and any static/assets folders before starting Airflow, or `StaticFiles` will raise on import.
from pathlib import Path
from airflow.plugins_manager import AirflowPlugin
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
BASE_DIR = Path(__file__).parent
app = FastAPI(title="My Plugin")
# Both directories must exist before Airflow starts or FastAPI raises on import
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
app.mount("/assets", StaticFiles(directory=BASE_DIR / "assets"), name="assets")
class MyPlugin(AirflowPlugin):
name = "my_plugin"
fastapi_apps = [
{
"app": app,
"url_prefix": "/my-plugin", # plugin available at {AIRFLOW_HOST}/my-plugin/
"name": "My Plugin",
}
]
external_views = [
{
"name": "My Plugin",
"href": "my-plugin/ui", # NO leading slash — breaks on Astro and reverse proxies
"destination": "nav", # see locations table below
"category": "browse", # nav bar category (nav destination only)
"url_route": "my-plugin", # unique route name (required for React apps)
"icon": "/my-plugin/static/icon.svg" # DOES use a leading slash — served by FastAPI
}
]External view locations
| `destination` | Where it appears | |--------------|-----------------| | `"nav"` | Left navigation bar (also set `category`) | | `"dag"` | Extra tab on every Dag page | | `"dag_run"` | Extra tab on every Dag run page | | `"task"` | Extra tab on every task page | | `"task_instance"` | Extra tab on every task instance page |
Nav bar categories (`destination: "nav"`)
Set `"category"` to place the link under a specific nav group: `"browse"`, `"admin"`, or omit for top-level.
External URLs and minimal plugins
`href` can be a relative path to an internal endpoint (`"my-plugin/ui"`) or a full external URL. A plugin with only `external_views` and no `fastapi_apps` is valid — no backend needed for a simple link or tab:
from airf
AI 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-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 - /annotating-task-lineage
Annotate Airflow tasks with data lineage using inlets and outlets. Use when the user wants to add lineage metadata to tasks, specify input/output datasets, or enable lineage tracking for operators without built-in OpenLineage extraction.
Open skill

