Plugins — Extending the Airflow UI Itself
Packaging More Than Just Operators
The Custom Operators page (Module 03) already covered packaging reusable operators/hooks/sensors under a plugins/ folder. Airflow Plugins are the formal mechanism behind that folder — and they can add more than code reuse: custom menu links, entirely new UI views, and Flask blueprints served alongside Airflow's own pages.
The Simplest Plugin: A Menu Link
# plugins/demo plugin.py
from airflow.plugins_manager import AirflowPlugin
class DemoMenuPlugin(AirflowPlugin):
name = "demo_menu_plugin"
appbuilder_menu_items = [
{
"name": "Data Catalog",
"href": "https://internal-wiki.company.com/data-catalog",
"category": "Docs",
}
]
Any .py file in the plugins/ folder defining a subclass of AirflowPlugin is picked up automatically. This one adds a link to an internal tool directly in Airflow's own top navigation — useful for pointing teams at a data catalog, a runbook wiki, or an internal dashboard without leaving the Airflow UI.
Unlike DAG files (picked up automatically by the DAG File Processor on its normal scan interval), plugin changes need the webserver restarted to take effect — the navigation menu and any custom views are built once at webserver startup, not re-scanned continuously.
Custom Operators, Hooks, and Sensors as a Plugin
from airflow.plugins_manager import AirflowPlugin
from my_company.operators.row_count_validator import RowCountValidatorOperator
class CompanyOperatorsPlugin(AirflowPlugin):
name = "company_operators"
operators = [RowCountValidatorOperator]
This is the same RowCountValidatorOperator built in Module 03 — declaring it through a Plugin (rather than just importing it directly in each DAG file) makes Airflow aware of it as a first-class extension, which matters for the macro/view registration below.
Custom Views and Macros
from airflow.plugins_manager import AirflowPlugin
from flask import Blueprint
from flask_appbuilder import BaseView, expose
class TeamDashboardView(BaseView):
route_base = "/team_dashboard"
@expose("/")
def list(self):
return self.render_template("team_dashboard/index.html")
class TeamDashboardPlugin(AirflowPlugin):
name = "team_dashboard"
appbuilder_views = [{
"name": "Team Dashboard",
"category": "Reports",
"view": TeamDashboardView(),
}]
macros = [lambda: "some_reusable_jinja_helper"]
A full BaseView gives a plugin an entire new page inside Airflow's own UI shell (same nav bar, same auth) — the mechanism managed services like Astronomer use to add their own environment-specific pages alongside the stock Airflow UI.
Full custom views are powerful but rare in practice - building and maintaining a Flask/Jinja UI inside Airflow is real ongoing engineering work. Most plugin usage in the wild is exactly the first two patterns on this page: a link to an existing internal tool, and packaging custom operators for reuse.