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 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.
- Skill ID
- astronomer/agents/airflow-plugins
- Publisher
- astronomer
- Repository
- agents
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
astronomer/agents/airflow-pluginsInstalls these files- SKILL.md
What this skill tells the agent
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. SetAIRFLOW__CORE__LAZY_LOAD_PLUGINS=Falseduring development to load plugins at startup rather than lazily. Relative paths always: Inexternal_views,hrefmust have no leading slash. In HTML and JavaScript, use relative paths for all assets andfetch()calls. Absolute paths break behind reverse proxies.
Before writing any code, verify
- Am I using
fastapi_apps/ FastAPI — notappbuilder_views/ Flask? - Are all HTML/JS asset paths and
fetch()calls relative (no leading slash)? - Are all synchronous SDK or SQLAlchemy calls wrapped in
asyncio.to_thread()? - Do the
static/andassets/directories exist before the FastAPI app mounts them? - 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.svgBASE_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 airflow.plugins_manager import AirflowPlugin
class LearnViewPlugin(AirflowPlugin):
name = "learn_view_plugin"
external_views = [
{
"name": "Learn Airflow 3",
"href": "https://www.astronomer.io/docs/learn",
"destination": "dag", # adds a tab to every Dag page
"url_route": "learn"
}
]The no-leading-slash rule applies to internal paths only — full https:// URLs are fine.
Step 3: Serve the UI entry point
@app.get("/ui", response_class=FileResponse)
async def serve_ui():
return FileResponse(BASE_DIR / "static" / "index.html")In HTML, always use relative paths. Absolute paths break when Airflow is mounted at a sub-path:
<!-- correct -->
<link rel="stylesheet" href="static/app.css" />
<script src="static/app.js?v=20240315"></script>
<!-- breaks behind a reverse proxy -->
<script src="/my-plugin/static/app.js"></script>Same rule in JavaScript:
fetch('api/dags') // correct — relative to current page
fetch('/my-plugin/api/dags') // breaks on Astro and sub-path deploysStep 4: Call the Airflow API from your plugin
Only needed if your plugin calls the Airflow REST API. Plugins that only serve static files, register external_views, or use direct DB access do not need this step — skip to Step 5 or Step 6.Add the dependency
Only if REST API communication is being implemented: add apache-airflow-client to the project's dependencies. Check which file exists and act accordingly:
| File found | Action |
|---|---|
requirements.txt | Append apache-airflow-client |
pyproject.toml (uv / poetry) | uv add apache-airflow-client or poetry add apache-airflow-client |
| None of the above | Tell the user: "Add apache-airflow-client to your dependencies before running the plugin." |
Use apache-airflow-client to talk to Airflow's own REST API. The SDK is synchronous but FastAPI routes are async — never call blocking SDK methods directly inside async def or you will stall the event loop and freeze all concurrent requests.
JWT token management
Cache one token per process. Refresh 5 minutes before the 1-hour expiry. Use double-checked locking so multiple concurrent requests don't all race to refresh simultaneously:
ReplaceMYPLUGIN_with a short uppercase prefix derived from the plugin name (e.g. if the plugin is called "Trip Analyzer", useTRIP_ANALYZER_). If no plugin name has been given yet, ask the user before writing env var names.
import asyncio
import os
import threading
import time
import airflow_client.client as airflow_sdk
import requests
More from astronomer
Use this skill in a project
Open any RiverX project, search the Skills panel for astronomer/agents/airflow-plugins, and install it in one click.
