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
By astronomer · 526 installs
npx skills add astronomer/agents --skill airflow-plugins
Source repository · Upstream listing
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:
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.
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:
The no leading slash rule applies to internal paths only — full https:// URLs are fine.
Step 3: Serve the UI entry point
In HTML, always use relative paths . Absolute paths break when Airflow is mounted at a sub path:
Same rule in JavaScript:
Step 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:
Replace MYPLUGIN with a short uppercase prefix derived from the plugin name (e.g. if the plugin is called "Trip Analyzer", use TRIP ANALYZER ). If no plugin name has been given yet, ask the user before writing env var names.
After implementing auth, tell the user:
Local development : set MYPLUGIN USERNAME and MYPLUGIN PASSWORD in .env — JWT exchange happens automatically.
Astronomer Astro (production) : create a Deployment API token and set it as MYPLUGIN TOKEN — the JWT exchange is skipped entirely:
1. Astro UI → open the Deployment → Access → API Tokens → + Deployment API Token
2. Copy the token value (shown only once)
3. astro deployment variable create MYPLUGIN TOKEN=<token
MYPLUGIN USERNAME and MYPLUGIN PASSWORD are not needed on Astro.
Wrapping SDK calls with asyncio.to thread
API field names : Never guess response field names — verify against the [REST API reference](https://airflow.apache.org/docs/apache airflow/stable/stable rest api ref.html). Key DAGResponse fields: dag id , dag display name , description , is paused , timetable summary , timetable description , fileloc , owners , tags .
The pattern is always: define a plain inner def fetch() with all SDK logic, then await asyncio.to thread( fetch) .
Alternative: Direct database access
Warning — use with caution and tell the user. The Airflow metadb is not a public interface. Direct writes or poorly formed queries can corrupt scheduler state. Whenever you use this pattern, explicitly tell the user: "This accesses Airflow's internal database directly. The internal models are not part of the public API, can change between Airflow versions, and incorrect queries can cause issues in the metadb. Prefer apache airflow client unless the operation is not exposed via the REST API."
Since FastAPI plugin endpoints run inside the API server process (not in a task worker), they have direct access to Airflow's internal SQLAlchemy models — no HTTP round trip or JWT needed. Use only for read operations not exposed via the REST API, or when the extra HTTP overhead genuinely matters. Always wrap DB calls in asyncio.to thread() — SQLAlchemy queries are blocking.
Step 5: Common API endpoint patterns
If you need an SDK method or field not shown in the examples below , verify it before generating code — do not guess. Either run python3 c "from airflow client.client.api import <Class ; print([m for m in dir(<Class ) if not m.startswith(' ')])" in any environment where the SDK is installed, or search the [ apache/airflow client python ](https://github.com/apache/airflow client python) repo for the class definition.
DAG runs, task instances, and logs
These are the most common calls beyond basic DAG CRUD. For anything not shown here, consult the [REST API reference](https://airflow.apache.org/docs/apache airflow/stable/stable rest api ref.html) for available endpoints and the matching Python SDK class/method names.
Streaming proxy
Use StreamingResponse to proxy binary content from an external URL through the plugin — useful when the browser can't fetch the resource directly (CORS, auth, etc.):
Note that requests.get() is blocking — fetch in asyncio.to thread so the event loop isn't stalled while waiting for the remote server.
Step 6: Other plugin component types
Macros
Macros are loaded by the scheduler (and DAG processor), not the API server. Restart the scheduler after changes.
Use in any templated field — including with XCom:
The naming pattern is always macros.{plugin name}.{function name} .
Middleware
Middleware applies to all Airflow API requests, including the built in REST API and any FastAPI plugins. Use sparingly and filter requests explicitly if needed:
Operator extra links
React apps
React apps are embedded as JavaScript bundles served via FastAPI. The bundle must expose itself as a global variable matching the plugin name:
The same bundle can be registered to multiple destinations by adding multiple entries — each needs a unique url route :
React app integration is experimental in Airflow 3.1. Interfaces may change in future releases.
Step 7: Environment variables and deployment
Never hardcode credentials:
Local Astro CLI:
Production Astronomer:
Auto reload during development (skips lazy loading):
Cache busting for static files after deploy:
Verify the plugin loaded : open Admin Plugins in the Airflow UI.
OpenAPI docs are auto generated for FastAPI plugins:
Swagger UI: {AIRFLOW HOST}/{url prefix}/docs
OpenAPI JSON: {AIRFLOW HOST}/{url prefix}/openapi.json
Common pitfalls
Problem Cause Fix
Nav link goes to 404 Leading / in href "my plugin/ui" not "/my plugin/ui"
Nav icon not showing Missing / in icon icon takes an absolute path: "/my plugin/static/icon.svg"
Event loop freezes under load Sync SDK called directly in async def Wrap with asyncio.to thread()
401 errors after 1 hour JWT expires with no refresh Use the 5 minute pre expiry refresh pattern
StaticFiles raises on startup Directory missing Create assets/ and static/ before starting
Plugin not showing up Python file changed without restart astro dev restart
Endpoints accessible without login FastAPI apps are not auto authenticated Add FastAPI security (e.g. OAuth2, API key) if endpoints must be private
Middleware affecting wrong routes Middleware applies to all API traffic Filter by request.url.path inside dispatch()
JS fetch() breaks on Astro Absolute path in fetch() Always use relative paths: fetch('api/dags')
References
[Airflow plugins documentation](https://airflow.apache.org/docs/apache airflow/stable/administration and deployment/plugins.html)
[Airflow REST API reference](https://airflow.apache.org/docs/apache airflow/stable/stable rest api ref.html) — full endpoint list with SDK class/method names
[Astronomer: Using Airflow plugins](https://www.astronomer.io/docs/learn/using airflow plugins)